Files
ai-dimension/server/src/index.js
T

133 lines
3.5 KiB
JavaScript
Executable File
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 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