/** * Web服务器脚本 * 提供静态文件服务和API接口 */ const http = require('http'); const fs = require('fs'); const path = require('path'); const url = require('url'); const dotenv = require('dotenv'); dotenv.config({path: ['.env.local', '.env'], quiet: true}); console.log('环境变量:', process.env.BASE_DIR); const BASE_DIR = process.env.BASE_DIR; const WEB_DIR = path.join(BASE_DIR, 'web'); const DATA_DIR = path.join(BASE_DIR, 'data'); const PIC_DIR = path.join(BASE_DIR, 'pic'); const PORT = process.env.PORT || 8080; // MIME类型映射 const mimeTypes = { '.html': 'text/html', '.css': 'text/css', '.js': 'application/javascript', '.json': 'application/json', '.png': 'image/png', '.jpg': 'image/jpeg', '.gif': 'image/gif', '.ico': 'image/x-icon' }; // 获取MIME类型 function getMimeType(filePath) { const ext = path.extname(filePath).toLowerCase(); return mimeTypes[ext] || 'application/octet-stream'; } // 读取文件 function readFile(filePath) { return new Promise((resolve, reject) => { fs.readFile(filePath, (err, data) => { if (err) reject(err); else resolve(data); }); }); } // 列出可用日期 function listAvailableDates() { try { const files = fs.readdirSync(DATA_DIR); return files .filter(f => f.endsWith('.json') && !f.includes('_raw') && !f.includes('test')) .map(f => f.replace('.json', '')) .sort() .reverse(); } catch (err) { return []; } } // 创建服务器 const server = http.createServer(async (req, res) => { const parsedUrl = url.parse(req.url, true); let pathname = parsedUrl.pathname; // 设置CORS头 res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); if (req.method === 'OPTIONS') { res.writeHead(200); res.end(); return; } try { // API: 列出可用日期 if (pathname === '/api/dates') { const dates = listAvailableDates(); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ dates })); return; } // API: 获取指定日期数据 if (pathname.startsWith('/api/data/')) { const date = pathname.replace('/api/data/', ''); const filePath = path.join(DATA_DIR, `${date}.json`); if (fs.existsSync(filePath)) { const data = await readFile(filePath); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(data); } else { res.writeHead(404, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: '数据不存在' })); } return; } // 静态文件服务 if (pathname === '/') { pathname = '/index.html'; } // 处理 data 和 pic 路径 let filePath; if (pathname.startsWith('/data/')) { filePath = path.join(DATA_DIR, pathname.replace('/data/', '')); } else if (pathname.startsWith('/pic/')) { filePath = path.join(PIC_DIR, pathname.replace('/pic/', '')); } else if (pathname.startsWith('/public/')) { filePath = path.join(BASE_DIR, pathname); } else { filePath = path.join(WEB_DIR, pathname); } // 安全检查:防止目录遍历 if (!filePath.startsWith(BASE_DIR)) { res.writeHead(403, { 'Content-Type': 'text/plain' }); res.end('Forbidden'); return; } const data = await readFile(filePath); const mimeType = getMimeType(filePath); res.writeHead(200, { 'Content-Type': mimeType }); res.end(data); } catch (err) { console.error('文件读取错误:', err); if (err.code === 'ENOENT') { res.writeHead(404, { 'Content-Type': 'text/plain' }); res.end('Not Found'); } else { console.error('服务器错误:', err); res.writeHead(500, { 'Content-Type': 'text/plain' }); res.end('Internal Server Error'); } } }); // 启动服务器 server.listen(PORT, 'localhost', () => { console.log('=========================================='); console.log('北京市房地产数据监控服务器已启动'); console.log(`访问地址: http://localhost:${PORT}`); console.log('按 Ctrl+C 停止服务器'); console.log('=========================================='); }); // 优雅关闭 process.on('SIGTERM', () => { console.log('\nSIGTERM\n正在关闭服务器...'); server.close(() => { console.log('服务器已关闭'); process.exit(0); }); }); process.on('SIGINT', () => { console.log('\nSIGINT\n正在关闭服务器...'); server.close(() => { console.log('服务器已关闭'); process.exit(0); }); });