gangbi_web/backend/server.js

724 lines
20 KiB
JavaScript
Raw Permalink Normal View History

2025-02-07 05:10:01 +00:00
// 确保在最开始就加载环境变量
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);