| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458 |
- // 引入需要的模块
- 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`); // 新增提示
- });
|