dvc890 commited on
Commit
720777f
·
verified ·
1 Parent(s): 57073bc

Upload 54 files

Browse files
Files changed (1) hide show
  1. server.js +520 -95
server.js CHANGED
@@ -1,123 +1,548 @@
1
 
 
 
 
 
 
 
 
 
 
 
2
  const express = require('express');
3
  const mongoose = require('mongoose');
4
- const bodyParser = require('body-parser');
5
  const cors = require('cors');
 
6
  const path = require('path');
7
- const compression = require('compression');
8
-
9
- // Import Routes
10
- const aiRoutes = require('./ai-routes');
11
- // Note: Assuming other routes (auth, students, etc.) are handled in a separate file or inline.
12
- // Since previous file content was overwritten/unclear, we'll setup a basic server structure
13
- // that includes the AI routes and generic CRUD handlers if they were meant to be here.
14
- // For now, we focus on fixing the start-up error.
15
 
16
- const {
17
- User, Student, Course, Score, ClassModel, SubjectModel, ExamModel, ScheduleModel,
18
- ConfigModel, NotificationModel, GameSessionModel, StudentRewardModel, LuckyDrawConfigModel,
19
- GameMonsterConfigModel, GameZenConfigModel, AchievementConfigModel, TeacherExchangeConfigModel,
20
- StudentAchievementModel, AttendanceModel, LeaveRequestModel, SchoolCalendarModel,
21
- WishModel, FeedbackModel, TodoModel, School
22
- } = require('./models');
23
 
24
  const app = express();
25
- const PORT = process.env.PORT || 7860;
26
 
27
- // Middleware
 
 
 
 
 
 
 
 
28
  app.use(cors());
29
- app.use(compression());
30
- app.use(bodyParser.json({ limit: '50mb' }));
31
- app.use(bodyParser.urlencoded({ extended: true, limit: '50mb' }));
 
 
 
 
 
 
 
32
 
33
- // Database Connection
34
- mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/school_db')
35
- .then(() => console.log('MongoDB Connected'))
36
- .catch(err => console.error('MongoDB Connection Error:', err));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
 
38
- // --- Routes ---
 
 
 
 
39
 
40
- // AI Routes
41
  app.use('/api/ai', aiRoutes);
42
 
43
- // Generic CRUD Handlers (Restoring presumed functionality based on api.ts)
44
- // Auth
45
- app.post('/api/auth/login', async (req, res) => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  try {
47
- const { username, password } = req.body;
48
- const user = await User.findOne({ username, password });
49
- if (!user) return res.status(401).json({ error: 'Invalid credentials' });
50
- if (user.status === 'banned') return res.status(403).json({ error: 'BANNED' });
51
- if (user.status === 'pending') return res.status(403).json({ error: 'PENDING_APPROVAL' });
52
- res.json({ token: 'mock-token-' + user._id, user });
53
- } catch (e) { res.status(500).json({ error: e.message }); }
54
  });
55
 
56
- app.post('/api/auth/register', async (req, res) => {
 
57
  try {
58
- const newUser = new User({ ...req.body, status: 'pending', createTime: new Date() });
59
- await newUser.save();
60
- res.json(newUser);
61
- } catch (e) { res.status(500).json({ error: e.message }); }
 
 
 
 
 
 
 
 
 
 
 
 
62
  });
63
 
64
- app.get('/api/auth/me', async (req, res) => {
65
- // Simple mock auth check
66
- const token = req.headers.authorization;
67
- if (!token) return res.status(401).json({ error: 'No token' });
68
- // In real app, decode token. Here we assume client has valid session if they have a token.
69
- // For refresh, we'd need user ID. Since this is a fix, we'll return a mock or rely on client data.
70
- res.json({});
71
- });
72
-
73
- // Basic Resource Routes (Simplified for fix)
74
- const resources = {
75
- 'students': Student,
76
- 'classes': ClassModel,
77
- 'courses': Course,
78
- 'scores': Score,
79
- 'subjects': SubjectModel,
80
- 'users': User,
81
- 'schools': School,
82
- 'exams': ExamModel,
83
- 'todos': TodoModel
84
- };
85
 
86
- Object.keys(resources).forEach(path => {
87
- const Model = resources[path];
88
- app.get(`/api/${path}`, async (req, res) => {
89
- try { res.json(await Model.find(req.query)); } catch (e) { res.status(500).json({ error: e.message }); }
90
- });
91
- app.post(`/api/${path}`, async (req, res) => {
92
- try { const item = new Model(req.body); await item.save(); res.json(item); } catch (e) { res.status(500).json({ error: e.message }); }
93
- });
94
- app.put(`/api/${path}/:id`, async (req, res) => {
95
- try { await Model.findByIdAndUpdate(req.params.id, req.body); res.json({ success: true }); } catch (e) { res.status(500).json({ error: e.message }); }
96
- });
97
- app.delete(`/api/${path}/:id`, async (req, res) => {
98
- try { await Model.findByIdAndDelete(req.params.id); res.json({ success: true }); } catch (e) { res.status(500).json({ error: e.message }); }
99
- });
100
- });
101
-
102
- // Config Route
103
- app.get('/api/config', async (req, res) => res.json(await ConfigModel.findOne({ key: 'main' }) || {}));
104
- app.post('/api/config', async (req, res) => {
105
- await ConfigModel.findOneAndUpdate({ key: 'main' }, req.body, { upsert: true });
106
  res.json({ success: true });
107
  });
108
- app.get('/api/public/config', async (req, res) => {
109
- const cfg = await ConfigModel.findOne({ key: 'main' });
110
- if(cfg) {
111
- // Hide keys
112
- const { apiKeys, ...publicCfg } = cfg.toObject();
113
- res.json(publicCfg);
114
- } else res.json({});
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
 
117
- // Serve Frontend
118
- app.use(express.static(path.join(__dirname, 'dist')));
119
- app.get('*', (req, res) => {
120
- res.sendFile(path.join(__dirname, 'dist', 'index.html'));
 
 
 
121
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
 
123
- app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
 
 
1
 
2
+ const {
3
+ School, User, Student, Course, Score, ClassModel, SubjectModel, ExamModel, ScheduleModel,
4
+ ConfigModel, NotificationModel, GameSessionModel, StudentRewardModel, LuckyDrawConfigModel, GameMonsterConfigModel, GameZenConfigModel,
5
+ AchievementConfigModel, TeacherExchangeConfigModel, StudentAchievementModel, AttendanceModel, LeaveRequestModel, SchoolCalendarModel,
6
+ WishModel, FeedbackModel, TodoModel
7
+ } = require('./models');
8
+
9
+ // Import AI Routes
10
+ const aiRoutes = require('./ai-routes');
11
+
12
  const express = require('express');
13
  const mongoose = require('mongoose');
 
14
  const cors = require('cors');
15
+ const bodyParser = require('body-parser');
16
  const path = require('path');
17
+ const compression = require('compression');
 
 
 
 
 
 
 
18
 
19
+ const PORT = 7860;
20
+ const MONGO_URI = 'mongodb+srv://dv890a:db8822723@chatpro.gw3v0v7.mongodb.net/chatpro?retryWrites=true&w=majority&appName=chatpro&authSource=admin';
 
 
 
 
 
21
 
22
  const app = express();
 
23
 
24
+ app.use(compression({
25
+ filter: (req, res) => {
26
+ if (req.originalUrl && req.originalUrl.includes('/api/ai/chat')) {
27
+ return false;
28
+ }
29
+ return compression.filter(req, res);
30
+ }
31
+ }));
32
+
33
  app.use(cors());
34
+ app.use(bodyParser.json({ limit: '50mb' }));
35
+ app.use(express.static(path.join(__dirname, 'dist'), {
36
+ setHeaders: (res, filePath) => {
37
+ if (filePath.endsWith('.html')) {
38
+ res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
39
+ } else {
40
+ res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
41
+ }
42
+ }
43
+ }));
44
 
45
+ const InMemoryDB = { schools: [], users: [], isFallback: false };
46
+ const connectDB = async () => {
47
+ try {
48
+ await mongoose.connect(MONGO_URI, { serverSelectionTimeoutMS: 30000 });
49
+ console.log('✅ MongoDB 连接成功 (Real Data)');
50
+
51
+ // FIX: Drop the restrictive index that prevents multiple schedules per slot
52
+ try {
53
+ await ScheduleModel.collection.dropIndex('schoolId_1_className_1_dayOfWeek_1_period_1');
54
+ console.log('✅ Dropped restrictive schedule index');
55
+ } catch (e) {
56
+ // Ignore error if index doesn't exist
57
+ }
58
+
59
+ } catch (err) {
60
+ console.error('❌ MongoDB 连接失败:', err.message);
61
+ InMemoryDB.isFallback = true;
62
+ }
63
+ };
64
+ connectDB();
65
+
66
+ const getQueryFilter = (req) => {
67
+ const s = req.headers['x-school-id'];
68
+ const role = req.headers['x-user-role'];
69
+ if (role === 'PRINCIPAL') {
70
+ if (!s) return { _id: null };
71
+ return { schoolId: s };
72
+ }
73
+ if (!s) return {};
74
+ return {
75
+ $or: [
76
+ { schoolId: s },
77
+ { schoolId: { $exists: false } },
78
+ { schoolId: null }
79
+ ]
80
+ };
81
+ };
82
+ const injectSchoolId = (req, b) => ({ ...b, schoolId: req.headers['x-school-id'] });
83
+
84
+ const getGameOwnerFilter = async (req) => {
85
+ const role = req.headers['x-user-role'];
86
+ const username = req.headers['x-user-username'];
87
+ if (role === 'TEACHER') {
88
+ const user = await User.findOne({ username });
89
+ return { ownerId: user ? user._id.toString() : 'unknown' };
90
+ }
91
+ return {};
92
+ };
93
+
94
+ const getAutoSemester = () => {
95
+ const now = new Date();
96
+ const month = now.getMonth() + 1;
97
+ const year = now.getFullYear();
98
+ if (month >= 8 || month === 1) {
99
+ const startYear = month === 1 ? year - 1 : year;
100
+ return `${startYear}-${startYear + 1}学年 第一学期`;
101
+ } else {
102
+ const startYear = year - 1;
103
+ return `${startYear}-${startYear + 1}学年 第二学期`;
104
+ }
105
+ };
106
 
107
+ const generateStudentNo = async () => {
108
+ const year = new Date().getFullYear();
109
+ const random = Math.floor(100000 + Math.random() * 900000);
110
+ return `${year}${random}`;
111
+ };
112
 
113
+ // MOUNT AI ROUTES
114
  app.use('/api/ai', aiRoutes);
115
 
116
+ // ... (Rest of Existing Routes) ...
117
+
118
+ // --- TODO LIST ENDPOINTS ---
119
+ app.get('/api/todos', async (req, res) => {
120
+ const username = req.headers['x-user-username'];
121
+ if (!username) return res.status(401).json({ error: 'Unauthorized' });
122
+ const user = await User.findOne({ username });
123
+ if (!user) return res.status(404).json({ error: 'User not found' });
124
+ res.json(await TodoModel.find({ userId: user._id.toString() }).sort({ isCompleted: 1, createTime: -1 }));
125
+ });
126
+
127
+ app.post('/api/todos', async (req, res) => {
128
+ const username = req.headers['x-user-username'];
129
+ const user = await User.findOne({ username });
130
+ if (!user) return res.status(404).json({ error: 'User not found' });
131
+ await TodoModel.create({ ...req.body, userId: user._id.toString() });
132
+ res.json({ success: true });
133
+ });
134
+
135
+ app.put('/api/todos/:id', async (req, res) => {
136
+ await TodoModel.findByIdAndUpdate(req.params.id, req.body);
137
+ res.json({ success: true });
138
+ });
139
+
140
+ app.delete('/api/todos/:id', async (req, res) => {
141
+ await TodoModel.findByIdAndDelete(req.params.id);
142
+ res.json({ success: true });
143
+ });
144
+
145
+ // --- UPDATED SCHEDULE ENDPOINTS ---
146
+ app.get('/api/schedules', async (req, res) => {
147
+ const query = { ...getQueryFilter(req), ...req.query };
148
+ if (query.grade) { query.className = { $regex: '^' + query.grade }; delete query.grade; }
149
+ res.json(await ScheduleModel.find(query));
150
+ });
151
+
152
+ // NEW: Update by ID (Exact Update)
153
+ app.put('/api/schedules/:id', async (req, res) => {
154
  try {
155
+ await ScheduleModel.findByIdAndUpdate(req.params.id, req.body);
156
+ res.json({ success: true });
157
+ } catch (e) {
158
+ res.status(500).json({ error: e.message });
159
+ }
 
 
160
  });
161
 
162
+ // Create or Update by Logic (Upsert)
163
+ app.post('/api/schedules', async (req, res) => {
164
  try {
165
+ // Updated Filter: Include weekType to allow separate ODD/EVEN records for same slot
166
+ const filter = {
167
+ className: req.body.className,
168
+ dayOfWeek: req.body.dayOfWeek,
169
+ period: req.body.period,
170
+ weekType: req.body.weekType || 'ALL'
171
+ };
172
+ const sId = req.headers['x-school-id'];
173
+ if(sId) filter.schoolId = sId;
174
+
175
+ await ScheduleModel.findOneAndUpdate(filter, injectSchoolId(req, req.body), {upsert:true});
176
+ res.json({});
177
+ } catch (e) {
178
+ console.error("Save schedule error:", e);
179
+ res.status(500).json({ error: e.message });
180
+ }
181
  });
182
 
183
+ app.delete('/api/schedules', async (req, res) => {
184
+ try {
185
+ // Support deleting by ID if provided
186
+ if (req.query.id) {
187
+ await ScheduleModel.findByIdAndDelete(req.query.id);
188
+ } else {
189
+ await ScheduleModel.deleteOne({...getQueryFilter(req), ...req.query});
190
+ }
191
+ res.json({});
192
+ } catch (e) {
193
+ res.status(500).json({ error: e.message });
194
+ }
195
+ });
 
 
 
 
 
 
 
 
196
 
197
+ // --- USER MENU ORDER ---
198
+ app.put('/api/users/:id/menu-order', async (req, res) => {
199
+ const { menuOrder } = req.body;
200
+ await User.findByIdAndUpdate(req.params.id, { menuOrder });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
  res.json({ success: true });
202
  });
203
+
204
+ // ... (Rest of existing routes unchanged) ...
205
+ app.get('/api/classes/:className/teachers', async (req, res) => {
206
+ const { className } = req.params;
207
+ const schoolId = req.headers['x-school-id'];
208
+ const normalize = (s) => (s || '').replace(/\s+/g, '');
209
+ const searchName = normalize(decodeURIComponent(className));
210
+ const teacherIds = new Set();
211
+ const teacherNamesToResolve = new Set();
212
+ try {
213
+ const allClasses = await ClassModel.find({ schoolId });
214
+ const matchedClass = allClasses.find(c => {
215
+ const full = normalize(c.grade + c.className);
216
+ const sub = normalize(c.className);
217
+ return full === searchName || sub === searchName;
218
+ });
219
+ if (matchedClass) {
220
+ if (matchedClass.homeroomTeacherIds && matchedClass.homeroomTeacherIds.length > 0) {
221
+ matchedClass.homeroomTeacherIds.forEach(id => teacherIds.add(id));
222
+ }
223
+ if (matchedClass.teacherName) { matchedClass.teacherName.split(/[,,]/).forEach(n => { if(n.trim()) teacherNamesToResolve.add(n.trim()); }); }
224
+ }
225
+ const allCourses = await Course.find({ schoolId });
226
+ allCourses.forEach(c => {
227
+ if (normalize(c.className) === searchName) {
228
+ if (c.teacherId) { teacherIds.add(c.teacherId); } else if (c.teacherName) { teacherNamesToResolve.add(c.teacherName); }
229
+ }
230
+ });
231
+ if (teacherNamesToResolve.size > 0) {
232
+ const names = Array.from(teacherNamesToResolve);
233
+ const users = await User.find({ schoolId, role: 'TEACHER', $or: [ { trueName: { $in: names } }, { username: { $in: names } } ] });
234
+ users.forEach(u => teacherIds.add(u._id.toString()));
235
+ }
236
+ if (teacherIds.size === 0) return res.json([]);
237
+ const teachers = await User.find({ _id: { $in: Array.from(teacherIds) } }, 'trueName username _id teachingSubject');
238
+ res.json(teachers);
239
+ } catch (e) { res.json([]); }
240
+ });
241
+
242
+ app.get('/api/wishes', async (req, res) => {
243
+ const { teacherId, studentId, status } = req.query;
244
+ const filter = getQueryFilter(req);
245
+ if (teacherId) filter.teacherId = teacherId;
246
+ if (studentId) filter.studentId = studentId;
247
+ if (status) filter.status = status;
248
+ res.json(await WishModel.find(filter).sort({ status: -1, createTime: -1 }));
249
+ });
250
+
251
+ app.post('/api/wishes', async (req, res) => {
252
+ const data = injectSchoolId(req, req.body);
253
+ const existing = await WishModel.findOne({ studentId: data.studentId, status: 'PENDING' });
254
+ if (existing) { return res.status(400).json({ error: 'LIMIT_REACHED', message: '您还有一个未实现的愿望,请等待实现后再许愿!' }); }
255
+ await WishModel.create(data);
256
+ res.json({ success: true });
257
+ });
258
+
259
+ app.post('/api/wishes/:id/fulfill', async (req, res) => {
260
+ await WishModel.findByIdAndUpdate(req.params.id, { status: 'FULFILLED', fulfillTime: new Date() });
261
+ res.json({ success: true });
262
+ });
263
+
264
+ app.post('/api/wishes/random-fulfill', async (req, res) => {
265
+ const { teacherId } = req.body;
266
+ const pendingWishes = await WishModel.find({ teacherId, status: 'PENDING' });
267
+ if (pendingWishes.length === 0) { return res.status(404).json({ error: 'NO_WISHES', message: '暂无待实现的愿望' }); }
268
+ const randomWish = pendingWishes[Math.floor(Math.random() * pendingWishes.length)];
269
+ await WishModel.findByIdAndUpdate(randomWish._id, { status: 'FULFILLED', fulfillTime: new Date() });
270
+ res.json({ success: true, wish: randomWish });
271
+ });
272
+
273
+ app.get('/api/feedback', async (req, res) => {
274
+ const { targetId, creatorId, type, status } = req.query;
275
+ const filter = getQueryFilter(req);
276
+ if (targetId) filter.targetId = targetId;
277
+ if (creatorId) filter.creatorId = creatorId;
278
+ if (type) filter.type = type;
279
+ if (status) { const statuses = status.split(','); if (statuses.length > 1) { filter.status = { $in: statuses }; } else { filter.status = status; } }
280
+ res.json(await FeedbackModel.find(filter).sort({ createTime: -1 }));
281
+ });
282
+
283
+ app.post('/api/feedback', async (req, res) => {
284
+ const data = injectSchoolId(req, req.body);
285
+ await FeedbackModel.create(data);
286
+ res.json({ success: true });
287
+ });
288
+
289
+ app.put('/api/feedback/:id', async (req, res) => {
290
+ const { id } = req.params; const { status, reply } = req.body;
291
+ const updateData = { updateTime: new Date() };
292
+ if (status) updateData.status = status;
293
+ if (reply !== undefined) updateData.reply = reply;
294
+ await FeedbackModel.findByIdAndUpdate(id, updateData);
295
+ res.json({ success: true });
296
+ });
297
+
298
+ app.post('/api/feedback/ignore-all', async (req, res) => {
299
+ const { targetId } = req.body;
300
+ await FeedbackModel.updateMany( { targetId, status: 'PENDING' }, { status: 'IGNORED', updateTime: new Date() } );
301
+ res.json({ success: true });
302
+ });
303
+
304
+ app.get('/api/games/lucky-config', async (req, res) => {
305
+ const filter = getQueryFilter(req);
306
+ if (req.query.ownerId) { filter.ownerId = req.query.ownerId; } else { const ownerFilter = await getGameOwnerFilter(req); Object.assign(filter, ownerFilter); }
307
+ if (req.query.className) filter.className = req.query.className;
308
+ const config = await LuckyDrawConfigModel.findOne(filter);
309
+ res.json(config || null);
310
+ });
311
+ app.get('/api/achievements/config', async (req, res) => {
312
+ const { className } = req.query;
313
+ const filter = getQueryFilter(req);
314
+ if (className) filter.className = className;
315
+ res.json(await AchievementConfigModel.findOne(filter));
316
+ });
317
+ app.post('/api/achievements/config', async (req, res) => {
318
+ const data = injectSchoolId(req, req.body);
319
+ await AchievementConfigModel.findOneAndUpdate( { className: data.className, ...getQueryFilter(req) }, data, { upsert: true } );
320
+ res.json({ success: true });
321
+ });
322
+ app.get('/api/achievements/teacher-rules', async (req, res) => {
323
+ const filter = getQueryFilter(req);
324
+ if (req.query.teacherId) { filter.teacherId = req.query.teacherId; } else if (req.headers['x-user-role'] === 'TEACHER') { const user = await User.findOne({ username: req.headers['x-user-username'] }); if (user) filter.teacherId = user._id.toString(); }
325
+ if (req.query.teacherIds) { const ids = req.query.teacherIds.split(','); delete filter.teacherId; filter.teacherId = { $in: ids }; const configs = await TeacherExchangeConfigModel.find(filter); return res.json(configs); }
326
+ const config = await TeacherExchangeConfigModel.findOne(filter);
327
+ res.json(config || { rules: [] });
328
+ });
329
+ app.post('/api/achievements/teacher-rules', async (req, res) => {
330
+ const data = injectSchoolId(req, req.body);
331
+ const user = await User.findOne({ username: req.headers['x-user-username'] });
332
+ if (!user) return res.status(404).json({ error: 'User not found' });
333
+ data.teacherId = user._id.toString();
334
+ data.teacherName = user.trueName || user.username;
335
+ await TeacherExchangeConfigModel.findOneAndUpdate( { teacherId: data.teacherId, ...getQueryFilter(req) }, data, { upsert: true } );
336
+ res.json({ success: true });
337
+ });
338
+ app.get('/api/achievements/student', async (req, res) => {
339
+ const { studentId, semester } = req.query;
340
+ const filter = { studentId };
341
+ if (semester) filter.semester = semester;
342
+ res.json(await StudentAchievementModel.find(filter).sort({ createTime: -1 }));
343
+ });
344
+ app.post('/api/achievements/grant', async (req, res) => {
345
+ const { studentId, achievementId, semester } = req.body;
346
+ const sId = req.headers['x-school-id'];
347
+ const student = await Student.findById(studentId);
348
+ if (!student) return res.status(404).json({ error: 'Student not found' });
349
+ const config = await AchievementConfigModel.findOne({ className: student.className, schoolId: sId });
350
+ const achievement = config?.achievements.find(a => a.id === achievementId);
351
+ if (!achievement) return res.status(404).json({ error: 'Achievement not found' });
352
+ await StudentAchievementModel.create({ schoolId: sId, studentId, studentName: student.name, achievementId: achievement.id, achievementName: achievement.name, achievementIcon: achievement.icon, semester, createTime: new Date() });
353
+ await Student.findByIdAndUpdate(studentId, { $inc: { flowerBalance: achievement.points } });
354
+ res.json({ success: true });
355
+ });
356
+ app.post('/api/achievements/exchange', async (req, res) => {
357
+ const { studentId, ruleId, teacherId } = req.body;
358
+ const sId = req.headers['x-school-id'];
359
+ const student = await Student.findById(studentId);
360
+ if (!student) return res.status(404).json({ error: 'Student not found' });
361
+ let rule = null;
362
+ let ownerId = null;
363
+ if (teacherId) { const tConfig = await TeacherExchangeConfigModel.findOne({ teacherId, schoolId: sId }); rule = tConfig?.rules.find(r => r.id === ruleId); ownerId = teacherId; } else { const config = await AchievementConfigModel.findOne({ className: student.className, schoolId: sId }); rule = config?.exchangeRules?.find(r => r.id === ruleId); }
364
+ if (!rule) return res.status(404).json({ error: 'Rule not found' });
365
+ if (student.flowerBalance < rule.cost) { return res.status(400).json({ error: 'INSUFFICIENT_FUNDS', message: '小红花余额不足' }); }
366
+ await Student.findByIdAndUpdate(studentId, { $inc: { flowerBalance: -rule.cost } });
367
+ await StudentRewardModel.create({ schoolId: sId, studentId, studentName: student.name, rewardType: rule.rewardType, name: rule.rewardName, count: rule.rewardValue, status: rule.rewardType === 'DRAW_COUNT' ? 'REDEEMED' : 'PENDING', source: '积分兑换', ownerId, createTime: new Date() });
368
+ if (rule.rewardType === 'DRAW_COUNT') { await Student.findByIdAndUpdate(studentId, { $inc: { drawAttempts: rule.rewardValue } }); }
369
+ res.json({ success: true });
370
+ });
371
+ app.get('/api/auth/me', async (req, res) => {
372
+ const username = req.headers['x-user-username'];
373
+ if (!username) return res.status(401).json({ error: 'Unauthorized' });
374
+ const user = await User.findOne({ username });
375
+ if (!user) return res.status(404).json({ error: 'User not found' });
376
+ res.json(user);
377
+ });
378
+ app.post('/api/auth/update-profile', async (req, res) => {
379
+ const { userId, trueName, phone, avatar, currentPassword, newPassword } = req.body;
380
+ try {
381
+ const user = await User.findById(userId);
382
+ if (!user) return res.status(404).json({ error: 'User not found' });
383
+ if (newPassword) { if (user.password !== currentPassword) return res.status(401).json({ error: 'INVALID_PASSWORD', message: '旧密码错误' }); user.password = newPassword; }
384
+ if (trueName) user.trueName = trueName; if (phone) user.phone = phone; if (avatar) user.avatar = avatar;
385
+ await user.save();
386
+ if (user.role === 'STUDENT') await Student.findOneAndUpdate({ studentNo: user.studentNo }, { name: user.trueName || user.username, phone: user.phone });
387
+ res.json({ success: true, user });
388
+ } catch (e) { res.status(500).json({ error: e.message }); }
389
+ });
390
+ app.post('/api/auth/register', async (req, res) => {
391
+ const { role, username, password, schoolId, trueName, seatNo } = req.body;
392
+ const className = req.body.className || req.body.homeroomClass;
393
+ try {
394
+ if (role === 'STUDENT') {
395
+ if (!trueName || !className) return res.status(400).json({ error: 'MISSING_FIELDS', message: '姓名和班级不能为空' });
396
+ const cleanName = trueName.trim(); const cleanClass = className.trim();
397
+ const existingProfile = await Student.findOne({ schoolId, name: { $regex: new RegExp(`^${cleanName}$`, 'i') }, className: cleanClass });
398
+ let finalUsername = '';
399
+ if (existingProfile) {
400
+ if (existingProfile.studentNo && existingProfile.studentNo.length > 5) finalUsername = existingProfile.studentNo;
401
+ else { finalUsername = await generateStudentNo(); existingProfile.studentNo = finalUsername; await existingProfile.save(); }
402
+ const userExists = await User.findOne({ username: finalUsername, schoolId });
403
+ if (userExists) return res.status(409).json({ error: userExists.status === 'active' ? 'ACCOUNT_EXISTS' : 'ACCOUNT_PENDING', message: '账号已存在' });
404
+ } else finalUsername = await generateStudentNo();
405
+ 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() });
406
+ return res.json({ username: finalUsername });
407
+ }
408
+ const existing = await User.findOne({ username });
409
+ if (existing) return res.status(409).json({ error: 'USERNAME_EXISTS', message: '用户名已存在' });
410
+ await User.create({...req.body, status: 'pending', createTime: new Date()});
411
+ res.json({ username });
412
+ } catch(e) { res.status(500).json({ error: e.message }); }
413
+ });
414
+ app.get('/api/users', async (req, res) => {
415
+ const filter = getQueryFilter(req);
416
+ if (req.headers['x-user-role'] === 'PRINCIPAL') filter.role = { $ne: 'ADMIN' };
417
+ if (req.query.role) filter.role = req.query.role;
418
+ res.json(await User.find(filter).sort({ createTime: -1 }));
419
+ });
420
+ app.put('/api/users/:id', async (req, res) => {
421
+ const userId = req.params.id; const updates = req.body;
422
+ try {
423
+ const user = await User.findById(userId);
424
+ if (!user) return res.status(404).json({ error: 'User not found' });
425
+ if (user.status !== 'active' && updates.status === 'active' && user.role === 'STUDENT') {
426
+ 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 });
427
+ }
428
+ await User.findByIdAndUpdate(userId, updates);
429
+ res.json({});
430
+ } catch (e) { res.status(500).json({ error: e.message }); }
431
+ });
432
+ app.post('/api/users/class-application', async (req, res) => {
433
+ const { userId, type, targetClass, action } = req.body;
434
+ const userRole = req.headers['x-user-role']; const schoolId = req.headers['x-school-id'];
435
+ 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 }); } }
436
+ if (userRole === 'ADMIN' || userRole === 'PRINCIPAL') {
437
+ const user = await User.findById(userId);
438
+ if (!user || !user.classApplication) return res.status(404).json({ error: 'Application not found' });
439
+ const appType = user.classApplication.type; const appTarget = user.classApplication.targetClass;
440
+ if (action === 'APPROVE') {
441
+ const updates = { classApplication: null }; const classes = await ClassModel.find({ schoolId });
442
+ 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 }); } }
443
+ await User.findByIdAndUpdate(userId, updates);
444
+ } else await User.findByIdAndUpdate(userId, { 'classApplication.status': 'REJECTED' });
445
+ return res.json({ success: true });
446
+ }
447
+ res.status(403).json({ error: 'Permission denied' });
448
+ });
449
+ app.post('/api/students/promote', async (req, res) => {
450
+ const { teacherFollows } = req.body; const sId = req.headers['x-school-id'];
451
+ const GRADE_MAP = { '一年级': '二年级', '二年级': '三年级', '三年级': '四年级', '四年级': '五年级', '五年级': '六年级', '六年级': '毕业', '初一': '初二', '七年级': '八年级', '初二': '初三', '八年级': '九年级', '初三': '毕业', '九年级': '毕业', '高一': '高二', '高二': '高三', '高三': '毕业' };
452
+ const classes = await ClassModel.find(getQueryFilter(req)); let promotedCount = 0;
453
+ for (const cls of classes) {
454
+ const currentGrade = cls.grade; const nextGrade = GRADE_MAP[currentGrade] || currentGrade; const suffix = cls.className;
455
+ if (nextGrade === '毕业') { const oldFullClass = cls.grade + cls.className; await Student.updateMany({ className: oldFullClass, ...getQueryFilter(req) }, { status: 'Graduated', className: '已毕业' }); if (teacherFollows && cls.homeroomTeacherIds?.length) { await User.updateMany({ _id: { $in: cls.homeroomTeacherIds }, schoolId: sId }, { homeroomClass: '' }); await ClassModel.findByIdAndUpdate(cls._id, { teacherName: '', homeroomTeacherIds: [] }); } } 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, homeroomTeacherIds: teacherFollows ? cls.homeroomTeacherIds : [] }, { upsert: true }); const result = await Student.updateMany({ className: oldFullClass, status: 'Enrolled', ...getQueryFilter(req) }, { className: newFullClass }); promotedCount += result.modifiedCount; if (teacherFollows && cls.homeroomTeacherIds?.length) { await User.updateMany({ _id: { $in: cls.homeroomTeacherIds }, schoolId: sId, homeroomClass: oldFullClass }, { homeroomClass: newFullClass }); await ClassModel.findByIdAndUpdate(cls._id, { teacherName: '', homeroomTeacherIds: [] }); } }
456
+ }
457
+ res.json({ success: true, count: promotedCount });
458
  });
459
+ app.post('/api/games/lucky-config', async (req, res) => { const data = injectSchoolId(req, req.body); if (req.headers['x-user-role'] === 'TEACHER') { const user = await User.findOne({ username: req.headers['x-user-username'] }); data.ownerId = user ? user._id.toString() : null; } await LuckyDrawConfigModel.findOneAndUpdate({ className: data.className, ...getQueryFilter(req), ownerId: data.ownerId }, data, { upsert: true }); res.json({ success: true }); });
460
+ 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' }); let configFilter = { className: student.className, schoolId }; if (userRole === 'TEACHER') { const user = await User.findOne({ username: req.headers['x-user-username'] }); configFilter.ownerId = user ? user._id.toString() : null; } const config = await LuckyDrawConfigModel.findOne(configFilter); 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 } }); } let ownerId = config?.ownerId; await StudentRewardModel.create({ schoolId, studentId, studentName: student.name, rewardType, name: selectedPrize, count: 1, status: 'PENDING', source: '幸运大抽奖', ownerId }); res.json({ prize: selectedPrize, rewardType }); } catch (e) { res.status(500).json({ error: e.message }); } });
461
+ app.get('/api/games/monster-config', async (req, res) => { const filter = getQueryFilter(req); const ownerFilter = await getGameOwnerFilter(req); if (req.query.className) filter.className = req.query.className; Object.assign(filter, ownerFilter); const config = await GameMonsterConfigModel.findOne(filter); res.json(config || {}); });
462
+ app.post('/api/games/monster-config', async (req, res) => { const data = injectSchoolId(req, req.body); if (req.headers['x-user-role'] === 'TEACHER') { const user = await User.findOne({ username: req.headers['x-user-username'] }); data.ownerId = user ? user._id.toString() : null; } await GameMonsterConfigModel.findOneAndUpdate({ className: data.className, ...getQueryFilter(req), ownerId: data.ownerId }, data, { upsert: true }); res.json({ success: true }); });
463
+ app.get('/api/games/zen-config', async (req, res) => { const filter = getQueryFilter(req); const ownerFilter = await getGameOwnerFilter(req); if (req.query.className) filter.className = req.query.className; Object.assign(filter, ownerFilter); const config = await GameZenConfigModel.findOne(filter); res.json(config || {}); });
464
+ app.post('/api/games/zen-config', async (req, res) => { const data = injectSchoolId(req, req.body); if (req.headers['x-user-role'] === 'TEACHER') { const user = await User.findOne({ username: req.headers['x-user-username'] }); data.ownerId = user ? user._id.toString() : null; } await GameZenConfigModel.findOneAndUpdate({ className: data.className, ...getQueryFilter(req), ownerId: data.ownerId }, data, { upsert: true }); res.json({ success: true }); });
465
+ app.get('/api/games/mountain', async (req, res) => { res.json(await GameSessionModel.findOne({...getQueryFilter(req), className: req.query.className})); });
466
+ app.post('/api/games/mountain', async (req, res) => { const { className } = req.body; const sId = req.headers['x-school-id']; const role = req.headers['x-user-role']; const username = req.headers['x-user-username']; if (role === 'TEACHER') { const user = await User.findOne({ username }); const cls = await ClassModel.findOne({ schoolId: sId, $expr: { $eq: [{ $concat: ["$grade", "$className"] }, className] } }); if (!cls) return res.status(404).json({ error: 'Class not found' }); const allowedIds = cls.homeroomTeacherIds || []; if (!allowedIds.includes(user._id.toString())) return res.status(403).json({ error: 'PERMISSION_DENIED', message: '只有班主任可以操作登峰游戏' }); } await GameSessionModel.findOneAndUpdate({ className, ...getQueryFilter(req) }, injectSchoolId(req, req.body), {upsert:true}); res.json({}); });
467
+ app.get('/api/rewards', async (req, res) => { const filter = getQueryFilter(req); if (req.headers['x-user-role'] === 'TEACHER') { const user = await User.findOne({ username: req.headers['x-user-username'] }); if (user) filter.ownerId = user._id.toString(); } 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 }); });
468
+ app.post('/api/rewards', async (req, res) => { const data = injectSchoolId(req, req.body); if (!data.count) data.count = 1; if (req.headers['x-user-role'] === 'TEACHER') { const user = await User.findOne({ username: req.headers['x-user-username'] }); data.ownerId = user ? user._id.toString() : null; } if(data.rewardType==='DRAW_COUNT') { data.status='REDEEMED'; await Student.findByIdAndUpdate(data.studentId, {$inc:{drawAttempts:data.count}}); } await StudentRewardModel.create(data); res.json({}); });
469
+ 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' ? '抽奖券' : '奖品'); let ownerId = null; if (req.headers['x-user-role'] === 'TEACHER') { const user = await User.findOne({ username: req.headers['x-user-username'] }); ownerId = user ? user._id.toString() : null; } 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: '教师发放', ownerId }); res.json({}); });
470
+ app.put('/api/classes/:id', async (req, res) => {
471
+ const classId = req.params.id;
472
+ const { grade, className, teacherName, homeroomTeacherIds, periodConfig } = req.body;
473
+ const sId = req.headers['x-school-id'];
474
+ const oldClass = await ClassModel.findById(classId);
475
+ if (!oldClass) return res.status(404).json({ error: 'Class not found' });
476
+ const newFullClass = grade + className;
477
+ const oldFullClass = oldClass.grade + oldClass.className;
478
+ const oldTeacherIds = oldClass.homeroomTeacherIds || [];
479
+ const newTeacherIds = homeroomTeacherIds || [];
480
+ const removedIds = oldTeacherIds.filter(id => !newTeacherIds.includes(id));
481
+ if (removedIds.length > 0) await User.updateMany({ _id: { $in: removedIds }, schoolId: sId }, { homeroomClass: '' });
482
+ if (newTeacherIds.length > 0) await User.updateMany({ _id: { $in: newTeacherIds }, schoolId: sId }, { homeroomClass: newFullClass });
483
+ let displayTeacherName = teacherName;
484
+ if (newTeacherIds.length > 0) {
485
+ const teachers = await User.find({ _id: { $in: newTeacherIds } });
486
+ displayTeacherName = teachers.map(t => t.trueName || t.username).join(', ');
487
+ }
488
+
489
+ // FIX: Explicitly update periodConfig if present in the body
490
+ const updatePayload = { grade, className, teacherName: displayTeacherName, homeroomTeacherIds: newTeacherIds };
491
+ if (periodConfig) updatePayload.periodConfig = periodConfig;
492
 
493
+ await ClassModel.findByIdAndUpdate(classId, updatePayload);
494
+
495
+ if (oldFullClass !== newFullClass) {
496
+ await Student.updateMany({ className: oldFullClass, schoolId: sId }, { className: newFullClass });
497
+ await User.updateMany({ homeroomClass: oldFullClass, schoolId: sId }, { homeroomClass: newFullClass });
498
+ }
499
+ res.json({ success: true });
500
  });
501
+ app.post('/api/classes', async (req, res) => { const data = injectSchoolId(req, req.body); const { homeroomTeacherIds } = req.body; if (homeroomTeacherIds && homeroomTeacherIds.length > 0) { const teachers = await User.find({ _id: { $in: homeroomTeacherIds } }); data.teacherName = teachers.map(t => t.trueName || t.username).join(', '); } await ClassModel.create(data); if (homeroomTeacherIds && homeroomTeacherIds.length > 0) await User.updateMany({ _id: { $in: homeroomTeacherIds }, schoolId: data.schoolId }, { homeroomClass: data.grade + data.className }); res.json({}); });
502
+ app.get('/api/courses', async (req, res) => { const filter = getQueryFilter(req); if (req.query.teacherId) filter.teacherId = req.query.teacherId; res.json(await Course.find(filter)); });
503
+ app.post('/api/courses', async (req, res) => { const data = injectSchoolId(req, req.body); try { await Course.create(data); res.json({}); } catch(e) { if (e.code === 11000) return res.status(409).json({ error: 'DUPLICATE', message: '该班级该科目已有任课老师' }); res.status(500).json({ error: e.message }); } });
504
+ app.get('/api/public/schools', async (req, res) => { res.json(await School.find({}, 'name code _id')); });
505
+ 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); });
506
+ 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 }) }); });
507
+ 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); });
508
+ app.get('/api/schools', async (req, res) => { res.json(await School.find()); });
509
+ app.post('/api/schools', async (req, res) => { res.json(await School.create(req.body)); });
510
+ app.put('/api/schools/:id', async (req, res) => { await School.findByIdAndUpdate(req.params.id, req.body); res.json({}); });
511
+ 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 }); await WishModel.deleteMany({ schoolId }); await FeedbackModel.deleteMany({ schoolId }); await TodoModel.deleteMany({}); res.json({ success: true }); } catch (e) { res.status(500).json({ error: e.message }); } });
512
+ 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({}); });
513
+ app.get('/api/students', async (req, res) => { res.json(await Student.find(getQueryFilter(req))); });
514
+ 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 }); } });
515
+ app.put('/api/students/:id', async (req, res) => { await Student.findByIdAndUpdate(req.params.id, req.body); res.json({}); });
516
+ app.delete('/api/students/:id', async (req, res) => { await Student.findByIdAndDelete(req.params.id); res.json({}); });
517
+ 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); });
518
+ app.delete('/api/classes/:id', async (req, res) => { const cls = await ClassModel.findById(req.params.id); if (cls && cls.homeroomTeacherIds) await User.updateMany({ _id: { $in: cls.homeroomTeacherIds }, schoolId: cls.schoolId }, { homeroomClass: '' }); await ClassModel.findByIdAndDelete(req.params.id); res.json({}); });
519
+ app.get('/api/subjects', async (req, res) => { res.json(await SubjectModel.find(getQueryFilter(req))); });
520
+ app.post('/api/subjects', async (req, res) => { await SubjectModel.create(injectSchoolId(req, req.body)); res.json({}); });
521
+ app.put('/api/subjects/:id', async (req, res) => { await SubjectModel.findByIdAndUpdate(req.params.id, req.body); res.json({}); });
522
+ app.delete('/api/subjects/:id', async (req, res) => { await SubjectModel.findByIdAndDelete(req.params.id); res.json({}); });
523
+ app.put('/api/courses/:id', async (req, res) => { await Course.findByIdAndUpdate(req.params.id, req.body); res.json({}); });
524
+ app.delete('/api/courses/:id', async (req, res) => { await Course.findByIdAndDelete(req.params.id); res.json({}); });
525
+ app.get('/api/scores', async (req, res) => { res.json(await Score.find(getQueryFilter(req))); });
526
+ app.post('/api/scores', async (req, res) => { await Score.create(injectSchoolId(req, req.body)); res.json({}); });
527
+ app.put('/api/scores/:id', async (req, res) => { await Score.findByIdAndUpdate(req.params.id, req.body); res.json({}); });
528
+ app.delete('/api/scores/:id', async (req, res) => { await Score.findByIdAndDelete(req.params.id); res.json({}); });
529
+ app.get('/api/exams', async (req, res) => { res.json(await ExamModel.find(getQueryFilter(req))); });
530
+ app.post('/api/exams', async (req, res) => { await ExamModel.findOneAndUpdate({name:req.body.name}, injectSchoolId(req, req.body), {upsert:true}); res.json({}); });
531
+ 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 }); });
532
+ 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); });
533
+ app.post('/api/config', async (req, res) => { await ConfigModel.findOneAndUpdate({key:'main'}, req.body, {upsert:true}); res.json({}); });
534
+ app.put('/api/rewards/:id', async (req, res) => { await StudentRewardModel.findByIdAndUpdate(req.params.id, req.body); res.json({}); });
535
+ 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({}); });
536
+ app.post('/api/rewards/:id/redeem', async (req, res) => { await StudentRewardModel.findByIdAndUpdate(req.params.id, {status:'REDEEMED'}); res.json({}); });
537
+ 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)); });
538
+ 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 }); });
539
+ 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 }); });
540
+ 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 }); });
541
+ 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 }); });
542
+ app.get('/api/attendance/calendar', async (req, res) => { const { className } = req.query; const filter = getQueryFilter(req); const query = { $and: [ filter, { $or: [{ className: { $exists: false } }, { className: null }, { className }] } ] }; res.json(await SchoolCalendarModel.find(query)); });
543
+ app.post('/api/attendance/calendar', async (req, res) => { await SchoolCalendarModel.create(injectSchoolId(req, req.body)); res.json({ success: true }); });
544
+ app.delete('/api/attendance/calendar/:id', async (req, res) => { await SchoolCalendarModel.findByIdAndDelete(req.params.id); res.json({}); });
545
+ 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({}); });
546
 
547
+ app.get('*', (req, res) => { res.sendFile(path.join(__dirname, 'dist', 'index.html')); });
548
+ app.listen(PORT, () => console.log(`🚀 Server running on port ${PORT}`));