// 引入需要的模块 const express = require('express'); const multer = require('multer'); const cors = require('cors'); const fs = require('fs'); const path = require('path'); // 创建Express应用 const app = express(); const PORT = 3001; // 解决跨域问题 app.use(cors({ origin: '*', methods: ['GET', 'POST', 'DELETE'], allowedHeaders: ['Content-Type'] })); // ========== 关键修改1:解除Express请求体大小限制(支持大文件上传) ========== app.use(express.json({ limit: '1000mb' })); // JSON请求体限制提升至1000MB app.use(express.urlencoded({ extended: true, limit: '1000mb' })); // 表单请求体限制提升至1000MB // ======================== 核心修复1:数据文件处理(防JSON解析失败) ======================== const dataFilePath = path.join(__dirname, 'data.json'); // 初始化data.json(如果不存在则创建) const initDataFile = () => { const defaultData = { categories: [ { parent_id: 1, name: "数字孪生项目", children: [{ id: 101, name: "平原河网" }, { id: 102, name: "水文站" }, { id: 103, name: "智慧楼宇" }, { id: 104, name: "灌区" }] }, { parent_id: 2, name: "业务项目", children: [{ id: 201, name: "数据管理" }, { id: 202, name: "系统集成" }, { id: 203, name: "运维服务" }] } ], projects: [], project_files: [] }; try { if (!fs.existsSync(dataFilePath)) { fs.writeFileSync(dataFilePath, JSON.stringify(defaultData, null, 2), 'utf8'); console.log('初始化data.json成功'); } } catch (err) { console.error('初始化data.json失败:', err); } }; initDataFile(); // 读取data.json(带异常捕获) const readData = () => { try { if (!fs.existsSync(dataFilePath)) return { categories: [], projects: [], project_files: [] }; const data = fs.readFileSync(dataFilePath, 'utf8'); return JSON.parse(data); } catch (err) { console.error('读取data.json失败:', err); return { categories: [], projects: [], project_files: [] }; } }; // 写入data.json(带JSON校验) const writeData = (newData) => { try { const jsonStr = JSON.stringify(newData, null, 2); JSON.parse(jsonStr); // 验证JSON格式 fs.writeFileSync(dataFilePath, jsonStr, { encoding: 'utf8', mode: 0o777 }); return true; } catch (err) { console.error('写入data.json失败:', err); return false; } }; // ======================== 核心修复2:文件存储目录(递归创建+权限) ======================== const imageDir = path.join(__dirname, 'uploads/images'); const videoDir = path.join(__dirname, 'uploads/videos'); // 递归创建目录(防路径不存在) try { if (!fs.existsSync(imageDir)) fs.mkdirSync(imageDir, { recursive: true, mode: 0o777 }); if (!fs.existsSync(videoDir)) fs.mkdirSync(videoDir, { recursive: true, mode: 0o777 }); console.log('文件存储目录创建成功'); } catch (err) { console.error('创建存储目录失败:', err); } // ======================== 核心修复3:multer配置(防文件类型/大小错误) ======================== const storage = multer.diskStorage({ destination: (req, file, cb) => { try { // 按文件类型分目录 if (file.mimetype.startsWith('image/')) { cb(null, imageDir); } else if (file.mimetype.startsWith('video/')) { cb(null, videoDir); } else { cb(new Error('仅支持图片和视频文件!'), null); } } catch (err) { cb(err, null); } }, filename: (req, file, cb) => { // 重命名文件(防特殊字符) const ext = path.extname(file.originalname); const fileName = Date.now() + '-' + Math.random().toString(36).substr(2, 8) + ext; cb(null, fileName); } }); // ========== 关键修改2:提升文件大小限制至2GB(解决100M限制问题) ========== const upload = multer({ storage: storage, limits: { fileSize: 2 * 1024 * 1024 * 1024 }, // 2GB(可根据需要调整) fileFilter: (req, file, cb) => { const allowTypes = ['image/jpeg', 'image/png', 'image/gif', 'video/mp4', 'video/avi', 'video/quicktime']; if (allowTypes.includes(file.mimetype)) { cb(null, true); } else { cb(new Error('仅支持JPG/PNG/GIF图片和MP4/AVI/MOV视频!'), false); } } }).single('file'); // ======================== 接口:查询所有分类 ======================== app.get('/api/categories', (req, res) => { try { const data = readData(); res.json({ code: 200, data: data.categories }); } catch (err) { console.error('/api/categories异常:', err); res.status(500).json({ code: 500, msg: '服务器错误:' + err.message }); } }); // ======================== 接口:查询项目列表 ======================== app.get('/api/projects', (req, res) => { try { const { category_id, big_category_id } = req.query; const data = readData(); // 全量查询 if (big_category_id === 'all') { let filterProjects = data.projects; if (category_id && category_id !== 'all') { filterProjects = filterProjects.filter(item => item.category_id == category_id); } // ========== 关键优化:返回项目时附带已有功能名称列表(解决前端功能选择问题) ========== const projectsWithFuncs = filterProjects.map(project => { // 提取该项目的所有功能名称(去重) const funcNames = [...new Set(data.project_files.filter(f => f.project_id === project.id).map(f => f.func_name))]; return { ...project, funcNames }; }); return res.json({ code: 200, data: projectsWithFuncs }); } // 按大分类查询 if (!big_category_id) { return res.status(400).json({ code: 400, msg: '请传入大分类ID!' }); } let filterProjects = []; data.projects.forEach(item => { let targetSmallCategory = null; data.categories.forEach(bigCat => { const smallCat = bigCat.children.find(child => child.id == item.category_id); if (smallCat) targetSmallCategory = smallCat; }); if (targetSmallCategory) { const targetBigCategory = data.categories.find(bigCat => bigCat.children.some(child => child.id == targetSmallCategory.id) ); if (targetBigCategory && targetBigCategory.parent_id == big_category_id) { filterProjects.push(item); } } }); if (category_id && category_id !== 'all') { filterProjects = filterProjects.filter(item => item.category_id == category_id); } // ========== 关键优化:返回项目时附带已有功能名称列表 ========== const projectsWithFuncs = filterProjects.map(project => { const funcNames = [...new Set(data.project_files.filter(f => f.project_id === project.id).map(f => f.func_name))]; return { ...project, funcNames }; }); res.json({ code: 200, data: projectsWithFuncs }); } catch (err) { console.error('/api/projects异常:', err); res.status(500).json({ code: 500, msg: '服务器错误:' + err.message }); } }); // ======================== 接口:查询单个项目详情 ======================== app.get('/api/project/:id', (req, res) => { try { const { id } = req.params; if (!id) return res.status(400).json({ code: 400, msg: '请传入项目ID!' }); const data = readData(); const project = data.projects.find(item => item.id == id); if (!project) return res.status(404).json({ code: 404, msg: '项目不存在!' }); const files = data.project_files.filter(item => item.project_id == id); res.json({ code: 200, data: { project, files } }); } catch (err) { console.error('/api/project/:id异常:', err); res.status(500).json({ code: 500, msg: '服务器错误:' + err.message }); } }); // ======================== 接口:创建新项目 ======================== app.post('/api/project', (req, res) => { try { const { name, category_id, summary } = req.body; if (!name || !category_id) { return res.status(400).json({ code: 400, msg: '项目名称和分类ID不能为空!' }); } const data = readData(); const maxId = data.projects.length > 0 ? Math.max(...data.projects.map(item => item.id)) : 0; const newProject = { id: maxId + 1, name, category_id: Number(category_id), summary: summary || '', create_time: new Date().toISOString() }; data.projects.push(newProject); const writeResult = writeData(data); if (writeResult) { res.json({ code: 200, msg: '项目创建成功!', data: { project_id: newProject.id } }); } else { res.status(500).json({ code: 500, msg: '保存项目信息失败!' }); } } catch (err) { console.error('/api/project POST异常:', err); res.status(500).json({ code: 500, msg: '服务器错误:' + err.message }); } }); // ======================== 核心修复4:上传功能文件接口(全量异常捕获) ======================== app.post('/api/project/file', (req, res) => { // 手动调用multer(方便捕获错误) upload(req, res, async (err) => { try { // 1. 捕获multer文件上传错误 if (err) { console.error('文件上传错误:', err); return res.status(400).json({ code: 400, msg: '文件上传失败:' + err.message }); } // 2. 校验参数 const { project_id, func_name } = req.body; if (!project_id || !func_name || !req.file) { return res.status(400).json({ code: 400, msg: '项目ID、功能名称和文件不能为空!' }); } // 3. 校验项目是否存在 const data = readData(); const projectExist = data.projects.some(item => item.id == project_id); if (!projectExist) { return res.status(404).json({ code: 404, msg: '关联的项目不存在!' }); } // 4. 组装文件信息(支持同功能名称追加文件,不新增功能) const fileType = req.file.mimetype.startsWith('image/') ? 'image' : 'video'; const filePath = `/uploads/${fileType}s/${req.file.filename}`; const maxFileId = data.project_files.length > 0 ? Math.max(...data.project_files.map(item => item.id)) : 0; const newFile = { id: maxFileId + 1, project_id: Number(project_id), func_name, // 直接使用传入的功能名称,不做修改(核心:实现同功能追加) file_path: filePath, file_type: fileType, file_name: req.file.originalname, file_size: req.file.size, create_time: new Date().toISOString() }; // 5. 写入数据(直接追加,不做功能名称去重,支持同功能多文件) data.project_files.push(newFile); const writeResult = writeData(data); if (writeResult) { res.json({ code: 200, msg: '功能文件上传成功!', data: newFile }); } else { res.status(500).json({ code: 500, msg: '保存文件信息失败!' }); } } catch (err) { // 捕获所有未预期的异常 console.error('/api/project/file 严重异常:', err); res.status(500).json({ code: 500, msg: '服务器内部错误:' + err.message }); } }); }); // ======================== 通用上传接口(兼容) ======================== app.post('/api/upload', (req, res) => { upload(req, res, (err) => { try { if (err) throw err; if (!req.file) return res.status(400).json({ code: 400, msg: '请选择文件!' }); const fileType = req.file.mimetype.startsWith('image/') ? 'image' : 'video'; const filePath = `/uploads/${fileType}s/${req.file.filename}`; res.json({ code: 200, msg: '文件上传成功!', data: { name: req.file.originalname, path: filePath, size: req.file.size, type: fileType } }); } catch (err) { console.error('/api/upload异常:', err); res.status(500).json({ code: 500, msg: '服务器错误:' + err.message }); } }); }); // ======================== 托管上传文件 ======================== app.use('/uploads', express.static(path.join(__dirname, 'uploads'))); // ======================== 接口:删除单个功能文件 ======================== app.delete('/api/project/file/:fileId', (req, res) => { try { const { fileId } = req.params; if (!fileId) return res.status(400).json({ code: 400, msg: '请传入文件ID!' }); const data = readData(); const fileIndex = data.project_files.findIndex(item => item.id == fileId); if (fileIndex === -1) return res.status(404).json({ code: 404, msg: '文件不存在!' }); // 删除物理文件(可选) const deletedFile = data.project_files[fileIndex]; const filePath = path.join(__dirname, deletedFile.file_path.substring(1)); // 去掉开头的/ if (fs.existsSync(filePath)) { fs.unlinkSync(filePath); console.log(`物理文件已删除:${filePath}`); } // 删除数据中的文件记录 data.project_files.splice(fileIndex, 1); const writeResult = writeData(data); if (writeResult) { res.json({ code: 200, msg: '功能文件删除成功!' }); } else { res.status(500).json({ code: 500, msg: '删除文件记录失败!' }); } } catch (err) { console.error('/api/project/file/:fileId 删除异常:', err); res.status(500).json({ code: 500, msg: '服务器错误:' + err.message }); } }); // ======================== 接口:删除项目下的某个功能(所有文件) ======================== app.delete('/api/project/func/:projectId/:funcName', (req, res) => { try { const { projectId, funcName } = req.params; if (!projectId || !funcName) { return res.status(400).json({ code: 400, msg: '项目ID和功能名称不能为空!' }); } const data = readData(); // 校验项目是否存在 const projectExist = data.projects.some(item => item.id == projectId); if (!projectExist) return res.status(404).json({ code: 404, msg: '项目不存在!' }); // 解码功能名称 const decodeFuncName = decodeURIComponent(funcName); // 找到该功能下的所有文件 const funcFiles = data.project_files.filter(item => item.project_id == projectId && item.func_name === decodeFuncName ); if (funcFiles.length === 0) return res.status(404).json({ code: 404, msg: '该功能下无文件!' }); // 删除物理文件(可选) funcFiles.forEach(file => { const filePath = path.join(__dirname, file.file_path.substring(1)); if (fs.existsSync(filePath)) { fs.unlinkSync(filePath); console.log(`物理文件已删除:${filePath}`); } }); // 删除数据中的文件记录 data.project_files = data.project_files.filter(item => !(item.project_id == projectId && item.func_name === decodeFuncName) ); const writeResult = writeData(data); if (writeResult) { res.json({ code: 200, msg: `成功删除【${decodeFuncName}】功能下的所有文件!` }); } else { res.status(500).json({ code: 500, msg: '删除功能记录失败!' }); } } catch (err) { console.error('/api/project/func/:projectId/:funcName 删除异常:', err); res.status(500).json({ code: 500, msg: '服务器错误:' + err.message }); } }); // ======================== 接口:删除整个项目(含所有文件) ======================== app.delete('/api/project/:projectId', (req, res) => { try { const { projectId } = req.params; if (!projectId) return res.status(400).json({ code: 400, msg: '请传入项目ID!' }); const data = readData(); // 校验项目是否存在 const projectIndex = data.projects.findIndex(item => item.id == projectId); if (projectIndex === -1) return res.status(404).json({ code: 404, msg: '项目不存在!' }); // 删除项目下的所有物理文件(可选) const projectFiles = data.project_files.filter(item => item.project_id == projectId); projectFiles.forEach(file => { const filePath = path.join(__dirname, file.file_path.substring(1)); if (fs.existsSync(filePath)) { fs.unlinkSync(filePath); console.log(`物理文件已删除:${filePath}`); } }); // 删除项目记录和文件记录 data.projects.splice(projectIndex, 1); data.project_files = data.project_files.filter(item => item.project_id != projectId); const writeResult = writeData(data); if (writeResult) { res.json({ code: 200, msg: '项目及旗下所有文件已成功删除!' }); } else { res.status(500).json({ code: 500, msg: '删除项目记录失败!' }); } } catch (err) { console.error('/api/project/:projectId 删除异常:', err); res.status(500).json({ code: 500, msg: '服务器错误:' + err.message }); } }); // ======================== 启动服务 ======================== app.listen(PORT, () => { console.log(`✅ 后端服务已启动!访问地址:http://localhost:${PORT}`); console.log(`📂 分类查询接口:http://localhost:${PORT}/api/categories`); console.log(`🗂️ 项目查询接口:http://localhost:${PORT}/api/projects`); console.log(`🖼️ 文件访问前缀:http://localhost:${PORT}/uploads/`); console.log(`📤 支持最大文件上传大小:2GB`); // 新增提示 });