初次提交
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
node_modules
|
||||
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'
|
||||
}
|
||||
};
|
||||
1105
config/database-init.js
Normal file
1105
config/database-init.js
Normal file
File diff suppressed because it is too large
Load Diff
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元注册费
|
||||
}
|
||||
};
|
||||
34
config/withdrawal-init.sql
Normal file
34
config/withdrawal-init.sql
Normal file
@@ -0,0 +1,34 @@
|
||||
-- 创建代理提现记录表
|
||||
CREATE TABLE IF NOT EXISTS agent_withdrawals (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
agent_id INT NOT NULL,
|
||||
amount DECIMAL(10,2) NOT NULL,
|
||||
payment_type ENUM('bank', 'wechat', 'alipay', 'unionpay') DEFAULT 'bank' COMMENT '收款方式类型',
|
||||
bank_name VARCHAR(100) COMMENT '银行名称',
|
||||
account_number VARCHAR(50) COMMENT '账号/银行账号',
|
||||
account_holder VARCHAR(100) COMMENT '持有人姓名',
|
||||
qr_code_url VARCHAR(255) COMMENT '收款码图片URL',
|
||||
status ENUM('pending', 'approved', 'rejected', 'completed') DEFAULT 'pending',
|
||||
apply_note TEXT,
|
||||
admin_note TEXT,
|
||||
processed_by INT NULL,
|
||||
processed_at TIMESTAMP NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (agent_id) REFERENCES regional_agents(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (processed_by) REFERENCES users(id) ON DELETE SET NULL,
|
||||
-- 兼容旧字段
|
||||
bank_account VARCHAR(50) COMMENT '银行账号(兼容旧版本)'
|
||||
);
|
||||
|
||||
-- 为regional_agents表添加提现相关字段
|
||||
ALTER TABLE regional_agents ADD COLUMN withdrawn_amount DECIMAL(10,2) DEFAULT 0.00 COMMENT '已提现金额';
|
||||
ALTER TABLE regional_agents ADD COLUMN pending_withdrawal DECIMAL(10,2) DEFAULT 0.00 COMMENT '待审核提现金额';
|
||||
ALTER TABLE regional_agents ADD COLUMN payment_type ENUM('bank', 'wechat', 'alipay', 'unionpay') DEFAULT 'bank' COMMENT '收款方式类型';
|
||||
ALTER TABLE regional_agents ADD COLUMN bank_name VARCHAR(100) COMMENT '银行名称';
|
||||
ALTER TABLE regional_agents ADD COLUMN account_number VARCHAR(50) COMMENT '账号/银行账号';
|
||||
ALTER TABLE regional_agents ADD COLUMN account_holder VARCHAR(100) COMMENT '持有人姓名';
|
||||
ALTER TABLE regional_agents ADD COLUMN qr_code_url VARCHAR(255) COMMENT '收款码图片URL';
|
||||
|
||||
-- 兼容旧字段(可选,用于数据迁移)
|
||||
ALTER TABLE regional_agents ADD COLUMN bank_account VARCHAR(50) COMMENT '银行账号(兼容旧版本)';
|
||||
158
database.js
Normal file
158
database.js
Normal file
@@ -0,0 +1,158 @@
|
||||
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',
|
||||
// 连接池配置
|
||||
connectionLimit: 20, // 连接池最大连接数
|
||||
queueLimit: 0, // 排队等待连接的最大数量,0表示无限制
|
||||
// 连接超时配置
|
||||
// acquireTimeout: 60000, // 获取连接超时时间 60秒
|
||||
// timeout: 60000, // 查询超时时间 60秒
|
||||
// reconnect: true, // 自动重连
|
||||
// 连接保活配置
|
||||
multipleStatements: true,
|
||||
// 空闲连接超时配置
|
||||
idleTimeout: 300000, // 5分钟空闲超时
|
||||
// maxLifetime: 1800000, // 30分钟最大生命周期
|
||||
// 连接保活设置
|
||||
keepAliveInitialDelay: 0, // 开始保活探测前的延迟时间
|
||||
enableKeepAlive: true, // 启用TCP保活
|
||||
// 添加类型转换配置
|
||||
typeCast: function (field, next) {
|
||||
if (field.type === 'TINY' && field.length === 1) {
|
||||
return (field.string() === '1'); // 1 = true, 0 = false
|
||||
}
|
||||
return next();
|
||||
},
|
||||
// 确保参数正确处理
|
||||
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('acquire', function (connection) {
|
||||
// console.log('连接池获取连接:', connection.threadId);
|
||||
// });
|
||||
|
||||
// pool.on('release', 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,
|
||||
executeQuery,
|
||||
dbConfig
|
||||
};
|
||||
0
logs/audit.log
Normal file
0
logs/audit.log
Normal file
11
logs/combined.log
Normal file
11
logs/combined.log
Normal file
@@ -0,0 +1,11 @@
|
||||
{"ip":"::1","level":"error","message":"Error occurred: ENOENT: no such file or directory, stat 'D:\\work\\客户\\毛总\\code\\jurong_circle_agent_backend\\agent-admin\\dist\\index.html'","method":"GET","service":"integrated-system","stack":"Error: ENOENT: no such file or directory, stat 'D:\\work\\客户\\毛总\\code\\jurong_circle_agent_backend\\agent-admin\\dist\\index.html'","timestamp":"2025-09-03 15:11:57","url":"/agent-admin?ide_webview_request_time=1756883517386","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Trae/1.100.3 Chrome/132.0.6834.210 Electron/34.5.1 Safari/537.36"}
|
||||
{"duration":"10ms","ip":"::1","level":"info","message":"HTTP Request","method":"GET","service":"integrated-system","statusCode":404,"timestamp":"2025-09-03 15:11:57","url":"/agent-admin?ide_webview_request_time=1756883517386","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Trae/1.100.3 Chrome/132.0.6834.210 Electron/34.5.1 Safari/537.36"}
|
||||
{"ip":"::1","level":"error","message":"Error occurred: 路径 /@vite/client 未找到","method":"GET","service":"integrated-system","stack":"Error: 路径 /@vite/client 未找到\n at notFound (D:\\work\\客户\\毛总\\code\\jurong_circle_agent_backend\\middleware\\errorHandler.js:107:17)\n at Layer.handle [as handle_request] (D:\\work\\客户\\毛总\\code\\jurong_circle_agent_backend\\node_modules\\express\\lib\\router\\layer.js:95:5)\n at trim_prefix (D:\\work\\客户\\毛总\\code\\jurong_circle_agent_backend\\node_modules\\express\\lib\\router\\index.js:328:13)\n at D:\\work\\客户\\毛总\\code\\jurong_circle_agent_backend\\node_modules\\express\\lib\\router\\index.js:286:9\n at Function.process_params (D:\\work\\客户\\毛总\\code\\jurong_circle_agent_backend\\node_modules\\express\\lib\\router\\index.js:346:12)\n at next (D:\\work\\客户\\毛总\\code\\jurong_circle_agent_backend\\node_modules\\express\\lib\\router\\index.js:280:10)\n at D:\\work\\客户\\毛总\\code\\jurong_circle_agent_backend\\server.js:72:3\n at Layer.handle [as handle_request] (D:\\work\\客户\\毛总\\code\\jurong_circle_agent_backend\\node_modules\\express\\lib\\router\\layer.js:95:5)\n at trim_prefix (D:\\work\\客户\\毛总\\code\\jurong_circle_agent_backend\\node_modules\\express\\lib\\router\\index.js:328:13)\n at D:\\work\\客户\\毛总\\code\\jurong_circle_agent_backend\\node_modules\\express\\lib\\router\\index.js:286:9","timestamp":"2025-09-03 15:11:57","url":"/@vite/client","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Trae/1.100.3 Chrome/132.0.6834.210 Electron/34.5.1 Safari/537.36"}
|
||||
{"duration":"2ms","ip":"::1","level":"info","message":"HTTP Request","method":"GET","service":"integrated-system","statusCode":404,"timestamp":"2025-09-03 15:11:57","url":"/@vite/client","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Trae/1.100.3 Chrome/132.0.6834.210 Electron/34.5.1 Safari/537.36"}
|
||||
{"level":"error","message":"Error occurred: ENOENT: no such file or directory, stat 'D:\\work\\客户\\毛总\\code\\jurong_circle_agent_backend\\agent-admin\\dist\\index.html'","method":"GET","service":"integrated-system","stack":"Error: ENOENT: no such file or directory, stat 'D:\\work\\客户\\毛总\\code\\jurong_circle_agent_backend\\agent-admin\\dist\\index.html'","timestamp":"2025-09-03 15:13:56","url":"/agent-admin?ide_webview_request_time=1756883636612","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Trae/1.100.3 Chrome/132.0.6834.210 Electron/34.5.1 Safari/537.36"}
|
||||
{"ip":"::1","level":"error","message":"Error occurred: ENOENT: no such file or directory, stat 'D:\\work\\客户\\毛总\\code\\jurong_circle_agent_backend\\agent-admin\\dist\\index.html'","method":"GET","service":"integrated-system","stack":"Error: ENOENT: no such file or directory, stat 'D:\\work\\客户\\毛总\\code\\jurong_circle_agent_backend\\agent-admin\\dist\\index.html'","timestamp":"2025-09-03 15:13:59","url":"/agent-admin?ide_webview_request_time=1756883636612","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Trae/1.100.3 Chrome/132.0.6834.210 Electron/34.5.1 Safari/537.36"}
|
||||
{"duration":"3ms","ip":"::1","level":"info","message":"HTTP Request","method":"GET","service":"integrated-system","statusCode":404,"timestamp":"2025-09-03 15:13:59","url":"/agent-admin?ide_webview_request_time=1756883636612","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Trae/1.100.3 Chrome/132.0.6834.210 Electron/34.5.1 Safari/537.36"}
|
||||
{"ip":"::1","level":"error","message":"Error occurred: ENOENT: no such file or directory, stat 'D:\\work\\客户\\毛总\\code\\jurong_circle_agent_backend\\agent-admin\\dist\\index.html'","method":"GET","service":"integrated-system","stack":"Error: ENOENT: no such file or directory, stat 'D:\\work\\客户\\毛总\\code\\jurong_circle_agent_backend\\agent-admin\\dist\\index.html'","timestamp":"2025-09-03 15:13:59","url":"/agent-admin?ide_webview_request_time=1756883636612","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Trae/1.100.3 Chrome/132.0.6834.210 Electron/34.5.1 Safari/537.36"}
|
||||
{"duration":"2ms","ip":"::1","level":"info","message":"HTTP Request","method":"GET","service":"integrated-system","statusCode":404,"timestamp":"2025-09-03 15:13:59","url":"/agent-admin?ide_webview_request_time=1756883636612","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Trae/1.100.3 Chrome/132.0.6834.210 Electron/34.5.1 Safari/537.36"}
|
||||
{"ip":"::1","level":"error","message":"Error occurred: 路径 /@vite/client 未找到","method":"GET","service":"integrated-system","stack":"Error: 路径 /@vite/client 未找到\n at notFound (D:\\work\\客户\\毛总\\code\\jurong_circle_agent_backend\\middleware\\errorHandler.js:107:17)\n at Layer.handle [as handle_request] (D:\\work\\客户\\毛总\\code\\jurong_circle_agent_backend\\node_modules\\express\\lib\\router\\layer.js:95:5)\n at trim_prefix (D:\\work\\客户\\毛总\\code\\jurong_circle_agent_backend\\node_modules\\express\\lib\\router\\index.js:328:13)\n at D:\\work\\客户\\毛总\\code\\jurong_circle_agent_backend\\node_modules\\express\\lib\\router\\index.js:286:9\n at Function.process_params (D:\\work\\客户\\毛总\\code\\jurong_circle_agent_backend\\node_modules\\express\\lib\\router\\index.js:346:12)\n at next (D:\\work\\客户\\毛总\\code\\jurong_circle_agent_backend\\node_modules\\express\\lib\\router\\index.js:280:10)\n at D:\\work\\客户\\毛总\\code\\jurong_circle_agent_backend\\server.js:72:3\n at Layer.handle [as handle_request] (D:\\work\\客户\\毛总\\code\\jurong_circle_agent_backend\\node_modules\\express\\lib\\router\\layer.js:95:5)\n at trim_prefix (D:\\work\\客户\\毛总\\code\\jurong_circle_agent_backend\\node_modules\\express\\lib\\router\\index.js:328:13)\n at D:\\work\\客户\\毛总\\code\\jurong_circle_agent_backend\\node_modules\\express\\lib\\router\\index.js:286:9","timestamp":"2025-09-03 15:13:59","url":"/@vite/client","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Trae/1.100.3 Chrome/132.0.6834.210 Electron/34.5.1 Safari/537.36"}
|
||||
{"duration":"1ms","ip":"::1","level":"info","message":"HTTP Request","method":"GET","service":"integrated-system","statusCode":404,"timestamp":"2025-09-03 15:13:59","url":"/@vite/client","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Trae/1.100.3 Chrome/132.0.6834.210 Electron/34.5.1 Safari/537.36"}
|
||||
6
logs/error.log
Normal file
6
logs/error.log
Normal file
@@ -0,0 +1,6 @@
|
||||
{"ip":"::1","level":"error","message":"Error occurred: ENOENT: no such file or directory, stat 'D:\\work\\客户\\毛总\\code\\jurong_circle_agent_backend\\agent-admin\\dist\\index.html'","method":"GET","service":"integrated-system","stack":"Error: ENOENT: no such file or directory, stat 'D:\\work\\客户\\毛总\\code\\jurong_circle_agent_backend\\agent-admin\\dist\\index.html'","timestamp":"2025-09-03 15:11:57","url":"/agent-admin?ide_webview_request_time=1756883517386","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Trae/1.100.3 Chrome/132.0.6834.210 Electron/34.5.1 Safari/537.36"}
|
||||
{"ip":"::1","level":"error","message":"Error occurred: 路径 /@vite/client 未找到","method":"GET","service":"integrated-system","stack":"Error: 路径 /@vite/client 未找到\n at notFound (D:\\work\\客户\\毛总\\code\\jurong_circle_agent_backend\\middleware\\errorHandler.js:107:17)\n at Layer.handle [as handle_request] (D:\\work\\客户\\毛总\\code\\jurong_circle_agent_backend\\node_modules\\express\\lib\\router\\layer.js:95:5)\n at trim_prefix (D:\\work\\客户\\毛总\\code\\jurong_circle_agent_backend\\node_modules\\express\\lib\\router\\index.js:328:13)\n at D:\\work\\客户\\毛总\\code\\jurong_circle_agent_backend\\node_modules\\express\\lib\\router\\index.js:286:9\n at Function.process_params (D:\\work\\客户\\毛总\\code\\jurong_circle_agent_backend\\node_modules\\express\\lib\\router\\index.js:346:12)\n at next (D:\\work\\客户\\毛总\\code\\jurong_circle_agent_backend\\node_modules\\express\\lib\\router\\index.js:280:10)\n at D:\\work\\客户\\毛总\\code\\jurong_circle_agent_backend\\server.js:72:3\n at Layer.handle [as handle_request] (D:\\work\\客户\\毛总\\code\\jurong_circle_agent_backend\\node_modules\\express\\lib\\router\\layer.js:95:5)\n at trim_prefix (D:\\work\\客户\\毛总\\code\\jurong_circle_agent_backend\\node_modules\\express\\lib\\router\\index.js:328:13)\n at D:\\work\\客户\\毛总\\code\\jurong_circle_agent_backend\\node_modules\\express\\lib\\router\\index.js:286:9","timestamp":"2025-09-03 15:11:57","url":"/@vite/client","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Trae/1.100.3 Chrome/132.0.6834.210 Electron/34.5.1 Safari/537.36"}
|
||||
{"level":"error","message":"Error occurred: ENOENT: no such file or directory, stat 'D:\\work\\客户\\毛总\\code\\jurong_circle_agent_backend\\agent-admin\\dist\\index.html'","method":"GET","service":"integrated-system","stack":"Error: ENOENT: no such file or directory, stat 'D:\\work\\客户\\毛总\\code\\jurong_circle_agent_backend\\agent-admin\\dist\\index.html'","timestamp":"2025-09-03 15:13:56","url":"/agent-admin?ide_webview_request_time=1756883636612","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Trae/1.100.3 Chrome/132.0.6834.210 Electron/34.5.1 Safari/537.36"}
|
||||
{"ip":"::1","level":"error","message":"Error occurred: ENOENT: no such file or directory, stat 'D:\\work\\客户\\毛总\\code\\jurong_circle_agent_backend\\agent-admin\\dist\\index.html'","method":"GET","service":"integrated-system","stack":"Error: ENOENT: no such file or directory, stat 'D:\\work\\客户\\毛总\\code\\jurong_circle_agent_backend\\agent-admin\\dist\\index.html'","timestamp":"2025-09-03 15:13:59","url":"/agent-admin?ide_webview_request_time=1756883636612","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Trae/1.100.3 Chrome/132.0.6834.210 Electron/34.5.1 Safari/537.36"}
|
||||
{"ip":"::1","level":"error","message":"Error occurred: ENOENT: no such file or directory, stat 'D:\\work\\客户\\毛总\\code\\jurong_circle_agent_backend\\agent-admin\\dist\\index.html'","method":"GET","service":"integrated-system","stack":"Error: ENOENT: no such file or directory, stat 'D:\\work\\客户\\毛总\\code\\jurong_circle_agent_backend\\agent-admin\\dist\\index.html'","timestamp":"2025-09-03 15:13:59","url":"/agent-admin?ide_webview_request_time=1756883636612","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Trae/1.100.3 Chrome/132.0.6834.210 Electron/34.5.1 Safari/537.36"}
|
||||
{"ip":"::1","level":"error","message":"Error occurred: 路径 /@vite/client 未找到","method":"GET","service":"integrated-system","stack":"Error: 路径 /@vite/client 未找到\n at notFound (D:\\work\\客户\\毛总\\code\\jurong_circle_agent_backend\\middleware\\errorHandler.js:107:17)\n at Layer.handle [as handle_request] (D:\\work\\客户\\毛总\\code\\jurong_circle_agent_backend\\node_modules\\express\\lib\\router\\layer.js:95:5)\n at trim_prefix (D:\\work\\客户\\毛总\\code\\jurong_circle_agent_backend\\node_modules\\express\\lib\\router\\index.js:328:13)\n at D:\\work\\客户\\毛总\\code\\jurong_circle_agent_backend\\node_modules\\express\\lib\\router\\index.js:286:9\n at Function.process_params (D:\\work\\客户\\毛总\\code\\jurong_circle_agent_backend\\node_modules\\express\\lib\\router\\index.js:346:12)\n at next (D:\\work\\客户\\毛总\\code\\jurong_circle_agent_backend\\node_modules\\express\\lib\\router\\index.js:280:10)\n at D:\\work\\客户\\毛总\\code\\jurong_circle_agent_backend\\server.js:72:3\n at Layer.handle [as handle_request] (D:\\work\\客户\\毛总\\code\\jurong_circle_agent_backend\\node_modules\\express\\lib\\router\\layer.js:95:5)\n at trim_prefix (D:\\work\\客户\\毛总\\code\\jurong_circle_agent_backend\\node_modules\\express\\lib\\router\\index.js:328:13)\n at D:\\work\\客户\\毛总\\code\\jurong_circle_agent_backend\\node_modules\\express\\lib\\router\\index.js:286:9","timestamp":"2025-09-03 15:13:59","url":"/@vite/client","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Trae/1.100.3 Chrome/132.0.6834.210 Electron/34.5.1 Safari/537.36"}
|
||||
171
middleware/agentAuth.js
Normal file
171
middleware/agentAuth.js
Normal file
@@ -0,0 +1,171 @@
|
||||
const jwt = require('jsonwebtoken');
|
||||
const { getDB } = require('../database');
|
||||
const { logger } = require('../config/logger');
|
||||
|
||||
// JWT密钥
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'agent_jwt_secret_key_2024';
|
||||
|
||||
/**
|
||||
* 代理身份验证中间件
|
||||
* 验证JWT token并确保用户是激活的代理
|
||||
*/
|
||||
const agentAuth = async (req, res, next) => {
|
||||
try {
|
||||
const token = req.headers.authorization?.replace('Bearer ', '');
|
||||
|
||||
if (!token) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: '未提供认证令牌'
|
||||
});
|
||||
}
|
||||
|
||||
// 验证JWT token
|
||||
const decoded = jwt.verify(token, JWT_SECRET);
|
||||
|
||||
// 检查是否是代理角色
|
||||
if (decoded.role !== 'agent') {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: '权限不足,需要代理身份'
|
||||
});
|
||||
}
|
||||
|
||||
// 查询代理信息确认状态
|
||||
const [agents] = await getDB().execute(`
|
||||
SELECT
|
||||
ra.id as agent_id,
|
||||
ra.user_id,
|
||||
ra.agent_code,
|
||||
ra.status,
|
||||
ra.region_id,
|
||||
u.phone,
|
||||
u.real_name
|
||||
FROM regional_agents ra
|
||||
LEFT JOIN users u ON ra.user_id = u.id
|
||||
WHERE ra.id = ?
|
||||
`, [decoded.agentId]);
|
||||
|
||||
if (agents.length === 0) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: '代理账号不存在'
|
||||
});
|
||||
}
|
||||
|
||||
const agent = agents[0];
|
||||
|
||||
// 检查代理状态
|
||||
if (agent.status !== 'active') {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: '代理账号已被禁用或未激活'
|
||||
});
|
||||
}
|
||||
|
||||
// 将代理信息添加到请求对象中
|
||||
req.agent = {
|
||||
id: agent.agent_id,
|
||||
userId: agent.user_id,
|
||||
agentCode: agent.agent_code,
|
||||
regionId: agent.region_id,
|
||||
phone: agent.phone,
|
||||
realName: agent.real_name
|
||||
};
|
||||
|
||||
req.user = {
|
||||
id: agent.user_id,
|
||||
role: 'agent'
|
||||
};
|
||||
|
||||
next();
|
||||
|
||||
} catch (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: '认证令牌已过期,请重新登录'
|
||||
});
|
||||
}
|
||||
|
||||
logger.error('代理身份验证失败', {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
ip: req.ip
|
||||
});
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '身份验证失败'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 可选的代理身份验证中间件
|
||||
* 如果提供了token则验证,否则继续执行
|
||||
*/
|
||||
const optionalAgentAuth = async (req, res, next) => {
|
||||
try {
|
||||
const token = req.headers.authorization?.replace('Bearer ', '');
|
||||
|
||||
if (!token) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// 验证JWT token
|
||||
const decoded = jwt.verify(token, JWT_SECRET);
|
||||
|
||||
if (decoded.role === 'agent') {
|
||||
// 查询代理信息
|
||||
const [agents] = await getDB().execute(`
|
||||
SELECT
|
||||
ra.id as agent_id,
|
||||
ra.user_id,
|
||||
ra.agent_code,
|
||||
ra.status,
|
||||
ra.region_id,
|
||||
u.phone,
|
||||
u.real_name
|
||||
FROM regional_agents ra
|
||||
LEFT JOIN users u ON ra.user_id = u.id
|
||||
WHERE ra.id = ? AND ra.status = 'active'
|
||||
`, [decoded.agentId]);
|
||||
|
||||
if (agents.length > 0) {
|
||||
const agent = agents[0];
|
||||
req.agent = {
|
||||
id: agent.agent_id,
|
||||
userId: agent.user_id,
|
||||
agentCode: agent.agent_code,
|
||||
regionId: agent.region_id,
|
||||
phone: agent.phone,
|
||||
realName: agent.real_name
|
||||
};
|
||||
|
||||
req.user = {
|
||||
id: agent.user_id,
|
||||
role: 'agent'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
next();
|
||||
|
||||
} catch (error) {
|
||||
// 可选验证失败时不阻止请求继续
|
||||
next();
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
agentAuth,
|
||||
optionalAgentAuth
|
||||
};
|
||||
112
middleware/auth.js
Normal file
112
middleware/auth.js
Normal file
@@ -0,0 +1,112 @@
|
||||
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') {
|
||||
console.log(11111);
|
||||
|
||||
return res.status(200).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
|
||||
};
|
||||
3750
package-lock.json
generated
Normal file
3750
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
48
package.json
Normal file
48
package.json
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"name": "jurong-circle-agent-backend",
|
||||
"version": "1.0.0",
|
||||
"description": "炬融圈代理后台API服务",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"dev": "nodemon server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@alicloud/dysmsapi20170525": "^4.1.2",
|
||||
"@alicloud/openapi-client": "^0.4.15",
|
||||
"axios": "^1.11.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"body-parser": "^1.20.2",
|
||||
"cors": "^2.8.5",
|
||||
"crypto": "^1.0.1",
|
||||
"dayjs": "^1.11.13",
|
||||
"dotenv": "^17.2.1",
|
||||
"express": "^4.18.2",
|
||||
"express-rate-limit": "^7.1.5",
|
||||
"express-validator": "^7.2.1",
|
||||
"helmet": "^8.1.0",
|
||||
"joi": "^17.13.3",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"minio": "^8.0.5",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"mysql2": "^3.14.3",
|
||||
"node-cron": "^4.2.1",
|
||||
"qrcode": "^1.5.4",
|
||||
"swagger-jsdoc": "^6.2.8",
|
||||
"swagger-ui-express": "^5.0.1",
|
||||
"winston": "^3.17.0",
|
||||
"xml2js": "^0.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"concurrently": "^8.2.2",
|
||||
"nodemon": "^3.0.2"
|
||||
},
|
||||
"keywords": [
|
||||
"vue3",
|
||||
"nodejs",
|
||||
"express",
|
||||
"mysql",
|
||||
"element-plus"
|
||||
],
|
||||
"author": "",
|
||||
"license": "MIT"
|
||||
}
|
||||
294
routes/agent.js
Normal file
294
routes/agent.js
Normal file
@@ -0,0 +1,294 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { getDB } = require('../database');
|
||||
const { agentAuth } = require('../middleware/agentAuth');
|
||||
const { logger } = require('../config/logger');
|
||||
|
||||
/**
|
||||
* 获取代理统计数据
|
||||
* GET /api/agent/stats
|
||||
*/
|
||||
router.get('/stats', agentAuth, async (req, res) => {
|
||||
try {
|
||||
const agentId = req.agent.id;
|
||||
|
||||
// 获取下级用户统计
|
||||
const [userStats] = await getDB().execute(`
|
||||
SELECT
|
||||
COUNT(*) as total_users,
|
||||
COUNT(CASE WHEN DATE(created_at) = CURDATE() THEN 1 END) as today_new_users,
|
||||
COUNT(CASE WHEN last_login_at >= DATE_SUB(NOW(), INTERVAL 7 DAY) THEN 1 END) as active_users,
|
||||
CAST(COALESCE(SUM(balance), 0) AS DECIMAL(10,2)) as total_balance
|
||||
FROM agent_merchants
|
||||
WHERE agent_id = ?
|
||||
`, [agentId]);
|
||||
|
||||
// 获取佣金统计
|
||||
const [commissionStats] = await getDB().execute(`
|
||||
SELECT
|
||||
CAST(COALESCE(SUM(commission_amount), 0) AS DECIMAL(10,2)) as total_commission,
|
||||
CAST(COALESCE(SUM(CASE WHEN DATE(created_at) = CURDATE() THEN commission_amount ELSE 0 END), 0) AS DECIMAL(10,2)) as today_commission,
|
||||
CAST(COALESCE(SUM(CASE WHEN status = 'paid' THEN commission_amount ELSE 0 END), 0) AS DECIMAL(10,2)) as paid_commission,
|
||||
CAST(COALESCE(SUM(CASE WHEN status = 'pending' THEN commission_amount ELSE 0 END), 0) AS DECIMAL(10,2)) as pending_commission
|
||||
FROM agent_commission_records
|
||||
WHERE agent_id = ?
|
||||
`, [agentId]);
|
||||
|
||||
// 获取转账统计
|
||||
const [transferStats] = await getDB().execute(`
|
||||
SELECT
|
||||
COUNT(*) as total_transfers,
|
||||
COUNT(CASE WHEN DATE(created_at) = CURDATE() THEN 1 END) as today_transfers,
|
||||
CAST(COALESCE(SUM(amount), 0) AS DECIMAL(10,2)) as total_amount,
|
||||
CAST(COALESCE(SUM(CASE WHEN DATE(created_at) = CURDATE() THEN amount ELSE 0 END), 0) AS DECIMAL(10,2)) as today_amount
|
||||
FROM transfers t
|
||||
INNER JOIN agent_merchants am ON (t.from_user_id = am.merchant_id OR t.to_user_id = am.merchant_id)
|
||||
WHERE am.agent_id = ?
|
||||
`, [agentId]);
|
||||
|
||||
const stats = {
|
||||
users: userStats[0] || {
|
||||
total_users: 0,
|
||||
today_new_users: 0,
|
||||
active_users: 0,
|
||||
total_balance: '0.00'
|
||||
},
|
||||
commissions: commissionStats[0] || {
|
||||
total_commission: '0.00',
|
||||
today_commission: '0.00',
|
||||
paid_commission: '0.00',
|
||||
pending_commission: '0.00'
|
||||
},
|
||||
transfers: transferStats[0] || {
|
||||
total_transfers: 0,
|
||||
today_transfers: 0,
|
||||
total_amount: '0.00',
|
||||
today_amount: '0.00'
|
||||
}
|
||||
};
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: stats
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('获取代理统计数据失败', {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
agentId: req.agent?.id
|
||||
});
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取统计数据失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取用户增长趋势数据
|
||||
* GET /api/agent/user-growth-trend
|
||||
*/
|
||||
router.get('/user-growth-trend', agentAuth, async (req, res) => {
|
||||
try {
|
||||
const agentId = req.agent.id;
|
||||
const { days = 7 } = req.query;
|
||||
|
||||
const [trendData] = await getDB().execute(`
|
||||
SELECT
|
||||
DATE(created_at) as date,
|
||||
COUNT(*) as count
|
||||
FROM agent_merchants
|
||||
WHERE agent_id = ?
|
||||
AND created_at >= DATE_SUB(CURDATE(), INTERVAL ? DAY)
|
||||
GROUP BY DATE(created_at)
|
||||
ORDER BY date ASC
|
||||
`, [agentId, parseInt(days)]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: trendData
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('获取用户增长趋势失败', {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
agentId: req.agent?.id
|
||||
});
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取趋势数据失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取佣金收入趋势数据
|
||||
* GET /api/agent/commission-trend
|
||||
*/
|
||||
router.get('/commission-trend', agentAuth, async (req, res) => {
|
||||
try {
|
||||
const agentId = req.agent.id;
|
||||
const { days = 7 } = req.query;
|
||||
|
||||
const [trendData] = await getDB().execute(`
|
||||
SELECT
|
||||
DATE(created_at) as date,
|
||||
CAST(COALESCE(SUM(commission_amount), 0) AS DECIMAL(10,2)) as amount
|
||||
FROM agent_commission_records
|
||||
WHERE agent_id = ?
|
||||
AND created_at >= DATE_SUB(CURDATE(), INTERVAL ? DAY)
|
||||
GROUP BY DATE(created_at)
|
||||
ORDER BY date ASC
|
||||
`, [agentId, parseInt(days)]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: trendData
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('获取佣金趋势失败', {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
agentId: req.agent?.id
|
||||
});
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取趋势数据失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取佣金类型分布数据
|
||||
* GET /api/agent/commission-distribution
|
||||
*/
|
||||
router.get('/commission-distribution', agentAuth, async (req, res) => {
|
||||
try {
|
||||
const agentId = req.agent.id;
|
||||
|
||||
const [distributionData] = await getDB().execute(`
|
||||
SELECT
|
||||
commission_type as type,
|
||||
COUNT(*) as count,
|
||||
CAST(COALESCE(SUM(commission_amount), 0) AS DECIMAL(10,2)) as amount
|
||||
FROM agent_commission_records
|
||||
WHERE agent_id = ?
|
||||
GROUP BY commission_type
|
||||
ORDER BY amount DESC
|
||||
`, [agentId]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: distributionData
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('获取佣金分布失败', {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
agentId: req.agent?.id
|
||||
});
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取分布数据失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取最新下级用户
|
||||
* GET /api/agent/recent-users
|
||||
*/
|
||||
router.get('/recent-users', agentAuth, async (req, res) => {
|
||||
try {
|
||||
const agentId = req.agent.id;
|
||||
const { limit = 10 } = req.query;
|
||||
|
||||
const [recentUsers] = await getDB().execute(`
|
||||
SELECT
|
||||
u.id,
|
||||
u.username,
|
||||
u.real_name,
|
||||
u.phone,
|
||||
u.avatar,
|
||||
u.balance,
|
||||
u.created_at,
|
||||
am.created_at as join_date
|
||||
FROM agent_merchants am
|
||||
LEFT JOIN users u ON am.merchant_id = u.id
|
||||
WHERE am.agent_id = ?
|
||||
ORDER BY am.created_at DESC
|
||||
LIMIT ?
|
||||
`, [agentId, parseInt(limit)]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: recentUsers
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('获取最新用户失败', {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
agentId: req.agent?.id
|
||||
});
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取最新用户失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取最新佣金记录
|
||||
* GET /api/agent/recent-commissions
|
||||
*/
|
||||
router.get('/recent-commissions', agentAuth, async (req, res) => {
|
||||
try {
|
||||
const agentId = req.agent.id;
|
||||
const { limit = 10 } = req.query;
|
||||
|
||||
const [recentCommissions] = await getDB().execute(`
|
||||
SELECT
|
||||
acr.id,
|
||||
acr.commission_type,
|
||||
acr.commission_amount,
|
||||
acr.status,
|
||||
acr.created_at,
|
||||
u.username,
|
||||
u.real_name
|
||||
FROM agent_commission_records acr
|
||||
LEFT JOIN users u ON acr.merchant_id = u.id
|
||||
WHERE acr.agent_id = ?
|
||||
ORDER BY acr.created_at DESC
|
||||
LIMIT ?
|
||||
`, [agentId, parseInt(limit)]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: recentCommissions
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('获取最新佣金失败', {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
agentId: req.agent?.id
|
||||
});
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取最新佣金失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
226
routes/auth.js
Normal file
226
routes/auth.js
Normal file
@@ -0,0 +1,226 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const bcrypt = require('bcryptjs');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const { getDB } = require('../database');
|
||||
const { logger } = require('../config/logger');
|
||||
|
||||
// JWT密钥
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'agent_jwt_secret_key_2024';
|
||||
|
||||
/**
|
||||
* 代理登录
|
||||
* POST /api/auth/login
|
||||
*/
|
||||
router.post('/login', async (req, res) => {
|
||||
try {
|
||||
const { phone, password } = req.body;
|
||||
|
||||
if (!phone || !password) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '请输入手机号和密码'
|
||||
});
|
||||
}
|
||||
|
||||
// 查询代理信息
|
||||
const [agents] = await getDB().execute(`
|
||||
SELECT
|
||||
ra.id as agent_id,
|
||||
ra.user_id,
|
||||
ra.agent_code,
|
||||
ra.status as agent_status,
|
||||
ra.region_id,
|
||||
u.id as user_id,
|
||||
u.username,
|
||||
u.phone,
|
||||
u.password,
|
||||
u.real_name,
|
||||
u.avatar,
|
||||
zr.city_name,
|
||||
zr.district_name
|
||||
FROM regional_agents ra
|
||||
LEFT JOIN users u ON ra.user_id = u.id
|
||||
LEFT JOIN zhejiang_regions zr ON ra.region_id = zr.id
|
||||
WHERE u.phone = ? AND ra.status = 'active'
|
||||
`, [phone]);
|
||||
|
||||
if (agents.length === 0) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: '手机号不存在或代理账号未激活'
|
||||
});
|
||||
}
|
||||
|
||||
const agent = agents[0];
|
||||
|
||||
// 验证密码
|
||||
const isPasswordValid = await bcrypt.compare(password, agent.password);
|
||||
if (!isPasswordValid) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: '密码错误'
|
||||
});
|
||||
}
|
||||
|
||||
// 生成JWT token
|
||||
const token = jwt.sign(
|
||||
{
|
||||
userId: agent.user_id,
|
||||
agentId: agent.agent_id,
|
||||
phone: agent.phone,
|
||||
role: 'agent'
|
||||
},
|
||||
JWT_SECRET,
|
||||
{ expiresIn: '24h' }
|
||||
);
|
||||
|
||||
// 记录登录日志
|
||||
logger.info('代理登录成功', {
|
||||
agentId: agent.agent_id,
|
||||
phone: agent.phone,
|
||||
ip: req.ip
|
||||
});
|
||||
|
||||
// 返回登录成功信息
|
||||
res.json({
|
||||
success: true,
|
||||
message: '登录成功',
|
||||
data: {
|
||||
token,
|
||||
agent: {
|
||||
id: agent.agent_id,
|
||||
userId: agent.user_id,
|
||||
agentCode: agent.agent_code,
|
||||
phone: agent.phone,
|
||||
realName: agent.real_name,
|
||||
avatar: agent.avatar,
|
||||
region: {
|
||||
id: agent.region_id,
|
||||
cityName: agent.city_name,
|
||||
districtName: agent.district_name
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('代理登录失败', {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
ip: req.ip
|
||||
});
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '登录失败,请稍后重试'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取当前代理信息
|
||||
* GET /api/auth/me
|
||||
*/
|
||||
router.get('/me', async (req, res) => {
|
||||
try {
|
||||
const token = req.headers.authorization?.replace('Bearer ', '');
|
||||
|
||||
if (!token) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: '未提供认证令牌'
|
||||
});
|
||||
}
|
||||
|
||||
// 验证token
|
||||
const decoded = jwt.verify(token, JWT_SECRET);
|
||||
|
||||
// 查询代理信息
|
||||
const [agents] = await getDB().execute(`
|
||||
SELECT
|
||||
ra.id as agent_id,
|
||||
ra.user_id,
|
||||
ra.agent_code,
|
||||
ra.status as agent_status,
|
||||
ra.region_id,
|
||||
u.phone,
|
||||
u.real_name,
|
||||
u.avatar,
|
||||
zr.city_name,
|
||||
zr.district_name
|
||||
FROM regional_agents ra
|
||||
LEFT JOIN users u ON ra.user_id = u.id
|
||||
LEFT JOIN zhejiang_regions zr ON ra.region_id = zr.id
|
||||
WHERE ra.id = ? AND ra.status = 'active'
|
||||
`, [decoded.agentId]);
|
||||
|
||||
if (agents.length === 0) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: '代理账号不存在或已被禁用'
|
||||
});
|
||||
}
|
||||
|
||||
const agent = agents[0];
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
agent: {
|
||||
id: agent.agent_id,
|
||||
userId: agent.user_id,
|
||||
agentCode: agent.agent_code,
|
||||
phone: agent.phone,
|
||||
realName: agent.real_name,
|
||||
avatar: agent.avatar,
|
||||
region: {
|
||||
id: agent.region_id,
|
||||
cityName: agent.city_name,
|
||||
districtName: agent.district_name
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
} catch (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: '认证令牌已过期'
|
||||
});
|
||||
}
|
||||
|
||||
logger.error('获取代理信息失败', {
|
||||
error: error.message,
|
||||
stack: error.stack
|
||||
});
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取用户信息失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 代理登出
|
||||
* POST /api/auth/logout
|
||||
*/
|
||||
router.post('/logout', (req, res) => {
|
||||
// 由于使用JWT,登出主要在前端处理(删除token)
|
||||
// 这里只是提供一个标准的登出接口
|
||||
res.json({
|
||||
success: true,
|
||||
message: '登出成功'
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
540
routes/commissions.js
Normal file
540
routes/commissions.js
Normal file
@@ -0,0 +1,540 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { getDB } = require('../database');
|
||||
const { agentAuth } = require('../middleware/agentAuth');
|
||||
const { logger } = require('../config/logger');
|
||||
|
||||
/**
|
||||
* 获取代理佣金记录列表
|
||||
* GET /api/commissions
|
||||
*/
|
||||
router.get('/', agentAuth, async (req, res) => {
|
||||
try {
|
||||
const agentId = req.agent.id;
|
||||
const {
|
||||
page = 1,
|
||||
limit = 20,
|
||||
search,
|
||||
status,
|
||||
commission_type,
|
||||
start_date,
|
||||
end_date,
|
||||
min_amount,
|
||||
max_amount,
|
||||
sort_by = 'created_at',
|
||||
sort_order = 'desc'
|
||||
} = req.query;
|
||||
|
||||
const pageNum = parseInt(page) || 1;
|
||||
const limitNum = parseInt(limit) || 20;
|
||||
const offset = (pageNum - 1) * limitNum;
|
||||
|
||||
// 构建查询条件
|
||||
let whereConditions = ['acr.agent_id = ?'];
|
||||
let queryParams = [agentId];
|
||||
|
||||
if (search) {
|
||||
whereConditions.push('(u.username LIKE ? OR u.real_name LIKE ? OR u.phone LIKE ?)');
|
||||
queryParams.push(`%${search}%`, `%${search}%`, `%${search}%`);
|
||||
}
|
||||
|
||||
if (status) {
|
||||
whereConditions.push('acr.status = ?');
|
||||
queryParams.push(status);
|
||||
}
|
||||
|
||||
if (commission_type) {
|
||||
whereConditions.push('acr.commission_type = ?');
|
||||
queryParams.push(commission_type);
|
||||
}
|
||||
|
||||
if (start_date) {
|
||||
whereConditions.push('DATE(acr.created_at) >= ?');
|
||||
queryParams.push(start_date);
|
||||
}
|
||||
|
||||
if (end_date) {
|
||||
whereConditions.push('DATE(acr.created_at) <= ?');
|
||||
queryParams.push(end_date);
|
||||
}
|
||||
|
||||
if (min_amount) {
|
||||
whereConditions.push('acr.commission_amount >= ?');
|
||||
queryParams.push(parseFloat(min_amount));
|
||||
}
|
||||
|
||||
if (max_amount) {
|
||||
whereConditions.push('acr.commission_amount <= ?');
|
||||
queryParams.push(parseFloat(max_amount));
|
||||
}
|
||||
|
||||
const whereClause = whereConditions.join(' AND ');
|
||||
|
||||
// 验证排序字段
|
||||
const allowedSortFields = ['created_at', 'commission_amount', 'status', 'commission_type'];
|
||||
const sortBy = allowedSortFields.includes(sort_by) ? sort_by : 'created_at';
|
||||
const sortOrder = sort_order.toLowerCase() === 'asc' ? 'ASC' : 'DESC';
|
||||
|
||||
// 查询佣金记录列表
|
||||
const commissionsQuery = `
|
||||
SELECT
|
||||
acr.id,
|
||||
acr.agent_id,
|
||||
acr.merchant_id,
|
||||
acr.commission_type,
|
||||
acr.commission_amount,
|
||||
acr.status,
|
||||
acr.description,
|
||||
acr.reference_id,
|
||||
acr.created_at,
|
||||
acr.updated_at,
|
||||
acr.paid_at,
|
||||
u.username,
|
||||
u.real_name,
|
||||
u.phone,
|
||||
u.avatar
|
||||
FROM agent_commission_records acr
|
||||
LEFT JOIN users u ON acr.merchant_id = u.id
|
||||
WHERE ${whereClause}
|
||||
ORDER BY acr.${sortBy} ${sortOrder}
|
||||
LIMIT ${limitNum} OFFSET ${offset}
|
||||
`;
|
||||
|
||||
const [commissions] = await getDB().execute(commissionsQuery, queryParams);
|
||||
|
||||
// 查询总数
|
||||
const countQuery = `
|
||||
SELECT COUNT(*) as total
|
||||
FROM agent_commission_records acr
|
||||
LEFT JOIN users u ON acr.merchant_id = u.id
|
||||
WHERE ${whereClause}
|
||||
`;
|
||||
|
||||
const [countResult] = await getDB().execute(countQuery, queryParams);
|
||||
const total = countResult[0]?.total || 0;
|
||||
|
||||
// 查询统计信息
|
||||
const [statsResult] = await getDB().execute(`
|
||||
SELECT
|
||||
COUNT(*) as total_commissions,
|
||||
COUNT(CASE WHEN status = 'pending' THEN 1 END) as pending_commissions,
|
||||
COUNT(CASE WHEN status = 'paid' THEN 1 END) as paid_commissions,
|
||||
CAST(COALESCE(SUM(commission_amount), 0) AS DECIMAL(10,2)) as total_amount,
|
||||
CAST(COALESCE(SUM(CASE WHEN status = 'pending' THEN commission_amount ELSE 0 END), 0) AS DECIMAL(10,2)) as pending_amount,
|
||||
CAST(COALESCE(SUM(CASE WHEN status = 'paid' THEN commission_amount ELSE 0 END), 0) AS DECIMAL(10,2)) as paid_amount,
|
||||
CAST(COALESCE(SUM(CASE WHEN DATE(created_at) = CURDATE() THEN commission_amount ELSE 0 END), 0) AS DECIMAL(10,2)) as today_amount
|
||||
FROM agent_commission_records
|
||||
WHERE agent_id = ?
|
||||
`, [agentId]);
|
||||
|
||||
const stats = statsResult[0] || {
|
||||
total_commissions: 0,
|
||||
pending_commissions: 0,
|
||||
paid_commissions: 0,
|
||||
total_amount: '0.00',
|
||||
pending_amount: '0.00',
|
||||
paid_amount: '0.00',
|
||||
today_amount: '0.00'
|
||||
};
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
commissions,
|
||||
pagination: {
|
||||
current_page: pageNum,
|
||||
per_page: limitNum,
|
||||
total,
|
||||
total_pages: Math.ceil(total / limitNum)
|
||||
},
|
||||
stats
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('获取佣金记录失败', {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
agentId: req.agent?.id
|
||||
});
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取佣金记录失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取单个佣金记录详情
|
||||
* GET /api/commissions/:id
|
||||
*/
|
||||
router.get('/:id', agentAuth, async (req, res) => {
|
||||
try {
|
||||
const agentId = req.agent.id;
|
||||
const commissionId = req.params.id;
|
||||
|
||||
// 查询佣金记录详情
|
||||
const [commissions] = await getDB().execute(`
|
||||
SELECT
|
||||
acr.id,
|
||||
acr.agent_id,
|
||||
acr.merchant_id,
|
||||
acr.commission_type,
|
||||
acr.commission_amount,
|
||||
acr.status,
|
||||
acr.description,
|
||||
acr.reference_id,
|
||||
acr.created_at,
|
||||
acr.updated_at,
|
||||
acr.paid_at,
|
||||
u.username,
|
||||
u.real_name,
|
||||
u.phone,
|
||||
u.avatar,
|
||||
u.city,
|
||||
u.district
|
||||
FROM agent_commission_records acr
|
||||
LEFT JOIN users u ON acr.merchant_id = u.id
|
||||
WHERE acr.id = ? AND acr.agent_id = ?
|
||||
`, [commissionId, agentId]);
|
||||
|
||||
if (commissions.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '佣金记录不存在或无权限查看'
|
||||
});
|
||||
}
|
||||
|
||||
const commission = commissions[0];
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: commission
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('获取佣金记录详情失败', {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
agentId: req.agent?.id,
|
||||
commissionId: req.params.id
|
||||
});
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取佣金记录详情失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 申请佣金发放(单个)
|
||||
* POST /api/commissions/:id/request-payment
|
||||
*/
|
||||
router.post('/:id/request-payment', agentAuth, async (req, res) => {
|
||||
try {
|
||||
const agentId = req.agent.id;
|
||||
const commissionId = req.params.id;
|
||||
|
||||
// 检查佣金记录是否存在且属于当前代理
|
||||
const [commissions] = await getDB().execute(`
|
||||
SELECT id, status, commission_amount
|
||||
FROM agent_commission_records
|
||||
WHERE id = ? AND agent_id = ?
|
||||
`, [commissionId, agentId]);
|
||||
|
||||
if (commissions.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '佣金记录不存在或无权限操作'
|
||||
});
|
||||
}
|
||||
|
||||
const commission = commissions[0];
|
||||
|
||||
if (commission.status !== 'pending') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '只能申请待发放状态的佣金'
|
||||
});
|
||||
}
|
||||
|
||||
// 更新佣金状态为申请中
|
||||
await getDB().execute(`
|
||||
UPDATE agent_commission_records
|
||||
SET status = 'requested', updated_at = NOW()
|
||||
WHERE id = ?
|
||||
`, [commissionId]);
|
||||
|
||||
logger.info('代理申请佣金发放', {
|
||||
agentId,
|
||||
commissionId,
|
||||
amount: commission.commission_amount
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '佣金发放申请已提交,请等待审核'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('申请佣金发放失败', {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
agentId: req.agent?.id,
|
||||
commissionId: req.params.id
|
||||
});
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '申请佣金发放失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 批量申请佣金发放
|
||||
* POST /api/commissions/batch-request-payment
|
||||
*/
|
||||
router.post('/batch-request-payment', agentAuth, async (req, res) => {
|
||||
try {
|
||||
const agentId = req.agent.id;
|
||||
const { commission_ids } = req.body;
|
||||
|
||||
if (!commission_ids || !Array.isArray(commission_ids) || commission_ids.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '请选择要申请发放的佣金记录'
|
||||
});
|
||||
}
|
||||
|
||||
// 检查所有佣金记录是否存在且属于当前代理
|
||||
const placeholders = commission_ids.map(() => '?').join(',');
|
||||
const [commissions] = await getDB().execute(`
|
||||
SELECT id, status, commission_amount
|
||||
FROM agent_commission_records
|
||||
WHERE id IN (${placeholders}) AND agent_id = ?
|
||||
`, [...commission_ids, agentId]);
|
||||
|
||||
if (commissions.length !== commission_ids.length) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '部分佣金记录不存在或无权限操作'
|
||||
});
|
||||
}
|
||||
|
||||
// 检查状态
|
||||
const invalidCommissions = commissions.filter(c => c.status !== 'pending');
|
||||
if (invalidCommissions.length > 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '只能申请待发放状态的佣金'
|
||||
});
|
||||
}
|
||||
|
||||
// 批量更新状态
|
||||
await getDB().execute(`
|
||||
UPDATE agent_commission_records
|
||||
SET status = 'requested', updated_at = NOW()
|
||||
WHERE id IN (${placeholders}) AND agent_id = ?
|
||||
`, [...commission_ids, agentId]);
|
||||
|
||||
const totalAmount = commissions.reduce((sum, c) => sum + parseFloat(c.commission_amount), 0);
|
||||
|
||||
logger.info('代理批量申请佣金发放', {
|
||||
agentId,
|
||||
commissionIds: commission_ids,
|
||||
count: commission_ids.length,
|
||||
totalAmount
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `已提交${commission_ids.length}条佣金发放申请,总金额${totalAmount.toFixed(2)}元`
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('批量申请佣金发放失败', {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
agentId: req.agent?.id
|
||||
});
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '批量申请佣金发放失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取佣金趋势数据
|
||||
* GET /api/commissions/trend
|
||||
*/
|
||||
router.get('/trend/data', agentAuth, async (req, res) => {
|
||||
try {
|
||||
const agentId = req.agent.id;
|
||||
const { days = 7, type = 'amount' } = req.query;
|
||||
|
||||
let selectField = 'CAST(COALESCE(SUM(commission_amount), 0) AS DECIMAL(10,2)) as value';
|
||||
if (type === 'count') {
|
||||
selectField = 'COUNT(*) as value';
|
||||
}
|
||||
|
||||
const [trendData] = await getDB().execute(`
|
||||
SELECT
|
||||
DATE(created_at) as date,
|
||||
${selectField}
|
||||
FROM agent_commission_records
|
||||
WHERE agent_id = ?
|
||||
AND created_at >= DATE_SUB(CURDATE(), INTERVAL ? DAY)
|
||||
GROUP BY DATE(created_at)
|
||||
ORDER BY date ASC
|
||||
`, [agentId, parseInt(days)]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: trendData
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('获取佣金趋势失败', {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
agentId: req.agent?.id
|
||||
});
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取佣金趋势失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 导出佣金记录
|
||||
* GET /api/commissions/export
|
||||
*/
|
||||
router.get('/export/data', agentAuth, async (req, res) => {
|
||||
try {
|
||||
const agentId = req.agent.id;
|
||||
const {
|
||||
format = 'json',
|
||||
search,
|
||||
status,
|
||||
commission_type,
|
||||
start_date,
|
||||
end_date,
|
||||
min_amount,
|
||||
max_amount
|
||||
} = req.query;
|
||||
|
||||
// 构建查询条件
|
||||
let whereConditions = ['acr.agent_id = ?'];
|
||||
let queryParams = [agentId];
|
||||
|
||||
if (search) {
|
||||
whereConditions.push('(u.username LIKE ? OR u.real_name LIKE ? OR u.phone LIKE ?)');
|
||||
queryParams.push(`%${search}%`, `%${search}%`, `%${search}%`);
|
||||
}
|
||||
|
||||
if (status) {
|
||||
whereConditions.push('acr.status = ?');
|
||||
queryParams.push(status);
|
||||
}
|
||||
|
||||
if (commission_type) {
|
||||
whereConditions.push('acr.commission_type = ?');
|
||||
queryParams.push(commission_type);
|
||||
}
|
||||
|
||||
if (start_date) {
|
||||
whereConditions.push('DATE(acr.created_at) >= ?');
|
||||
queryParams.push(start_date);
|
||||
}
|
||||
|
||||
if (end_date) {
|
||||
whereConditions.push('DATE(acr.created_at) <= ?');
|
||||
queryParams.push(end_date);
|
||||
}
|
||||
|
||||
if (min_amount) {
|
||||
whereConditions.push('acr.commission_amount >= ?');
|
||||
queryParams.push(parseFloat(min_amount));
|
||||
}
|
||||
|
||||
if (max_amount) {
|
||||
whereConditions.push('acr.commission_amount <= ?');
|
||||
queryParams.push(parseFloat(max_amount));
|
||||
}
|
||||
|
||||
const whereClause = whereConditions.join(' AND ');
|
||||
|
||||
// 查询佣金记录
|
||||
const [commissions] = await getDB().execute(`
|
||||
SELECT
|
||||
acr.id,
|
||||
acr.commission_type,
|
||||
acr.commission_amount,
|
||||
acr.status,
|
||||
acr.description,
|
||||
acr.reference_id,
|
||||
acr.created_at,
|
||||
acr.paid_at,
|
||||
u.username,
|
||||
u.real_name,
|
||||
u.phone
|
||||
FROM agent_commission_records acr
|
||||
LEFT JOIN users u ON acr.merchant_id = u.id
|
||||
WHERE ${whereClause}
|
||||
ORDER BY acr.created_at DESC
|
||||
`, queryParams);
|
||||
|
||||
if (format === 'csv') {
|
||||
// 生成CSV格式
|
||||
const csvHeader = 'ID,佣金类型,佣金金额,状态,描述,关联ID,用户名,真实姓名,手机号,创建时间,发放时间\n';
|
||||
const csvData = commissions.map(commission => {
|
||||
return [
|
||||
commission.id,
|
||||
commission.commission_type || '',
|
||||
commission.commission_amount,
|
||||
commission.status || '',
|
||||
(commission.description || '').replace(/,/g, ','), // 替换逗号避免CSV格式问题
|
||||
commission.reference_id || '',
|
||||
commission.username || '',
|
||||
commission.real_name || '',
|
||||
commission.phone || '',
|
||||
commission.created_at || '',
|
||||
commission.paid_at || ''
|
||||
].join(',');
|
||||
}).join('\n');
|
||||
|
||||
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
|
||||
res.setHeader('Content-Disposition', `attachment; filename="commissions_${Date.now()}.csv"`);
|
||||
res.send(csvHeader + csvData);
|
||||
} else {
|
||||
// 默认JSON格式
|
||||
res.json({
|
||||
success: true,
|
||||
data: commissions,
|
||||
exported_at: new Date().toISOString(),
|
||||
total: commissions.length
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
logger.error('导出佣金记录失败', {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
agentId: req.agent?.id
|
||||
});
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '导出佣金记录失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
434
routes/transfers.js
Normal file
434
routes/transfers.js
Normal file
@@ -0,0 +1,434 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { getDB } = require('../database');
|
||||
const { agentAuth } = require('../middleware/agentAuth');
|
||||
const { logger } = require('../config/logger');
|
||||
|
||||
/**
|
||||
* 获取代理下级用户转账记录列表
|
||||
* GET /api/transfers
|
||||
*/
|
||||
router.get('/', agentAuth, async (req, res) => {
|
||||
try {
|
||||
const agentId = req.agent.id;
|
||||
const {
|
||||
page = 1,
|
||||
limit = 20,
|
||||
search,
|
||||
status,
|
||||
type,
|
||||
start_date,
|
||||
end_date,
|
||||
min_amount,
|
||||
max_amount,
|
||||
sort_by = 'created_at',
|
||||
sort_order = 'desc'
|
||||
} = req.query;
|
||||
|
||||
const pageNum = parseInt(page) || 1;
|
||||
const limitNum = parseInt(limit) || 20;
|
||||
const offset = (pageNum - 1) * limitNum;
|
||||
|
||||
// 构建查询条件
|
||||
let whereConditions = [
|
||||
'(am1.agent_id = ? OR am2.agent_id = ?)' // 转出方或转入方属于当前代理
|
||||
];
|
||||
let queryParams = [agentId, agentId];
|
||||
|
||||
if (search) {
|
||||
whereConditions.push('(u1.username LIKE ? OR u1.real_name LIKE ? OR u1.phone LIKE ? OR u2.username LIKE ? OR u2.real_name LIKE ? OR u2.phone LIKE ?)');
|
||||
queryParams.push(`%${search}%`, `%${search}%`, `%${search}%`, `%${search}%`, `%${search}%`, `%${search}%`);
|
||||
}
|
||||
|
||||
if (status) {
|
||||
whereConditions.push('t.status = ?');
|
||||
queryParams.push(status);
|
||||
}
|
||||
|
||||
if (type) {
|
||||
whereConditions.push('t.type = ?');
|
||||
queryParams.push(type);
|
||||
}
|
||||
|
||||
if (start_date) {
|
||||
whereConditions.push('DATE(t.created_at) >= ?');
|
||||
queryParams.push(start_date);
|
||||
}
|
||||
|
||||
if (end_date) {
|
||||
whereConditions.push('DATE(t.created_at) <= ?');
|
||||
queryParams.push(end_date);
|
||||
}
|
||||
|
||||
if (min_amount) {
|
||||
whereConditions.push('t.amount >= ?');
|
||||
queryParams.push(parseFloat(min_amount));
|
||||
}
|
||||
|
||||
if (max_amount) {
|
||||
whereConditions.push('t.amount <= ?');
|
||||
queryParams.push(parseFloat(max_amount));
|
||||
}
|
||||
|
||||
const whereClause = whereConditions.join(' AND ');
|
||||
|
||||
// 验证排序字段
|
||||
const allowedSortFields = ['created_at', 'amount', 'status'];
|
||||
const sortBy = allowedSortFields.includes(sort_by) ? sort_by : 'created_at';
|
||||
const sortOrder = sort_order.toLowerCase() === 'asc' ? 'ASC' : 'DESC';
|
||||
|
||||
// 查询转账记录列表
|
||||
const transfersQuery = `
|
||||
SELECT
|
||||
t.id,
|
||||
t.from_user_id,
|
||||
t.to_user_id,
|
||||
t.amount,
|
||||
t.type,
|
||||
t.status,
|
||||
t.description,
|
||||
t.transaction_id,
|
||||
t.created_at,
|
||||
t.updated_at,
|
||||
u1.username as from_username,
|
||||
u1.real_name as from_real_name,
|
||||
u1.phone as from_phone,
|
||||
u1.avatar as from_avatar,
|
||||
u2.username as to_username,
|
||||
u2.real_name as to_real_name,
|
||||
u2.phone as to_phone,
|
||||
u2.avatar as to_avatar,
|
||||
CASE
|
||||
WHEN am1.agent_id = ? THEN 'out'
|
||||
WHEN am2.agent_id = ? THEN 'in'
|
||||
ELSE 'both'
|
||||
END as direction
|
||||
FROM transfers t
|
||||
LEFT JOIN users u1 ON t.from_user_id = u1.id
|
||||
LEFT JOIN users u2 ON t.to_user_id = u2.id
|
||||
LEFT JOIN agent_merchants am1 ON t.from_user_id = am1.merchant_id
|
||||
LEFT JOIN agent_merchants am2 ON t.to_user_id = am2.merchant_id
|
||||
WHERE ${whereClause}
|
||||
ORDER BY t.${sortBy} ${sortOrder}
|
||||
LIMIT ${limitNum} OFFSET ${offset}
|
||||
`;
|
||||
|
||||
const [transfers] = await getDB().execute(transfersQuery, [agentId, agentId, ...queryParams]);
|
||||
|
||||
// 查询总数
|
||||
const countQuery = `
|
||||
SELECT COUNT(*) as total
|
||||
FROM transfers t
|
||||
LEFT JOIN agent_merchants am1 ON t.from_user_id = am1.merchant_id
|
||||
LEFT JOIN agent_merchants am2 ON t.to_user_id = am2.merchant_id
|
||||
LEFT JOIN users u1 ON t.from_user_id = u1.id
|
||||
LEFT JOIN users u2 ON t.to_user_id = u2.id
|
||||
WHERE ${whereClause}
|
||||
`;
|
||||
|
||||
const [countResult] = await getDB().execute(countQuery, [agentId, agentId, ...queryParams]);
|
||||
const total = countResult[0]?.total || 0;
|
||||
|
||||
// 查询统计信息
|
||||
const [statsResult] = await getDB().execute(`
|
||||
SELECT
|
||||
COUNT(*) as total_transfers,
|
||||
COUNT(CASE WHEN t.status = 'completed' THEN 1 END) as completed_transfers,
|
||||
COUNT(CASE WHEN t.status = 'pending' THEN 1 END) as pending_transfers,
|
||||
COUNT(CASE WHEN t.status = 'failed' THEN 1 END) as failed_transfers,
|
||||
CAST(COALESCE(SUM(CASE WHEN t.status = 'completed' THEN t.amount ELSE 0 END), 0) AS DECIMAL(10,2)) as total_amount,
|
||||
CAST(COALESCE(SUM(CASE WHEN t.status = 'completed' AND DATE(t.created_at) = CURDATE() THEN t.amount ELSE 0 END), 0) AS DECIMAL(10,2)) as today_amount,
|
||||
COUNT(CASE WHEN DATE(t.created_at) = CURDATE() THEN 1 END) as today_transfers
|
||||
FROM transfers t
|
||||
LEFT JOIN agent_merchants am1 ON t.from_user_id = am1.merchant_id
|
||||
LEFT JOIN agent_merchants am2 ON t.to_user_id = am2.merchant_id
|
||||
WHERE (am1.agent_id = ? OR am2.agent_id = ?)
|
||||
`, [agentId, agentId]);
|
||||
|
||||
const stats = statsResult[0] || {
|
||||
total_transfers: 0,
|
||||
completed_transfers: 0,
|
||||
pending_transfers: 0,
|
||||
failed_transfers: 0,
|
||||
total_amount: '0.00',
|
||||
today_amount: '0.00',
|
||||
today_transfers: 0
|
||||
};
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
transfers,
|
||||
pagination: {
|
||||
current_page: pageNum,
|
||||
per_page: limitNum,
|
||||
total,
|
||||
total_pages: Math.ceil(total / limitNum)
|
||||
},
|
||||
stats
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('获取转账记录失败', {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
agentId: req.agent?.id
|
||||
});
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取转账记录失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取单个转账记录详情
|
||||
* GET /api/transfers/:id
|
||||
*/
|
||||
router.get('/:id', agentAuth, async (req, res) => {
|
||||
try {
|
||||
const agentId = req.agent.id;
|
||||
const transferId = req.params.id;
|
||||
|
||||
// 查询转账记录详情
|
||||
const [transfers] = await getDB().execute(`
|
||||
SELECT
|
||||
t.id,
|
||||
t.from_user_id,
|
||||
t.to_user_id,
|
||||
t.amount,
|
||||
t.type,
|
||||
t.status,
|
||||
t.description,
|
||||
t.transaction_id,
|
||||
t.created_at,
|
||||
t.updated_at,
|
||||
u1.username as from_username,
|
||||
u1.real_name as from_real_name,
|
||||
u1.phone as from_phone,
|
||||
u1.avatar as from_avatar,
|
||||
u1.city as from_city,
|
||||
u1.district as from_district,
|
||||
u2.username as to_username,
|
||||
u2.real_name as to_real_name,
|
||||
u2.phone as to_phone,
|
||||
u2.avatar as to_avatar,
|
||||
u2.city as to_city,
|
||||
u2.district as to_district
|
||||
FROM transfers t
|
||||
LEFT JOIN users u1 ON t.from_user_id = u1.id
|
||||
LEFT JOIN users u2 ON t.to_user_id = u2.id
|
||||
LEFT JOIN agent_merchants am1 ON t.from_user_id = am1.merchant_id
|
||||
LEFT JOIN agent_merchants am2 ON t.to_user_id = am2.merchant_id
|
||||
WHERE t.id = ? AND (am1.agent_id = ? OR am2.agent_id = ?)
|
||||
`, [transferId, agentId, agentId]);
|
||||
|
||||
if (transfers.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '转账记录不存在或无权限查看'
|
||||
});
|
||||
}
|
||||
|
||||
const transfer = transfers[0];
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: transfer
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('获取转账记录详情失败', {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
agentId: req.agent?.id,
|
||||
transferId: req.params.id
|
||||
});
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取转账记录详情失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取转账趋势数据
|
||||
* GET /api/transfers/trend
|
||||
*/
|
||||
router.get('/trend/data', agentAuth, async (req, res) => {
|
||||
try {
|
||||
const agentId = req.agent.id;
|
||||
const { days = 7, type = 'amount' } = req.query;
|
||||
|
||||
let selectField = 'CAST(COALESCE(SUM(t.amount), 0) AS DECIMAL(10,2)) as value';
|
||||
if (type === 'count') {
|
||||
selectField = 'COUNT(*) as value';
|
||||
}
|
||||
|
||||
const [trendData] = await getDB().execute(`
|
||||
SELECT
|
||||
DATE(t.created_at) as date,
|
||||
${selectField}
|
||||
FROM transfers t
|
||||
LEFT JOIN agent_merchants am1 ON t.from_user_id = am1.merchant_id
|
||||
LEFT JOIN agent_merchants am2 ON t.to_user_id = am2.merchant_id
|
||||
WHERE (am1.agent_id = ? OR am2.agent_id = ?)
|
||||
AND t.status = 'completed'
|
||||
AND t.created_at >= DATE_SUB(CURDATE(), INTERVAL ? DAY)
|
||||
GROUP BY DATE(t.created_at)
|
||||
ORDER BY date ASC
|
||||
`, [agentId, agentId, parseInt(days)]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: trendData
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('获取转账趋势失败', {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
agentId: req.agent?.id
|
||||
});
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取转账趋势失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 导出转账记录
|
||||
* GET /api/transfers/export
|
||||
*/
|
||||
router.get('/export/data', agentAuth, async (req, res) => {
|
||||
try {
|
||||
const agentId = req.agent.id;
|
||||
const {
|
||||
format = 'json',
|
||||
search,
|
||||
status,
|
||||
type,
|
||||
start_date,
|
||||
end_date,
|
||||
min_amount,
|
||||
max_amount
|
||||
} = req.query;
|
||||
|
||||
// 构建查询条件
|
||||
let whereConditions = ['(am1.agent_id = ? OR am2.agent_id = ?)'];
|
||||
let queryParams = [agentId, agentId];
|
||||
|
||||
if (search) {
|
||||
whereConditions.push('(u1.username LIKE ? OR u1.real_name LIKE ? OR u2.username LIKE ? OR u2.real_name LIKE ?)');
|
||||
queryParams.push(`%${search}%`, `%${search}%`, `%${search}%`, `%${search}%`);
|
||||
}
|
||||
|
||||
if (status) {
|
||||
whereConditions.push('t.status = ?');
|
||||
queryParams.push(status);
|
||||
}
|
||||
|
||||
if (type) {
|
||||
whereConditions.push('t.type = ?');
|
||||
queryParams.push(type);
|
||||
}
|
||||
|
||||
if (start_date) {
|
||||
whereConditions.push('DATE(t.created_at) >= ?');
|
||||
queryParams.push(start_date);
|
||||
}
|
||||
|
||||
if (end_date) {
|
||||
whereConditions.push('DATE(t.created_at) <= ?');
|
||||
queryParams.push(end_date);
|
||||
}
|
||||
|
||||
if (min_amount) {
|
||||
whereConditions.push('t.amount >= ?');
|
||||
queryParams.push(parseFloat(min_amount));
|
||||
}
|
||||
|
||||
if (max_amount) {
|
||||
whereConditions.push('t.amount <= ?');
|
||||
queryParams.push(parseFloat(max_amount));
|
||||
}
|
||||
|
||||
const whereClause = whereConditions.join(' AND ');
|
||||
|
||||
// 查询转账记录
|
||||
const [transfers] = await getDB().execute(`
|
||||
SELECT
|
||||
t.id,
|
||||
t.amount,
|
||||
t.type,
|
||||
t.status,
|
||||
t.description,
|
||||
t.transaction_id,
|
||||
t.created_at,
|
||||
u1.username as from_username,
|
||||
u1.real_name as from_real_name,
|
||||
u1.phone as from_phone,
|
||||
u2.username as to_username,
|
||||
u2.real_name as to_real_name,
|
||||
u2.phone as to_phone
|
||||
FROM transfers t
|
||||
LEFT JOIN users u1 ON t.from_user_id = u1.id
|
||||
LEFT JOIN users u2 ON t.to_user_id = u2.id
|
||||
LEFT JOIN agent_merchants am1 ON t.from_user_id = am1.merchant_id
|
||||
LEFT JOIN agent_merchants am2 ON t.to_user_id = am2.merchant_id
|
||||
WHERE ${whereClause}
|
||||
ORDER BY t.created_at DESC
|
||||
`, queryParams);
|
||||
|
||||
if (format === 'csv') {
|
||||
// 生成CSV格式
|
||||
const csvHeader = 'ID,金额,类型,状态,描述,交易ID,转出用户,转出手机,转入用户,转入手机,创建时间\n';
|
||||
const csvData = transfers.map(transfer => {
|
||||
return [
|
||||
transfer.id,
|
||||
transfer.amount,
|
||||
transfer.type || '',
|
||||
transfer.status || '',
|
||||
(transfer.description || '').replace(/,/g, ','), // 替换逗号避免CSV格式问题
|
||||
transfer.transaction_id || '',
|
||||
transfer.from_real_name || transfer.from_username || '',
|
||||
transfer.from_phone || '',
|
||||
transfer.to_real_name || transfer.to_username || '',
|
||||
transfer.to_phone || '',
|
||||
transfer.created_at || ''
|
||||
].join(',');
|
||||
}).join('\n');
|
||||
|
||||
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
|
||||
res.setHeader('Content-Disposition', `attachment; filename="transfers_${Date.now()}.csv"`);
|
||||
res.send(csvHeader + csvData);
|
||||
} else {
|
||||
// 默认JSON格式
|
||||
res.json({
|
||||
success: true,
|
||||
data: transfers,
|
||||
exported_at: new Date().toISOString(),
|
||||
total: transfers.length
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
logger.error('导出转账记录失败', {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
agentId: req.agent?.id
|
||||
});
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '导出转账记录失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
474
routes/upload.js
Normal file
474
routes/upload.js
Normal file
@@ -0,0 +1,474 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const multer = require('multer');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const { agentAuth } = require('../middleware/agentAuth');
|
||||
const { logger } = require('../config/logger');
|
||||
|
||||
// 确保上传目录存在
|
||||
const uploadDir = path.join(__dirname, '../uploads');
|
||||
if (!fs.existsSync(uploadDir)) {
|
||||
fs.mkdirSync(uploadDir, { recursive: true });
|
||||
}
|
||||
|
||||
// 配置multer存储
|
||||
const storage = multer.diskStorage({
|
||||
destination: function (req, file, cb) {
|
||||
// 根据文件类型创建不同的子目录
|
||||
let subDir = 'others';
|
||||
|
||||
if (file.fieldname === 'avatar') {
|
||||
subDir = 'avatars';
|
||||
} else if (file.fieldname === 'qr_code') {
|
||||
subDir = 'qrcodes';
|
||||
} else if (file.fieldname === 'id_card_front' || file.fieldname === 'id_card_back') {
|
||||
subDir = 'idcards';
|
||||
} else if (file.fieldname === 'business_license') {
|
||||
subDir = 'licenses';
|
||||
}
|
||||
|
||||
const targetDir = path.join(uploadDir, subDir);
|
||||
if (!fs.existsSync(targetDir)) {
|
||||
fs.mkdirSync(targetDir, { recursive: true });
|
||||
}
|
||||
|
||||
cb(null, targetDir);
|
||||
},
|
||||
filename: function (req, file, cb) {
|
||||
// 生成唯一文件名
|
||||
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
|
||||
const ext = path.extname(file.originalname);
|
||||
cb(null, file.fieldname + '-' + uniqueSuffix + ext);
|
||||
}
|
||||
});
|
||||
|
||||
// 文件过滤器
|
||||
const fileFilter = (req, file, cb) => {
|
||||
// 允许的图片类型
|
||||
const allowedImageTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp'];
|
||||
|
||||
if (allowedImageTypes.includes(file.mimetype)) {
|
||||
cb(null, true);
|
||||
} else {
|
||||
cb(new Error('只允许上传图片文件 (JPEG, PNG, GIF, WebP)'), false);
|
||||
}
|
||||
};
|
||||
|
||||
// 配置multer
|
||||
const upload = multer({
|
||||
storage: storage,
|
||||
fileFilter: fileFilter,
|
||||
limits: {
|
||||
fileSize: 5 * 1024 * 1024, // 5MB限制
|
||||
files: 10 // 最多10个文件
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 单文件上传
|
||||
* POST /api/upload/single
|
||||
*/
|
||||
router.post('/single', agentAuth, upload.single('file'), async (req, res) => {
|
||||
try {
|
||||
if (!req.file) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '请选择要上传的文件'
|
||||
});
|
||||
}
|
||||
|
||||
const file = req.file;
|
||||
const relativePath = path.relative(path.join(__dirname, '../'), file.path).replace(/\\/g, '/');
|
||||
const fileUrl = `/uploads/${relativePath.split('/').slice(1).join('/')}`;
|
||||
|
||||
logger.info('文件上传成功', {
|
||||
agentId: req.agent.id,
|
||||
filename: file.filename,
|
||||
originalname: file.originalname,
|
||||
size: file.size,
|
||||
path: fileUrl
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '文件上传成功',
|
||||
data: {
|
||||
filename: file.filename,
|
||||
originalname: file.originalname,
|
||||
size: file.size,
|
||||
mimetype: file.mimetype,
|
||||
url: fileUrl,
|
||||
path: fileUrl // 兼容前端可能使用的字段名
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('文件上传失败', {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
agentId: req.agent?.id
|
||||
});
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || '文件上传失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 多文件上传
|
||||
* POST /api/upload/multiple
|
||||
*/
|
||||
router.post('/multiple', agentAuth, upload.array('files', 10), async (req, res) => {
|
||||
try {
|
||||
if (!req.files || req.files.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '请选择要上传的文件'
|
||||
});
|
||||
}
|
||||
|
||||
const files = req.files.map(file => {
|
||||
const relativePath = path.relative(path.join(__dirname, '../'), file.path).replace(/\\/g, '/');
|
||||
const fileUrl = `/uploads/${relativePath.split('/').slice(1).join('/')}`;
|
||||
|
||||
return {
|
||||
filename: file.filename,
|
||||
originalname: file.originalname,
|
||||
size: file.size,
|
||||
mimetype: file.mimetype,
|
||||
url: fileUrl,
|
||||
path: fileUrl
|
||||
};
|
||||
});
|
||||
|
||||
logger.info('多文件上传成功', {
|
||||
agentId: req.agent.id,
|
||||
count: files.length,
|
||||
totalSize: req.files.reduce((sum, file) => sum + file.size, 0)
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `成功上传${files.length}个文件`,
|
||||
data: files
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('多文件上传失败', {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
agentId: req.agent?.id
|
||||
});
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || '文件上传失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 头像上传
|
||||
* POST /api/upload/avatar
|
||||
*/
|
||||
router.post('/avatar', agentAuth, upload.single('avatar'), async (req, res) => {
|
||||
try {
|
||||
if (!req.file) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '请选择头像文件'
|
||||
});
|
||||
}
|
||||
|
||||
const file = req.file;
|
||||
const relativePath = path.relative(path.join(__dirname, '../'), file.path).replace(/\\/g, '/');
|
||||
const fileUrl = `/uploads/${relativePath.split('/').slice(1).join('/')}`;
|
||||
|
||||
logger.info('头像上传成功', {
|
||||
agentId: req.agent.id,
|
||||
filename: file.filename,
|
||||
size: file.size
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '头像上传成功',
|
||||
data: {
|
||||
filename: file.filename,
|
||||
originalname: file.originalname,
|
||||
size: file.size,
|
||||
url: fileUrl,
|
||||
path: fileUrl
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('头像上传失败', {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
agentId: req.agent?.id
|
||||
});
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || '头像上传失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 收款码上传
|
||||
* POST /api/upload/qrcode
|
||||
*/
|
||||
router.post('/qrcode', agentAuth, upload.single('qr_code'), async (req, res) => {
|
||||
try {
|
||||
if (!req.file) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '请选择收款码文件'
|
||||
});
|
||||
}
|
||||
|
||||
const file = req.file;
|
||||
const relativePath = path.relative(path.join(__dirname, '../'), file.path).replace(/\\/g, '/');
|
||||
const fileUrl = `/uploads/${relativePath.split('/').slice(1).join('/')}`;
|
||||
|
||||
logger.info('收款码上传成功', {
|
||||
agentId: req.agent.id,
|
||||
filename: file.filename,
|
||||
size: file.size
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '收款码上传成功',
|
||||
data: {
|
||||
filename: file.filename,
|
||||
originalname: file.originalname,
|
||||
size: file.size,
|
||||
url: fileUrl,
|
||||
path: fileUrl
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('收款码上传失败', {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
agentId: req.agent?.id
|
||||
});
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || '收款码上传失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 身份证上传
|
||||
* POST /api/upload/idcard
|
||||
*/
|
||||
router.post('/idcard', agentAuth, upload.fields([
|
||||
{ name: 'id_card_front', maxCount: 1 },
|
||||
{ name: 'id_card_back', maxCount: 1 }
|
||||
]), async (req, res) => {
|
||||
try {
|
||||
if (!req.files || (!req.files.id_card_front && !req.files.id_card_back)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '请选择身份证文件'
|
||||
});
|
||||
}
|
||||
|
||||
const result = {};
|
||||
|
||||
if (req.files.id_card_front) {
|
||||
const file = req.files.id_card_front[0];
|
||||
const relativePath = path.relative(path.join(__dirname, '../'), file.path).replace(/\\/g, '/');
|
||||
const fileUrl = `/uploads/${relativePath.split('/').slice(1).join('/')}`;
|
||||
|
||||
result.front = {
|
||||
filename: file.filename,
|
||||
originalname: file.originalname,
|
||||
size: file.size,
|
||||
url: fileUrl,
|
||||
path: fileUrl
|
||||
};
|
||||
}
|
||||
|
||||
if (req.files.id_card_back) {
|
||||
const file = req.files.id_card_back[0];
|
||||
const relativePath = path.relative(path.join(__dirname, '../'), file.path).replace(/\\/g, '/');
|
||||
const fileUrl = `/uploads/${relativePath.split('/').slice(1).join('/')}`;
|
||||
|
||||
result.back = {
|
||||
filename: file.filename,
|
||||
originalname: file.originalname,
|
||||
size: file.size,
|
||||
url: fileUrl,
|
||||
path: fileUrl
|
||||
};
|
||||
}
|
||||
|
||||
logger.info('身份证上传成功', {
|
||||
agentId: req.agent.id,
|
||||
hasFront: !!result.front,
|
||||
hasBack: !!result.back
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '身份证上传成功',
|
||||
data: result
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('身份证上传失败', {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
agentId: req.agent?.id
|
||||
});
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || '身份证上传失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 营业执照上传
|
||||
* POST /api/upload/license
|
||||
*/
|
||||
router.post('/license', agentAuth, upload.single('business_license'), async (req, res) => {
|
||||
try {
|
||||
if (!req.file) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '请选择营业执照文件'
|
||||
});
|
||||
}
|
||||
|
||||
const file = req.file;
|
||||
const relativePath = path.relative(path.join(__dirname, '../'), file.path).replace(/\\/g, '/');
|
||||
const fileUrl = `/uploads/${relativePath.split('/').slice(1).join('/')}`;
|
||||
|
||||
logger.info('营业执照上传成功', {
|
||||
agentId: req.agent.id,
|
||||
filename: file.filename,
|
||||
size: file.size
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '营业执照上传成功',
|
||||
data: {
|
||||
filename: file.filename,
|
||||
originalname: file.originalname,
|
||||
size: file.size,
|
||||
url: fileUrl,
|
||||
path: fileUrl
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('营业执照上传失败', {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
agentId: req.agent?.id
|
||||
});
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || '营业执照上传失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 删除文件
|
||||
* DELETE /api/upload/file
|
||||
*/
|
||||
router.delete('/file', agentAuth, async (req, res) => {
|
||||
try {
|
||||
const { path: filePath } = req.body;
|
||||
|
||||
if (!filePath) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '请提供文件路径'
|
||||
});
|
||||
}
|
||||
|
||||
// 构建完整的文件路径
|
||||
const fullPath = path.join(__dirname, '../', filePath);
|
||||
|
||||
// 检查文件是否存在
|
||||
if (!fs.existsSync(fullPath)) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '文件不存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 删除文件
|
||||
fs.unlinkSync(fullPath);
|
||||
|
||||
logger.info('文件删除成功', {
|
||||
agentId: req.agent.id,
|
||||
filePath: filePath
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '文件删除成功'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('文件删除失败', {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
agentId: req.agent?.id
|
||||
});
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
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: '文件大小超过限制(最大5MB)'
|
||||
});
|
||||
}
|
||||
if (error.code === 'LIMIT_FILE_COUNT') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '文件数量超过限制(最多10个)'
|
||||
});
|
||||
}
|
||||
if (error.code === 'LIMIT_UNEXPECTED_FILE') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '意外的文件字段'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || '文件上传失败'
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
354
routes/users.js
Normal file
354
routes/users.js
Normal file
@@ -0,0 +1,354 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { getDB } = require('../database');
|
||||
const { agentAuth } = require('../middleware/agentAuth');
|
||||
const { logger } = require('../config/logger');
|
||||
|
||||
/**
|
||||
* 获取代理下级用户列表
|
||||
* GET /api/users
|
||||
*/
|
||||
router.get('/', agentAuth, async (req, res) => {
|
||||
try {
|
||||
const agentId = req.agent.id;
|
||||
const {
|
||||
page = 1,
|
||||
limit = 20,
|
||||
search,
|
||||
role,
|
||||
sort_by = 'created_at',
|
||||
sort_order = 'desc',
|
||||
city,
|
||||
district
|
||||
} = req.query;
|
||||
|
||||
const pageNum = parseInt(page) || 1;
|
||||
const limitNum = parseInt(limit) || 20;
|
||||
const offset = (pageNum - 1) * limitNum;
|
||||
|
||||
// 构建查询条件
|
||||
let whereConditions = ['am.agent_id = ?'];
|
||||
let queryParams = [agentId];
|
||||
|
||||
if (search) {
|
||||
whereConditions.push('(u.username LIKE ? OR u.real_name LIKE ? OR u.phone LIKE ?)');
|
||||
queryParams.push(`%${search}%`, `%${search}%`, `%${search}%`);
|
||||
}
|
||||
|
||||
if (role) {
|
||||
whereConditions.push('u.role = ?');
|
||||
queryParams.push(role);
|
||||
}
|
||||
|
||||
if (city) {
|
||||
whereConditions.push('u.city = ?');
|
||||
queryParams.push(city);
|
||||
}
|
||||
|
||||
if (district) {
|
||||
whereConditions.push('u.district = ?');
|
||||
queryParams.push(district);
|
||||
}
|
||||
|
||||
const whereClause = whereConditions.join(' AND ');
|
||||
|
||||
// 验证排序字段
|
||||
const allowedSortFields = ['created_at', 'updated_at', 'balance', 'username', 'real_name'];
|
||||
const sortBy = allowedSortFields.includes(sort_by) ? sort_by : 'created_at';
|
||||
const sortOrder = sort_order.toLowerCase() === 'asc' ? 'ASC' : 'DESC';
|
||||
|
||||
// 查询用户列表
|
||||
const usersQuery = `
|
||||
SELECT
|
||||
u.id,
|
||||
u.username,
|
||||
u.real_name,
|
||||
u.phone,
|
||||
u.email,
|
||||
u.avatar,
|
||||
u.role,
|
||||
u.city,
|
||||
u.district,
|
||||
u.account_type,
|
||||
u.balance,
|
||||
u.points,
|
||||
u.status,
|
||||
u.last_login_at,
|
||||
u.created_at,
|
||||
u.updated_at,
|
||||
am.created_at as join_date,
|
||||
(
|
||||
SELECT CAST(COALESCE(SUM(amount), 0) AS DECIMAL(10,2))
|
||||
FROM transfers
|
||||
WHERE from_user_id = u.id
|
||||
AND DATE(created_at) = CURDATE()
|
||||
) as today_transfer_out,
|
||||
(
|
||||
SELECT CAST(COALESCE(SUM(amount), 0) AS DECIMAL(10,2))
|
||||
FROM transfers
|
||||
WHERE to_user_id = u.id
|
||||
AND DATE(created_at) = CURDATE()
|
||||
) as today_transfer_in
|
||||
FROM agent_merchants am
|
||||
LEFT JOIN users u ON am.merchant_id = u.id
|
||||
WHERE ${whereClause}
|
||||
ORDER BY u.${sortBy} ${sortOrder}
|
||||
LIMIT ${limitNum} OFFSET ${offset}
|
||||
`;
|
||||
|
||||
const [users] = await getDB().execute(usersQuery, queryParams);
|
||||
|
||||
// 查询总数
|
||||
const countQuery = `
|
||||
SELECT COUNT(*) as total
|
||||
FROM agent_merchants am
|
||||
LEFT JOIN users u ON am.merchant_id = u.id
|
||||
WHERE ${whereClause}
|
||||
`;
|
||||
|
||||
const [countResult] = await getDB().execute(countQuery, queryParams);
|
||||
const total = countResult[0]?.total || 0;
|
||||
|
||||
// 查询统计信息
|
||||
const [statsResult] = await getDB().execute(`
|
||||
SELECT
|
||||
COUNT(*) as total_users,
|
||||
COUNT(CASE WHEN u.status = 'active' THEN 1 END) as active_users,
|
||||
CAST(COALESCE(SUM(u.balance), 0) AS DECIMAL(10,2)) as total_balance,
|
||||
COUNT(CASE WHEN DATE(am.created_at) = CURDATE() THEN 1 END) as today_new_users
|
||||
FROM agent_merchants am
|
||||
LEFT JOIN users u ON am.merchant_id = u.id
|
||||
WHERE am.agent_id = ?
|
||||
`, [agentId]);
|
||||
|
||||
const stats = statsResult[0] || {
|
||||
total_users: 0,
|
||||
active_users: 0,
|
||||
total_balance: '0.00',
|
||||
today_new_users: 0
|
||||
};
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
users,
|
||||
pagination: {
|
||||
current_page: pageNum,
|
||||
per_page: limitNum,
|
||||
total,
|
||||
total_pages: Math.ceil(total / limitNum)
|
||||
},
|
||||
stats
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('获取用户列表失败', {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
agentId: req.agent?.id
|
||||
});
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取用户列表失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取单个用户详情
|
||||
* GET /api/users/:id
|
||||
*/
|
||||
router.get('/:id', agentAuth, async (req, res) => {
|
||||
try {
|
||||
const agentId = req.agent.id;
|
||||
const userId = req.params.id;
|
||||
|
||||
// 验证用户是否属于当前代理
|
||||
const [users] = await getDB().execute(`
|
||||
SELECT
|
||||
u.id,
|
||||
u.username,
|
||||
u.real_name,
|
||||
u.phone,
|
||||
u.email,
|
||||
u.avatar,
|
||||
u.role,
|
||||
u.city,
|
||||
u.district,
|
||||
u.account_type,
|
||||
u.balance,
|
||||
u.points,
|
||||
u.status,
|
||||
u.id_card,
|
||||
u.business_license,
|
||||
u.payment_qr_code,
|
||||
u.last_login_at,
|
||||
u.created_at,
|
||||
u.updated_at,
|
||||
am.created_at as join_date
|
||||
FROM agent_merchants am
|
||||
LEFT JOIN users u ON am.merchant_id = u.id
|
||||
WHERE am.agent_id = ? AND u.id = ?
|
||||
`, [agentId, userId]);
|
||||
|
||||
if (users.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '用户不存在或不属于当前代理'
|
||||
});
|
||||
}
|
||||
|
||||
const user = users[0];
|
||||
|
||||
// 获取用户转账统计
|
||||
const [transferStats] = await getDB().execute(`
|
||||
SELECT
|
||||
COUNT(*) as total_transfers,
|
||||
CAST(COALESCE(SUM(CASE WHEN from_user_id = ? THEN amount ELSE 0 END), 0) AS DECIMAL(10,2)) as total_transfer_out,
|
||||
CAST(COALESCE(SUM(CASE WHEN to_user_id = ? THEN amount ELSE 0 END), 0) AS DECIMAL(10,2)) as total_transfer_in,
|
||||
COUNT(CASE WHEN DATE(created_at) = CURDATE() THEN 1 END) as today_transfers,
|
||||
CAST(COALESCE(SUM(CASE WHEN from_user_id = ? AND DATE(created_at) = CURDATE() THEN amount ELSE 0 END), 0) AS DECIMAL(10,2)) as today_transfer_out,
|
||||
CAST(COALESCE(SUM(CASE WHEN to_user_id = ? AND DATE(created_at) = CURDATE() THEN amount ELSE 0 END), 0) AS DECIMAL(10,2)) as today_transfer_in
|
||||
FROM transfers
|
||||
WHERE from_user_id = ? OR to_user_id = ?
|
||||
`, [userId, userId, userId, userId, userId, userId]);
|
||||
|
||||
user.transfer_stats = transferStats[0] || {
|
||||
total_transfers: 0,
|
||||
total_transfer_out: '0.00',
|
||||
total_transfer_in: '0.00',
|
||||
today_transfers: 0,
|
||||
today_transfer_out: '0.00',
|
||||
today_transfer_in: '0.00'
|
||||
};
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: user
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('获取用户详情失败', {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
agentId: req.agent?.id,
|
||||
userId: req.params.id
|
||||
});
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取用户详情失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 导出用户数据
|
||||
* GET /api/users/export
|
||||
*/
|
||||
router.get('/export/data', agentAuth, async (req, res) => {
|
||||
try {
|
||||
const agentId = req.agent.id;
|
||||
const { format = 'json', search, role, city, district } = req.query;
|
||||
|
||||
// 构建查询条件
|
||||
let whereConditions = ['am.agent_id = ?'];
|
||||
let queryParams = [agentId];
|
||||
|
||||
if (search) {
|
||||
whereConditions.push('(u.username LIKE ? OR u.real_name LIKE ? OR u.phone LIKE ?)');
|
||||
queryParams.push(`%${search}%`, `%${search}%`, `%${search}%`);
|
||||
}
|
||||
|
||||
if (role) {
|
||||
whereConditions.push('u.role = ?');
|
||||
queryParams.push(role);
|
||||
}
|
||||
|
||||
if (city) {
|
||||
whereConditions.push('u.city = ?');
|
||||
queryParams.push(city);
|
||||
}
|
||||
|
||||
if (district) {
|
||||
whereConditions.push('u.district = ?');
|
||||
queryParams.push(district);
|
||||
}
|
||||
|
||||
const whereClause = whereConditions.join(' AND ');
|
||||
|
||||
// 查询用户数据
|
||||
const [users] = await getDB().execute(`
|
||||
SELECT
|
||||
u.id,
|
||||
u.username,
|
||||
u.real_name,
|
||||
u.phone,
|
||||
u.email,
|
||||
u.role,
|
||||
u.city,
|
||||
u.district,
|
||||
u.account_type,
|
||||
u.balance,
|
||||
u.points,
|
||||
u.status,
|
||||
u.created_at,
|
||||
am.created_at as join_date
|
||||
FROM agent_merchants am
|
||||
LEFT JOIN users u ON am.merchant_id = u.id
|
||||
WHERE ${whereClause}
|
||||
ORDER BY u.created_at DESC
|
||||
`, queryParams);
|
||||
|
||||
if (format === 'csv') {
|
||||
// 生成CSV格式
|
||||
const csvHeader = 'ID,用户名,真实姓名,手机号,邮箱,角色,城市,地区,账户类型,余额,积分,状态,注册时间,加入时间\n';
|
||||
const csvData = users.map(user => {
|
||||
return [
|
||||
user.id,
|
||||
user.username || '',
|
||||
user.real_name || '',
|
||||
user.phone || '',
|
||||
user.email || '',
|
||||
user.role || '',
|
||||
user.city || '',
|
||||
user.district || '',
|
||||
user.account_type || '',
|
||||
user.balance || '0.00',
|
||||
user.points || 0,
|
||||
user.status || '',
|
||||
user.created_at || '',
|
||||
user.join_date || ''
|
||||
].join(',');
|
||||
}).join('\n');
|
||||
|
||||
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
|
||||
res.setHeader('Content-Disposition', `attachment; filename="users_${Date.now()}.csv"`);
|
||||
res.send(csvHeader + csvData);
|
||||
} else {
|
||||
// 默认JSON格式
|
||||
res.json({
|
||||
success: true,
|
||||
data: users,
|
||||
exported_at: new Date().toISOString(),
|
||||
total: users.length
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
logger.error('导出用户数据失败', {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
agentId: req.agent?.id
|
||||
});
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '导出用户数据失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
196
server.js
Normal file
196
server.js
Normal file
@@ -0,0 +1,196 @@
|
||||
// 加载环境变量配置
|
||||
require('dotenv').config();
|
||||
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const bodyParser = require('body-parser');
|
||||
const path = require('path');
|
||||
const mysql = require('mysql2/promise');
|
||||
const rateLimit = require('express-rate-limit');
|
||||
const helmet = require('helmet');
|
||||
const { initDB, getDB, dbConfig } = require('./database');
|
||||
const { logger } = require('./config/logger');
|
||||
const { errorHandler, notFound } = require('./middleware/errorHandler');
|
||||
const fs = require('fs');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.AGENT_PORT || 3001;
|
||||
|
||||
// 确保日志目录存在
|
||||
const logDir = path.join(__dirname, 'logs');
|
||||
if (!fs.existsSync(logDir)) {
|
||||
fs.mkdirSync(logDir, { recursive: true });
|
||||
}
|
||||
|
||||
// 安全中间件
|
||||
app.use(helmet({
|
||||
contentSecurityPolicy: false, // 为了支持前端应用
|
||||
crossOriginEmbedderPolicy: false,
|
||||
crossOriginOpenerPolicy: false, // 禁用 COOP 头部以避免非 HTTPS 环境的警告
|
||||
originAgentCluster: false // 禁用Origin-Agent-Cluster头部
|
||||
}));
|
||||
|
||||
// 中间件配置
|
||||
// CORS配置 - 允许代理前端访问
|
||||
app.use(cors({
|
||||
origin: [
|
||||
'http://localhost:5173',
|
||||
'http://localhost:5176',
|
||||
'http://localhost:5174',
|
||||
'http://localhost:3002',
|
||||
'https://agent.zrbjr.com',
|
||||
'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分钟内最多1000个请求
|
||||
message: {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'RATE_LIMIT_EXCEEDED',
|
||||
message: '请求过于频繁,请稍后再试'
|
||||
}
|
||||
}
|
||||
});
|
||||
app.use('/api', limiter);
|
||||
|
||||
// 静态文件服务 - 为代理后台前端提供服务
|
||||
app.use('/agent-admin', express.static(path.join(__dirname, 'agent-admin/dist'), {
|
||||
setHeaders: (res, filePath) => {
|
||||
// 设置CORS头部
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
||||
|
||||
// 设置缓存策略
|
||||
if (filePath.includes('.js') || filePath.includes('.css')) {
|
||||
res.setHeader('Cache-Control', 'public, max-age=31536000'); // 1年缓存
|
||||
} else {
|
||||
res.setHeader('Cache-Control', 'public, max-age=86400'); // 1天缓存
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
// 上传文件静态服务
|
||||
app.use('/uploads', express.static(path.join(__dirname, 'uploads'), {
|
||||
setHeaders: (res, filePath) => {
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
||||
}
|
||||
}));
|
||||
|
||||
// 代理后端共用现有数据库,无需初始化
|
||||
|
||||
// API路由 - 代理专用路由
|
||||
app.use('/api/auth', require('./routes/auth'));
|
||||
app.use('/api/agent', require('./routes/agent'));
|
||||
app.use('/api/users', require('./routes/users'));
|
||||
app.use('/api/transfers', require('./routes/transfers'));
|
||||
app.use('/api/commissions', require('./routes/commissions'));
|
||||
app.use('/api/upload', require('./routes/upload'));
|
||||
|
||||
// 代理后台首页路由
|
||||
app.get('/', (req, res) => {
|
||||
res.json({
|
||||
message: '炬融圈代理后台API服务',
|
||||
version: '1.0.0',
|
||||
status: 'running'
|
||||
});
|
||||
});
|
||||
|
||||
// 代理后台前端路由
|
||||
app.get('/agent-admin*', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, 'agent-admin/dist/index.html'));
|
||||
});
|
||||
|
||||
// 404处理
|
||||
app.use(notFound);
|
||||
|
||||
// 错误处理
|
||||
app.use(errorHandler);
|
||||
|
||||
// 导出app供测试使用
|
||||
module.exports = {
|
||||
app, getDB
|
||||
};
|
||||
|
||||
// 启动服务器
|
||||
app.listen(PORT, async () => {
|
||||
try {
|
||||
// 初始化数据库连接
|
||||
await initDB();
|
||||
console.log(`代理后台API服务器运行在端口 ${PORT}`);
|
||||
console.log(`代理后台管理界面: http://localhost:${PORT}/agent-admin`);
|
||||
|
||||
// 代理后端共用现有数据库,无需初始化表结构
|
||||
|
||||
} catch (error) {
|
||||
console.error('服务器启动失败:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
// 优雅关闭处理
|
||||
process.on('SIGTERM', async () => {
|
||||
console.log('收到SIGTERM信号,正在关闭服务器...');
|
||||
try {
|
||||
await closeDB();
|
||||
console.log('数据库连接已关闭');
|
||||
} catch (error) {
|
||||
console.error('关闭数据库连接时出错:', error);
|
||||
}
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('SIGINT', async () => {
|
||||
console.log('收到SIGINT信号,正在关闭服务器...');
|
||||
try {
|
||||
await closeDB();
|
||||
console.log('数据库连接已关闭');
|
||||
} catch (error) {
|
||||
console.error('关闭数据库连接时出错:', error);
|
||||
}
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
console.error('未处理的Promise拒绝:', reason);
|
||||
});
|
||||
|
||||
process.on('uncaughtException', (error) => {
|
||||
console.error('未捕获的异常:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user