Files
jurong_circle_black/server.js
2025-09-15 17:27:13 +08:00

360 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 加载环境变量配置
require('dotenv').config();
const express = require('express');
const cors = require('cors');
const bodyParser = require('body-parser');
const path = require('path');
const rateLimit = require('express-rate-limit');
const 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.PORT || 3000;
// 确保日志目录存在
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:5175',
'http://localhost:5174',
'http://localhost:3001',
'https://www.zrbjr.com',
'https://zrbjr.com'
],
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With']
}));
app.use(bodyParser.json({ limit: '10mb' }));
app.use(bodyParser.urlencoded({ extended: true, limit: '10mb' }));
// 请求日志中间件
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
// 只记录非正常状态码的请求日志过滤掉200、304等正常返回
if (res.statusCode >= 400 || res.statusCode < 200) {
logger.info('HTTP Request', {
method: req.method,
url: req.originalUrl,
statusCode: res.statusCode,
duration: `${duration}ms`,
ip: req.ip,
userAgent: req.get('User-Agent')
});
}
});
next();
});
// 限流中间件
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15分钟
max: 1000, // 限制每个IP 15分钟内最多100个请求
message: {
success: false,
error: {
code: 'RATE_LIMIT_EXCEEDED',
message: '请求过于频繁,请稍后再试'
}
}
});
app.use('/api', limiter);
// 静态文件服务 - 必须在API路由之前
// 为uploads路径配置CORS和静态文件服务
app.use('/uploads', express.static(path.join(__dirname, 'uploads'), {
setHeaders: (res, filePath) => {
// 设置CORS头部
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
// 设置缓存和内容类型
if (filePath.endsWith('.jpg') || filePath.endsWith('.jpeg')) {
res.setHeader('Content-Type', 'image/jpeg');
} else if (filePath.endsWith('.png')) {
res.setHeader('Content-Type', 'image/png');
} else if (filePath.endsWith('.gif')) {
res.setHeader('Content-Type', 'image/gif');
} else if (filePath.endsWith('.webp')) {
res.setHeader('Content-Type', 'image/webp');
}
res.setHeader('Cache-Control', 'public, max-age=86400'); // 1天缓存
}
}));
// 处理vite.svg请求
app.get('/vite.svg', (req, res) => {
const referer = req.get('Referer');
if (referer && referer.includes('/admin')) {
// 为admin页面提供logo.svg
res.setHeader('Content-Type', 'image/svg+xml');
res.sendFile(path.join(__dirname, 'admin/dist/logo.svg'));
} else {
// 前端页面没有vite.svg返回404
res.status(404).send('File not found');
}
});
// 静态文件服务配置
// 专门处理admin路径下的assets
app.use('/admin/assets', express.static(path.join(__dirname, 'admin/dist/assets'), {
setHeaders: (res, filePath) => {
res.removeHeader('Origin-Agent-Cluster');
if (filePath.endsWith('.css')) {
res.setHeader('Content-Type', 'text/css; charset=utf-8');
res.setHeader('Cache-Control', 'public, max-age=31536000');
} else if (filePath.endsWith('.js')) {
res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
res.setHeader('Cache-Control', 'public, max-age=31536000');
}
}
}));
// 为admin页面的assets提供服务当从admin页面访问/assets/时)
app.use('/assets', (req, res, next) => {
// 检查referer来判断是否来自admin页面
const referer = req.get('Referer');
if (referer && referer.includes('/admin')) {
// 如果来自admin页面从admin/dist/assets提供文件
express.static(path.join(__dirname, 'admin/dist/assets'), {
setHeaders: (res, filePath) => {
res.removeHeader('Origin-Agent-Cluster');
if (filePath.endsWith('.css')) {
res.setHeader('Content-Type', 'text/css; charset=utf-8');
res.setHeader('Cache-Control', 'public, max-age=31536000');
} else if (filePath.endsWith('.js')) {
res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
res.setHeader('Cache-Control', 'public, max-age=31536000');
}
}
})(req, res, next);
} else {
// 否则从frontend/dist/assets提供文件
express.static(path.join(__dirname, 'frontend/dist/assets'), {
setHeaders: (res, filePath) => {
res.removeHeader('Origin-Agent-Cluster');
if (filePath.endsWith('.css')) {
res.setHeader('Content-Type', 'text/css; charset=utf-8');
res.setHeader('Cache-Control', 'public, max-age=31536000');
} else if (filePath.endsWith('.js')) {
res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
res.setHeader('Cache-Control', 'public, max-age=31536000');
}
}
})(req, res, next);
}
});
app.use('/admin', express.static(path.join(__dirname, 'admin/dist'), {
setHeaders: (res, filePath) => {
// 移除Origin-Agent-Cluster头部以避免冲突
res.removeHeader('Origin-Agent-Cluster');
if (filePath.endsWith('.css')) {
res.setHeader('Content-Type', 'text/css; charset=utf-8');
res.setHeader('Cache-Control', 'public, max-age=31536000'); // 1年缓存
} else if (filePath.endsWith('.js')) {
res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
res.setHeader('Cache-Control', 'public, max-age=31536000'); // 1年缓存
} else if (filePath.endsWith('.html')) {
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); // HTML文件不缓存
res.setHeader('Pragma', 'no-cache');
res.setHeader('Expires', '0');
} else if (filePath.endsWith('.svg')) {
res.setHeader('Content-Type', 'image/svg+xml');
}
}
}));
app.use(express.static(path.join(__dirname, 'frontend/dist'), {
setHeaders: (res, filePath) => {
// 移除Origin-Agent-Cluster头部以避免冲突
res.removeHeader('Origin-Agent-Cluster');
if (filePath.endsWith('.css')) {
res.setHeader('Content-Type', 'text/css; charset=utf-8');
res.setHeader('Cache-Control', 'public, max-age=31536000'); // 1年缓存
} else if (filePath.endsWith('.js')) {
res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
res.setHeader('Cache-Control', 'public, max-age=31536000'); // 1年缓存
} else if (filePath.endsWith('.html')) {
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); // HTML文件不缓存
res.setHeader('Pragma', 'no-cache');
res.setHeader('Expires', '0');
} else if (filePath.endsWith('.svg')) {
res.setHeader('Content-Type', 'image/svg+xml');
}
}
}));
// 引入数据库初始化模块
const { initDatabase } = require('./config/database-init');
// API路由
app.use('/api/auth', require('./routes/auth'));
app.use('/api/users', require('./routes/users'));
app.use('/api/user', require('./routes/users')); // 添加单数形式的路由映射
app.use('/api/products', require('./routes/products'));
app.use('/api/specifications', require('./routes/specifications'));
app.use('/api/orders', require('./routes/orders'));
app.use('/api/points', require('./routes/points'));
app.use('/api/captcha', require('./routes/captcha')); // 验证码路由
app.use('/api/sms', require('./routes/sms')); // 短信验证路由
app.use('/api/upload', require('./routes/upload'));
app.use('/api/transfers', require('./routes/transfers'));
app.use('/api/matching', require('./routes/matching'));
app.use('/api/admin/matching', require('./routes/matchingAdmin'));
app.use('/api/system', require('./routes/system'));
app.use('/api/risk', require('./routes/riskManagement'));
app.use('/api/agents', require('./routes/agents'));
app.use('/api/admin/agents', require('./routes/agents/agents'));
app.use('/api/admin/withdrawals', require('./routes/agents/withdrawals'));
app.use('/api/agent-withdrawals', require('./routes/agent-withdrawals'));
app.use('/api/regions', require('./routes/regions'));
app.use('/api/addresses', require('./routes/addresses'));
app.use('/api/address-labels', require('./routes/address-labels'));
app.use('/api/cart', require('./routes/cart'));
app.use('/api/announcements', require('./routes/announcements')); // 通知公告路由
app.use('/api/wechat-pay', require('./routes/wechatPay')); // 只保留微信支付
app.use('/api/payment', require('./routes/payment'));
// 前端路由 - 必须在最后作为fallback
app.get('/', (req, res) => {
res.removeHeader('Origin-Agent-Cluster');
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
res.setHeader('Pragma', 'no-cache');
res.setHeader('Expires', '0');
res.sendFile(path.join(__dirname, 'frontend/dist/index.html'));
});
app.get('/admin*', (req, res) => {
res.removeHeader('Origin-Agent-Cluster');
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
res.setHeader('Pragma', 'no-cache');
res.setHeader('Expires', '0');
res.sendFile(path.join(__dirname, 'admin/dist/index.html'));
});
app.get('/frontend*', (req, res) => {
res.removeHeader('Origin-Agent-Cluster');
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
res.setHeader('Pragma', 'no-cache');
res.setHeader('Expires', '0');
res.sendFile(path.join(__dirname, 'frontend/dist/index.html'));
});
// SPA fallback - 处理前端路由
app.get('*', (req, res) => {
// 如果请求的是静态资源但找不到返回404不返回JSON
if (req.path.includes('.')) {
return res.status(404).send('File not found');
}
// 否则返回前端应用的index.html
res.removeHeader('Origin-Agent-Cluster');
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
res.setHeader('Pragma', 'no-cache');
res.setHeader('Expires', '0');
res.sendFile(path.join(__dirname, 'frontend/dist/index.html'));
});
// 404处理
app.use(notFound);
// 全局错误处理中间件
app.use(errorHandler);
// 导出数据库连接供路由使用
module.exports = {
get db() { return getDB(); }
};
// 启动服务器
app.listen(PORT, async () => {
try {
logger.info('Server starting', { port: PORT });
console.log(`服务器运行在 http://localhost:${PORT}`);
await initDatabase();
// global.sqlReq = mysql.createConnection()
// 启动转账超时检查服务
const timeoutService = require('./services/timeoutService');
timeoutService.startTimeoutChecker();
console.log('转账超时检查服务已启动');
// 启动数据库连接监控
// const dbMonitor = require('./db-monitor');
// dbMonitor.startMonitoring(60000); // 每分钟监控一次
// console.log('数据库连接监控已启动');
global.captchaStore = new Map();
logger.info('Server started successfully', {
port: PORT,
environment: process.env.NODE_ENV || 'development'
});
} catch (error) {
logger.error('Failed to start server', { error: error.message });
process.exit(1);
}
});
// 优雅关闭
process.on('SIGTERM', async () => {
logger.info('SIGTERM received, shutting down gracefully');
try {
const { closeDB } = require('./database');
await closeDB();
} catch (error) {
logger.error('Error closing database', { error: error.message });
}
process.exit(0);
});
process.on('SIGINT', async () => {
logger.info('SIGINT received, shutting down gracefully');
try {
const { closeDB } = require('./database');
await closeDB();
} catch (error) {
logger.error('Error closing database', { error: error.message });
}
process.exit(0);
});
process.on('unhandledRejection', (reason, promise) => {
logger.error('Unhandled Rejection', { reason, promise });
});
process.on('uncaughtException', (error) => {
logger.error('Uncaught Exception', { error: error.message, stack: error.stack });
process.exit(1);
});