dvc890 commited on
Commit
e5e2b62
·
verified ·
1 Parent(s): ce28e6a

Update server.js

Browse files
Files changed (1) hide show
  1. server.js +91 -36
server.js CHANGED
@@ -37,7 +37,8 @@ const InMemoryDB = {
37
 
38
  const connectDB = async () => {
39
  try {
40
- await mongoose.connect(MONGO_URI, { serverSelectionTimeoutMS: 5000 });
 
41
  console.log('✅ MongoDB 连接成功 (Real Data)');
42
  } catch (err) {
43
  console.error('❌ MongoDB 连接失败:', err.message);
@@ -53,7 +54,6 @@ const connectDB = async () => {
53
  connectDB();
54
 
55
  // ... All Schema Definitions ...
56
- // (Retain existing Schemas: School, User, Student, Course, Score, Class, Subject, Exam, Schedule, Config, Notification)
57
  const SchoolSchema = new mongoose.Schema({ name: String, code: String });
58
  const School = mongoose.model('School', SchoolSchema);
59
  const UserSchema = new mongoose.Schema({ username: String, password: String, trueName: String, phone: String, email: String, schoolId: String, role: String, status: String, avatar: String, createTime: Date, teachingSubject: String, homeroomClass: String, studentNo: String, parentName: String, parentPhone: String, address: String, gender: String });
@@ -85,17 +85,23 @@ const StudentRewardModel = mongoose.model('StudentReward', StudentRewardSchema);
85
  const LuckyDrawConfigSchema = new mongoose.Schema({ schoolId: String, prizes: [{ id: String, name: String, probability: Number, count: Number, icon: String }], dailyLimit: Number, cardCount: Number, defaultPrize: String });
86
  const LuckyDrawConfigModel = mongoose.model('LuckyDrawConfig', LuckyDrawConfigSchema);
87
 
88
- // ... Helpers (notify, syncTeacher, syncStudent, initData, getQueryFilter, injectSchoolId) ...
89
- const getQueryFilter = (req) => { const s = req.headers['x-school-id']; return s ? {schoolId:s} : {}; };
 
 
 
 
 
 
 
 
 
 
 
 
90
  const injectSchoolId = (req, b) => ({ ...b, schoolId: req.headers['x-school-id'] });
91
 
92
- // ... Routes (retain all existing routes except Lucky Draw override below) ...
93
- // (I will omit re-declaring all previous routes to keep response concise, ONLY replacing modified ones)
94
-
95
- // --- ALL EXISTING ROUTES HERE ---
96
- // (Assume standard CRUD routes exist)
97
-
98
- // REPLACING/ADDING Game Routes
99
 
100
  app.get('/api/games/lucky-config', async (req, res) => {
101
  const filter = getQueryFilter(req);
@@ -105,25 +111,29 @@ app.get('/api/games/lucky-config', async (req, res) => {
105
  app.post('/api/games/lucky-config', async (req, res) => {
106
  const data = injectSchoolId(req, req.body);
107
  if (InMemoryDB.isFallback) { InMemoryDB.luckyConfig = data; return res.json({}); }
 
 
108
  await LuckyDrawConfigModel.findOneAndUpdate({ schoolId: data.schoolId }, data, { upsert: true });
109
  res.json({ success: true });
110
  });
111
 
112
- // Secure Lucky Draw Endpoint (Modified for Teacher Proxy)
113
  app.post('/api/games/lucky-draw', async (req, res) => {
114
- const { studentId } = req.body; // Passed from frontend (Teacher selects student, or Student uses self)
115
  const schoolId = req.headers['x-school-id'];
116
 
117
  try {
118
  if (InMemoryDB.isFallback) return res.json({ prize: '模拟奖品' });
119
 
120
- // 1. Get Student & Check Attempts
121
  const student = await Student.findById(studentId);
122
  if (!student) return res.status(404).json({ error: 'Student not found' });
123
  if (student.drawAttempts <= 0) return res.status(403).json({ error: '次数不足' });
124
 
125
  // 2. Get Config
126
- const config = await LuckyDrawConfigModel.findOne({ schoolId });
 
 
127
  const prizes = config?.prizes || [];
128
  const defaultPrize = config?.defaultPrize || '再接再厉';
129
 
@@ -149,11 +159,16 @@ app.post('/api/games/lucky-draw', async (req, res) => {
149
 
150
  if (matchedPrize) {
151
  selectedPrize = matchedPrize.name;
 
152
  if (matchedPrize.count !== undefined && matchedPrize.count > 0) {
153
- await LuckyDrawConfigModel.updateOne(
154
- { schoolId, "prizes.id": matchedPrize.id },
155
- { $inc: { "prizes.$.count": -1 } }
156
- );
 
 
 
 
157
  }
158
  }
159
 
@@ -161,8 +176,6 @@ app.post('/api/games/lucky-draw', async (req, res) => {
161
  await Student.findByIdAndUpdate(studentId, { $inc: { drawAttempts: -1 } });
162
 
163
  // 6. Record Reward
164
- // Important: If it's a DRAW_COUNT reward (rare in direct draw, usually Item), handle it.
165
- // But usually Draw yields Items. Assuming Items here.
166
  await StudentRewardModel.create({
167
  schoolId,
168
  studentId,
@@ -181,26 +194,25 @@ app.post('/api/games/lucky-draw', async (req, res) => {
181
  }
182
  });
183
 
184
- app.get('*', (req, res) => {
185
- res.sendFile(path.join(__dirname, 'dist', 'index.html'));
186
- });
187
-
188
- // Start Server
189
- // (If standard routes are missing in this block, assume they are part of the original file context provided by user)
190
- // Since I must output full file content if updating, I will include a placeholder comment for standard routes or rely on the fact that I am rewriting the file.
191
- // I will output the FULL server.js content to ensure consistency.
192
-
193
- // ... (Re-inserting all standard routes for completeness) ...
194
  app.get('/api/notifications', async (req, res) => {
195
  const schoolId = req.headers['x-school-id'];
196
  const { role, userId } = req.query;
197
  if (InMemoryDB.isFallback) return res.json(InMemoryDB.notifications);
198
- const query = { schoolId, $or: [ { targetRole: null, targetUserId: null }, { targetRole: role }, { targetUserId: userId } ] };
 
 
 
 
 
 
199
  res.json(await NotificationModel.find(query).sort({ createTime: -1 }).limit(20));
200
  });
 
201
  app.get('/api/public/schools', async (req, res) => { res.json(await School.find({}, 'name code _id')); });
202
  app.get('/api/public/config', async (req, res) => { res.json(await ConfigModel.findOne({ key: 'main' }) || { allowRegister: true }); });
203
  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 }) }); });
 
204
  app.post('/api/auth/login', async (req, res) => {
205
  const { username, password } = req.body;
206
  const user = await User.findOne({ username, password });
@@ -211,16 +223,20 @@ app.post('/api/auth/login', async (req, res) => {
211
  app.post('/api/auth/register', async (req, res) => {
212
  try { await User.create({...req.body, status: 'pending'}); res.json({}); } catch(e) { res.status(500).json({}); }
213
  });
 
214
  app.get('/api/schools', async (req, res) => { res.json(await School.find()); });
215
  app.post('/api/schools', async (req, res) => { res.json(await School.create(req.body)); });
216
  app.put('/api/schools/:id', async (req, res) => { await School.findByIdAndUpdate(req.params.id, req.body); res.json({}); });
 
217
  app.get('/api/users', async (req, res) => { res.json(await User.find(getQueryFilter(req))); });
218
  app.put('/api/users/:id', async (req, res) => { await User.findByIdAndUpdate(req.params.id, req.body); res.json({}); });
219
  app.delete('/api/users/:id', async (req, res) => { await User.findByIdAndDelete(req.params.id); res.json({}); });
 
220
  app.get('/api/students', async (req, res) => { res.json(await Student.find(getQueryFilter(req))); });
221
  app.post('/api/students', async (req, res) => { await Student.findOneAndUpdate({ studentNo: req.body.studentNo }, injectSchoolId(req, req.body), {upsert:true}); res.json({}); });
222
  app.put('/api/students/:id', async (req, res) => { await Student.findByIdAndUpdate(req.params.id, req.body); res.json({}); });
223
  app.delete('/api/students/:id', async (req, res) => { await Student.findByIdAndDelete(req.params.id); res.json({}); });
 
224
  app.get('/api/classes', async (req, res) => {
225
  const cls = await ClassModel.find(getQueryFilter(req));
226
  const resData = await Promise.all(cls.map(async c => ({...c.toObject(), studentCount: await Student.countDocuments({className:c.grade+c.className})})));
@@ -228,36 +244,67 @@ app.get('/api/classes', async (req, res) => {
228
  });
229
  app.post('/api/classes', async (req, res) => { await ClassModel.create(injectSchoolId(req, req.body)); res.json({}); });
230
  app.delete('/api/classes/:id', async (req, res) => { await ClassModel.findByIdAndDelete(req.params.id); res.json({}); });
 
231
  app.get('/api/subjects', async (req, res) => { res.json(await SubjectModel.find(getQueryFilter(req))); });
232
  app.post('/api/subjects', async (req, res) => { await SubjectModel.create(injectSchoolId(req, req.body)); res.json({}); });
233
  app.put('/api/subjects/:id', async (req, res) => { await SubjectModel.findByIdAndUpdate(req.params.id, req.body); res.json({}); });
234
  app.delete('/api/subjects/:id', async (req, res) => { await SubjectModel.findByIdAndDelete(req.params.id); res.json({}); });
 
235
  app.get('/api/courses', async (req, res) => { res.json(await Course.find(getQueryFilter(req))); });
236
  app.post('/api/courses', async (req, res) => { await Course.create(injectSchoolId(req, req.body)); res.json({}); });
237
  app.put('/api/courses/:id', async (req, res) => { await Course.findByIdAndUpdate(req.params.id, req.body); res.json({}); });
238
  app.delete('/api/courses/:id', async (req, res) => { await Course.findByIdAndDelete(req.params.id); res.json({}); });
 
239
  app.get('/api/scores', async (req, res) => { res.json(await Score.find(getQueryFilter(req))); });
240
  app.post('/api/scores', async (req, res) => { await Score.create(injectSchoolId(req, req.body)); res.json({}); });
241
  app.put('/api/scores/:id', async (req, res) => { await Score.findByIdAndUpdate(req.params.id, req.body); res.json({}); });
242
  app.delete('/api/scores/:id', async (req, res) => { await Score.findByIdAndDelete(req.params.id); res.json({}); });
 
243
  app.get('/api/exams', async (req, res) => { res.json(await ExamModel.find(getQueryFilter(req))); });
244
  app.post('/api/exams', async (req, res) => { await ExamModel.findOneAndUpdate({name:req.body.name}, injectSchoolId(req, req.body), {upsert:true}); res.json({}); });
 
245
  app.get('/api/schedules', async (req, res) => { res.json(await ScheduleModel.find({...getQueryFilter(req), ...req.query})); });
246
- app.post('/api/schedules', async (req, res) => { await ScheduleModel.findOneAndUpdate({schoolId:req.headers['x-school-id'], className:req.body.className, dayOfWeek:req.body.dayOfWeek, period:req.body.period}, injectSchoolId(req, req.body), {upsert:true}); res.json({}); });
 
 
 
 
 
 
 
 
247
  app.delete('/api/schedules', async (req, res) => { await ScheduleModel.deleteOne({...getQueryFilter(req), ...req.query}); res.json({}); });
 
248
  app.get('/api/stats', async (req, res) => { res.json({studentCount: await Student.countDocuments(getQueryFilter(req))}); });
 
249
  app.get('/api/config', async (req, res) => { res.json(await ConfigModel.findOne({key:'main'})); });
250
  app.post('/api/config', async (req, res) => { await ConfigModel.findOneAndUpdate({key:'main'}, req.body, {upsert:true}); res.json({}); });
251
 
252
  // Additional Game Routes
253
  app.get('/api/games/mountain', async (req, res) => { res.json(await GameSessionModel.findOne({...getQueryFilter(req), className: req.query.className})); });
254
- app.post('/api/games/mountain', async (req, res) => { await GameSessionModel.findOneAndUpdate({schoolId:req.headers['x-school-id'], className:req.body.className}, injectSchoolId(req, req.body), {upsert:true}); res.json({}); });
 
 
 
 
 
 
 
255
  app.post('/api/games/grant-draw', async (req, res) => {
256
  const { studentId, count } = req.body;
257
  await Student.findByIdAndUpdate(studentId, { $inc: { drawAttempts: count } });
258
- await StudentRewardModel.create({ schoolId: req.headers['x-school-id'], studentId, studentName: (await Student.findById(studentId)).name, rewardType: 'DRAW_COUNT', name: '抽奖券', status: 'REDEEMED', source: '教师发放' });
 
 
 
 
 
 
 
 
259
  res.json({});
260
  });
 
261
  app.get('/api/rewards', async (req, res) => {
262
  const filter = getQueryFilter(req);
263
  if(req.query.studentId) filter.studentId = req.query.studentId;
@@ -265,15 +312,23 @@ app.get('/api/rewards', async (req, res) => {
265
  });
266
  app.post('/api/rewards', async (req, res) => {
267
  const data = injectSchoolId(req, req.body);
268
- if(data.rewardType==='DRAW_COUNT') { data.status='REDEEMED'; await Student.findByIdAndUpdate(data.studentId, {$inc:{drawAttempts:1}}); }
 
 
 
269
  await StudentRewardModel.create(data);
270
  res.json({});
271
  });
272
  app.post('/api/rewards/:id/redeem', async (req, res) => { await StudentRewardModel.findByIdAndUpdate(req.params.id, {status:'REDEEMED'}); res.json({}); });
 
273
  app.post('/api/batch-delete', async (req, res) => {
274
  if(req.body.type==='student') await Student.deleteMany({_id:{$in:req.body.ids}});
275
  if(req.body.type==='score') await Score.deleteMany({_id:{$in:req.body.ids}});
276
  res.json({});
277
  });
278
 
 
 
 
 
279
  app.listen(PORT, () => console.log(`🚀 Server running on port ${PORT}`));
 
37
 
38
  const connectDB = async () => {
39
  try {
40
+ // Increased timeout to 30s to prevent premature fallback
41
+ await mongoose.connect(MONGO_URI, { serverSelectionTimeoutMS: 30000 });
42
  console.log('✅ MongoDB 连接成功 (Real Data)');
43
  } catch (err) {
44
  console.error('❌ MongoDB 连接失败:', err.message);
 
54
  connectDB();
55
 
56
  // ... All Schema Definitions ...
 
57
  const SchoolSchema = new mongoose.Schema({ name: String, code: String });
58
  const School = mongoose.model('School', SchoolSchema);
59
  const UserSchema = new mongoose.Schema({ username: String, password: String, trueName: String, phone: String, email: String, schoolId: String, role: String, status: String, avatar: String, createTime: Date, teachingSubject: String, homeroomClass: String, studentNo: String, parentName: String, parentPhone: String, address: String, gender: String });
 
85
  const LuckyDrawConfigSchema = new mongoose.Schema({ schoolId: String, prizes: [{ id: String, name: String, probability: Number, count: Number, icon: String }], dailyLimit: Number, cardCount: Number, defaultPrize: String });
86
  const LuckyDrawConfigModel = mongoose.model('LuckyDrawConfig', LuckyDrawConfigSchema);
87
 
88
+ // ... Helpers ...
89
+ // IMPORTANT FIX: Allow fetching legacy data that doesn't have a schoolId yet
90
+ const getQueryFilter = (req) => {
91
+ const s = req.headers['x-school-id'];
92
+ if (!s) return {};
93
+ // Return records that match schoolId OR have no schoolId (legacy data)
94
+ return {
95
+ $or: [
96
+ { schoolId: s },
97
+ { schoolId: { $exists: false } },
98
+ { schoolId: null }
99
+ ]
100
+ };
101
+ };
102
  const injectSchoolId = (req, b) => ({ ...b, schoolId: req.headers['x-school-id'] });
103
 
104
+ // ... Routes ...
 
 
 
 
 
 
105
 
106
  app.get('/api/games/lucky-config', async (req, res) => {
107
  const filter = getQueryFilter(req);
 
111
  app.post('/api/games/lucky-config', async (req, res) => {
112
  const data = injectSchoolId(req, req.body);
113
  if (InMemoryDB.isFallback) { InMemoryDB.luckyConfig = data; return res.json({}); }
114
+ // Use updateOne with upsert to handle legacy data without schoolId more gracefully if needed,
115
+ // but findOneAndUpdate is fine. We force schoolId on save.
116
  await LuckyDrawConfigModel.findOneAndUpdate({ schoolId: data.schoolId }, data, { upsert: true });
117
  res.json({ success: true });
118
  });
119
 
120
+ // Secure Lucky Draw Endpoint
121
  app.post('/api/games/lucky-draw', async (req, res) => {
122
+ const { studentId } = req.body;
123
  const schoolId = req.headers['x-school-id'];
124
 
125
  try {
126
  if (InMemoryDB.isFallback) return res.json({ prize: '模拟奖品' });
127
 
128
+ // 1. Get Student
129
  const student = await Student.findById(studentId);
130
  if (!student) return res.status(404).json({ error: 'Student not found' });
131
  if (student.drawAttempts <= 0) return res.status(403).json({ error: '次数不足' });
132
 
133
  // 2. Get Config
134
+ // Use permissive filter for reading config
135
+ const filter = schoolId ? { $or: [{ schoolId }, { schoolId: { $exists: false } }] } : {};
136
+ const config = await LuckyDrawConfigModel.findOne(filter);
137
  const prizes = config?.prizes || [];
138
  const defaultPrize = config?.defaultPrize || '再接再厉';
139
 
 
159
 
160
  if (matchedPrize) {
161
  selectedPrize = matchedPrize.name;
162
+ // Only decrease count if it's not infinite (undefined)
163
  if (matchedPrize.count !== undefined && matchedPrize.count > 0) {
164
+ // Need to update the specific array element.
165
+ // If config has no ID, we might have trouble updating. Assuming config has _id.
166
+ if (config._id) {
167
+ await LuckyDrawConfigModel.updateOne(
168
+ { _id: config._id, "prizes.id": matchedPrize.id },
169
+ { $inc: { "prizes.$.count": -1 } }
170
+ );
171
+ }
172
  }
173
  }
174
 
 
176
  await Student.findByIdAndUpdate(studentId, { $inc: { drawAttempts: -1 } });
177
 
178
  // 6. Record Reward
 
 
179
  await StudentRewardModel.create({
180
  schoolId,
181
  studentId,
 
194
  }
195
  });
196
 
197
+ // Standard Routes with permissive filter
 
 
 
 
 
 
 
 
 
198
  app.get('/api/notifications', async (req, res) => {
199
  const schoolId = req.headers['x-school-id'];
200
  const { role, userId } = req.query;
201
  if (InMemoryDB.isFallback) return res.json(InMemoryDB.notifications);
202
+ // Relaxed query
203
+ const query = {
204
+ $and: [
205
+ getQueryFilter(req),
206
+ { $or: [ { targetRole: null, targetUserId: null }, { targetRole: role }, { targetUserId: userId } ] }
207
+ ]
208
+ };
209
  res.json(await NotificationModel.find(query).sort({ createTime: -1 }).limit(20));
210
  });
211
+
212
  app.get('/api/public/schools', async (req, res) => { res.json(await School.find({}, 'name code _id')); });
213
  app.get('/api/public/config', async (req, res) => { res.json(await ConfigModel.findOne({ key: 'main' }) || { allowRegister: true }); });
214
  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 }) }); });
215
+
216
  app.post('/api/auth/login', async (req, res) => {
217
  const { username, password } = req.body;
218
  const user = await User.findOne({ username, password });
 
223
  app.post('/api/auth/register', async (req, res) => {
224
  try { await User.create({...req.body, status: 'pending'}); res.json({}); } catch(e) { res.status(500).json({}); }
225
  });
226
+
227
  app.get('/api/schools', async (req, res) => { res.json(await School.find()); });
228
  app.post('/api/schools', async (req, res) => { res.json(await School.create(req.body)); });
229
  app.put('/api/schools/:id', async (req, res) => { await School.findByIdAndUpdate(req.params.id, req.body); res.json({}); });
230
+
231
  app.get('/api/users', async (req, res) => { res.json(await User.find(getQueryFilter(req))); });
232
  app.put('/api/users/:id', async (req, res) => { await User.findByIdAndUpdate(req.params.id, req.body); res.json({}); });
233
  app.delete('/api/users/:id', async (req, res) => { await User.findByIdAndDelete(req.params.id); res.json({}); });
234
+
235
  app.get('/api/students', async (req, res) => { res.json(await Student.find(getQueryFilter(req))); });
236
  app.post('/api/students', async (req, res) => { await Student.findOneAndUpdate({ studentNo: req.body.studentNo }, injectSchoolId(req, req.body), {upsert:true}); res.json({}); });
237
  app.put('/api/students/:id', async (req, res) => { await Student.findByIdAndUpdate(req.params.id, req.body); res.json({}); });
238
  app.delete('/api/students/:id', async (req, res) => { await Student.findByIdAndDelete(req.params.id); res.json({}); });
239
+
240
  app.get('/api/classes', async (req, res) => {
241
  const cls = await ClassModel.find(getQueryFilter(req));
242
  const resData = await Promise.all(cls.map(async c => ({...c.toObject(), studentCount: await Student.countDocuments({className:c.grade+c.className})})));
 
244
  });
245
  app.post('/api/classes', async (req, res) => { await ClassModel.create(injectSchoolId(req, req.body)); res.json({}); });
246
  app.delete('/api/classes/:id', async (req, res) => { await ClassModel.findByIdAndDelete(req.params.id); res.json({}); });
247
+
248
  app.get('/api/subjects', async (req, res) => { res.json(await SubjectModel.find(getQueryFilter(req))); });
249
  app.post('/api/subjects', async (req, res) => { await SubjectModel.create(injectSchoolId(req, req.body)); res.json({}); });
250
  app.put('/api/subjects/:id', async (req, res) => { await SubjectModel.findByIdAndUpdate(req.params.id, req.body); res.json({}); });
251
  app.delete('/api/subjects/:id', async (req, res) => { await SubjectModel.findByIdAndDelete(req.params.id); res.json({}); });
252
+
253
  app.get('/api/courses', async (req, res) => { res.json(await Course.find(getQueryFilter(req))); });
254
  app.post('/api/courses', async (req, res) => { await Course.create(injectSchoolId(req, req.body)); res.json({}); });
255
  app.put('/api/courses/:id', async (req, res) => { await Course.findByIdAndUpdate(req.params.id, req.body); res.json({}); });
256
  app.delete('/api/courses/:id', async (req, res) => { await Course.findByIdAndDelete(req.params.id); res.json({}); });
257
+
258
  app.get('/api/scores', async (req, res) => { res.json(await Score.find(getQueryFilter(req))); });
259
  app.post('/api/scores', async (req, res) => { await Score.create(injectSchoolId(req, req.body)); res.json({}); });
260
  app.put('/api/scores/:id', async (req, res) => { await Score.findByIdAndUpdate(req.params.id, req.body); res.json({}); });
261
  app.delete('/api/scores/:id', async (req, res) => { await Score.findByIdAndDelete(req.params.id); res.json({}); });
262
+
263
  app.get('/api/exams', async (req, res) => { res.json(await ExamModel.find(getQueryFilter(req))); });
264
  app.post('/api/exams', async (req, res) => { await ExamModel.findOneAndUpdate({name:req.body.name}, injectSchoolId(req, req.body), {upsert:true}); res.json({}); });
265
+
266
  app.get('/api/schedules', async (req, res) => { res.json(await ScheduleModel.find({...getQueryFilter(req), ...req.query})); });
267
+ app.post('/api/schedules', async (req, res) => {
268
+ // For upsert, we need to include schoolId in the query to avoid overwriting other schools' schedules
269
+ const filter = { className:req.body.className, dayOfWeek:req.body.dayOfWeek, period:req.body.period };
270
+ const sId = req.headers['x-school-id'];
271
+ if(sId) filter.schoolId = sId;
272
+
273
+ await ScheduleModel.findOneAndUpdate(filter, injectSchoolId(req, req.body), {upsert:true});
274
+ res.json({});
275
+ });
276
  app.delete('/api/schedules', async (req, res) => { await ScheduleModel.deleteOne({...getQueryFilter(req), ...req.query}); res.json({}); });
277
+
278
  app.get('/api/stats', async (req, res) => { res.json({studentCount: await Student.countDocuments(getQueryFilter(req))}); });
279
+
280
  app.get('/api/config', async (req, res) => { res.json(await ConfigModel.findOne({key:'main'})); });
281
  app.post('/api/config', async (req, res) => { await ConfigModel.findOneAndUpdate({key:'main'}, req.body, {upsert:true}); res.json({}); });
282
 
283
  // Additional Game Routes
284
  app.get('/api/games/mountain', async (req, res) => { res.json(await GameSessionModel.findOne({...getQueryFilter(req), className: req.query.className})); });
285
+ app.post('/api/games/mountain', async (req, res) => {
286
+ const filter = { className: req.body.className };
287
+ const sId = req.headers['x-school-id'];
288
+ if(sId) filter.schoolId = sId;
289
+ await GameSessionModel.findOneAndUpdate(filter, injectSchoolId(req, req.body), {upsert:true});
290
+ res.json({});
291
+ });
292
+
293
  app.post('/api/games/grant-draw', async (req, res) => {
294
  const { studentId, count } = req.body;
295
  await Student.findByIdAndUpdate(studentId, { $inc: { drawAttempts: count } });
296
+ await StudentRewardModel.create({
297
+ schoolId: req.headers['x-school-id'],
298
+ studentId,
299
+ studentName: (await Student.findById(studentId)).name,
300
+ rewardType: 'DRAW_COUNT',
301
+ name: '抽奖券',
302
+ status: 'REDEEMED',
303
+ source: '教师发放'
304
+ });
305
  res.json({});
306
  });
307
+
308
  app.get('/api/rewards', async (req, res) => {
309
  const filter = getQueryFilter(req);
310
  if(req.query.studentId) filter.studentId = req.query.studentId;
 
312
  });
313
  app.post('/api/rewards', async (req, res) => {
314
  const data = injectSchoolId(req, req.body);
315
+ if(data.rewardType==='DRAW_COUNT') {
316
+ data.status='REDEEMED';
317
+ await Student.findByIdAndUpdate(data.studentId, {$inc:{drawAttempts:1}});
318
+ }
319
  await StudentRewardModel.create(data);
320
  res.json({});
321
  });
322
  app.post('/api/rewards/:id/redeem', async (req, res) => { await StudentRewardModel.findByIdAndUpdate(req.params.id, {status:'REDEEMED'}); res.json({}); });
323
+
324
  app.post('/api/batch-delete', async (req, res) => {
325
  if(req.body.type==='student') await Student.deleteMany({_id:{$in:req.body.ids}});
326
  if(req.body.type==='score') await Score.deleteMany({_id:{$in:req.body.ids}});
327
  res.json({});
328
  });
329
 
330
+ app.get('*', (req, res) => {
331
+ res.sendFile(path.join(__dirname, 'dist', 'index.html'));
332
+ });
333
+
334
  app.listen(PORT, () => console.log(`🚀 Server running on port ${PORT}`));