server.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  1. // 引入需要的模块
  2. const express = require('express');
  3. const multer = require('multer');
  4. const cors = require('cors');
  5. const fs = require('fs');
  6. const path = require('path');
  7. // 创建Express应用
  8. const app = express();
  9. const PORT = 3001;
  10. // 解决跨域问题
  11. app.use(cors({
  12. origin: '*',
  13. methods: ['GET', 'POST', 'DELETE'],
  14. allowedHeaders: ['Content-Type']
  15. }));
  16. // ========== 关键修改1:解除Express请求体大小限制(支持大文件上传) ==========
  17. app.use(express.json({ limit: '1000mb' })); // JSON请求体限制提升至1000MB
  18. app.use(express.urlencoded({ extended: true, limit: '1000mb' })); // 表单请求体限制提升至1000MB
  19. // ======================== 核心修复1:数据文件处理(防JSON解析失败) ========================
  20. const dataFilePath = path.join(__dirname, 'data.json');
  21. // 初始化data.json(如果不存在则创建)
  22. const initDataFile = () => {
  23. const defaultData = {
  24. categories: [
  25. {
  26. parent_id: 1,
  27. name: "数字孪生项目",
  28. children: [{ id: 101, name: "平原河网" }, { id: 102, name: "水文站" }, { id: 103, name: "智慧楼宇" }, { id: 104, name: "灌区" }]
  29. },
  30. {
  31. parent_id: 2,
  32. name: "业务项目",
  33. children: [{ id: 201, name: "数据管理" }, { id: 202, name: "系统集成" }, { id: 203, name: "运维服务" }]
  34. }
  35. ],
  36. projects: [],
  37. project_files: []
  38. };
  39. try {
  40. if (!fs.existsSync(dataFilePath)) {
  41. fs.writeFileSync(dataFilePath, JSON.stringify(defaultData, null, 2), 'utf8');
  42. console.log('初始化data.json成功');
  43. }
  44. } catch (err) {
  45. console.error('初始化data.json失败:', err);
  46. }
  47. };
  48. initDataFile();
  49. // 读取data.json(带异常捕获)
  50. const readData = () => {
  51. try {
  52. if (!fs.existsSync(dataFilePath)) return { categories: [], projects: [], project_files: [] };
  53. const data = fs.readFileSync(dataFilePath, 'utf8');
  54. return JSON.parse(data);
  55. } catch (err) {
  56. console.error('读取data.json失败:', err);
  57. return { categories: [], projects: [], project_files: [] };
  58. }
  59. };
  60. // 写入data.json(带JSON校验)
  61. const writeData = (newData) => {
  62. try {
  63. const jsonStr = JSON.stringify(newData, null, 2);
  64. JSON.parse(jsonStr); // 验证JSON格式
  65. fs.writeFileSync(dataFilePath, jsonStr, { encoding: 'utf8', mode: 0o777 });
  66. return true;
  67. } catch (err) {
  68. console.error('写入data.json失败:', err);
  69. return false;
  70. }
  71. };
  72. // ======================== 核心修复2:文件存储目录(递归创建+权限) ========================
  73. const imageDir = path.join(__dirname, 'uploads/images');
  74. const videoDir = path.join(__dirname, 'uploads/videos');
  75. // 递归创建目录(防路径不存在)
  76. try {
  77. if (!fs.existsSync(imageDir)) fs.mkdirSync(imageDir, { recursive: true, mode: 0o777 });
  78. if (!fs.existsSync(videoDir)) fs.mkdirSync(videoDir, { recursive: true, mode: 0o777 });
  79. console.log('文件存储目录创建成功');
  80. } catch (err) {
  81. console.error('创建存储目录失败:', err);
  82. }
  83. // ======================== 核心修复3:multer配置(防文件类型/大小错误) ========================
  84. const storage = multer.diskStorage({
  85. destination: (req, file, cb) => {
  86. try {
  87. // 按文件类型分目录
  88. if (file.mimetype.startsWith('image/')) {
  89. cb(null, imageDir);
  90. } else if (file.mimetype.startsWith('video/')) {
  91. cb(null, videoDir);
  92. } else {
  93. cb(new Error('仅支持图片和视频文件!'), null);
  94. }
  95. } catch (err) {
  96. cb(err, null);
  97. }
  98. },
  99. filename: (req, file, cb) => {
  100. // 重命名文件(防特殊字符)
  101. const ext = path.extname(file.originalname);
  102. const fileName = Date.now() + '-' + Math.random().toString(36).substr(2, 8) + ext;
  103. cb(null, fileName);
  104. }
  105. });
  106. // ========== 关键修改2:提升文件大小限制至2GB(解决100M限制问题) ==========
  107. const upload = multer({
  108. storage: storage,
  109. limits: { fileSize: 2 * 1024 * 1024 * 1024 }, // 2GB(可根据需要调整)
  110. fileFilter: (req, file, cb) => {
  111. const allowTypes = ['image/jpeg', 'image/png', 'image/gif', 'video/mp4', 'video/avi', 'video/quicktime'];
  112. if (allowTypes.includes(file.mimetype)) {
  113. cb(null, true);
  114. } else {
  115. cb(new Error('仅支持JPG/PNG/GIF图片和MP4/AVI/MOV视频!'), false);
  116. }
  117. }
  118. }).single('file');
  119. // ======================== 接口:查询所有分类 ========================
  120. app.get('/api/categories', (req, res) => {
  121. try {
  122. const data = readData();
  123. res.json({ code: 200, data: data.categories });
  124. } catch (err) {
  125. console.error('/api/categories异常:', err);
  126. res.status(500).json({ code: 500, msg: '服务器错误:' + err.message });
  127. }
  128. });
  129. // ======================== 接口:查询项目列表 ========================
  130. app.get('/api/projects', (req, res) => {
  131. try {
  132. const { category_id, big_category_id } = req.query;
  133. const data = readData();
  134. // 全量查询
  135. if (big_category_id === 'all') {
  136. let filterProjects = data.projects;
  137. if (category_id && category_id !== 'all') {
  138. filterProjects = filterProjects.filter(item => item.category_id == category_id);
  139. }
  140. // ========== 关键优化:返回项目时附带已有功能名称列表(解决前端功能选择问题) ==========
  141. const projectsWithFuncs = filterProjects.map(project => {
  142. // 提取该项目的所有功能名称(去重)
  143. const funcNames = [...new Set(data.project_files.filter(f => f.project_id === project.id).map(f => f.func_name))];
  144. return { ...project, funcNames };
  145. });
  146. return res.json({ code: 200, data: projectsWithFuncs });
  147. }
  148. // 按大分类查询
  149. if (!big_category_id) {
  150. return res.status(400).json({ code: 400, msg: '请传入大分类ID!' });
  151. }
  152. let filterProjects = [];
  153. data.projects.forEach(item => {
  154. let targetSmallCategory = null;
  155. data.categories.forEach(bigCat => {
  156. const smallCat = bigCat.children.find(child => child.id == item.category_id);
  157. if (smallCat) targetSmallCategory = smallCat;
  158. });
  159. if (targetSmallCategory) {
  160. const targetBigCategory = data.categories.find(bigCat =>
  161. bigCat.children.some(child => child.id == targetSmallCategory.id)
  162. );
  163. if (targetBigCategory && targetBigCategory.parent_id == big_category_id) {
  164. filterProjects.push(item);
  165. }
  166. }
  167. });
  168. if (category_id && category_id !== 'all') {
  169. filterProjects = filterProjects.filter(item => item.category_id == category_id);
  170. }
  171. // ========== 关键优化:返回项目时附带已有功能名称列表 ==========
  172. const projectsWithFuncs = filterProjects.map(project => {
  173. const funcNames = [...new Set(data.project_files.filter(f => f.project_id === project.id).map(f => f.func_name))];
  174. return { ...project, funcNames };
  175. });
  176. res.json({ code: 200, data: projectsWithFuncs });
  177. } catch (err) {
  178. console.error('/api/projects异常:', err);
  179. res.status(500).json({ code: 500, msg: '服务器错误:' + err.message });
  180. }
  181. });
  182. // ======================== 接口:查询单个项目详情 ========================
  183. app.get('/api/project/:id', (req, res) => {
  184. try {
  185. const { id } = req.params;
  186. if (!id) return res.status(400).json({ code: 400, msg: '请传入项目ID!' });
  187. const data = readData();
  188. const project = data.projects.find(item => item.id == id);
  189. if (!project) return res.status(404).json({ code: 404, msg: '项目不存在!' });
  190. const files = data.project_files.filter(item => item.project_id == id);
  191. res.json({ code: 200, data: { project, files } });
  192. } catch (err) {
  193. console.error('/api/project/:id异常:', err);
  194. res.status(500).json({ code: 500, msg: '服务器错误:' + err.message });
  195. }
  196. });
  197. // ======================== 接口:创建新项目 ========================
  198. app.post('/api/project', (req, res) => {
  199. try {
  200. const { name, category_id, summary } = req.body;
  201. if (!name || !category_id) {
  202. return res.status(400).json({ code: 400, msg: '项目名称和分类ID不能为空!' });
  203. }
  204. const data = readData();
  205. const maxId = data.projects.length > 0 ? Math.max(...data.projects.map(item => item.id)) : 0;
  206. const newProject = {
  207. id: maxId + 1,
  208. name,
  209. category_id: Number(category_id),
  210. summary: summary || '',
  211. create_time: new Date().toISOString()
  212. };
  213. data.projects.push(newProject);
  214. const writeResult = writeData(data);
  215. if (writeResult) {
  216. res.json({ code: 200, msg: '项目创建成功!', data: { project_id: newProject.id } });
  217. } else {
  218. res.status(500).json({ code: 500, msg: '保存项目信息失败!' });
  219. }
  220. } catch (err) {
  221. console.error('/api/project POST异常:', err);
  222. res.status(500).json({ code: 500, msg: '服务器错误:' + err.message });
  223. }
  224. });
  225. // ======================== 核心修复4:上传功能文件接口(全量异常捕获) ========================
  226. app.post('/api/project/file', (req, res) => {
  227. // 手动调用multer(方便捕获错误)
  228. upload(req, res, async (err) => {
  229. try {
  230. // 1. 捕获multer文件上传错误
  231. if (err) {
  232. console.error('文件上传错误:', err);
  233. return res.status(400).json({ code: 400, msg: '文件上传失败:' + err.message });
  234. }
  235. // 2. 校验参数
  236. const { project_id, func_name } = req.body;
  237. if (!project_id || !func_name || !req.file) {
  238. return res.status(400).json({ code: 400, msg: '项目ID、功能名称和文件不能为空!' });
  239. }
  240. // 3. 校验项目是否存在
  241. const data = readData();
  242. const projectExist = data.projects.some(item => item.id == project_id);
  243. if (!projectExist) {
  244. return res.status(404).json({ code: 404, msg: '关联的项目不存在!' });
  245. }
  246. // 4. 组装文件信息(支持同功能名称追加文件,不新增功能)
  247. const fileType = req.file.mimetype.startsWith('image/') ? 'image' : 'video';
  248. const filePath = `/uploads/${fileType}s/${req.file.filename}`;
  249. const maxFileId = data.project_files.length > 0 ? Math.max(...data.project_files.map(item => item.id)) : 0;
  250. const newFile = {
  251. id: maxFileId + 1,
  252. project_id: Number(project_id),
  253. func_name, // 直接使用传入的功能名称,不做修改(核心:实现同功能追加)
  254. file_path: filePath,
  255. file_type: fileType,
  256. file_name: req.file.originalname,
  257. file_size: req.file.size,
  258. create_time: new Date().toISOString()
  259. };
  260. // 5. 写入数据(直接追加,不做功能名称去重,支持同功能多文件)
  261. data.project_files.push(newFile);
  262. const writeResult = writeData(data);
  263. if (writeResult) {
  264. res.json({ code: 200, msg: '功能文件上传成功!', data: newFile });
  265. } else {
  266. res.status(500).json({ code: 500, msg: '保存文件信息失败!' });
  267. }
  268. } catch (err) {
  269. // 捕获所有未预期的异常
  270. console.error('/api/project/file 严重异常:', err);
  271. res.status(500).json({ code: 500, msg: '服务器内部错误:' + err.message });
  272. }
  273. });
  274. });
  275. // ======================== 通用上传接口(兼容) ========================
  276. app.post('/api/upload', (req, res) => {
  277. upload(req, res, (err) => {
  278. try {
  279. if (err) throw err;
  280. if (!req.file) return res.status(400).json({ code: 400, msg: '请选择文件!' });
  281. const fileType = req.file.mimetype.startsWith('image/') ? 'image' : 'video';
  282. const filePath = `/uploads/${fileType}s/${req.file.filename}`;
  283. res.json({
  284. code: 200,
  285. msg: '文件上传成功!',
  286. data: {
  287. name: req.file.originalname,
  288. path: filePath,
  289. size: req.file.size,
  290. type: fileType
  291. }
  292. });
  293. } catch (err) {
  294. console.error('/api/upload异常:', err);
  295. res.status(500).json({ code: 500, msg: '服务器错误:' + err.message });
  296. }
  297. });
  298. });
  299. // ======================== 托管上传文件 ========================
  300. app.use('/uploads', express.static(path.join(__dirname, 'uploads')));
  301. // ======================== 接口:删除单个功能文件 ========================
  302. app.delete('/api/project/file/:fileId', (req, res) => {
  303. try {
  304. const { fileId } = req.params;
  305. if (!fileId) return res.status(400).json({ code: 400, msg: '请传入文件ID!' });
  306. const data = readData();
  307. const fileIndex = data.project_files.findIndex(item => item.id == fileId);
  308. if (fileIndex === -1) return res.status(404).json({ code: 404, msg: '文件不存在!' });
  309. // 删除物理文件(可选)
  310. const deletedFile = data.project_files[fileIndex];
  311. const filePath = path.join(__dirname, deletedFile.file_path.substring(1)); // 去掉开头的/
  312. if (fs.existsSync(filePath)) {
  313. fs.unlinkSync(filePath);
  314. console.log(`物理文件已删除:${filePath}`);
  315. }
  316. // 删除数据中的文件记录
  317. data.project_files.splice(fileIndex, 1);
  318. const writeResult = writeData(data);
  319. if (writeResult) {
  320. res.json({ code: 200, msg: '功能文件删除成功!' });
  321. } else {
  322. res.status(500).json({ code: 500, msg: '删除文件记录失败!' });
  323. }
  324. } catch (err) {
  325. console.error('/api/project/file/:fileId 删除异常:', err);
  326. res.status(500).json({ code: 500, msg: '服务器错误:' + err.message });
  327. }
  328. });
  329. // ======================== 接口:删除项目下的某个功能(所有文件) ========================
  330. app.delete('/api/project/func/:projectId/:funcName', (req, res) => {
  331. try {
  332. const { projectId, funcName } = req.params;
  333. if (!projectId || !funcName) {
  334. return res.status(400).json({ code: 400, msg: '项目ID和功能名称不能为空!' });
  335. }
  336. const data = readData();
  337. // 校验项目是否存在
  338. const projectExist = data.projects.some(item => item.id == projectId);
  339. if (!projectExist) return res.status(404).json({ code: 404, msg: '项目不存在!' });
  340. // 解码功能名称
  341. const decodeFuncName = decodeURIComponent(funcName);
  342. // 找到该功能下的所有文件
  343. const funcFiles = data.project_files.filter(item =>
  344. item.project_id == projectId && item.func_name === decodeFuncName
  345. );
  346. if (funcFiles.length === 0) return res.status(404).json({ code: 404, msg: '该功能下无文件!' });
  347. // 删除物理文件(可选)
  348. funcFiles.forEach(file => {
  349. const filePath = path.join(__dirname, file.file_path.substring(1));
  350. if (fs.existsSync(filePath)) {
  351. fs.unlinkSync(filePath);
  352. console.log(`物理文件已删除:${filePath}`);
  353. }
  354. });
  355. // 删除数据中的文件记录
  356. data.project_files = data.project_files.filter(item =>
  357. !(item.project_id == projectId && item.func_name === decodeFuncName)
  358. );
  359. const writeResult = writeData(data);
  360. if (writeResult) {
  361. res.json({ code: 200, msg: `成功删除【${decodeFuncName}】功能下的所有文件!` });
  362. } else {
  363. res.status(500).json({ code: 500, msg: '删除功能记录失败!' });
  364. }
  365. } catch (err) {
  366. console.error('/api/project/func/:projectId/:funcName 删除异常:', err);
  367. res.status(500).json({ code: 500, msg: '服务器错误:' + err.message });
  368. }
  369. });
  370. // ======================== 接口:删除整个项目(含所有文件) ========================
  371. app.delete('/api/project/:projectId', (req, res) => {
  372. try {
  373. const { projectId } = req.params;
  374. if (!projectId) return res.status(400).json({ code: 400, msg: '请传入项目ID!' });
  375. const data = readData();
  376. // 校验项目是否存在
  377. const projectIndex = data.projects.findIndex(item => item.id == projectId);
  378. if (projectIndex === -1) return res.status(404).json({ code: 404, msg: '项目不存在!' });
  379. // 删除项目下的所有物理文件(可选)
  380. const projectFiles = data.project_files.filter(item => item.project_id == projectId);
  381. projectFiles.forEach(file => {
  382. const filePath = path.join(__dirname, file.file_path.substring(1));
  383. if (fs.existsSync(filePath)) {
  384. fs.unlinkSync(filePath);
  385. console.log(`物理文件已删除:${filePath}`);
  386. }
  387. });
  388. // 删除项目记录和文件记录
  389. data.projects.splice(projectIndex, 1);
  390. data.project_files = data.project_files.filter(item => item.project_id != projectId);
  391. const writeResult = writeData(data);
  392. if (writeResult) {
  393. res.json({ code: 200, msg: '项目及旗下所有文件已成功删除!' });
  394. } else {
  395. res.status(500).json({ code: 500, msg: '删除项目记录失败!' });
  396. }
  397. } catch (err) {
  398. console.error('/api/project/:projectId 删除异常:', err);
  399. res.status(500).json({ code: 500, msg: '服务器错误:' + err.message });
  400. }
  401. });
  402. // ======================== 启动服务 ========================
  403. app.listen(PORT, () => {
  404. console.log(`✅ 后端服务已启动!访问地址:http://localhost:${PORT}`);
  405. console.log(`📂 分类查询接口:http://localhost:${PORT}/api/categories`);
  406. console.log(`🗂️ 项目查询接口:http://localhost:${PORT}/api/projects`);
  407. console.log(`🖼️ 文件访问前缀:http://localhost:${PORT}/uploads/`);
  408. console.log(`📤 支持最大文件上传大小:2GB`); // 新增提示
  409. });