Files
suanming/server/index.cjs
patdelphi 65bd6a7759 fix: Improve database persistence in Koyeb deployment
- Remove forced database initialization from Dockerfile CMD
- App now auto-initializes database on startup without data loss
- Skip sample data creation in production environment
- Ensure admin user exists in production without recreating
- Fixes database reset issue on every Koyeb deployment
- Maintains data persistence with proper volume mounting
2025-08-22 17:09:18 +08:00

230 lines
6.6 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.
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const path = require('path');
const { dbManager } = require('./database/index.cjs');
// 导入路由
const authRoutes = require('./routes/auth.cjs');
const analysisRoutes = require('./routes/analysis.cjs');
const historyRoutes = require('./routes/history.cjs');
const profileRoutes = require('./routes/profile.cjs');
const downloadRoutes = require('./routes/download.cjs');
const aiInterpretationRoutes = require('./routes/aiInterpretation.cjs');
// 导入中间件
const { errorHandler } = require('./middleware/errorHandler.cjs');
const { requestLogger } = require('./middleware/logger.cjs');
const app = express();
const PORT = process.env.PORT || 3001;
// 初始化数据库
try {
dbManager.init();
console.log('数据库连接成功');
// 在生产环境中,确保管理员用户存在
if (process.env.NODE_ENV === 'production') {
const db = dbManager.getDatabase();
const adminExists = db.prepare('SELECT id FROM users WHERE email = ?').get('admin@localhost');
if (!adminExists) {
const bcrypt = require('bcryptjs');
const adminPassword = bcrypt.hashSync('admin123', 12);
// 创建管理员用户
const insertAdmin = db.prepare(
'INSERT INTO users (email, password_hash) VALUES (?, ?)'
);
const adminResult = insertAdmin.run('admin@localhost', adminPassword);
// 创建管理员档案
const insertAdminProfile = db.prepare(
'INSERT INTO user_profiles (user_id, full_name, username) VALUES (?, ?, ?)'
);
insertAdminProfile.run(adminResult.lastInsertRowid, '系统管理员', 'admin');
console.log('✅ 管理员用户创建成功');
}
}
} catch (error) {
console.error('数据库连接失败:', error);
process.exit(1);
}
// 安全中间件
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: [
"'self'",
"https://api.openai.com",
"https://open.bigmodel.cn",
"https://*.openai.azure.com",
"https://api.anthropic.com",
"https://generativelanguage.googleapis.com"
],
},
},
crossOriginEmbedderPolicy: false
}));
// CORS配置
app.use(cors({
origin: process.env.NODE_ENV === 'production'
? (origin, callback) => {
// 生产环境的严格检查
const allowedOrigins = [
'http://localhost:5173',
'http://localhost:4173',
process.env.CORS_ORIGIN
].filter(Boolean);
// 允许所有.koyeb.app域名
if (!origin || origin.endsWith('.koyeb.app') || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
}
: true, // 开发环境允许所有域名
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization']
}));
// 基础中间件
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
app.use(requestLogger);
// 健康检查端点
app.get('/health', (req, res) => {
res.json({
status: 'ok',
timestamp: new Date().toISOString(),
database: 'connected'
});
});
// API健康检查端点用于Koyeb监控
app.get('/api/health', (req, res) => {
res.status(200).json({
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
database: 'connected'
});
});
// API路由
app.use('/api/auth', authRoutes);
app.use('/api/analysis', analysisRoutes);
app.use('/api/history', historyRoutes);
app.use('/api/profile', profileRoutes);
app.use('/api/download', downloadRoutes);
app.use('/api/ai-interpretation', aiInterpretationRoutes);
// 静态文件服务 (用于生产环境)
// 强制在 Koyeb 部署时启用静态文件服务
const isProduction = process.env.NODE_ENV === 'production' || process.env.PORT === '8000';
console.log('当前环境:', process.env.NODE_ENV);
console.log('端口:', process.env.PORT);
console.log('是否为生产环境:', isProduction);
if (isProduction) {
const distPath = path.join(__dirname, '../dist');
const indexPath = path.join(distPath, 'index.html');
console.log('静态文件目录:', distPath);
console.log('index.html路径:', indexPath);
// 检查文件是否存在
const fs = require('fs');
console.log('dist目录存在:', fs.existsSync(distPath));
console.log('index.html存在:', fs.existsSync(indexPath));
if (fs.existsSync(distPath)) {
console.log('dist目录内容:', fs.readdirSync(distPath));
}
app.use(express.static(distPath));
// SPA路由处理 - 只处理非API请求
app.get('*', (req, res, next) => {
if (req.path.startsWith('/api')) {
return next(); // 让后续的404处理器处理API请求
}
if (fs.existsSync(indexPath)) {
res.sendFile(indexPath);
} else {
console.error('index.html文件不存在:', indexPath);
res.status(404).json({
error: {
code: 'STATIC_FILE_NOT_FOUND',
message: '静态文件不存在,请检查构建过程'
}
});
}
});
}
// 404处理
app.use('*', (req, res) => {
res.status(404).json({
error: {
code: 'NOT_FOUND',
message: '请求的资源不存在'
}
});
});
// 错误处理中间件
app.use(errorHandler);
// 启动服务器
const server = app.listen(PORT, '0.0.0.0', () => {
console.log(`🚀 服务器运行在 http://0.0.0.0:${PORT}`);
console.log(`📊 数据库文件: ${path.resolve('./numerology.db')}`);
console.log(`🌍 环境: ${process.env.NODE_ENV || 'development'}`);
console.log(`🏥 健康检查: http://0.0.0.0:${PORT}/api/health`);
});
// 优雅关闭
process.on('SIGTERM', () => {
console.log('收到SIGTERM信号开始优雅关闭...');
server.close(() => {
console.log('HTTP服务器已关闭');
dbManager.close();
process.exit(0);
});
});
process.on('SIGINT', () => {
console.log('收到SIGINT信号开始优雅关闭...');
server.close(() => {
console.log('HTTP服务器已关闭');
dbManager.close();
process.exit(0);
});
});
// 未捕获异常处理
process.on('uncaughtException', (error) => {
console.error('未捕获的异常:', error);
dbManager.close();
process.exit(1);
});
process.on('unhandledRejection', (reason, promise) => {
console.error('未处理的Promise拒绝:', reason);
console.error('Promise:', promise);
});
module.exports = app;