require('dotenv').config() const express = require('express') const cors = require('cors') const helmet = require('helmet') const compression = require('compression') const morgan = require('morgan') const rateLimit = require('express-rate-limit') const connectDB = require('./config/database') const initRoutes = require('./routes') const seoRoutes = require('./routes/seo') const registerWxMsgRoutes = require('./routes/wxmsg') const logger = require('./utils/logger') // 创建 Express 应用 const app = express() // 信任反向代理(nginx/宝塔),使 req.ip 取到真实客户端 IP app.set('trust proxy', 1) // 数据库连接 connectDB() // SEO/GEO 路由(robots/sitemap/llms/about) app.use('/', seoRoutes) // 中间件配置 app.use(helmet({ contentSecurityPolicy: false, // H5 与小程序客户端各自管理 CSP;API 服务无需开启 crossOriginResourcePolicy: { policy: 'cross-origin' } })) // 安全头部 app.use(cors()) // 跨域支持 app.use(compression()) // Gzip 压缩 app.use(express.json({ limit: '1mb' })) // JSON 解析(普通接口无需 10mb 大包) app.use(express.urlencoded({ extended: true, limit: '1mb' })) // URL 编码解析 // 微信消息推送为 text/xml 体,透传为字符串供 /api/wx/msg 解析 app.use((req, res, next) => { if (req.method === 'POST' && (req.headers['content-type'] || '').includes('xml')) { let data = '' req.setEncoding('utf8') req.on('data', (c) => (data += c)) req.on('end', () => { req.body = data; next() }) return } next() }) // 日志中间件 if (process.env.NODE_ENV === 'development') { app.use(morgan('dev')) } else { app.use(morgan('combined')) } // 速率限制 const limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 分钟 max: 1000, // 每个 IP 最多 1000 次请求 message: { success: false, error: '请求过于频繁,请稍后再试' } }) app.use('/api/', limiter) // 健康检查 app.get('/health', (req, res) => { res.status(200).json({ status: 'ok', timestamp: new Date().toISOString(), uptime: process.uptime() }) }) // API 路由 registerWxMsgRoutes(app) initRoutes(app) // 404 处理 app.use((req, res) => { res.status(404).json({ success: false, error: '接口不存在', path: req.path }) }) // 全局错误处理(不向客户端泄露内部错误细节) app.use((err, req, res, next) => { logger.error('服务器错误:', err) // JSON 解析错误等客户端问题返回 400 if (err.type === 'entity.parse.failed' || err.type === 'entity.too.large') { return res.status(400).json({ success: false, error: '请求体格式错误' }) } res.status(err.status || 500).json({ success: false, error: process.env.NODE_ENV === 'production' ? '服务器内部错误' : err.message }) }) // 启动服务器(仅在直接运行时启动,被测试 import 时不启动) const PORT = process.env.PORT || 3000 let server = null if (require.main === module) { server = app.listen(PORT, () => { logger.info(`🚀 服务器启动成功`) logger.info(`📍 端口: ${PORT}`) logger.info(`🌐 环境: ${process.env.NODE_ENV || 'development'}`) logger.info(`📊 健康检查: http://localhost:${PORT}/health`) }) } // 优雅关闭 process.on('SIGTERM', () => { logger.info('📴 接收到 SIGTERM 信号,正在关闭服务器...') if (server) { server.close(() => { logger.info('✅ 服务器已关闭') process.exit(0) }) } else { process.exit(0) } }) module.exports = app