Files
website/nodejs-project/naga/server.js
2026-01-18 20:39:34 +08:00

272 lines
8.0 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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`);
});