const express = require('express'); const multer = require('multer'); const path = require('path'); const fs = require('fs'); const app = express(); const PORT = process.env.PORT || 3000; // 中间件配置 app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use(express.static('public')); app.use('/uploads', express.static('uploads')); // 确保必要的目录存在 const dataDir = path.join(__dirname, 'data'); const uploadsDir = path.join(__dirname, 'uploads'); if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir); if (!fs.existsSync(uploadsDir)) fs.mkdirSync(uploadsDir); // 数据文件路径 const dataFile = path.join(dataDir, 'data.json'); // 初始化数据文件 if (!fs.existsSync(dataFile)) { fs.writeFileSync(dataFile, JSON.stringify({ records: [], nextId: 1 }, null, 2)); } // 读取数据 function readData() { const raw = fs.readFileSync(dataFile, 'utf-8'); return JSON.parse(raw); } // 写入数据 function writeData(data) { fs.writeFileSync(dataFile, JSON.stringify(data, null, 2)); } // 配置 multer 图片上传 const storage = multer.diskStorage({ destination: (req, file, cb) => { cb(null, uploadsDir); }, filename: (req, file, cb) => { const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9); cb(null, uniqueSuffix + path.extname(file.originalname)); } }); const upload = multer({ storage: storage, limits: { fileSize: 5 * 1024 * 1024 }, // 5MB fileFilter: (req, file, cb) => { const allowedTypes = /jpeg|jpg|png|gif|webp/; const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase()); const mimetype = allowedTypes.test(file.mimetype); if (extname && mimetype) { cb(null, true); } else { cb(new Error('只支持图片格式:jpeg, jpg, png, gif, webp')); } } }); // URL 格式校验 function isValidUrl(string) { try { const url = new URL(string); return url.protocol === 'http:' || url.protocol === 'https:'; } catch (_) { return false; } } // 格式化时间为精确到分钟 function getFormattedTime() { const now = new Date(); const year = now.getFullYear(); const month = String(now.getMonth() + 1).padStart(2, '0'); const day = String(now.getDate()).padStart(2, '0'); const hours = String(now.getHours()).padStart(2, '0'); const minutes = String(now.getMinutes()).padStart(2, '0'); return `${year}-${month}-${day} ${hours}:${minutes}`; } // API: 获取所有记录 app.get('/api/list', (req, res) => { try { const data = readData(); res.json({ success: true, records: data.records }); } catch (error) { res.status(500).json({ success: false, message: '读取数据失败: ' + error.message }); } }); // API: 普通用户提交记录(不含图片) app.post('/api/submit', (req, res) => { try { const { name, link, note } = req.body; // 数据校验 if (!name || !name.trim()) { return res.status(400).json({ success: false, message: '人名不能为空' }); } if (!link || !link.trim()) { return res.status(400).json({ success: false, message: '牌谱链接不能为空' }); } if (!isValidUrl(link)) { return res.status(400).json({ success: false, message: '牌谱链接格式不正确' }); } // 备注改为非必填,仅验证长度 if (note && note.trim() && note.length > 200) { return res.status(400).json({ success: false, message: '备注说明不能超过 200 字' }); } const data = readData(); // 创建新记录(无图片) const newRecord = { id: data.nextId, submitTime: getFormattedTime(), name: name.trim(), link: link.trim(), note: note ? note.trim() : '', nagaLink: '', nagaNote: '', images: [] }; data.records.push(newRecord); data.nextId += 1; writeData(data); res.json({ success: true, message: '提交成功', record: newRecord }); } catch (error) { res.status(500).json({ success: false, message: '提交失败: ' + error.message }); } }); // API: 管理员更新 Naga 字段和图片 app.post('/api/admin/update', upload.array('images', 10), (req, res) => { try { const { id, nagaLink, nagaNote, existingImages } = req.body; if (!id) { return res.status(400).json({ success: false, message: '记录 ID 不能为空' }); } // Naga 链接校验(如果填写) if (nagaLink && nagaLink.trim() && !isValidUrl(nagaLink)) { return res.status(400).json({ success: false, message: 'Naga 链接格式不正确' }); } // Naga 说明字数校验(如果填写) if (nagaNote && nagaNote.length > 200) { return res.status(400).json({ success: false, message: 'Naga 说明不能超过 200 字' }); } const data = readData(); const record = data.records.find(r => r.id === parseInt(id)); if (!record) { return res.status(404).json({ success: false, message: '记录不存在' }); } // 处理新上传的图片 const newImages = req.files ? req.files.map(file => `/uploads/${file.filename}`) : []; // 合并已有图片和新图片 let allImages = []; if (existingImages) { // existingImages 可能是字符串或数组 if (typeof existingImages === 'string') { try { allImages = JSON.parse(existingImages); } catch (e) { allImages = existingImages ? [existingImages] : []; } } else if (Array.isArray(existingImages)) { allImages = existingImages; } } allImages = [...allImages, ...newImages]; // 更新 Naga 字段和图片 record.nagaLink = nagaLink ? nagaLink.trim() : ''; record.nagaNote = nagaNote ? nagaNote.trim() : ''; record.images = allImages; writeData(data); res.json({ success: true, message: '更新成功', record: record }); } catch (error) { res.status(500).json({ success: false, message: '更新失败: ' + error.message }); } }); // API: 管理员删除单张图片 app.delete('/api/admin/delete-image', (req, res) => { try { const { id, imagePath } = req.body; if (!id || !imagePath) { return res.status(400).json({ success: false, message: '参数不完整' }); } const data = readData(); const record = data.records.find(r => r.id === parseInt(id)); if (!record) { return res.status(404).json({ success: false, message: '记录不存在' }); } // 从记录中移除图片路径 const imageIndex = record.images.indexOf(imagePath); if (imageIndex > -1) { record.images.splice(imageIndex, 1); } // 删除物理文件 const filePath = path.join(__dirname, imagePath.replace(/^\//, '')); if (fs.existsSync(filePath)) { fs.unlinkSync(filePath); } writeData(data); res.json({ success: true, message: '图片删除成功', record: record }); } catch (error) { res.status(500).json({ success: false, message: '删除图片失败: ' + error.message }); } }); // API: 删除记录 app.delete('/api/admin/delete/:id', (req, res) => { try { const id = parseInt(req.params.id); const data = readData(); const index = data.records.findIndex(r => r.id === id); if (index === -1) { return res.status(404).json({ success: false, message: '记录不存在' }); } // 删除关联的图片文件 const record = data.records[index]; if (record.images && record.images.length > 0) { record.images.forEach(imagePath => { const filePath = path.join(__dirname, imagePath.replace(/^\//, '')); if (fs.existsSync(filePath)) { fs.unlinkSync(filePath); } }); } data.records.splice(index, 1); writeData(data); res.json({ success: true, message: '删除成功' }); } catch (error) { res.status(500).json({ success: false, message: '删除失败: ' + error.message }); } }); // 启动服务器 app.listen(PORT, '0.0.0.0', () => { console.log(`服务器运行在端口 ${PORT}`); console.log(`普通访客页面: http://localhost:${PORT}`); console.log(`管理员页面: http://localhost:${PORT}/admin.html`); });