// server-mysql.js - 使用MySQL数据库的校园圈后端服务器 (Hugging Face Spaces版本) require('dotenv').config(); const express = require('express'); const cors = require('cors'); const helmet = require('helmet'); const multer = require('multer'); const path = require('path'); const fs = require('fs'); const bcrypt = require('bcrypt'); const jwt = require('jsonwebtoken'); const { body, validationResult } = require('express-validator'); const winston = require('winston'); const { pool, initDatabase } = require('./db-config'); const { r2Storage } = require('./r2-storage'); // JWT密钥 const JWT_SECRET = process.env.JWT_SECRET || 'campus-circle-secret-key-2024'; // 服务器端口 - Hugging Face Spaces 使用 7860 const PORT = process.env.PORT || 7860; // 创建日志记录器 const logger = winston.createLogger({ level: 'info', format: winston.format.combine( winston.format.timestamp(), winston.format.json() ), transports: [ new winston.transports.Console(), new winston.transports.File({ filename: 'server.log' }) ] }); const app = express(); // 确保uploads目录存在 - 使用Hugging Face Spaces持久化目录 const uploadsDir = process.env.HF_SPACE ? path.join('/tmp', 'uploads') : // Hugging Face Spaces临时目录 path.join(__dirname, 'uploads'); // 本地开发目录 if (!fs.existsSync(uploadsDir)) { fs.mkdirSync(uploadsDir, { recursive: true }); console.log('📁 创建uploads目录:', uploadsDir); } // 创建静态文件目录(用于持久化存储) const staticDir = path.join(__dirname, 'static'); if (!fs.existsSync(staticDir)) { fs.mkdirSync(staticDir, { recursive: true }); console.log('📁 创建static目录:', staticDir); } // 添加调试信息 - 列出现有文件 try { const files = fs.readdirSync(uploadsDir); console.log('📂 uploads目录现有文件:', files); } catch (error) { console.log('📂 无法读取uploads目录:', error.message); } // 配置multer用于文件上传 const storage = multer.diskStorage({ destination: function (req, file, cb) { // 使用/tmp目录,Hugging Face Spaces中有写权限 const uploadPath = uploadsDir; // 确保目录存在 if (!fs.existsSync(uploadPath)) { try { fs.mkdirSync(uploadPath, { recursive: true }); console.log('📁 创建上传目录:', uploadPath); } catch (error) { console.error('❌ 创建上传目录失败:', error); } } cb(null, uploadPath); }, filename: function (req, file, cb) { // 生成唯一文件名 const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9); const ext = path.extname(file.originalname); const filename = file.fieldname + '-' + uniqueSuffix + ext; console.log('📸 生成文件名:', filename); cb(null, filename); } }); const upload = multer({ storage: storage, limits: { fileSize: 2 * 1024 * 1024 // 2MB限制,减少base64大小 }, fileFilter: function (req, file, cb) { console.log('📸 文件过滤检查:', file.mimetype); // 只允许图片文件 if (file.mimetype.startsWith('image/')) { cb(null, true); } else { cb(new Error('只允许上传图片文件')); } } }); // 中间件 app.use(helmet()); app.use(cors()); app.use(express.json({ limit: '10mb' })); app.use(express.urlencoded({ extended: true, limit: '10mb' })); // 静态文件服务 - 配置CORS和缓存 app.use('/uploads', express.static(uploadsDir, { setHeaders: (res, path, stat) => { res.set('Access-Control-Allow-Origin', '*'); res.set('Cross-Origin-Resource-Policy', 'cross-origin'); } })); app.use('/static', express.static(staticDir, { setHeaders: (res, path, stat) => { res.set('Access-Control-Allow-Origin', '*'); res.set('Cross-Origin-Resource-Policy', 'cross-origin'); } })); app.use('/images', express.static('images', { setHeaders: (res, path, stat) => { res.set('Access-Control-Allow-Origin', '*'); res.set('Cross-Origin-Resource-Policy', 'cross-origin'); } })); // JWT验证中间件 const authenticateToken = (req, res, next) => { const authHeader = req.headers['authorization']; const token = authHeader && authHeader.split(' ')[1]; if (!token) { return res.status(401).json({ error: '访问令牌缺失' }); } jwt.verify(token, JWT_SECRET, (err, user) => { if (err) { return res.status(403).json({ error: '令牌无效' }); } req.user = user; next(); }); }; // 图片上传接口 - 文件存储方案 app.post('/api/upload', authenticateToken, upload.single('file'), (req, res) => { try { if (!req.file) { return res.status(400).json({ success: false, error: '没有上传文件' }); } // 检查文件大小 if (req.file.size > 5 * 1024 * 1024) { // 5MB限制 // 删除上传的文件 if (fs.existsSync(req.file.path)) { fs.unlinkSync(req.file.path); } return res.status(400).json({ success: false, error: '图片文件过大,请选择小于5MB的图片' }); } // 生成文件URL const fileUrl = `/static/${req.file.filename}`; logger.info(`图片上传成功: ${req.file.originalname}, 大小: ${req.file.size}字节, 路径: ${req.file.path}`); res.json({ success: true, message: '文件上传成功', url: fileUrl, filename: req.file.filename, originalname: req.file.originalname, size: req.file.size }); } catch (error) { logger.error('文件上传失败:', error); // 清理上传的文件 if (req.file && req.file.path && fs.existsSync(req.file.path)) { try { fs.unlinkSync(req.file.path); } catch (cleanupError) { logger.error('清理上传文件失败:', cleanupError); } } res.status(500).json({ success: false, error: '文件上传失败: ' + error.message }); } }); // 健康检查端点 app.get('/api/health', (req, res) => { res.status(200).json({ status: 'ok', timestamp: new Date().toISOString(), service: 'campusloop-backend', port: PORT, platform: 'Hugging Face Spaces' }); }); // 获取平台统计数据(注册用户数、动态数、活跃聊天数) app.get('/api/stats', async (req, res) => { try { const connection = await pool.getConnection(); try { // 查询注册用户总数 const [usersResult] = await connection.query( 'SELECT COUNT(*) as count FROM users' ); const totalUsers = usersResult[0].count || 0; // 查询动态总数 const [postsResult] = await connection.query( 'SELECT COUNT(*) as count FROM posts' ); const totalPosts = postsResult[0].count || 0; // 查询活跃聊天数(最近7天有发送消息的用户数) const [chatsResult] = await connection.query( `SELECT COUNT(DISTINCT sender_id) as count FROM chat_messages WHERE created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)` ); const activeChats = chatsResult[0].count || 0; res.json({ success: true, stats: { totalUsers, totalPosts, activeChats } }); } finally { connection.release(); } } catch (error) { console.error('获取统计数据失败:', error); logger.error('获取统计数据失败:', error); // 返回默认值,避免前端显示错误 res.json({ success: true, stats: { totalUsers: 0, totalPosts: 0, activeChats: 0 } }); } }); // 根路径 app.get('/', (req, res) => { res.json({ message: '🎓 CampusLoop Backend API', version: '1.0.0', platform: 'Hugging Face Spaces', endpoints: { health: '/api/health', auth: { register: 'POST /api/auth/register', login: 'POST /api/auth/login' }, posts: { list: 'GET /api/posts', create: 'POST /api/posts', comments: 'GET /api/posts/:id/comments' }, forum: { posts: 'GET /api/forum/posts', create: 'POST /api/forum/posts' }, activities: { list: 'GET /api/activities', create: 'POST /api/activities' } } }); }); // 用户注册接口 app.post('/api/auth/register', [ body('username').isLength({ min: 3 }).withMessage('用户名至少3个字符'), body('email').isEmail().withMessage('请输入有效的邮箱地址'), body('password').isLength({ min: 6 }).withMessage('密码至少6个字符') ], async (req, res) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ errors: errors.array() }); } const { username, email, password } = req.body; try { const connection = await pool.getConnection(); try { // 检查用户名和邮箱是否已存在 const [existingUsers] = await connection.query( 'SELECT id FROM users WHERE username = ? OR email = ?', [username, email] ); if (existingUsers.length > 0) { return res.status(400).json({ error: '用户名或邮箱已存在' }); } // 加密密码 const hashedPassword = await bcrypt.hash(password, 10); // 插入新用户(设置默认头像) const [result] = await connection.query( 'INSERT INTO users (username, email, password, avatar) VALUES (?, ?, ?, ?)', [username, email, hashedPassword, '/images/default-avatar.svg'] ); const userId = result.insertId; const token = jwt.sign({ userId, username }, JWT_SECRET, { expiresIn: '24h' }); res.status(201).json({ message: '注册成功', token, user: { id: userId, username, email } }); } finally { connection.release(); } } catch (error) { logger.error('注册失败:', error); res.status(500).json({ error: '注册失败,请稍后重试' }); } }); // 用户登录接口 app.post('/api/auth/login', [ body('username').notEmpty().withMessage('用户名不能为空'), body('password').notEmpty().withMessage('密码不能为空') ], async (req, res) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ errors: errors.array() }); } const { username, password } = req.body; try { const connection = await pool.getConnection(); try { // 尝试查询包含role字段,如果失败则查询不包含role字段 let users; try { [users] = await connection.query( 'SELECT id, username, email, password, role FROM users WHERE username = ? OR email = ?', [username, username] ); } catch (error) { if (error.code === 'ER_BAD_FIELD_ERROR') { // role字段不存在,使用不带role的查询 [users] = await connection.query( 'SELECT id, username, email, password FROM users WHERE username = ? OR email = ?', [username, username] ); } else { throw error; } } if (users.length === 0) { return res.status(401).json({ error: '用户名或密码错误' }); } const user = users[0]; const isValidPassword = await bcrypt.compare(password, user.password); if (!isValidPassword) { return res.status(401).json({ error: '用户名或密码错误' }); } const token = jwt.sign( { userId: user.id, username: user.username }, JWT_SECRET, { expiresIn: '24h' } ); res.json({ message: '登录成功', token, user: { id: user.id, username: user.username, email: user.email, role: user.role || 'user' } }); } finally { connection.release(); } } catch (error) { logger.error('登录失败:', error); res.status(500).json({ error: '登录失败,请稍后重试' }); } }); // 获取个人资料接口 app.get('/api/profile', authenticateToken, async (req, res) => { try { const connection = await pool.getConnection(); try { // 查询用户信息,包含所有字段 const [users] = await connection.query( 'SELECT id, username, email, avatar, bio, role, phone, major, grade, gender, student_id, created_at FROM users WHERE id = ?', [req.user.userId] ); if (users.length === 0) { return res.status(404).json({ error: '用户不存在' }); } const user = users[0]; // 不返回密码字段 delete user.password; res.json({ success: true, user }); } finally { connection.release(); } } catch (error) { logger.error('获取用户信息失败:', error); res.status(500).json({ error: '获取用户信息失败' }); } }); // 获取主页成绩信息接口 app.get('/api/grades/home', authenticateToken, async (req, res) => { try { const connection = await pool.getConnection(); try { const [grades] = await connection.query( 'SELECT * FROM grades WHERE user_id = ? ORDER BY created_at DESC LIMIT 5', [req.user.userId] ); res.json({ grades }); } finally { connection.release(); } } catch (error) { logger.error('获取成绩失败:', error); res.status(500).json({ error: '获取成绩失败' }); } }); // 获取校园地图接口 app.get('/api/map', (req, res) => { // 返回校园地图数据 const mapData = { id: 1, name: '校园地图', imageUrl: '/images/campus-map.jpg', locations: [ { id: 1, name: '图书馆', x: 100, y: 150, type: 'library' }, { id: 2, name: '教学楼A', x: 200, y: 100, type: 'building' }, { id: 3, name: '食堂', x: 150, y: 200, type: 'restaurant' }, { id: 4, name: '体育馆', x: 300, y: 180, type: 'gym' } ] }; res.json({ map: mapData }); }); // 获取动态接口 app.get('/api/posts', async (req, res) => { try { const connection = await pool.getConnection(); try { const [posts] = await connection.query(` SELECT p.*, u.username, u.avatar FROM posts p JOIN users u ON p.user_id = u.id ORDER BY p.created_at DESC LIMIT 50 `); res.json({ success: true, posts }); } finally { connection.release(); } } catch (error) { console.error('获取动态失败:', error); logger.error('获取动态失败:', error); res.status(500).json({ success: false, error: '获取动态失败' }); } }); // 发布动态接口(支持图片上传) // 优先使用 Cloudflare R2 存储,如未配置则使用 Base64 存数据库 app.post('/api/posts', authenticateToken, upload.single('image'), async (req, res) => { const { content } = req.body; // 验证:至少要有内容或图片 if (!content && !req.file) { return res.status(400).json({ error: '请输入内容或上传图片' }); } try { const connection = await pool.getConnection(); try { let imageUrl = null; // 如果有上传图片 if (req.file) { const fileBuffer = fs.readFileSync(req.file.path); // 优先使用 R2 存储 if (r2Storage.isConfigured()) { try { logger.info('使用 Cloudflare R2 存储图片...'); const uploadResult = await r2Storage.uploadFile( fileBuffer, req.file.originalname, req.file.mimetype ); imageUrl = uploadResult.url; logger.info(`R2 上传成功: ${imageUrl}`); } catch (r2Error) { logger.error('R2 上传失败,回退到 Base64:', r2Error.message); // R2 失败时回退到 Base64 imageUrl = `data:${req.file.mimetype};base64,${fileBuffer.toString('base64')}`; } } else { // 未配置 R2,使用 Base64 存储 logger.info('R2 未配置,使用 Base64 存储图片'); imageUrl = `data:${req.file.mimetype};base64,${fileBuffer.toString('base64')}`; } // 删除临时文件 try { fs.unlinkSync(req.file.path); } catch (unlinkError) { logger.warn('删除临时文件失败:', unlinkError.message); } } const [result] = await connection.query( 'INSERT INTO posts (user_id, content, image) VALUES (?, ?, ?)', [req.user.userId, content || '', imageUrl] ); res.status(201).json({ success: true, message: '动态发布成功', postId: result.insertId, imageStorage: r2Storage.isConfigured() ? 'r2' : 'base64' }); } finally { connection.release(); } } catch (error) { console.error('发布动态失败:', error); logger.error('发布动态失败:', error); res.status(500).json({ error: '发布动态失败' }); } }); // 编辑动态接口 app.put('/api/posts/:id', authenticateToken, async (req, res) => { const { id } = req.params; const { content } = req.body; if (!content || !content.trim()) { return res.status(400).json({ error: '动态内容不能为空' }); } try { const connection = await pool.getConnection(); try { // 检查动态是否存在以及当前用户是否有权限编辑 const [posts] = await connection.execute( 'SELECT * FROM posts WHERE id = ? AND user_id = ?', [id, req.user.userId] ); if (posts.length === 0) { return res.status(404).json({ error: '动态不存在或无权限编辑' }); } // 执行更新操作 await connection.execute( 'UPDATE posts SET content = ? WHERE id = ?', [content.trim(), id] ); res.json({ success: true, message: '动态编辑成功' }); } finally { connection.release(); } } catch (error) { logger.error('编辑动态失败:', error); res.status(500).json({ error: '编辑动态失败' }); } }); // 删除动态接口(支持管理员删除任何动态) app.delete('/api/posts/:id', authenticateToken, async (req, res) => { const { id } = req.params; try { const connection = await pool.getConnection(); try { // 先获取当前用户的角色 const [users] = await connection.execute( 'SELECT role FROM users WHERE id = ?', [req.user.userId] ); const isAdmin = users.length > 0 && users[0].role === 'admin'; // 检查动态是否存在 const [posts] = await connection.execute( 'SELECT * FROM posts WHERE id = ?', [id] ); if (posts.length === 0) { return res.status(404).json({ error: '动态不存在' }); } // 检查权限:只有动态作者或管理员可以删除 if (posts[0].user_id !== req.user.userId && !isAdmin) { return res.status(403).json({ error: '无权限删除此动态' }); } // 删除动态(会级联删除相关的评论和点赞) await connection.execute('DELETE FROM posts WHERE id = ?', [id]); logger.info(`动态删除成功: postId=${id}, 操作者=${req.user.username}, 是否管理员=${isAdmin}`); res.json({ success: true, message: '动态删除成功' }); } finally { connection.release(); } } catch (error) { logger.error('删除动态失败:', error); res.status(500).json({ error: '删除动态失败' }); } }); // 点赞动态接口 app.post('/api/posts/:id/like', authenticateToken, async (req, res) => { const { id } = req.params; try { const connection = await pool.getConnection(); try { // 检查是否已经点赞 const [existingLikes] = await connection.execute( 'SELECT * FROM likes WHERE post_id = ? AND user_id = ?', [id, req.user.userId] ); if (existingLikes.length > 0) { // 取消点赞 await connection.execute( 'DELETE FROM likes WHERE post_id = ? AND user_id = ?', [id, req.user.userId] ); res.json({ success: true, message: '取消点赞成功', liked: false }); } else { // 添加点赞 await connection.execute( 'INSERT INTO likes (post_id, user_id) VALUES (?, ?)', [id, req.user.userId] ); res.json({ success: true, message: '点赞成功', liked: true }); } } finally { connection.release(); } } catch (error) { logger.error('点赞失败:', error); res.status(500).json({ error: '点赞失败' }); } }); // 获取动态评论接口 app.get('/api/posts/:id/comments', async (req, res) => { const { id } = req.params; try { const connection = await pool.getConnection(); try { const [comments] = await connection.execute(` SELECT c.*, u.username, u.avatar FROM comments c JOIN users u ON c.user_id = u.id WHERE c.post_id = ? ORDER BY c.created_at ASC `, [id]); res.json({ success: true, comments }); } finally { connection.release(); } } catch (error) { logger.error('获取评论失败:', error); res.status(500).json({ error: '获取评论失败' }); } }); // 发表评论接口 app.post('/api/posts/:id/comments', authenticateToken, async (req, res) => { const { id } = req.params; const { content } = req.body; if (!content || !content.trim()) { return res.status(400).json({ error: '评论内容不能为空' }); } try { const connection = await pool.getConnection(); try { const [result] = await connection.execute( 'INSERT INTO comments (post_id, user_id, content) VALUES (?, ?, ?)', [id, req.user.userId, content.trim()] ); res.json({ success: true, comment: { id: result.insertId, post_id: id, user_id: req.user.userId, username: req.user.username, content: content.trim(), created_at: new Date().toISOString() } }); } finally { connection.release(); } } catch (error) { logger.error('发表评论失败:', error); res.status(500).json({ error: '发表评论失败' }); } }); // 删除评论接口 app.delete('/api/posts/:postId/comments/:commentId', authenticateToken, async (req, res) => { const { postId, commentId } = req.params; try { const connection = await pool.getConnection(); try { // 检查评论是否存在以及是否属于当前用户 const [comments] = await connection.execute( 'SELECT * FROM comments WHERE id = ? AND post_id = ? AND user_id = ?', [commentId, postId, req.user.userId] ); if (comments.length === 0) { return res.status(404).json({ error: '评论不存在或无权限删除' }); } // 删除评论 await connection.execute('DELETE FROM comments WHERE id = ?', [commentId]); res.json({ success: true, message: '评论删除成功' }); } finally { connection.release(); } } catch (error) { logger.error('删除评论失败:', error); res.status(500).json({ error: '删除评论失败' }); } }); // 获取论坛帖子接口 app.get('/api/forum/posts', async (req, res) => { const { topic } = req.query; try { const connection = await pool.getConnection(); try { let query = ` SELECT fp.*, u.username, u.avatar FROM forum_posts fp JOIN users u ON fp.user_id = u.id `; let params = []; if (topic) { query += ' WHERE fp.topic = ?'; params.push(topic); } query += ' ORDER BY fp.created_at DESC LIMIT 50'; const [posts] = await connection.query(query, params); res.json({ posts }); } finally { connection.release(); } } catch (error) { logger.error('获取论坛帖子失败:', error); res.status(500).json({ error: '获取论坛帖子失败' }); } }); // 获取课程表接口 app.get('/api/schedule', authenticateToken, async (req, res) => { try { const connection = await pool.getConnection(); try { const [schedules] = await connection.query( 'SELECT * FROM schedules WHERE user_id = ? ORDER BY weekday, start_time', [req.user.userId] ); res.json({ success: true, schedules }); } finally { connection.release(); } } catch (error) { logger.error('获取课程表失败:', error); res.status(500).json({ success: false, error: '获取课程表失败' }); } }); // 添加课程接口 app.post('/api/schedule', authenticateToken, [ body('course_name').notEmpty().withMessage('课程名称不能为空'), body('teacher').notEmpty().withMessage('教师姓名不能为空'), body('weekday').isInt({ min: 1, max: 7 }).withMessage('星期必须是1-7之间的数字') ], async (req, res) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ errors: errors.array() }); } const { course_name, teacher, weekday, start_time, end_time, classroom } = req.body; try { const connection = await pool.getConnection(); try { const [result] = await connection.query( 'INSERT INTO schedules (user_id, course_name, teacher, weekday, start_time, end_time, classroom) VALUES (?, ?, ?, ?, ?, ?, ?)', [req.user.userId, course_name, teacher, weekday, start_time, end_time, classroom || ''] ); res.status(201).json({ message: '课程添加成功', scheduleId: result.insertId }); } finally { connection.release(); } } catch (error) { logger.error('添加课程失败:', error); res.status(500).json({ error: '添加课程失败' }); } }); // 初始化测试课程数据接口 app.post('/api/schedule/init-test-data', authenticateToken, async (req, res) => { try { const connection = await pool.getConnection(); try { // 检查是否已有课程 const [existing] = await connection.query( 'SELECT COUNT(*) as count FROM schedules WHERE user_id = ?', [req.user.userId] ); if (existing[0].count > 0) { return res.json({ success: true, message: '已有课程数据,无需初始化', count: existing[0].count }); } // 测试课程数据 const courses = [ { course_name: '高等数学', teacher: '张教授', weekday: 1, start_time: '08:00', end_time: '09:45', classroom: '教学楼A201', week_start: 1, week_end: 16 }, { course_name: '大学物理', teacher: '刘教授', weekday: 1, start_time: '10:00', end_time: '11:45', classroom: '理学楼B102', week_start: 1, week_end: 16 }, { course_name: '思想政治', teacher: '陈老师', weekday: 1, start_time: '14:00', end_time: '15:45', classroom: '综合楼C301', week_start: 1, week_end: 18 }, { course_name: '大学英语', teacher: '李老师', weekday: 2, start_time: '08:00', end_time: '09:45', classroom: '外语楼A305', week_start: 1, week_end: 16 }, { course_name: '线性代数', teacher: '王教授', weekday: 2, start_time: '10:00', end_time: '11:45', classroom: '教学楼A203', week_start: 1, week_end: 14 }, { course_name: '体育', teacher: '赵老师', weekday: 2, start_time: '14:00', end_time: '15:45', classroom: '体育馆', week_start: 1, week_end: 16 }, { course_name: '程序设计', teacher: '孙老师', weekday: 3, start_time: '08:00', end_time: '09:45', classroom: '实验楼C102', week_start: 1, week_end: 16 }, { course_name: '高等数学', teacher: '张教授', weekday: 3, start_time: '10:00', end_time: '11:45', classroom: '教学楼A201', week_start: 1, week_end: 16 }, { course_name: '数据结构', teacher: '周教授', weekday: 3, start_time: '14:00', end_time: '15:45', classroom: '实验楼C201', week_start: 3, week_end: 18 }, { course_name: '大学英语', teacher: '李老师', weekday: 4, start_time: '08:00', end_time: '09:45', classroom: '外语楼A305', week_start: 1, week_end: 16 }, { course_name: '物理实验', teacher: '刘教授', weekday: 4, start_time: '14:00', end_time: '15:45', classroom: '物理实验楼', week_start: 2, week_end: 16 }, { course_name: '计算机网络', teacher: '吴老师', weekday: 4, start_time: '16:00', end_time: '17:45', classroom: '实验楼C301', week_start: 5, week_end: 18 }, { course_name: '程序设计', teacher: '孙老师', weekday: 5, start_time: '08:00', end_time: '09:45', classroom: '实验楼C102', week_start: 1, week_end: 16 }, { course_name: '概率统计', teacher: '钱教授', weekday: 5, start_time: '10:00', end_time: '11:45', classroom: '教学楼A302', week_start: 1, week_end: 14 }, { course_name: '创新创业', teacher: '郑老师', weekday: 5, start_time: '14:00', end_time: '15:45', classroom: '创业楼D101', week_start: 1, week_end: 10 } ]; // 批量插入课程 for (const course of courses) { await connection.query( 'INSERT INTO schedules (user_id, course_name, teacher, weekday, start_time, end_time, classroom, week_start, week_end, week_type) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', [req.user.userId, course.course_name, course.teacher, course.weekday, course.start_time, course.end_time, course.classroom, course.week_start, course.week_end, 'all'] ); } res.json({ success: true, message: '测试课程数据初始化成功', count: courses.length }); } finally { connection.release(); } } catch (error) { logger.error('初始化测试课程失败:', error); res.status(500).json({ error: '初始化测试课程失败' }); } }); // 获取失物招领接口 app.get('/api/lostfound', async (req, res) => { const { category, status } = req.query; try { const connection = await pool.getConnection(); try { let query = ` SELECT lf.*, u.username FROM lost_found lf JOIN users u ON lf.user_id = u.id `; let conditions = []; let params = []; if (category) { conditions.push('lf.category = ?'); params.push(category); } if (status) { conditions.push('lf.status = ?'); params.push(status); } if (conditions.length > 0) { query += ' WHERE ' + conditions.join(' AND '); } query += ' ORDER BY lf.created_at DESC LIMIT 50'; const [items] = await connection.query(query, params); res.json({ items }); } finally { connection.release(); } } catch (error) { logger.error('获取失物招领失败:', error); res.status(500).json({ error: '获取失物招领失败' }); } }); // 发布失物招领接口 app.post('/api/lostfound', authenticateToken, [ body('title').notEmpty().withMessage('标题不能为空'), body('description').notEmpty().withMessage('描述不能为空'), body('category').notEmpty().withMessage('分类不能为空') ], async (req, res) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ errors: errors.array() }); } const { title, description, category, contact, images } = req.body; try { const connection = await pool.getConnection(); try { const imagesJson = images && images.length > 0 ? JSON.stringify(images) : null; // 先尝试包含images字段的插入 let result; try { [result] = await connection.query( 'INSERT INTO lost_found (user_id, title, description, category, contact, images) VALUES (?, ?, ?, ?, ?, ?)', [req.user.userId, title, description, category, contact || '', imagesJson] ); } catch (insertError) { if (insertError.code === 'ER_BAD_FIELD_ERROR') { // images字段不存在,使用不包含images的插入 console.log('images字段不存在,使用兼容模式插入'); [result] = await connection.query( 'INSERT INTO lost_found (user_id, title, description, category, contact) VALUES (?, ?, ?, ?, ?)', [req.user.userId, title, description, category, contact || ''] ); } else { throw insertError; } } res.status(201).json({ message: '失物招领发布成功', itemId: result.insertId }); } finally { connection.release(); } } catch (error) { logger.error('发布失物招领失败:', error); res.status(500).json({ error: '发布失物招领失败' }); } }); // 获取成绩接口 app.get('/api/grades', authenticateToken, async (req, res) => { const { semester } = req.query; try { const connection = await pool.getConnection(); try { let query = 'SELECT * FROM grades WHERE user_id = ?'; let params = [req.user.userId]; if (semester) { query += ' AND semester = ?'; params.push(semester); } query += ' ORDER BY created_at DESC'; const [grades] = await connection.query(query, params); res.json({ grades }); } finally { connection.release(); } } catch (error) { logger.error('获取成绩失败:', error); res.status(500).json({ error: '获取成绩失败' }); } }); // 添加成绩接口 app.post('/api/grades', authenticateToken, [ body('course_name').notEmpty().withMessage('课程名称不能为空'), body('score').isFloat({ min: 0, max: 100 }).withMessage('成绩必须是0-100之间的数字'), body('semester').notEmpty().withMessage('学期不能为空') ], async (req, res) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ errors: errors.array() }); } const { course_name, score, semester, credit } = req.body; try { const connection = await pool.getConnection(); try { const [result] = await connection.query( 'INSERT INTO grades (user_id, course_name, score, semester, credit) VALUES (?, ?, ?, ?, ?)', [req.user.userId, course_name, score, semester, credit || 3] ); res.status(201).json({ message: '成绩添加成功', gradeId: result.insertId }); } finally { connection.release(); } } catch (error) { logger.error('添加成绩失败:', error); res.status(500).json({ error: '添加成绩失败' }); } }); // 获取通知接口 app.get('/api/notifications', authenticateToken, async (req, res) => { try { const connection = await pool.getConnection(); try { const [notifications] = await connection.query( 'SELECT * FROM notifications WHERE user_id = ? ORDER BY created_at DESC LIMIT 50', [req.user.userId] ); res.json({ notifications }); } finally { connection.release(); } } catch (error) { logger.error('获取通知失败:', error); res.status(500).json({ error: '获取通知失败' }); } }); // 标记通知为已读接口 app.put('/api/notifications/:id', authenticateToken, async (req, res) => { const { id } = req.params; try { const connection = await pool.getConnection(); try { await connection.query( 'UPDATE notifications SET read_status = 1 WHERE id = ? AND user_id = ?', [id, req.user.userId] ); res.json({ success: true, message: '通知标记为已读' }); } finally { connection.release(); } } catch (error) { logger.error('标记通知为已读失败:', error); res.status(500).json({ error: '标记通知为已读失败' }); } }); // 获取活动接口 app.get('/api/activities', async (req, res) => { // 从请求头获取用户信息(可选) const authHeader = req.headers['authorization']; const token = authHeader && authHeader.split(' ')[1]; let userId = null; if (token) { try { const decoded = jwt.verify(token, JWT_SECRET); userId = decoded.userId; } catch (error) { // 忽略token错误,继续执行 } } try { const connection = await pool.getConnection(); try { const [activities] = await connection.query(` SELECT a.*, u.username as organizer_name, CASE WHEN ap.user_id IS NOT NULL THEN 1 ELSE 0 END as is_registered FROM activities a JOIN users u ON a.organizer_id = u.id LEFT JOIN activity_participants ap ON a.id = ap.activity_id AND ap.user_id = ? WHERE a.status = 'active' ORDER BY a.start_time ASC LIMIT 50 `, [userId]); res.json({ success: true, activities }); } finally { connection.release(); } } catch (error) { logger.error('获取活动失败:', error); res.status(500).json({ error: '获取活动失败' }); } }); // 报名活动接口 app.post('/api/activities/:id/register', authenticateToken, async (req, res) => { const activityId = req.params.id; const userId = req.user.userId; try { const connection = await pool.getConnection(); try { // 检查是否已经报名 const [existing] = await connection.query( 'SELECT * FROM activity_participants WHERE activity_id = ? AND user_id = ?', [activityId, userId] ); if (existing.length > 0) { return res.status(400).json({ error: '您已经报名了这个活动' }); } // 添加报名记录 await connection.query( 'INSERT INTO activity_participants (activity_id, user_id) VALUES (?, ?)', [activityId, userId] ); // 更新活动参与人数 await connection.query( 'UPDATE activities SET participants = participants + 1 WHERE id = ?', [activityId] ); res.json({ success: true, message: '报名成功' }); } finally { connection.release(); } } catch (error) { logger.error('报名活动失败:', error); res.status(500).json({ error: '报名失败' }); } }); // 取消报名活动接口 app.post('/api/activities/:id/cancel', authenticateToken, async (req, res) => { const activityId = req.params.id; const userId = req.user.userId; try { const connection = await pool.getConnection(); try { // 检查是否已经报名 const [existing] = await connection.query( 'SELECT * FROM activity_participants WHERE activity_id = ? AND user_id = ?', [activityId, userId] ); if (existing.length === 0) { return res.status(400).json({ error: '您尚未报名此活动' }); } // 删除报名记录 await connection.query( 'DELETE FROM activity_participants WHERE activity_id = ? AND user_id = ?', [activityId, userId] ); // 更新活动参与人数 await connection.query( 'UPDATE activities SET participants = GREATEST(0, participants - 1) WHERE id = ?', [activityId] ); res.json({ success: true, message: '取消报名成功' }); } finally { connection.release(); } } catch (error) { logger.error('取消报名失败:', error); res.status(500).json({ error: '取消报名失败' }); } }); // 获取单个活动详情接口 app.get('/api/activities/:id', async (req, res) => { const activityId = req.params.id; try { const connection = await pool.getConnection(); try { const [activities] = await connection.query(` SELECT a.*, u.username as organizer_name FROM activities a JOIN users u ON a.organizer_id = u.id WHERE a.id = ? `, [activityId]); if (activities.length === 0) { return res.status(404).json({ error: '活动不存在' }); } res.json({ success: true, activity: activities[0] }); } finally { connection.release(); } } catch (error) { logger.error('获取活动详情失败:', error); res.status(500).json({ error: '获取活动详情失败' }); } }); // 更新活动接口 app.put('/api/activities/:id', authenticateToken, [ body('title').notEmpty().withMessage('活动标题不能为空'), body('description').notEmpty().withMessage('活动描述不能为空'), body('location').notEmpty().withMessage('活动地点不能为空'), body('start_time').notEmpty().withMessage('开始时间不能为空'), body('end_time').notEmpty().withMessage('结束时间不能为空') ], async (req, res) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ errors: errors.array() }); } const activityId = req.params.id; const userId = req.user.userId; const { title, description, location, start_time, end_time } = req.body; try { const connection = await pool.getConnection(); try { console.log('更新活动请求:', { activityId, userId, title, description, location, start_time, end_time }); // 检查用户是否为管理员或活动创建者 const [activity] = await connection.query( 'SELECT organizer_id FROM activities WHERE id = ?', [activityId] ); if (activity.length === 0) { console.log('活动不存在:', activityId); return res.status(404).json({ error: '活动不存在' }); } // 检查用户权限 const [user] = await connection.query( 'SELECT role FROM users WHERE id = ?', [userId] ); const isAdmin = user.length > 0 && user[0].role === 'admin'; const isOrganizer = activity[0].organizer_id === userId; console.log('权限检查:', { isAdmin, isOrganizer, userId, organizerId: activity[0].organizer_id }); if (!isAdmin && !isOrganizer) { return res.status(403).json({ error: '您没有权限编辑此活动' }); } // 更新活动 const [result] = await connection.query( 'UPDATE activities SET title = ?, description = ?, location = ?, start_time = ?, end_time = ? WHERE id = ?', [title, description, location, start_time, end_time, activityId] ); console.log('更新结果:', result); res.json({ success: true, message: '活动更新成功' }); } finally { connection.release(); } } catch (error) { console.error('更新活动失败详细错误:', error); logger.error('更新活动失败:', error); res.status(500).json({ error: '更新活动失败', details: error.message }); } }); // 删除活动接口 app.delete('/api/activities/:id', authenticateToken, async (req, res) => { const activityId = req.params.id; const userId = req.user.userId; try { const connection = await pool.getConnection(); try { // 检查用户是否为管理员或活动创建者 const [activity] = await connection.query( 'SELECT organizer_id FROM activities WHERE id = ?', [activityId] ); if (activity.length === 0) { return res.status(404).json({ error: '活动不存在' }); } // 检查用户权限 const [user] = await connection.query( 'SELECT role FROM users WHERE id = ?', [userId] ); const isAdmin = user.length > 0 && user[0].role === 'admin'; const isOrganizer = activity[0].organizer_id === userId; if (!isAdmin && !isOrganizer) { return res.status(403).json({ error: '您没有权限删除此活动' }); } // 删除活动(会自动删除相关的参与记录,因为有外键约束) await connection.query('DELETE FROM activities WHERE id = ?', [activityId]); res.json({ success: true, message: '活动删除成功' }); } finally { connection.release(); } } catch (error) { logger.error('删除活动失败:', error); res.status(500).json({ error: '删除活动失败' }); } }); // 创建活动接口 app.post('/api/activities', authenticateToken, [ body('title').notEmpty().withMessage('活动标题不能为空'), body('description').notEmpty().withMessage('活动描述不能为空'), body('location').notEmpty().withMessage('活动地点不能为空'), body('start_time').notEmpty().withMessage('开始时间不能为空'), body('end_time').notEmpty().withMessage('结束时间不能为空') ], async (req, res) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ errors: errors.array() }); } const { title, description, location, start_time, end_time } = req.body; try { const connection = await pool.getConnection(); try { const [result] = await connection.query( 'INSERT INTO activities (title, description, location, start_time, end_time, organizer_id) VALUES (?, ?, ?, ?, ?, ?)', [title, description, location, start_time, end_time, req.user.userId] ); res.status(201).json({ success: true, message: '活动创建成功', activityId: result.insertId }); } finally { connection.release(); } } catch (error) { logger.error('创建活动失败:', error); res.status(500).json({ error: '创建活动失败' }); } }); // ============ 好友相关接口 ============ // 获取好友列表 app.get('/api/friends', authenticateToken, async (req, res) => { const userId = req.user.userId; try { const connection = await pool.getConnection(); try { // 查询好友列表(双向好友关系) const [friends] = await connection.query(` SELECT DISTINCT u.id, u.username, u.avatar, u.student_id FROM users u INNER JOIN ( SELECT friend_id as user_id FROM friendships WHERE user_id = ? AND status = 'accepted' UNION SELECT user_id FROM friendships WHERE friend_id = ? AND status = 'accepted' ) f ON u.id = f.user_id ORDER BY u.username `, [userId, userId]); res.json({ success: true, friends: friends || [] }); } finally { connection.release(); } } catch (error) { console.error('获取好友列表失败:', error); logger.error('获取好友列表失败:', error); res.status(500).json({ error: '获取好友列表失败' }); } }); // 搜索用户 app.get('/api/users/search', authenticateToken, async (req, res) => { const { keyword } = req.query; const currentUserId = req.user.userId; if (!keyword || keyword.trim() === '') { return res.json({ success: true, users: [] }); } try { const connection = await pool.getConnection(); try { // 搜索用户(排除自己) const [users] = await connection.query(` SELECT id, username, avatar, student_id FROM users WHERE (username LIKE ? OR student_id LIKE ?) AND id != ? LIMIT 20 `, [`%${keyword}%`, `%${keyword}%`, currentUserId]); // 检查每个用户是否已是好友 const usersWithFriendStatus = await Promise.all(users.map(async (user) => { const [friendship] = await connection.query(` SELECT status FROM friendships WHERE (user_id = ? AND friend_id = ?) OR (user_id = ? AND friend_id = ?) `, [currentUserId, user.id, user.id, currentUserId]); return { ...user, isFriend: friendship.length > 0 && friendship[0].status === 'accepted', friendshipStatus: friendship.length > 0 ? friendship[0].status : null }; })); res.json({ success: true, users: usersWithFriendStatus }); } finally { connection.release(); } } catch (error) { console.error('搜索用户失败:', error); logger.error('搜索用户失败:', error); res.status(500).json({ error: '搜索用户失败' }); } }); // ============ 用户资料相关接口 ============ // 上传头像 // 优先使用 Cloudflare R2 存储,如未配置则使用 Base64 存数据库 app.post('/api/profile/avatar', authenticateToken, upload.single('avatar'), async (req, res) => { try { console.log('📸 收到头像上传请求'); console.log('📸 用户ID:', req.user.userId); console.log('📸 文件信息:', req.file); if (!req.file) { console.error('❌ 没有收到文件'); return res.status(400).json({ error: '请选择头像文件' }); } const userId = req.user.userId; console.log('📸 文件路径:', req.file.path); console.log('📸 文件大小:', req.file.size); // 检查文件是否存在 if (!fs.existsSync(req.file.path)) { console.error('❌ 文件不存在:', req.file.path); return res.status(500).json({ error: '文件上传失败,请重试' }); } // 读取文件 const fileBuffer = fs.readFileSync(req.file.path); let avatarUrl = null; let storageType = 'base64'; // 优先使用 R2 存储 if (r2Storage.isConfigured()) { try { console.log('📸 使用 Cloudflare R2 存储头像...'); // 获取文件扩展名 const ext = req.file.originalname.split('.').pop() || 'jpg'; // 使用固定路径:avatars/用户ID/avatar.扩展名 // 每个用户有独立文件夹,每次上传覆盖同一文件 const customKey = `avatars/user-${userId}/avatar.${ext}`; const uploadResult = await r2Storage.uploadFile( fileBuffer, req.file.originalname, req.file.mimetype, { customKey: customKey } ); // 添加时间戳参数避免浏览器缓存 avatarUrl = `${uploadResult.url}?t=${Date.now()}`; storageType = 'r2'; console.log('📸 R2 上传成功:', avatarUrl); console.log('📸 存储路径:', customKey); } catch (r2Error) { console.error('📸 R2 上传失败,回退到 Base64:', r2Error.message); // R2 失败时回退到 Base64 avatarUrl = `data:${req.file.mimetype};base64,${fileBuffer.toString('base64')}`; } } else { // 未配置 R2,使用 Base64 存储 console.log('📸 R2 未配置,使用 Base64 存储头像'); avatarUrl = `data:${req.file.mimetype};base64,${fileBuffer.toString('base64')}`; } console.log('📸 头像URL长度:', avatarUrl.length); console.log('📸 存储类型:', storageType); // 删除临时文件 try { fs.unlinkSync(req.file.path); console.log('📸 临时文件已删除'); } catch (unlinkError) { console.error('⚠️ 删除临时文件失败:', unlinkError.message); } const connection = await pool.getConnection(); try { // 更新用户头像 await connection.query( 'UPDATE users SET avatar = ? WHERE id = ?', [avatarUrl, userId] ); console.log('✅ 头像上传成功'); res.json({ success: true, avatarUrl: avatarUrl, storageType: storageType, message: '头像上传成功' }); } finally { connection.release(); } } catch (error) { console.error('❌ 上传头像失败:', error); console.error('❌ 错误堆栈:', error.stack); logger.error('上传头像失败:', error); res.status(500).json({ error: '上传头像失败', message: error.message }); } }); // 更新用户资料 app.put('/api/profile', authenticateToken, async (req, res) => { const userId = req.user.userId; const { username, email, bio, phone, major, grade, gender, avatar } = req.body; try { const connection = await pool.getConnection(); try { // 构建更新字段 const updates = []; const values = []; if (username !== undefined) { updates.push('username = ?'); values.push(username); } if (email !== undefined) { updates.push('email = ?'); values.push(email); } if (bio !== undefined) { updates.push('bio = ?'); values.push(bio); } if (phone !== undefined) { updates.push('phone = ?'); values.push(phone); } if (major !== undefined) { updates.push('major = ?'); values.push(major); } if (grade !== undefined) { updates.push('grade = ?'); values.push(grade); } if (gender !== undefined) { updates.push('gender = ?'); values.push(gender); } if (avatar !== undefined) { updates.push('avatar = ?'); values.push(avatar); } if (updates.length === 0) { return res.status(400).json({ error: '没有要更新的字段' }); } values.push(userId); await connection.query( `UPDATE users SET ${updates.join(', ')} WHERE id = ?`, values ); res.json({ success: true, message: '资料更新成功' }); } finally { connection.release(); } } catch (error) { console.error('更新资料失败:', error); logger.error('更新资料失败:', error); res.status(500).json({ error: '更新资料失败' }); } }); // 添加好友 app.post('/api/friends', authenticateToken, async (req, res) => { const userId = req.user.userId; const { friendId } = req.body; if (!friendId) { return res.status(400).json({ error: '好友ID不能为空' }); } if (userId === friendId) { return res.status(400).json({ error: '不能添加自己为好友' }); } try { const connection = await pool.getConnection(); try { // 检查是否已经是好友 const [existing] = await connection.query(` SELECT * FROM friendships WHERE (user_id = ? AND friend_id = ?) OR (user_id = ? AND friend_id = ?) `, [userId, friendId, friendId, userId]); if (existing.length > 0) { return res.status(400).json({ error: '已经是好友或已发送好友请求' }); } // 添加好友关系(直接设为accepted,简化流程) await connection.query( 'INSERT INTO friendships (user_id, friend_id, status) VALUES (?, ?, ?)', [userId, friendId, 'accepted'] ); res.json({ success: true, message: '添加好友成功' }); } finally { connection.release(); } } catch (error) { console.error('添加好友失败:', error); logger.error('添加好友失败:', error); res.status(500).json({ error: '添加好友失败' }); } }); // ============ 聊天相关接口 ============ // 获取聊天消息 app.get('/api/chat/:friendId', authenticateToken, async (req, res) => { const userId = req.user.userId; const friendId = req.params.friendId; try { const connection = await pool.getConnection(); try { // 获取聊天消息 const [messages] = await connection.query(` SELECT m.*, u.username, u.avatar FROM chat_messages m JOIN users u ON m.sender_id = u.id WHERE (m.sender_id = ? AND m.receiver_id = ?) OR (m.sender_id = ? AND m.receiver_id = ?) ORDER BY m.created_at ASC LIMIT 100 `, [userId, friendId, friendId, userId]); res.json({ success: true, messages: messages || [] }); } finally { connection.release(); } } catch (error) { console.error('获取聊天消息失败:', error); logger.error('获取聊天消息失败:', error); res.status(500).json({ error: '获取聊天消息失败' }); } }); // 发送聊天消息 app.post('/api/chat', authenticateToken, async (req, res) => { const senderId = req.user.userId; const { receiverId, content, type = 'text' } = req.body; if (!receiverId || !content) { return res.status(400).json({ error: '接收者ID和消息内容不能为空' }); } try { const connection = await pool.getConnection(); try { // 插入消息 const [result] = await connection.query( 'INSERT INTO chat_messages (sender_id, receiver_id, content, type) VALUES (?, ?, ?, ?)', [senderId, receiverId, content, type] ); // 获取刚插入的消息 const [messages] = await connection.query(` SELECT m.*, u.username, u.avatar FROM chat_messages m JOIN users u ON m.sender_id = u.id WHERE m.id = ? `, [result.insertId]); res.json({ success: true, message: messages[0] }); } finally { connection.release(); } } catch (error) { console.error('发送消息失败:', error); logger.error('发送消息失败:', error); res.status(500).json({ error: '发送消息失败' }); } }); // ============ 兼容性路由 ============ // 失物招领兼容性路由 (前端调用的是 /api/lost-found) app.get('/api/lost-found', async (req, res) => { const { category, status } = req.query; try { const connection = await pool.getConnection(); try { let query = ` SELECT lf.*, u.username FROM lost_found lf JOIN users u ON lf.user_id = u.id `; let conditions = []; let params = []; if (category) { conditions.push('lf.category = ?'); params.push(category); } if (status) { conditions.push('lf.status = ?'); params.push(status); } if (conditions.length > 0) { query += ' WHERE ' + conditions.join(' AND '); } query += ' ORDER BY lf.created_at DESC LIMIT 50'; const [items] = await connection.query(query, params); res.json({ items }); } finally { connection.release(); } } catch (error) { logger.error('获取失物招领失败:', error); res.status(500).json({ error: '获取失物招领失败' }); } }); app.post('/api/lost-found', authenticateToken, [ body('title').notEmpty().withMessage('标题不能为空'), body('description').notEmpty().withMessage('描述不能为空'), body('category').notEmpty().withMessage('分类不能为空') ], async (req, res) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ errors: errors.array() }); } const { title, description, category, contact, images } = req.body; try { const connection = await pool.getConnection(); try { const imagesJson = images && images.length > 0 ? JSON.stringify(images) : null; // 先尝试包含images字段的插入 let result; try { [result] = await connection.query( 'INSERT INTO lost_found (user_id, title, description, category, contact, images) VALUES (?, ?, ?, ?, ?, ?)', [req.user.userId, title, description, category, contact || '', imagesJson] ); } catch (insertError) { if (insertError.code === 'ER_BAD_FIELD_ERROR') { // images字段不存在,使用不包含images的插入 console.log('images字段不存在,使用兼容模式插入'); [result] = await connection.query( 'INSERT INTO lost_found (user_id, title, description, category, contact) VALUES (?, ?, ?, ?, ?)', [req.user.userId, title, description, category, contact || ''] ); } else { throw insertError; } } res.status(201).json({ message: '失物招领发布成功', itemId: result.insertId }); } finally { connection.release(); } } catch (error) { logger.error('发布失物招领失败:', error); res.status(500).json({ error: '发布失物招领失败' }); } }); // 获取失物招领详情接口 app.get('/api/lost-found/:id', async (req, res) => { const { id } = req.params; try { const connection = await pool.getConnection(); try { const [items] = await connection.query(` SELECT lf.*, u.username FROM lost_found lf JOIN users u ON lf.user_id = u.id WHERE lf.id = ? `, [id]); if (items.length === 0) { return res.status(404).json({ error: '失物招领信息不存在' }); } res.json({ item: items[0] }); } finally { connection.release(); } } catch (error) { logger.error('获取失物招领详情失败:', error); res.status(500).json({ error: '获取失物招领详情失败' }); } }); // 更新失物招领状态接口 app.put('/api/lost-found/:id/status', authenticateToken, async (req, res) => { const { id } = req.params; const { status } = req.body; try { const connection = await pool.getConnection(); try { const [result] = await connection.query( 'UPDATE lost_found SET status = ? WHERE id = ?', [status, id] ); if (result.affectedRows === 0) { return res.status(404).json({ error: '失物招领信息不存在' }); } res.json({ success: true, message: '状态更新成功' }); } finally { connection.release(); } } catch (error) { logger.error('更新失物招领状态失败:', error); res.status(500).json({ error: '更新失物招领状态失败' }); } }); // 编辑失物招领接口 app.put('/api/lost-found/:id', authenticateToken, [ body('title').notEmpty().withMessage('标题不能为空'), body('description').notEmpty().withMessage('描述不能为空'), body('category').notEmpty().withMessage('分类不能为空') ], async (req, res) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ errors: errors.array() }); } const { id } = req.params; const { title, description, category, contact, images } = req.body; try { const connection = await pool.getConnection(); try { const imagesJson = images && images.length > 0 ? JSON.stringify(images) : null; // 先尝试包含images字段的更新 let result; try { [result] = await connection.query( 'UPDATE lost_found SET title = ?, description = ?, category = ?, contact = ?, images = ? WHERE id = ? AND user_id = ?', [title, description, category, contact || '', imagesJson, id, req.user.userId] ); } catch (updateError) { if (updateError.code === 'ER_BAD_FIELD_ERROR') { // images字段不存在,使用不包含images的更新 console.log('images字段不存在,使用兼容模式更新'); [result] = await connection.query( 'UPDATE lost_found SET title = ?, description = ?, category = ?, contact = ? WHERE id = ? AND user_id = ?', [title, description, category, contact || '', id, req.user.userId] ); } else { throw updateError; } } if (result.affectedRows === 0) { return res.status(404).json({ error: '失物招领信息不存在或无权限编辑' }); } res.json({ success: true, message: '编辑成功' }); } finally { connection.release(); } } catch (error) { logger.error('编辑失物招领失败:', error); res.status(500).json({ error: '编辑失物招领失败' }); } }); // 删除失物招领接口 app.delete('/api/lost-found/:id', authenticateToken, async (req, res) => { const { id } = req.params; try { const connection = await pool.getConnection(); try { const [result] = await connection.query( 'DELETE FROM lost_found WHERE id = ? AND user_id = ?', [id, req.user.userId] ); if (result.affectedRows === 0) { return res.status(404).json({ error: '失物招领信息不存在或无权限删除' }); } res.json({ success: true, message: '删除成功' }); } finally { connection.release(); } } catch (error) { logger.error('删除失物招领失败:', error); res.status(500).json({ error: '删除失物招领失败' }); } }); // 失物招领点赞接口 app.post('/api/lost-found/:id/like', authenticateToken, async (req, res) => { const { id } = req.params; try { const connection = await pool.getConnection(); try { // 检查是否已经点赞 const [existingLikes] = await connection.query( 'SELECT id FROM lost_found_likes WHERE item_id = ? AND user_id = ?', [id, req.user.userId] ); if (existingLikes.length > 0) { // 已经点赞,取消点赞 await connection.query( 'DELETE FROM lost_found_likes WHERE item_id = ? AND user_id = ?', [id, req.user.userId] ); await connection.query( 'UPDATE lost_found SET likes = likes - 1 WHERE id = ?', [id] ); res.json({ success: true, message: '取消点赞成功', liked: false }); } else { // 未点赞,添加点赞 await connection.query( 'INSERT INTO lost_found_likes (item_id, user_id) VALUES (?, ?)', [id, req.user.userId] ); await connection.query( 'UPDATE lost_found SET likes = likes + 1 WHERE id = ?', [id] ); res.json({ success: true, message: '点赞成功', liked: true }); } } finally { connection.release(); } } catch (error) { logger.error('失物招领点赞失败:', error); res.status(500).json({ error: '操作失败' }); } }); // 调试接口 - 检查文件是否存在 app.get('/api/debug/files', (req, res) => { try { // 检查uploads目录 const uploadsFiles = fs.existsSync(uploadsDir) ? fs.readdirSync(uploadsDir) : []; const uploadsDetails = uploadsFiles.map(filename => { const filePath = path.join(uploadsDir, filename); const stats = fs.statSync(filePath); return { filename, size: stats.size, created: stats.birthtime, modified: stats.mtime, path: filePath }; }); // 检查static目录 const staticFiles = fs.existsSync(staticDir) ? fs.readdirSync(staticDir) : []; const staticDetails = staticFiles.map(filename => { const filePath = path.join(staticDir, filename); const stats = fs.statSync(filePath); return { filename, size: stats.size, created: stats.birthtime, modified: stats.mtime, path: filePath }; }); res.json({ uploadsDir, staticDir, uploads: { totalFiles: uploadsFiles.length, files: uploadsDetails }, static: { totalFiles: staticFiles.length, files: staticDetails } }); } catch (error) { res.status(500).json({ error: error.message }); } }); // 调试接口 - 查看用户数据 app.get('/api/debug/users', async (req, res) => { try { const connection = await pool.getConnection(); try { const [users] = await connection.query('SELECT id, username, email, role, created_at FROM users'); res.json({ users: users.map(user => ({ id: user.id, username: user.username, email: user.email, role: user.role, created_at: user.created_at })), count: users.length, message: '调试信息 - 用户列表' }); } finally { connection.release(); } } catch (error) { res.status(500).json({ error: error.message, message: '查询用户数据失败' }); } }); // 调试接口 - 测试管理员登录 app.post('/api/debug/test-admin', async (req, res) => { try { const connection = await pool.getConnection(); try { const [users] = await connection.query( 'SELECT id, username, email, password, role FROM users WHERE username = ? OR email = ?', ['admin', 'admin'] ); if (users.length === 0) { return res.json({ success: false, message: '管理员用户不存在', query: 'SELECT * FROM users WHERE username = admin OR email = admin' }); } const user = users[0]; const bcrypt = require('bcrypt'); const isValidPassword = await bcrypt.compare('admin', user.password); res.json({ success: isValidPassword, message: isValidPassword ? '密码验证成功' : '密码验证失败', user: { id: user.id, username: user.username, email: user.email, role: user.role, passwordHash: user.password.substring(0, 20) + '...' } }); } finally { connection.release(); } } catch (error) { res.status(500).json({ error: error.message, message: '测试管理员登录失败' }); } }); // Multer错误处理中间件 app.use((error, req, res, next) => { if (error instanceof multer.MulterError) { console.error('❌ Multer错误:', error); if (error.code === 'LIMIT_FILE_SIZE') { return res.status(400).json({ error: '文件太大', message: '文件大小不能超过2MB' }); } return res.status(400).json({ error: '文件上传错误', message: error.message }); } if (error) { console.error('❌ 服务器错误:', error); return res.status(500).json({ error: '服务器错误', message: error.message }); } next(); }); // 404处理 app.use('*', (req, res) => { res.status(404).json({ error: 'API endpoint not found', message: 'Please check the API documentation at /' }); }); // 启动服务器 async function startServer() { try { console.log('🔄 正在连接MySQL数据库...'); console.log(`📍 数据库地址: ${process.env.MYSQL_HOST || 'gz-cdb-1xrcr3dt.sql.tencentcdb.com'}:${process.env.MYSQL_PORT || 23767}`); // 测试数据库连接 const connection = await pool.getConnection(); console.log('✅ MySQL数据库连接成功!'); connection.release(); // 初始化数据库表 await initDatabase(); // 启动服务器 app.listen(PORT, '0.0.0.0', () => { logger.info(`校园圈服务器运行在端口 ${PORT} - Hugging Face Spaces版本`); console.log(`\n🚀 CampusLoop Backend 启动成功!`); console.log(`🌐 服务器地址: http://0.0.0.0:${PORT}`); console.log(`🏥 健康检查: http://0.0.0.0:${PORT}/api/health`); console.log(`📅 当前时间: ${new Date().toLocaleString()}`); console.log(`🎯 平台: Hugging Face Spaces`); console.log(`\n📝 主要接口:`); console.log(` GET / - API文档`); console.log(` GET /api/health - 健康检查`); console.log(` POST /api/auth/register - 用户注册`); console.log(` POST /api/auth/login - 用户登录`); console.log(` GET /api/posts - 获取动态`); console.log(` POST /api/posts - 发布动态`); console.log(` GET /api/forum/posts - 获取论坛帖子`); console.log(` GET /api/activities - 获取活动`); }); } catch (error) { logger.error('服务器启动失败:', error); console.error('❌ 服务器启动失败:', error.message); if (error.code === 'ETIMEDOUT') { console.error('\n⚠️ 数据库连接超时!可能的原因:'); console.error(' 1. 数据库服务器防火墙未开放外部访问'); console.error(' 2. Hugging Face Spaces IP未加入数据库白名单'); console.error(' 3. 数据库地址或端口配置错误'); console.error('\n💡 解决方案:'); console.error(' 1. 在腾讯云MySQL控制台添加 0.0.0.0/0 到白名单(测试用)'); console.error(' 2. 检查Hugging Face Spaces环境变量配置'); console.error(' 3. 确认数据库服务正常运行'); } process.exit(1); } } startServer();