// ... imports const express = require('express'); const mongoose = require('mongoose'); const cors = require('cors'); const bodyParser = require('body-parser'); const path = require('path'); const compression = require('compression'); const { School, User, Student, Course, Score, ClassModel, SubjectModel, ExamModel, ScheduleModel, ConfigModel, NotificationModel, GameSessionModel, StudentRewardModel, LuckyDrawConfigModel, GameMonsterConfigModel, GameZenConfigModel, AchievementConfigModel, StudentAchievementModel, AttendanceModel, LeaveRequestModel, SchoolCalendarModel } = require('./models'); // ... constants const PORT = 7860; const MONGO_URI = 'mongodb+srv://dv890a:db8822723@chatpro.gw3v0v7.mongodb.net/chatpro?retryWrites=true&w=majority&appName=chatpro&authSource=admin'; const app = express(); // PERFORMANCE 1: Enable Gzip Compression app.use(compression()); app.use(cors()); app.use(bodyParser.json({ limit: '10mb' })); // PERFORMANCE 2: Smart Caching Strategy app.use(express.static(path.join(__dirname, 'dist'), { setHeaders: (res, filePath) => { if (filePath.endsWith('.html')) { res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); } else { res.setHeader('Cache-Control', 'public, max-age=31536000, immutable'); } } })); const InMemoryDB = { schools: [], users: [], // ... other mock data if needed, but we rely on Mongo mostly now isFallback: false }; const connectDB = async () => { try { await mongoose.connect(MONGO_URI, { serverSelectionTimeoutMS: 30000 }); console.log('✅ MongoDB 连接成功 (Real Data)'); } catch (err) { console.error('❌ MongoDB 连接失败:', err.message); console.warn('⚠️ 启动内存数据库模式 (Limited functionality)'); InMemoryDB.isFallback = true; } }; connectDB(); // ... Helpers ... const getQueryFilter = (req) => { const s = req.headers['x-school-id']; const role = req.headers['x-user-role']; if (role === 'PRINCIPAL') { if (!s) return { _id: null }; return { schoolId: s }; } if (!s) return {}; return { $or: [ { schoolId: s }, { schoolId: { $exists: false } }, { schoolId: null } ] }; }; const injectSchoolId = (req, b) => ({ ...b, schoolId: req.headers['x-school-id'] }); const getAutoSemester = () => { const now = new Date(); const month = now.getMonth() + 1; // 1-12 const year = now.getFullYear(); if (month >= 8 || month === 1) { const startYear = month === 1 ? year - 1 : year; return `${startYear}-${startYear + 1}学年 第一学期`; } else { const startYear = year - 1; return `${startYear}-${startYear + 1}学年 第二学期`; } }; const generateStudentNo = async () => { const year = new Date().getFullYear(); const random = Math.floor(100000 + Math.random() * 900000); return `${year}${random}`; }; // ... ROUTES ... app.get('/api/auth/me', async (req, res) => { const username = req.headers['x-user-username']; if (!username) return res.status(401).json({ error: 'Unauthorized' }); const user = await User.findOne({ username }); if (!user) return res.status(404).json({ error: 'User not found' }); res.json(user); }); app.post('/api/auth/update-profile', async (req, res) => { const { userId, trueName, phone, avatar, currentPassword, newPassword } = req.body; try { const user = await User.findById(userId); if (!user) return res.status(404).json({ error: 'User not found' }); // If changing password, verify old one if (newPassword) { if (user.password !== currentPassword) { return res.status(401).json({ error: 'INVALID_PASSWORD', message: '旧密码错误' }); } user.password = newPassword; } if (trueName) user.trueName = trueName; if (phone) user.phone = phone; if (avatar) user.avatar = avatar; await user.save(); // If user is a student, sync profile data to Student collection if (user.role === 'STUDENT') { await Student.findOneAndUpdate( { studentNo: user.studentNo }, { name: user.trueName || user.username, phone: user.phone } ); } // If user is a teacher, sync name to Class collection if (user.role === 'TEACHER' && user.homeroomClass) { const cls = await ClassModel.findOne({ schoolId: user.schoolId, $expr: { $eq: [{ $concat: ["$grade", "$className"] }, user.homeroomClass] } }); if (cls) { cls.teacherName = user.trueName || user.username; await cls.save(); } } res.json({ success: true, user }); } catch (e) { console.error(e); res.status(500).json({ error: e.message }); } }); app.post('/api/auth/register', async (req, res) => { const { role, username, password, schoolId, trueName, seatNo } = req.body; const className = req.body.className || req.body.homeroomClass; try { if (role === 'STUDENT') { if (!trueName || !className) { return res.status(400).json({ error: 'MISSING_FIELDS', message: '姓名和班级不能为空' }); } const cleanName = trueName.trim(); const cleanClass = className.trim(); const existingProfile = await Student.findOne({ schoolId, name: { $regex: new RegExp(`^${cleanName}$`, 'i') }, className: cleanClass }); let finalUsername = ''; if (existingProfile) { if (existingProfile.studentNo && existingProfile.studentNo.length > 5) { finalUsername = existingProfile.studentNo; } else { finalUsername = await generateStudentNo(); existingProfile.studentNo = finalUsername; await existingProfile.save(); } const userExists = await User.findOne({ username: finalUsername, schoolId }); if (userExists) { if (userExists.status === 'active') { return res.status(409).json({ error: 'ACCOUNT_EXISTS', message: '该学生账号已存在且激活,请直接登录。' }); } if (userExists.status === 'pending') { return res.status(409).json({ error: 'ACCOUNT_PENDING', message: '该学生的注册申请正在审核中,请耐心等待。' }); } } } else { finalUsername = await generateStudentNo(); } await User.create({ username: finalUsername, password, role: 'STUDENT', trueName: cleanName, schoolId, status: 'pending', homeroomClass: cleanClass, studentNo: finalUsername, seatNo: seatNo || '', parentName: req.body.parentName, parentPhone: req.body.parentPhone, address: req.body.address, idCard: req.body.idCard, gender: req.body.gender || 'Male', createTime: new Date() }); return res.json({ username: finalUsername }); } const existing = await User.findOne({ username }); if (existing) return res.status(409).json({ error: 'USERNAME_EXISTS', message: '用户名已存在' }); await User.create({...req.body, status: 'pending', createTime: new Date()}); res.json({ username }); } catch(e) { console.error(e); res.status(500).json({ error: e.message }); } }); app.get('/api/users', async (req, res) => { const filter = getQueryFilter(req); const requesterRole = req.headers['x-user-role']; if (requesterRole === 'PRINCIPAL') filter.role = { $ne: 'ADMIN' }; if (req.query.role) filter.role = req.query.role; res.json(await User.find(filter).sort({ createTime: -1 })); }); app.put('/api/users/:id', async (req, res) => { const userId = req.params.id; const updates = req.body; const requesterRole = req.headers['x-user-role']; try { const user = await User.findById(userId); if (!user) return res.status(404).json({ error: 'User not found' }); if (requesterRole === 'PRINCIPAL') { if (user.schoolId !== req.headers['x-school-id']) return res.status(403).json({ error: 'Permission denied' }); if (user.role === 'ADMIN' || updates.role === 'ADMIN') return res.status(403).json({ error: 'Cannot modify Admin users' }); } if (user.status !== 'active' && updates.status === 'active') { if (user.role === 'TEACHER' && user.homeroomClass) { await ClassModel.updateOne( { schoolId: user.schoolId, $expr: { $eq: [{ $concat: ["$grade", "$className"] }, user.homeroomClass] } }, { teacherName: user.trueName || user.username } ); } if (user.role === 'STUDENT') { const profileData = { schoolId: user.schoolId, studentNo: user.studentNo, seatNo: user.seatNo, name: user.trueName, className: user.homeroomClass, gender: user.gender || 'Male', parentName: user.parentName, parentPhone: user.parentPhone, address: user.address, idCard: user.idCard, status: 'Enrolled', birthday: '2015-01-01' }; await Student.findOneAndUpdate( { studentNo: user.studentNo, schoolId: user.schoolId }, { $set: profileData }, { upsert: true, new: true } ); } } await User.findByIdAndUpdate(userId, updates); res.json({}); } catch (e) { console.error(e); res.status(500).json({ error: e.message }); } }); app.post('/api/users/class-application', async (req, res) => { const { userId, type, targetClass, action } = req.body; const userRole = req.headers['x-user-role']; const schoolId = req.headers['x-school-id']; if (action === 'APPLY') { try { const user = await User.findById(userId); if(!user) return res.status(404).json({error:'User not found'}); await User.findByIdAndUpdate(userId, { classApplication: { type: type, targetClass: targetClass || '', status: 'PENDING' } }); const typeText = type === 'CLAIM' ? '申请班主任' : '申请卸任'; await NotificationModel.create({ schoolId, targetUserId: userId, title: '申请已提交', content: `您已成功提交 ${typeText} (${type === 'CLAIM' ? targetClass : user.homeroomClass}) 的申请,等待管理员审核。`, type: 'info' }); await NotificationModel.create({ schoolId, targetRole: 'ADMIN', title: '新的班主任任免申请', content: `${user.trueName || user.username} 申请 ${typeText},请及时处理。`, type: 'warning' }); await NotificationModel.create({ schoolId, targetRole: 'PRINCIPAL', title: '新的班主任任免申请', content: `${user.trueName || user.username} 申请 ${typeText},请及时处理。`, type: 'warning' }); return res.json({ success: true }); } catch (e) { console.error(e); return res.status(500).json({ error: e.message }); } } if (userRole === 'ADMIN' || userRole === 'PRINCIPAL') { const user = await User.findById(userId); if (!user || !user.classApplication) return res.status(404).json({ error: 'Application not found' }); const appType = user.classApplication.type; const appTarget = user.classApplication.targetClass; if (action === 'APPROVE') { const updates = { classApplication: null }; if (appType === 'CLAIM') { updates.homeroomClass = appTarget; const classes = await ClassModel.find({ schoolId }); const matchedClass = classes.find(c => (c.grade + c.className) === appTarget); if (matchedClass) { if (matchedClass.teacherName) await User.updateOne({ trueName: matchedClass.teacherName, schoolId }, { homeroomClass: '' }); await ClassModel.findByIdAndUpdate(matchedClass._id, { teacherName: user.trueName || user.username }); } } else if (appType === 'RESIGN') { updates.homeroomClass = ''; if (user.homeroomClass) { const classes = await ClassModel.find({ schoolId }); const matchedClass = classes.find(c => (c.grade + c.className) === user.homeroomClass); if (matchedClass) await ClassModel.findByIdAndUpdate(matchedClass._id, { teacherName: '' }); } } await User.findByIdAndUpdate(userId, updates); await NotificationModel.create({ schoolId, targetUserId: userId, title: '申请已通过', content: `管理员已同意您的${appType === 'CLAIM' ? '任教' : '卸任'}申请。`, type: 'success' }); } else { await User.findByIdAndUpdate(userId, { 'classApplication.status': 'REJECTED' }); await NotificationModel.create({ schoolId, targetUserId: userId, title: '申请被拒绝', content: `管理员拒绝了您的${appType === 'CLAIM' ? '任教' : '卸任'}申请。`, type: 'error' }); } return res.json({ success: true }); } res.status(403).json({ error: 'Permission denied' }); }); app.post('/api/students/promote', async (req, res) => { const { teacherFollows } = req.body; const sId = req.headers['x-school-id']; const role = req.headers['x-user-role']; if (role !== 'ADMIN' && role !== 'PRINCIPAL') return res.status(403).json({ error: 'Permission denied' }); const GRADE_MAP = { '一年级': '二年级', '二年级': '三年级', '三年级': '四年级', '四年级': '五年级', '五年级': '六年级', '六年级': '毕业', '初一': '初二', '七年级': '八年级', '初二': '初三', '八年级': '九年级', '初三': '毕业', '九年级': '毕业', '高一': '高二', '高二': '高三', '高三': '毕业' }; const classes = await ClassModel.find(getQueryFilter(req)); let promotedCount = 0; for (const cls of classes) { const currentGrade = cls.grade; const nextGrade = GRADE_MAP[currentGrade] || currentGrade; const suffix = cls.className; if (nextGrade === '毕业') { const oldFullClass = cls.grade + cls.className; await Student.updateMany({ className: oldFullClass, ...getQueryFilter(req) }, { status: 'Graduated', className: '已毕业' }); if (teacherFollows && cls.teacherName) { await User.updateOne({ trueName: cls.teacherName, schoolId: sId }, { homeroomClass: '' }); await ClassModel.findByIdAndUpdate(cls._id, { teacherName: '' }); } } else { const oldFullClass = cls.grade + cls.className; const newFullClass = nextGrade + suffix; await ClassModel.findOneAndUpdate({ grade: nextGrade, className: suffix, schoolId: sId }, { schoolId: sId, grade: nextGrade, className: suffix, teacherName: teacherFollows ? cls.teacherName : undefined }, { upsert: true }); const result = await Student.updateMany({ className: oldFullClass, status: 'Enrolled', ...getQueryFilter(req) }, { className: newFullClass }); promotedCount += result.modifiedCount; if (teacherFollows && cls.teacherName) { await User.updateOne({ trueName: cls.teacherName, schoolId: sId, homeroomClass: oldFullClass }, { homeroomClass: newFullClass }); await ClassModel.findByIdAndUpdate(cls._id, { teacherName: '' }); } } } res.json({ success: true, count: promotedCount }); }); app.post('/api/students/transfer', async (req, res) => { const { studentId, targetClass } = req.body; const student = await Student.findById(studentId); if (!student) return res.status(404).json({ error: 'Student not found' }); student.className = targetClass; await student.save(); res.json({ success: true }); }); app.get('/api/achievements/config', async (req, res) => { const { className } = req.query; if (!className) return res.status(400).json({ error: 'Class name required' }); const config = await AchievementConfigModel.findOne({ ...getQueryFilter(req), className }); res.json(config || { className, achievements: [], exchangeRules: [] }); }); app.post('/api/achievements/config', async (req, res) => { const { className } = req.body; const sId = req.headers['x-school-id']; await AchievementConfigModel.findOneAndUpdate({ className, schoolId: sId }, injectSchoolId(req, req.body), { upsert: true }); res.json({ success: true }); }); app.get('/api/achievements/student', async (req, res) => { const { studentId, semester } = req.query; const filter = getQueryFilter(req); if (studentId) filter.studentId = studentId; if (semester && semester !== '全部时间' && semester !== 'All') filter.semester = semester; const list = await StudentAchievementModel.find(filter).sort({ createTime: -1 }); res.json(list); }); app.post('/api/achievements/grant', async (req, res) => { const { studentId, achievementId, semester } = req.body; const sId = req.headers['x-school-id']; const student = await Student.findById(studentId); const config = await AchievementConfigModel.findOne({ className: student.className, ...getQueryFilter(req) }); const ach = config?.achievements.find(a => a.id === achievementId); if (!ach) return res.status(404).json({ error: 'Achievement not found' }); await StudentAchievementModel.create({ schoolId: sId, studentId, studentName: student.name, achievementId: ach.id, achievementName: ach.name, achievementIcon: ach.icon, semester: semester || '当前学期' }); await Student.findByIdAndUpdate(studentId, { $inc: { flowerBalance: ach.points } }); res.json({ success: true }); }); app.post('/api/achievements/exchange', async (req, res) => { const { studentId, ruleId } = req.body; const sId = req.headers['x-school-id']; const student = await Student.findById(studentId); const config = await AchievementConfigModel.findOne({ className: student.className, ...getQueryFilter(req) }); const rule = config?.exchangeRules.find(r => r.id === ruleId); if (!rule) return res.status(404).json({ error: 'Exchange rule not found' }); if ((student.flowerBalance || 0) < rule.cost) return res.status(400).json({ error: 'INSUFFICIENT_FLOWERS', message: '小红花余额不足' }); await Student.findByIdAndUpdate(studentId, { $inc: { flowerBalance: -rule.cost } }); let status = 'PENDING'; if (rule.rewardType === 'DRAW_COUNT') { status = 'REDEEMED'; await Student.findByIdAndUpdate(studentId, { $inc: { drawAttempts: rule.rewardValue } }); } await StudentRewardModel.create({ schoolId: sId, studentId, studentName: student.name, rewardType: rule.rewardType, name: rule.rewardName, count: rule.rewardValue, status: status, source: `积分兑换` }); res.json({ success: true }); }); app.get('/api/games/lucky-config', async (req, res) => { const filter = getQueryFilter(req); if (req.query.className) filter.className = req.query.className; const config = await LuckyDrawConfigModel.findOne(filter); res.json(config || { prizes: [], dailyLimit: 3, cardCount: 9, defaultPrize: '再接再厉' }); }); app.post('/api/games/lucky-config', async (req, res) => { const data = injectSchoolId(req, req.body); await LuckyDrawConfigModel.findOneAndUpdate({ className: data.className, ...getQueryFilter(req) }, data, { upsert: true }); res.json({ success: true }); }); app.post('/api/games/lucky-draw', async (req, res) => { const { studentId } = req.body; const schoolId = req.headers['x-school-id']; const userRole = req.headers['x-user-role']; try { const student = await Student.findById(studentId); if (!student) return res.status(404).json({ error: 'Student not found' }); const config = await LuckyDrawConfigModel.findOne({ className: student.className, ...getQueryFilter(req) }); const prizes = config?.prizes || []; const defaultPrize = config?.defaultPrize || '再接再厉'; const dailyLimit = config?.dailyLimit || 3; const consolationWeight = config?.consolationWeight || 0; const availablePrizes = prizes.filter(p => (p.count === undefined || p.count > 0)); if (availablePrizes.length === 0 && consolationWeight === 0) return res.status(400).json({ error: 'POOL_EMPTY', message: '奖品库存不足' }); if (userRole === 'STUDENT') { if (student.drawAttempts <= 0) return res.status(403).json({ error: '次数不足', message: '您的抽奖次数已用完' }); const today = new Date().toISOString().split('T')[0]; let dailyLog = student.dailyDrawLog || { date: today, count: 0 }; if (dailyLog.date !== today) dailyLog = { date: today, count: 0 }; if (dailyLog.count >= dailyLimit) return res.status(403).json({ error: 'DAILY_LIMIT_REACHED', message: `今日抽奖次数已达上限 (${dailyLimit}次)` }); dailyLog.count += 1; student.drawAttempts -= 1; student.dailyDrawLog = dailyLog; await student.save(); } else { if (student.drawAttempts <= 0) return res.status(403).json({ error: '次数不足', message: '该学生抽奖次数已用完' }); student.drawAttempts -= 1; await student.save(); } let totalWeight = consolationWeight; availablePrizes.forEach(p => totalWeight += (p.probability || 0)); let random = Math.random() * totalWeight; let selectedPrize = defaultPrize; let rewardType = 'CONSOLATION'; let matchedPrize = null; for (const p of availablePrizes) { random -= (p.probability || 0); if (random <= 0) { matchedPrize = p; break; } } if (matchedPrize) { selectedPrize = matchedPrize.name; rewardType = 'ITEM'; if (config._id) await LuckyDrawConfigModel.updateOne({ _id: config._id, "prizes.id": matchedPrize.id }, { $inc: { "prizes.$.count": -1 } }); } await StudentRewardModel.create({ schoolId, studentId, studentName: student.name, rewardType, name: selectedPrize, count: 1, status: 'PENDING', source: '幸运大抽奖' }); res.json({ prize: selectedPrize, rewardType }); } catch (e) { res.status(500).json({ error: e.message }); } }); app.get('/api/games/monster-config', async (req, res) => { const filter = getQueryFilter(req); if (req.query.className) filter.className = req.query.className; const config = await GameMonsterConfigModel.findOne(filter); res.json(config || {}); }); app.post('/api/games/monster-config', async (req, res) => { const data = injectSchoolId(req, req.body); await GameMonsterConfigModel.findOneAndUpdate({ className: data.className, ...getQueryFilter(req) }, data, { upsert: true }); res.json({ success: true }); }); app.get('/api/games/zen-config', async (req, res) => { const filter = getQueryFilter(req); if (req.query.className) filter.className = req.query.className; const config = await GameZenConfigModel.findOne(filter); res.json(config || {}); }); app.post('/api/games/zen-config', async (req, res) => { const data = injectSchoolId(req, req.body); await GameZenConfigModel.findOneAndUpdate({ className: data.className, ...getQueryFilter(req) }, data, { upsert: true }); res.json({ success: true }); }); app.get('/api/notifications', async (req, res) => { const { role, userId } = req.query; const query = { $and: [ getQueryFilter(req), { $or: [ { targetRole: null, targetUserId: null }, { targetRole: role }, { targetUserId: userId } ] } ] }; res.json(await NotificationModel.find(query).sort({ createTime: -1 }).limit(20)); }); app.get('/api/public/schools', async (req, res) => { res.json(await School.find({}, 'name code _id')); }); app.get('/api/public/config', async (req, res) => { const currentSem = getAutoSemester(); let config = await ConfigModel.findOne({ key: 'main' }); if (config) { let semesters = config.semesters || []; if (!semesters.includes(currentSem)) { semesters.unshift(currentSem); config.semesters = semesters; config.semester = currentSem; await ConfigModel.updateOne({ key: 'main' }, { semesters, semester: currentSem }); } } else { config = { key: 'main', allowRegister: true, semester: currentSem, semesters: [currentSem] }; } res.json(config); }); app.get('/api/public/meta', async (req, res) => { res.json({ classes: await ClassModel.find({ schoolId: req.query.schoolId }), subjects: await SubjectModel.find({ schoolId: req.query.schoolId }) }); }); app.post('/api/auth/login', async (req, res) => { const { username, password } = req.body; const user = await User.findOne({ username, password }); if (!user) return res.status(401).json({ message: 'Error' }); if (user.status !== 'active') return res.status(403).json({ error: 'PENDING_APPROVAL' }); res.json(user); }); app.get('/api/schools', async (req, res) => { res.json(await School.find()); }); app.post('/api/schools', async (req, res) => { res.json(await School.create(req.body)); }); app.put('/api/schools/:id', async (req, res) => { await School.findByIdAndUpdate(req.params.id, req.body); res.json({}); }); app.delete('/api/schools/:id', async (req, res) => { const schoolId = req.params.id; try { await School.findByIdAndDelete(schoolId); await User.deleteMany({ schoolId }); await Student.deleteMany({ schoolId }); await ClassModel.deleteMany({ schoolId }); await SubjectModel.deleteMany({ schoolId }); await Course.deleteMany({ schoolId }); await Score.deleteMany({ schoolId }); await ExamModel.deleteMany({ schoolId }); await ScheduleModel.deleteMany({ schoolId }); await NotificationModel.deleteMany({ schoolId }); await AttendanceModel.deleteMany({ schoolId }); await LeaveRequestModel.deleteMany({ schoolId }); await GameSessionModel.deleteMany({ schoolId }); await StudentRewardModel.deleteMany({ schoolId }); await LuckyDrawConfigModel.deleteMany({ schoolId }); await GameMonsterConfigModel.deleteMany({ schoolId }); await GameZenConfigModel.deleteMany({ schoolId }); await AchievementConfigModel.deleteMany({ schoolId }); await StudentAchievementModel.deleteMany({ schoolId }); await SchoolCalendarModel.deleteMany({ schoolId }); res.json({ success: true }); } catch (e) { res.status(500).json({ error: e.message }); } }); app.delete('/api/users/:id', async (req, res) => { const requesterRole = req.headers['x-user-role']; if (requesterRole === 'PRINCIPAL') { const user = await User.findById(req.params.id); if (!user || user.schoolId !== req.headers['x-school-id']) return res.status(403).json({error: 'Permission denied'}); if (user.role === 'ADMIN') return res.status(403).json({error: 'Cannot delete admin'}); } await User.findByIdAndDelete(req.params.id); res.json({}); }); app.get('/api/students', async (req, res) => { res.json(await Student.find(getQueryFilter(req))); }); app.post('/api/students', async (req, res) => { const data = injectSchoolId(req, req.body); if (data.studentNo === '') delete data.studentNo; try { const existing = await Student.findOne({ schoolId: data.schoolId, name: data.name, className: data.className }); if (existing) { Object.assign(existing, data); if (!existing.studentNo) { existing.studentNo = await generateStudentNo(); } await existing.save(); } else { if (!data.studentNo) { data.studentNo = await generateStudentNo(); } await Student.create(data); } res.json({ success: true }); } catch (e) { if (e.code === 11000) return res.status(409).json({ error: 'DUPLICATE', message: '保存失败:存在重复的系统ID' }); res.status(500).json({ error: e.message }); } }); app.put('/api/students/:id', async (req, res) => { await Student.findByIdAndUpdate(req.params.id, req.body); res.json({}); }); app.delete('/api/students/:id', async (req, res) => { await Student.findByIdAndDelete(req.params.id); res.json({}); }); app.get('/api/classes', async (req, res) => { const filter = getQueryFilter(req); const cls = await ClassModel.find(filter); const resData = await Promise.all(cls.map(async c => { const count = await Student.countDocuments({ className: c.grade + c.className, status: 'Enrolled', ...filter }); return { ...c.toObject(), studentCount: count }; })); res.json(resData); }); app.post('/api/classes', async (req, res) => { const data = injectSchoolId(req, req.body); await ClassModel.create(data); if (data.teacherName) await User.updateOne({ trueName: data.teacherName, schoolId: data.schoolId }, { homeroomClass: data.grade + data.className }); res.json({}); }); app.put('/api/classes/:id', async (req, res) => { const classId = req.params.id; const { grade, className, teacherName } = req.body; const sId = req.headers['x-school-id']; const oldClass = await ClassModel.findById(classId); if (!oldClass) return res.status(404).json({ error: 'Class not found' }); const newFullClass = grade + className; const oldFullClass = oldClass.grade + oldClass.className; if (oldClass.teacherName && (oldClass.teacherName !== teacherName || oldFullClass !== newFullClass)) { await User.updateOne({ trueName: oldClass.teacherName, schoolId: sId, homeroomClass: oldFullClass }, { homeroomClass: '' }); } if (teacherName) await User.updateOne({ trueName: teacherName, schoolId: sId }, { homeroomClass: newFullClass }); await ClassModel.findByIdAndUpdate(classId, { grade, className, teacherName }); if (oldFullClass !== newFullClass) await Student.updateMany({ className: oldFullClass, schoolId: sId }, { className: newFullClass }); res.json({ success: true }); }); app.delete('/api/classes/:id', async (req, res) => { const cls = await ClassModel.findById(req.params.id); if (cls && cls.teacherName) await User.updateOne({ trueName: cls.teacherName, schoolId: cls.schoolId, homeroomClass: cls.grade + cls.className }, { homeroomClass: '' }); await ClassModel.findByIdAndDelete(req.params.id); res.json({}); }); app.get('/api/subjects', async (req, res) => { res.json(await SubjectModel.find(getQueryFilter(req))); }); app.post('/api/subjects', async (req, res) => { await SubjectModel.create(injectSchoolId(req, req.body)); res.json({}); }); app.put('/api/subjects/:id', async (req, res) => { await SubjectModel.findByIdAndUpdate(req.params.id, req.body); res.json({}); }); app.delete('/api/subjects/:id', async (req, res) => { await SubjectModel.findByIdAndDelete(req.params.id); res.json({}); }); app.get('/api/courses', async (req, res) => { res.json(await Course.find(getQueryFilter(req))); }); app.post('/api/courses', async (req, res) => { await Course.create(injectSchoolId(req, req.body)); res.json({}); }); app.put('/api/courses/:id', async (req, res) => { await Course.findByIdAndUpdate(req.params.id, req.body); res.json({}); }); app.delete('/api/courses/:id', async (req, res) => { await Course.findByIdAndDelete(req.params.id); res.json({}); }); app.get('/api/scores', async (req, res) => { res.json(await Score.find(getQueryFilter(req))); }); app.post('/api/scores', async (req, res) => { await Score.create(injectSchoolId(req, req.body)); res.json({}); }); app.put('/api/scores/:id', async (req, res) => { await Score.findByIdAndUpdate(req.params.id, req.body); res.json({}); }); app.delete('/api/scores/:id', async (req, res) => { await Score.findByIdAndDelete(req.params.id); res.json({}); }); app.get('/api/exams', async (req, res) => { res.json(await ExamModel.find(getQueryFilter(req))); }); app.post('/api/exams', async (req, res) => { await ExamModel.findOneAndUpdate({name:req.body.name}, injectSchoolId(req, req.body), {upsert:true}); res.json({}); }); app.get('/api/schedules', async (req, res) => { const query = { ...getQueryFilter(req), ...req.query }; if (query.grade) { query.className = { $regex: '^' + query.grade }; delete query.grade; } res.json(await ScheduleModel.find(query)); }); app.post('/api/schedules', async (req, res) => { const filter = { className:req.body.className, dayOfWeek:req.body.dayOfWeek, period:req.body.period }; const sId = req.headers['x-school-id']; if(sId) filter.schoolId = sId; await ScheduleModel.findOneAndUpdate(filter, injectSchoolId(req, req.body), {upsert:true}); res.json({}); }); app.delete('/api/schedules', async (req, res) => { await ScheduleModel.deleteOne({...getQueryFilter(req), ...req.query}); res.json({}); }); app.get('/api/stats', async (req, res) => { const filter = getQueryFilter(req); const studentCount = await Student.countDocuments(filter); const courseCount = await Course.countDocuments(filter); const scores = await Score.find({...filter, status: 'Normal'}); let avgScore = 0; let excellentRate = '0%'; if (scores.length > 0) { const total = scores.reduce((sum, s) => sum + s.score, 0); avgScore = parseFloat((total / scores.length).toFixed(1)); const excellent = scores.filter(s => s.score >= 90).length; excellentRate = Math.round((excellent / scores.length) * 100) + '%'; } res.json({ studentCount, courseCount, avgScore, excellentRate }); }); app.get('/api/config', async (req, res) => { const currentSem = getAutoSemester(); let config = await ConfigModel.findOne({key:'main'}); if (config) { let semesters = config.semesters || []; if (!semesters.includes(currentSem)) { semesters.unshift(currentSem); config.semesters = semesters; config.semester = currentSem; await ConfigModel.updateOne({ key: 'main' }, { semesters, semester: currentSem }); } } else { config = { key: 'main', allowRegister: true, semester: currentSem, semesters: [currentSem] }; } res.json(config); }); app.post('/api/config', async (req, res) => { await ConfigModel.findOneAndUpdate({key:'main'}, req.body, {upsert:true}); res.json({}); }); app.get('/api/games/mountain', async (req, res) => { res.json(await GameSessionModel.findOne({...getQueryFilter(req), className: req.query.className})); }); app.post('/api/games/mountain', async (req, res) => { const filter = { className: req.body.className }; const sId = req.headers['x-school-id']; if(sId) filter.schoolId = sId; await GameSessionModel.findOneAndUpdate(filter, injectSchoolId(req, req.body), {upsert:true}); res.json({}); }); app.post('/api/games/grant-reward', async (req, res) => { const { studentId, count, rewardType, name } = req.body; const finalCount = count || 1; const finalName = name || (rewardType === 'DRAW_COUNT' ? '抽奖券' : '奖品'); if (rewardType === 'DRAW_COUNT') await Student.findByIdAndUpdate(studentId, { $inc: { drawAttempts: finalCount } }); await StudentRewardModel.create({ schoolId: req.headers['x-school-id'], studentId, studentName: (await Student.findById(studentId)).name, rewardType, name: finalName, count: finalCount, status: rewardType === 'DRAW_COUNT' ? 'REDEEMED' : 'PENDING', source: '教师发放' }); res.json({}); }); app.put('/api/rewards/:id', async (req, res) => { await StudentRewardModel.findByIdAndUpdate(req.params.id, req.body); res.json({}); }); app.delete('/api/rewards/:id', async (req, res) => { const reward = await StudentRewardModel.findById(req.params.id); if (!reward) return res.status(404).json({error: 'Not found'}); if (reward.rewardType === 'DRAW_COUNT') { const student = await Student.findById(reward.studentId); if (student && student.drawAttempts < reward.count) return res.status(400).json({ error: 'FAILED_REVOKE', message: '修改失败,次数已被使用' }); await Student.findByIdAndUpdate(reward.studentId, { $inc: { drawAttempts: -reward.count } }); } await StudentRewardModel.findByIdAndDelete(req.params.id); res.json({}); }); app.get('/api/rewards', async (req, res) => { const filter = getQueryFilter(req); if(req.query.studentId) filter.studentId = req.query.studentId; if (req.query.className) { const classStudents = await Student.find({ className: req.query.className, ...getQueryFilter(req) }, '_id'); filter.studentId = { $in: classStudents.map(s => s._id.toString()) }; } if (req.query.excludeType) filter.rewardType = { $ne: req.query.excludeType }; const page = parseInt(req.query.page) || 1; const limit = parseInt(req.query.limit) || 20; const skip = (page - 1) * limit; const total = await StudentRewardModel.countDocuments(filter); const list = await StudentRewardModel.find(filter).sort({createTime:-1}).skip(skip).limit(limit); res.json({ list, total }); }); app.post('/api/rewards', async (req, res) => { const data = injectSchoolId(req, req.body); if (!data.count) data.count = 1; if(data.rewardType==='DRAW_COUNT') { data.status='REDEEMED'; await Student.findByIdAndUpdate(data.studentId, {$inc:{drawAttempts:data.count}}); } await StudentRewardModel.create(data); res.json({}); }); app.post('/api/rewards/:id/redeem', async (req, res) => { await StudentRewardModel.findByIdAndUpdate(req.params.id, {status:'REDEEMED'}); res.json({}); }); app.get('/api/attendance', async (req, res) => { const { className, date, studentId } = req.query; const filter = getQueryFilter(req); if(className) filter.className = className; if(date) filter.date = date; if(studentId) filter.studentId = studentId; res.json(await AttendanceModel.find(filter)); }); app.post('/api/attendance/check-in', async (req, res) => { const { studentId, date, status } = req.body; const exists = await AttendanceModel.findOne({ studentId, date }); if (exists) return res.status(400).json({ error: 'ALREADY_CHECKED_IN', message: '今日已打卡' }); const student = await Student.findById(studentId); await AttendanceModel.create({ schoolId: req.headers['x-school-id'], studentId, studentName: student.name, className: student.className, date, status: status || 'Present', checkInTime: new Date() }); res.json({ success: true }); }); app.post('/api/attendance/batch', async (req, res) => { const { className, date, status } = req.body; const students = await Student.find({ className, ...getQueryFilter(req) }); const ops = students.map(s => ({ updateOne: { filter: { studentId: s._id, date }, update: { $setOnInsert: { schoolId: req.headers['x-school-id'], studentId: s._id, studentName: s.name, className: s.className, date, status: status || 'Present', checkInTime: new Date() } }, upsert: true } })); if (ops.length > 0) await AttendanceModel.bulkWrite(ops); res.json({ success: true, count: ops.length }); }); app.put('/api/attendance/update', async (req, res) => { const { studentId, date, status } = req.body; const student = await Student.findById(studentId); await AttendanceModel.findOneAndUpdate({ studentId, date }, { schoolId: req.headers['x-school-id'], studentId, studentName: student.name, className: student.className, date, status, checkInTime: new Date() }, { upsert: true }); res.json({ success: true }); }); app.post('/api/leave', async (req, res) => { await LeaveRequestModel.create(injectSchoolId(req, req.body)); const { studentId, startDate } = req.body; const student = await Student.findById(studentId); if (student) await AttendanceModel.findOneAndUpdate({ studentId, date: startDate }, { schoolId: req.headers['x-school-id'], studentId, studentName: student.name, className: student.className, date: startDate, status: 'Leave', checkInTime: new Date() }, { upsert: true }); res.json({ success: true }); }); // CALENDAR ROUTES app.get('/api/attendance/calendar', async (req, res) => { const { className } = req.query; const filter = getQueryFilter(req); // Return both global holidays (className undefined or null) and class specific ones const query = { $and: [ filter, { $or: [{ className: { $exists: false } }, { className: null }, { className }] } ] }; res.json(await SchoolCalendarModel.find(query)); }); app.post('/api/attendance/calendar', async (req, res) => { await SchoolCalendarModel.create(injectSchoolId(req, req.body)); res.json({ success: true }); }); app.delete('/api/attendance/calendar/:id', async (req, res) => { await SchoolCalendarModel.findByIdAndDelete(req.params.id); res.json({ success: true }); }); app.post('/api/batch-delete', async (req, res) => { if(req.body.type==='student') await Student.deleteMany({_id:{$in:req.body.ids}}); if(req.body.type==='score') await Score.deleteMany({_id:{$in:req.body.ids}}); res.json({}); }); app.get('*', (req, res) => { res.sendFile(path.join(__dirname, 'dist', 'index.html')); }); app.listen(PORT, () => console.log(`🚀 Server running on port ${PORT}`));