Spaces:
Sleeping
Sleeping
| const express = require('express'); | |
| const router = express.Router(); | |
| const { User, Student, ClassModel, NotificationModel, School } = require('../models'); | |
| // Helpers | |
| const getQueryFilter = (req) => { | |
| const s = req.headers['x-school-id']; | |
| const role = req.headers['x-user-role']; | |
| // 1. If requester is a Principal, they only see their own school's data | |
| if (role === 'PRINCIPAL') { | |
| if (!s) return { _id: null }; | |
| return { schoolId: s }; | |
| } | |
| // 2. If requester is ADMIN (Super Admin) | |
| if (role === 'ADMIN') { | |
| // If filtering by a specific school (e.g. from dropdown), return that school's data | |
| if (s) return { schoolId: s }; | |
| // If fetching "Global" list (no specific school selected or 'All' selected): | |
| // We want to see: | |
| // a) Users with NO schoolId (e.g. pending principals creating schools) | |
| // b) Users from ALL schools (if we want a truly global list, we might remove the filter entirely, | |
| // but usually 'getQueryFilter' is used to scope data. | |
| // For the "User Management" page specifically, the route handler below handles the global flag.) | |
| // Default behavior for other resources (like students/classes) when admin hasn't selected a school: | |
| return {}; | |
| } | |
| // 3. Teachers/Students | |
| if (!s) return {}; | |
| return { | |
| $or: [ | |
| { schoolId: s }, | |
| { schoolId: { $exists: false } }, | |
| { schoolId: null } | |
| ] | |
| }; | |
| }; | |
| const generateStudentNo = async () => { | |
| const year = new Date().getFullYear(); | |
| const random = Math.floor(100000 + Math.random() * 900000); | |
| return `${year}${random}`; | |
| }; | |
| // --- Auth Routes --- | |
| router.post('/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); | |
| }); | |
| router.get('/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); | |
| }); | |
| router.post('/register', async (req, res) => { | |
| const { role, username, password, schoolId, trueName, seatNo, isCreatingSchool, newSchoolName, newSchoolCode } = 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) return res.status(409).json({ error: userExists.status === 'active' ? 'ACCOUNT_EXISTS' : '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 }); | |
| } | |
| // Check Username Duplication | |
| const existing = await User.findOne({ username }); | |
| if (existing) return res.status(409).json({ error: 'USERNAME_EXISTS', message: '用户名已存在' }); | |
| // Handle Principal creating a school | |
| let pendingSchoolData = null; | |
| let finalSchoolId = schoolId; | |
| if (role === 'PRINCIPAL' && isCreatingSchool) { | |
| if (!newSchoolName || !newSchoolCode) return res.status(400).json({ error: 'MISSING_SCHOOL_INFO', message: '请输入学校名称和代码' }); | |
| // Check if school already exists | |
| const schoolExists = await School.findOne({ $or: [{ name: newSchoolName }, { code: newSchoolCode }] }); | |
| if (schoolExists) return res.status(409).json({ error: 'SCHOOL_EXISTS', message: '该学校名称或代码已存在,请选择“加入学校”' }); | |
| pendingSchoolData = { | |
| name: newSchoolName, | |
| code: newSchoolCode | |
| }; | |
| finalSchoolId = ''; // No school ID yet | |
| } | |
| await User.create({...req.body, schoolId: finalSchoolId, pendingSchoolData, status: 'pending', createTime: new Date()}); | |
| res.json({ username }); | |
| } catch(e) { res.status(500).json({ error: e.message }); } | |
| }); | |
| router.post('/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 (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.role === 'STUDENT') await Student.findOneAndUpdate({ studentNo: user.studentNo }, { name: user.trueName || user.username, phone: user.phone }); | |
| res.json({ success: true, user }); | |
| } catch (e) { res.status(500).json({ error: e.message }); } | |
| }); | |
| // --- User Management Routes --- | |
| router.get('/', async (req, res) => { | |
| let filter = {}; | |
| // If requesting "Global" list (e.g. Admin Panel with 'global=true'), ignore school filter | |
| if (req.query.global === 'true' && req.headers['x-user-role'] === 'ADMIN') { | |
| filter = {}; // Return all users | |
| } else { | |
| // Otherwise apply standard school scoping | |
| filter = getQueryFilter(req); | |
| } | |
| if (req.headers['x-user-role'] === 'PRINCIPAL') filter.role = { $ne: 'ADMIN' }; | |
| if (req.query.role) filter.role = req.query.role; | |
| // Special case: If Admin is viewing a specific school, but we also want to see | |
| // pending principals who HAVE NO schoolId yet (the "creating school" case), | |
| // we need to OR the query. | |
| // However, the UI usually separates "Global View" vs "School View". | |
| // If Admin is in "Global View", they will see everything (including empty schoolId). | |
| // If Admin selects a school, they only see that school's users. | |
| // The issue was likely that `getQueryFilter` was being applied strictly even for global admin view. | |
| // The change above (checking `req.query.global`) fixes this. | |
| res.json(await User.find(filter).sort({ createTime: -1 })); | |
| }); | |
| router.put('/:id', async (req, res) => { | |
| const userId = req.params.id; const updates = req.body; | |
| try { | |
| const user = await User.findById(userId); | |
| if (!user) return res.status(404).json({ error: 'User not found' }); | |
| // Handle Student activation logic (existing) | |
| if (user.status !== 'active' && updates.status === 'active' && user.role === 'STUDENT') { | |
| await Student.findOneAndUpdate({ studentNo: user.studentNo, schoolId: user.schoolId }, { $set: { 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' } }, { upsert: true, new: true }); | |
| } | |
| // NEW: Handle Principal School Creation on Activation | |
| if (user.status !== 'active' && updates.status === 'active' && user.role === 'PRINCIPAL' && user.pendingSchoolData && user.pendingSchoolData.name) { | |
| // Check again if school exists (double safety) | |
| const exists = await School.findOne({ $or: [{ name: user.pendingSchoolData.name }, { code: user.pendingSchoolData.code }] }); | |
| if (exists) { | |
| // Conflict! Just link to existing school? Or fail? | |
| // Let's link to existing to allow approval, but clear creation data. | |
| updates.schoolId = exists._id; | |
| updates.pendingSchoolData = null; | |
| } else { | |
| // Create New School | |
| const newSchool = await School.create({ | |
| name: user.pendingSchoolData.name, | |
| code: user.pendingSchoolData.code | |
| }); | |
| updates.schoolId = newSchool._id; | |
| updates.pendingSchoolData = null; | |
| } | |
| } | |
| await User.findByIdAndUpdate(userId, updates); | |
| res.json({}); | |
| } catch (e) { res.status(500).json({ error: e.message }); } | |
| }); | |
| router.delete('/: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({}); | |
| }); | |
| router.put('/:id/menu-order', async (req, res) => { | |
| const { menuOrder } = req.body; | |
| await User.findByIdAndUpdate(req.params.id, { menuOrder }); | |
| res.json({ success: true }); | |
| }); | |
| router.post('/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' } }); await NotificationModel.create({ schoolId, targetRole: 'ADMIN', title: '新的班主任任免申请', content: `${user.trueName || user.username} 申请 ${type === 'CLAIM' ? '任教' : '卸任'},请及时处理。`, type: 'warning' }); return res.json({ success: true }); } catch (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 }; const classes = await ClassModel.find({ schoolId }); | |
| if (appType === 'CLAIM') { updates.homeroomClass = appTarget; const matchedClass = classes.find(c => (c.grade + c.className) === appTarget); if (matchedClass) { const teacherIds = matchedClass.homeroomTeacherIds || []; if (!teacherIds.includes(userId)) { teacherIds.push(userId); const teachers = await User.find({ _id: { $in: teacherIds } }); const names = teachers.map(t => t.trueName || t.username).join(', '); await ClassModel.findByIdAndUpdate(matchedClass._id, { homeroomTeacherIds: teacherIds, teacherName: names }); } } } else if (appType === 'RESIGN') { updates.homeroomClass = ''; const matchedClass = classes.find(c => (c.grade + c.className) === user.homeroomClass); if (matchedClass) { const teacherIds = (matchedClass.homeroomTeacherIds || []).filter(id => id !== userId); const teachers = await User.find({ _id: { $in: teacherIds } }); const names = teachers.map(t => t.trueName || t.username).join(', '); await ClassModel.findByIdAndUpdate(matchedClass._id, { homeroomTeacherIds: teacherIds, teacherName: names }); } } | |
| await User.findByIdAndUpdate(userId, updates); | |
| } else await User.findByIdAndUpdate(userId, { 'classApplication.status': 'REJECTED' }); | |
| return res.json({ success: true }); | |
| } | |
| res.status(403).json({ error: 'Permission denied' }); | |
| }); | |
| module.exports = router; | |