gangbi_web/backend/server.js
2025-02-07 13:10:01 +08:00

724 lines
20 KiB
JavaScript
Raw 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.

// 确保在最开始就加载环境变量
require('dotenv').config();
console.log('Environment loaded:', {
DB_HOST: process.env.DB_HOST,
DB_USER: process.env.DB_USER,
DB_DATABASE: process.env.DB_DATABASE,
PORT: process.env.PORT
});
const express = require('express');
const http = require('http');
const cors = require('cors');
const multer = require('multer');
const path = require('path');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const auth = require('./middleware/auth');
const checkPermission = require('./middleware/checkPermission');
const db = require('./utils/db');
const app = express();
const systemSettingsRoutes = require('./routes/systemSettings');
const messagesRoutes = require('./routes/messages');
const tagsRoutes = require('./routes/tags');
const adminRoutes = require('./routes/admin');
const server = http.createServer(app);
const { initWebSocket } = require('./websocket');
// 请求日志中间件
app.use((req, res, next) => {
console.log(`${new Date().toISOString()} ${req.method} ${req.url}`);
next();
});
// 中间件
app.use(cors());
app.use(express.json());
// 配置文件上传
const storage = multer.diskStorage({
destination: (req, file, cb) => {
let uploadPath = 'uploads';
if (file.mimetype.startsWith('image/')) {
uploadPath += '/images';
} else if (file.mimetype.startsWith('video/')) {
uploadPath += '/videos';
}
// 确保目录存在
require('fs').mkdirSync(path.join(__dirname, uploadPath), { recursive: true });
cb(null, uploadPath);
},
filename: (req, file, cb) => {
cb(null, Date.now() + path.extname(file.originalname));
}
});
const upload = multer({
storage,
limits: {
fileSize: 10 * 1024 * 1024, // 10MB limit
},
fileFilter: (req, file, cb) => {
const allowedTypes = /jpeg|jpg|png|gif|mp4|webm/;
const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase());
const mimetype = allowedTypes.test(file.mimetype);
if (extname && mimetype) {
return cb(null, true);
}
cb(new Error('只支持图片和视频文件!'));
}
});
// API路由
app.get('/api/categories', async (req, res) => {
try {
const [categories] = await db.query('SELECT * FROM categories');
res.json(categories);
} catch (error) {
res.status(500).json({ message: error.message });
}
});
app.get('/api/games', async (req, res) => {
try {
const [games] = await db.query(`
SELECT g.*, c.name as category_name,
GROUP_CONCAT(DISTINCT gp.platform) as platforms,
GROUP_CONCAT(DISTINCT gt.tag) as tags
FROM games g
LEFT JOIN categories c ON g.category_id = c.id
LEFT JOIN game_platforms gp ON g.id = gp.game_id
LEFT JOIN game_tags gt ON g.id = gt.game_id
GROUP BY g.id
`);
// 处理平台和标签字符串
games.forEach(game => {
game.platforms = game.platforms ? game.platforms.split(',') : [];
game.tags = game.tags ? game.tags.split(',') : [];
});
res.json(games);
} catch (error) {
res.status(500).json({ message: error.message });
}
});
app.get('/api/games/:id', async (req, res) => {
try {
const game = await Game.findById(req.params.id);
if (!game) {
return res.status(404).json({ message: 'Game not found' });
}
// 获取游戏对应的分类信息
const category = await Category.findById(game.categoryId);
const gameWithCategory = {
...game.toObject(),
category
};
res.json(gameWithCategory);
} catch (error) {
res.status(500).json({ message: error.message });
}
});
app.get('/api/games/search', async (req, res) => {
try {
const { query, category } = req.query;
let searchQuery = {};
if (query) {
searchQuery.$or = [
{ title: new RegExp(query, 'i') },
{ description: new RegExp(query, 'i') }
];
}
if (category) {
searchQuery.categoryId = category;
}
const games = await Game.find(searchQuery);
res.json(games);
} catch (error) {
res.status(500).json({ message: error.message });
}
});
// 添加游戏
app.post('/api/games', auth, upload.single('image'), async (req, res) => {
try {
const { title, description, category_id, developer, release_date, platforms, tags } = req.body;
// 验证必填字段
if (!title || !category_id) {
return res.status(400).json({ message: '标题和分类为必填项' });
}
// 解析平台和标签数据因为是通过FormData传输的
const platformsArray = Array.isArray(platforms) ? platforms : platforms ? [platforms] : [];
const tagsArray = Array.isArray(tags) ? tags : tags ? [tags] : [];
const [result] = await db.query(
'INSERT INTO games (title, description, category_id, developer, release_date, image) VALUES (?, ?, ?, ?, ?, ?)',
[
title,
description || '',
parseInt(category_id),
developer || '',
release_date || null,
req.file ? req.file.path : null
]
);
const gameId = result.insertId;
// 添加平台信息
if (platformsArray.length > 0) {
const platformValues = platformsArray.map(platform => [gameId, platform]);
await db.query(
'INSERT INTO game_platforms (game_id, platform) VALUES ?',
[platformValues]
);
}
// 添加标签信息
if (tagsArray.length > 0) {
const tagValues = tagsArray.map(tag => [gameId, tag]);
await db.query(
'INSERT INTO game_tags (game_id, tag) VALUES ?',
[tagValues]
);
}
res.status(201).json({
message: '游戏添加成功',
id: gameId
});
} catch (error) {
console.error('Error adding game:', error);
res.status(500).json({
message: '添加游戏失败',
error: process.env.NODE_ENV === 'development' ? error.message : undefined
});
}
});
// 管理员注册
app.post('/api/admin/register', async (req, res) => {
try {
const { username, password, email } = req.body;
// 验证必填字段
if (!username || !password || !email) {
return res.status(400).json({ message: '用户名、密码和邮箱都是必填项' });
}
// 检查用户名是否已存在
const [existingUsers] = await db.query(
'SELECT id FROM admins WHERE username = ?',
[username]
);
if (existingUsers.length > 0) {
return res.status(400).json({ message: '用户名已存在' });
}
// 检查邮箱是否已存在
const [existingEmails] = await db.query(
'SELECT id FROM admins WHERE email = ?',
[email]
);
if (existingEmails.length > 0) {
return res.status(400).json({ message: '邮箱已被使用' });
}
// 加密密码
const hashedPassword = await bcrypt.hash(password, 10);
console.log('Hashed password:', hashedPassword);
// 开始事务
const connection = await db.getConnection();
await connection.beginTransaction();
try {
// 插入管理员记录
const [result] = await connection.query(
'INSERT INTO admins (username, password, email, role, status) VALUES (?, ?, ?, ?, ?)',
[username, hashedPassword, email, 'editor', 'active']
);
// 插入默认权限
await connection.query(
'INSERT INTO admin_permissions (admin_id, permission) VALUES (?, ?), (?, ?)',
[result.insertId, 'game:manage', result.insertId, 'media:manage']
);
await connection.commit();
// 生成JWT令牌
const token = jwt.sign(
{ id: result.insertId, role: 'editor' },
process.env.JWT_SECRET,
{ expiresIn: '24h' }
);
res.status(201).json({
message: '注册成功',
token,
admin: {
username,
role: 'editor',
permissions: ['game:manage', 'media:manage']
}
});
} catch (error) {
await connection.rollback();
throw error;
} finally {
connection.release();
}
} catch (error) {
console.error('Registration error:', error);
res.status(500).json({ message: '注册失败', error: error.message });
}
});
// 管理员登录
app.post('/api/admin/login', async (req, res) => {
try {
const { username, password } = req.body;
console.log('Login attempt:', { username }); // 添加登录尝试日志
const [admins] = await db.query(
'SELECT a.*, GROUP_CONCAT(ap.permission) as permissions FROM admins a ' +
'LEFT JOIN admin_permissions ap ON a.id = ap.admin_id ' +
'WHERE a.username = ? AND a.status = "active" ' +
'GROUP BY a.id',
[username]
);
console.log('Query result:', admins); // 添加查询结果日志
const admin = admins[0];
if (!admin) {
console.log('Admin not found');
return res.status(401).json({ message: '用户名或密码错误' });
}
console.log('Comparing password for:', admin.username);
console.log('Encrypted password:', admin.password);
const hashedPassword = await bcrypt.hash(password, 10);
console.log('Hashed password:', hashedPassword);
const isMatch = await bcrypt.compare(password, admin.password);
console.log('Password match result:', isMatch);
if (!isMatch) {
console.log('Password does not match');
return res.status(401).json({ message: '用户名或密码错误' });
}
// 更新最后登录时间
await db.query(
'UPDATE admins SET last_login = NOW() WHERE id = ?',
[admin.id]
);
const token = jwt.sign(
{ id: admin.id, role: admin.role },
process.env.JWT_SECRET,
{ expiresIn: '24h' }
);
console.log('Login successful for:', admin.username);
res.json({
token,
admin: {
username: admin.username,
role: admin.role,
permissions: admin.permissions ? admin.permissions.split(',') : []
}
});
} catch (error) {
console.error('Login error:', error);
res.status(500).json({ message: error.message });
}
});
// 媒体文件上传
app.post('/api/media/upload', auth, upload.single('file'), async (req, res) => {
try {
if (!req.file) {
return res.status(400).json({ message: '请选择要上传的文件' });
}
const media = new Media({
filename: req.file.filename,
originalname: req.file.originalname,
mimetype: req.file.mimetype,
size: req.file.size,
url: `/uploads/${req.file.filename}`,
type: req.file.mimetype.startsWith('image/') ? 'image' : 'video',
uploadedBy: req.admin.id
});
await media.save();
res.json(media);
} catch (error) {
res.status(500).json({ message: error.message });
}
});
// 获取媒体文件列表
app.get('/api/media', auth, async (req, res) => {
try {
const { type, gameId } = req.query;
let query = {};
if (type) query.type = type;
if (gameId) query.gameId = gameId;
const media = await Media.find(query)
.populate('uploadedBy', 'username')
.sort('-uploadedAt');
res.json(media);
} catch (error) {
res.status(500).json({ message: error.message });
}
});
// 删除媒体文件
app.delete('/api/media/:id', auth, async (req, res) => {
try {
const media = await Media.findById(req.params.id);
if (!media) {
return res.status(404).json({ message: '文件不存在' });
}
// 删除物理文件
const filePath = path.join(__dirname, media.url);
fs.unlink(filePath, async (err) => {
if (err) {
console.error('Error deleting file:', err);
}
await media.remove();
res.json({ message: '文件已删除' });
});
} catch (error) {
res.status(500).json({ message: error.message });
}
});
// 更新游戏
app.put('/api/games/:id', auth, upload.single('image'), async (req, res) => {
try {
const { title, description, category_id, developer, release_date, platforms, tags } = req.body;
// 验证游戏是否存在
const [existingGame] = await db.query(
'SELECT * FROM games WHERE id = ?',
[req.params.id]
);
if (!existingGame.length) {
return res.status(404).json({ message: '游戏不存在' });
}
// 更新游戏基本信息
await db.query(
`UPDATE games SET
title = ?,
description = ?,
category_id = ?,
developer = ?,
release_date = ?,
image = ?
WHERE id = ?`,
[
title,
description || '',
parseInt(category_id),
developer || '',
release_date || null,
req.file ? req.file.path : existingGame[0].image,
req.params.id
]
);
// 更新平台信息
if (platforms) {
// 先删除旧的平台信息
await db.query('DELETE FROM game_platforms WHERE game_id = ?', [req.params.id]);
// 添加新的平台信息
if (platforms.length > 0) {
const platformValues = platforms.map(platform => [req.params.id, platform]);
await db.query(
'INSERT INTO game_platforms (game_id, platform) VALUES ?',
[platformValues]
);
}
}
// 更新标签信息
if (tags) {
// 先删除旧的标签信息
await db.query('DELETE FROM game_tags WHERE game_id = ?', [req.params.id]);
// 添加新的标签信息
if (tags.length > 0) {
const tagValues = tags.map(tag => [req.params.id, tag]);
await db.query(
'INSERT INTO game_tags (game_id, tag) VALUES ?',
[tagValues]
);
}
}
res.json({ message: '游戏更新成功' });
} catch (error) {
console.error('Error updating game:', error);
res.status(500).json({ message: error.message });
}
});
// 添加统计API路由
app.get('/api/stats', auth, async (req, res) => {
try {
const [gamesCount, categoriesCount, mediaCount] = await Promise.all([
Game.countDocuments(),
Category.countDocuments(),
Media.countDocuments()
]);
res.json({
gamesCount,
categoriesCount,
mediaCount
});
} catch (error) {
res.status(500).json({ message: error.message });
}
});
// 用户管理API
app.get('/api/admins', auth, checkPermission('user:manage'), async (req, res) => {
try {
const admins = await Admin.find()
.select('-password')
.populate('createdBy', 'username');
res.json(admins);
} catch (error) {
res.status(500).json({ message: error.message });
}
});
app.post('/api/admins', auth, checkPermission('user:manage'), async (req, res) => {
try {
const { username, email, password, role } = req.body;
const admin = new Admin({
username,
email,
password,
role,
createdBy: req.admin.id
});
await admin.save();
res.status(201).json({ message: '用户创建成功' });
} catch (error) {
res.status(500).json({ message: error.message });
}
});
app.put('/api/admins/:id', auth, checkPermission('user:manage'), async (req, res) => {
try {
const { id } = req.params;
const updates = req.body;
// 不允许修改超级管理员
const admin = await Admin.findById(id);
if (admin.role === 'superadmin' && req.admin.role !== 'superadmin') {
return res.status(403).json({ message: '无权修改超级管理员' });
}
// 如果要修改密码,需要加密
if (updates.password) {
updates.password = await bcrypt.hash(updates.password, 10);
}
const updatedAdmin = await Admin.findByIdAndUpdate(
id,
updates,
{ new: true }
).select('-password');
res.json(updatedAdmin);
} catch (error) {
res.status(500).json({ message: error.message });
}
});
app.delete('/api/admins/:id', auth, checkPermission('user:manage'), async (req, res) => {
try {
const { id } = req.params;
// 不允许删除超级管理员
const admin = await Admin.findById(id);
if (admin.role === 'superadmin') {
return res.status(403).json({ message: '不能删除超级管理员' });
}
// 不能删除自己
if (id === req.admin.id) {
return res.status(400).json({ message: '不能删除自己的账号' });
}
await Admin.findByIdAndDelete(id);
res.json({ message: '用户删除成功' });
} catch (error) {
res.status(500).json({ message: error.message });
}
});
// 获取当前用户信息
app.get('/api/admins/me', auth, async (req, res) => {
try {
const admin = await Admin.findById(req.admin.id)
.select('-password');
res.json(admin);
} catch (error) {
res.status(500).json({ message: error.message });
}
});
// 修改密码
app.post('/api/admins/change-password', auth, async (req, res) => {
try {
const { oldPassword, newPassword } = req.body;
const admin = await Admin.findById(req.admin.id);
const isMatch = await bcrypt.compare(oldPassword, admin.password);
if (!isMatch) {
return res.status(400).json({ message: '原密码错误' });
}
admin.password = newPassword;
await admin.save();
res.json({ message: '密码修改成功' });
} catch (error) {
res.status(500).json({ message: error.message });
}
});
// 系统设置路由
app.use('/api/settings', systemSettingsRoutes);
// 留言管理路由
app.use('/api/messages', messagesRoutes);
// 标签管理路由
app.use('/api/tags', tagsRoutes);
// 用户管理路由
app.use('/api/admin', adminRoutes);
// 提供静态文件访问
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));
// 删除游戏
app.delete('/api/games/:id', auth, async (req, res) => {
try {
// 验证游戏是否存在
const [existingGame] = await db.query(
'SELECT * FROM games WHERE id = ?',
[req.params.id]
);
if (!existingGame.length) {
return res.status(404).json({ message: '游戏不存在' });
}
// 开始事务
const connection = await db.getConnection();
await connection.beginTransaction();
try {
// 删除游戏图片
if (existingGame[0].image) {
const imagePath = path.join(__dirname, existingGame[0].image);
try {
if (require('fs').existsSync(imagePath)) {
require('fs').unlinkSync(imagePath);
console.log(`Deleted image file: ${imagePath}`);
}
} catch (error) {
console.error('Error deleting image file:', error);
// 继续执行,不因文件删除失败而中断整个操作
}
}
// 删除相关的平台信息
await connection.query('DELETE FROM game_platforms WHERE game_id = ?', [req.params.id]);
// 删除相关的标签信息
await connection.query('DELETE FROM game_tags WHERE game_id = ?', [req.params.id]);
// 删除游戏记录
await connection.query('DELETE FROM games WHERE id = ?', [req.params.id]);
await connection.commit();
console.log(`Successfully deleted game with ID: ${req.params.id}`);
res.json({ message: '游戏删除成功' });
} catch (error) {
await connection.rollback();
console.error('Transaction failed:', error);
throw error;
} finally {
connection.release();
}
} catch (error) {
console.error('Error deleting game:', error);
res.status(500).json({
message: '删除游戏失败',
error: process.env.NODE_ENV === 'development' ? error.message : undefined
});
}
});
// 获取管理员数量
app.get('/api/admin/count', auth, async (req, res) => {
try {
const [result] = await db.query('SELECT COUNT(*) as count FROM admins');
res.json({ count: result[0].count });
} catch (error) {
console.error('Error getting admin count:', error);
res.status(500).json({ message: error.message });
}
});
// 错误处理中间件
app.use((err, req, res, next) => {
console.error('Server error:', err);
res.status(500).json({
message: 'Internal server error',
error: process.env.NODE_ENV === 'development' ? err.message : undefined
});
});
// 启动服务器
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
// 初始化 WebSocket
initWebSocket(server);