初次提交

This commit is contained in:
2025-09-04 10:49:10 +08:00
commit e704c8abca
26 changed files with 8917 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
node_modules

17
config/config.js Normal file
View File

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

70
config/constants.js Normal file
View File

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

1105
config/database-init.js Normal file

File diff suppressed because it is too large Load Diff

363
config/dbv2.js Normal file
View File

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

73
config/logger.js Normal file
View File

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

97
config/minio.js Normal file
View File

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

24
config/wechatPay.js Normal file
View File

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

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

11
logs/combined.log Normal file
View 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
View 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
View 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
View 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
View File

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

230
middleware/validation.js Normal file
View File

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

3750
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

48
package.json Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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);
});