dvc890 commited on
Commit
247df42
·
verified ·
1 Parent(s): 077bf16

Upload 45 files

Browse files
Files changed (7) hide show
  1. App.tsx +2 -6
  2. components/Sidebar.tsx +1 -2
  3. models.js +191 -256
  4. pages/CourseList.tsx +1 -1
  5. server.js +725 -858
  6. services/api.ts +4 -24
  7. types.ts +222 -173
App.tsx CHANGED
@@ -16,7 +16,6 @@ const SchoolList = React.lazy(() => import('./pages/SchoolList').then(module =>
16
  const Games = React.lazy(() => import('./pages/Games').then(module => ({ default: module.Games })));
17
  const AttendancePage = React.lazy(() => import('./pages/Attendance').then(module => ({ default: module.AttendancePage })));
18
  const Profile = React.lazy(() => import('./pages/Profile').then(module => ({ default: module.Profile })));
19
- const Wishes = React.lazy(() => import('./pages/Wishes').then(module => ({ default: module.Wishes }))); // NEW
20
 
21
  import { Login } from './pages/Login';
22
  import { User, UserRole } from './types';
@@ -24,7 +23,6 @@ import { api } from './services/api';
24
  import { AlertTriangle, Loader2 } from 'lucide-react';
25
 
26
  class ErrorBoundary extends React.Component<{children: React.ReactNode}, {hasError: boolean, error: Error | null}> {
27
- // ... (existing code)
28
  constructor(props: any) {
29
  super(props);
30
  this.state = { hasError: false, error: null };
@@ -125,7 +123,6 @@ const AppContent: React.FC = () => {
125
  case 'games': return <Games />;
126
  case 'attendance': return <AttendancePage />;
127
  case 'profile': return <Profile />;
128
- case 'wishes': return <Wishes />; // NEW
129
  default: return <Dashboard onNavigate={(view) => setCurrentView(view)} />;
130
  }
131
  };
@@ -143,8 +140,7 @@ const AppContent: React.FC = () => {
143
  schools: '学校维度管理',
144
  games: '互动教学中心',
145
  attendance: '考勤管理',
146
- profile: '个人中心',
147
- wishes: '许愿 & 反馈中心' // NEW
148
  };
149
 
150
  if (loading) return <div className="min-h-screen flex items-center justify-center bg-gray-50 text-blue-600">加载中...</div>;
@@ -165,7 +161,7 @@ const AppContent: React.FC = () => {
165
  <div className="flex-1 flex flex-col w-full">
166
  <Header user={currentUser!} title={viewTitles[currentView] || '智慧校园'} onMenuClick={() => setSidebarOpen(true)}/>
167
  <main className="flex-1 overflow-x-hidden overflow-y-auto bg-gray-50 p-4 md:p-6 w-full">
168
- <div className="max-w-7xl mx-auto w-full h-full">
169
  <Suspense fallback={<PageLoading />}>{renderContent()}</Suspense>
170
  </div>
171
  </main>
 
16
  const Games = React.lazy(() => import('./pages/Games').then(module => ({ default: module.Games })));
17
  const AttendancePage = React.lazy(() => import('./pages/Attendance').then(module => ({ default: module.AttendancePage })));
18
  const Profile = React.lazy(() => import('./pages/Profile').then(module => ({ default: module.Profile })));
 
19
 
20
  import { Login } from './pages/Login';
21
  import { User, UserRole } from './types';
 
23
  import { AlertTriangle, Loader2 } from 'lucide-react';
24
 
25
  class ErrorBoundary extends React.Component<{children: React.ReactNode}, {hasError: boolean, error: Error | null}> {
 
26
  constructor(props: any) {
27
  super(props);
28
  this.state = { hasError: false, error: null };
 
123
  case 'games': return <Games />;
124
  case 'attendance': return <AttendancePage />;
125
  case 'profile': return <Profile />;
 
126
  default: return <Dashboard onNavigate={(view) => setCurrentView(view)} />;
127
  }
128
  };
 
140
  schools: '学校维度管理',
141
  games: '互动教学中心',
142
  attendance: '考勤管理',
143
+ profile: '个人中心'
 
144
  };
145
 
146
  if (loading) return <div className="min-h-screen flex items-center justify-center bg-gray-50 text-blue-600">加载中...</div>;
 
161
  <div className="flex-1 flex flex-col w-full">
162
  <Header user={currentUser!} title={viewTitles[currentView] || '智慧校园'} onMenuClick={() => setSidebarOpen(true)}/>
163
  <main className="flex-1 overflow-x-hidden overflow-y-auto bg-gray-50 p-4 md:p-6 w-full">
164
+ <div className="max-w-7xl mx-auto w-full">
165
  <Suspense fallback={<PageLoading />}>{renderContent()}</Suspense>
166
  </div>
167
  </main>
components/Sidebar.tsx CHANGED
@@ -1,6 +1,6 @@
1
 
2
  import React from 'react';
3
- import { LayoutDashboard, Users, BookOpen, GraduationCap, Settings, LogOut, FileText, School, UserCog, Palette, X, Building, Gamepad2, CalendarCheck, UserCircle, MessageCircle } from 'lucide-react';
4
  import { UserRole } from '../types';
5
 
6
  interface SidebarProps {
@@ -19,7 +19,6 @@ export const Sidebar: React.FC<SidebarProps> = ({ currentView, onChangeView, use
19
  { id: 'dashboard', label: '工作台', icon: LayoutDashboard, roles: [UserRole.ADMIN, UserRole.PRINCIPAL, UserRole.TEACHER, UserRole.STUDENT] },
20
  { id: 'attendance', label: '考勤管理', icon: CalendarCheck, roles: [UserRole.TEACHER, UserRole.PRINCIPAL] },
21
  { id: 'games', label: '互动教学', icon: Gamepad2, roles: [UserRole.TEACHER, UserRole.STUDENT] }, // Removed PRINCIPAL
22
- { id: 'wishes', label: '许愿 & 反馈', icon: MessageCircle, roles: [UserRole.ADMIN, UserRole.PRINCIPAL, UserRole.TEACHER, UserRole.STUDENT] }, // NEW
23
  { id: 'students', label: '学生管理', icon: Users, roles: [UserRole.ADMIN, UserRole.PRINCIPAL, UserRole.TEACHER] },
24
  { id: 'classes', label: '班级管理', icon: School, roles: [UserRole.ADMIN, UserRole.PRINCIPAL] },
25
  { id: 'schools', label: '学校管理', icon: Building, roles: [UserRole.ADMIN] }, // Only Super Admin can manage schools
 
1
 
2
  import React from 'react';
3
+ import { LayoutDashboard, Users, BookOpen, GraduationCap, Settings, LogOut, FileText, School, UserCog, Palette, X, Building, Gamepad2, CalendarCheck, UserCircle } from 'lucide-react';
4
  import { UserRole } from '../types';
5
 
6
  interface SidebarProps {
 
19
  { id: 'dashboard', label: '工作台', icon: LayoutDashboard, roles: [UserRole.ADMIN, UserRole.PRINCIPAL, UserRole.TEACHER, UserRole.STUDENT] },
20
  { id: 'attendance', label: '考勤管理', icon: CalendarCheck, roles: [UserRole.TEACHER, UserRole.PRINCIPAL] },
21
  { id: 'games', label: '互动教学', icon: Gamepad2, roles: [UserRole.TEACHER, UserRole.STUDENT] }, // Removed PRINCIPAL
 
22
  { id: 'students', label: '学生管理', icon: Users, roles: [UserRole.ADMIN, UserRole.PRINCIPAL, UserRole.TEACHER] },
23
  { id: 'classes', label: '班级管理', icon: School, roles: [UserRole.ADMIN, UserRole.PRINCIPAL] },
24
  { id: 'schools', label: '学校管理', icon: Building, roles: [UserRole.ADMIN] }, // Only Super Admin can manage schools
models.js CHANGED
@@ -1,260 +1,195 @@
1
 
2
- // IN-MEMORY DATABASE ENGINE (MOCK MONGOOSE)
3
- // This allows the app to run without a real MongoDB connection.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
 
5
- class MockQuery {
6
- constructor(data) {
7
- this.data = data;
8
- }
9
-
10
- sort(criteria) {
11
- const keys = Object.keys(criteria);
12
- if (keys.length === 0) return this;
13
- const key = keys[0];
14
- const dir = criteria[key]; // 1 or -1
15
-
16
- this.data.sort((a, b) => {
17
- const valA = a[key] || 0;
18
- const valB = b[key] || 0;
19
- if (valA < valB) return -1 * dir;
20
- if (valA > valB) return 1 * dir;
21
- return 0;
22
- });
23
- return this;
24
- }
25
-
26
- limit(n) {
27
- this.data = this.data.slice(0, n);
28
- return this;
29
- }
30
-
31
- skip(n) {
32
- this.data = this.data.slice(n);
33
- return this;
34
- }
35
-
36
- then(resolve, reject) {
37
- resolve(this.data);
38
- }
39
- }
40
-
41
- class MockModel {
42
- constructor(name) {
43
- this.name = name;
44
- this.store = [];
45
- }
46
-
47
- // Helper to check if a document matches a filter
48
- _matches(doc, filter) {
49
- if (!filter) return true;
50
- for (const key of Object.keys(filter)) {
51
- const criteria = filter[key];
52
-
53
- // Handle $or
54
- if (key === '$or') {
55
- if (!Array.isArray(criteria)) continue;
56
- // If ANY condition in $or is true, pass
57
- if (!criteria.some(cond => this._matches(doc, cond))) return false;
58
- continue;
59
- }
60
-
61
- const docVal = doc[key];
62
-
63
- // Handle Complex Operators
64
- if (criteria && typeof criteria === 'object' && !Array.isArray(criteria) && !(criteria instanceof RegExp)) {
65
- if (criteria.$in) {
66
- // { field: { $in: [...] } }
67
- // docVal could be a single value or an array
68
- const allowed = criteria.$in.map(String);
69
- if (Array.isArray(docVal)) {
70
- if (!docVal.some(v => allowed.includes(String(v)))) return false;
71
- } else {
72
- if (!allowed.includes(String(docVal))) return false;
73
- }
74
- } else if (criteria.$ne !== undefined) {
75
- if (String(docVal) === String(criteria.$ne)) return false;
76
- } else if (criteria.$regex) {
77
- const re = new RegExp(criteria.$regex, criteria.$options);
78
- if (!re.test(docVal)) return false;
79
- }
80
- } else if (criteria instanceof RegExp) {
81
- if (!criteria.test(docVal)) return false;
82
- } else {
83
- // Simple Equality
84
- if (String(docVal) !== String(criteria)) return false;
85
- }
86
- }
87
- return true;
88
- }
89
-
90
- async find(filter = {}) {
91
- const res = this.store.filter(item => this._matches(item, filter));
92
- // Return a copy to mimic DB returning new objects
93
- return new MockQuery(JSON.parse(JSON.stringify(res)));
94
- }
95
-
96
- async findOne(filter = {}) {
97
- const item = this.store.find(item => this._matches(item, filter));
98
- return item ? JSON.parse(JSON.stringify(item)) : null;
99
- }
100
-
101
- async findById(id) {
102
- const item = this.store.find(i => i._id === id);
103
- return item ? JSON.parse(JSON.stringify(item)) : null;
104
- }
105
-
106
- async create(data) {
107
- const newItem = {
108
- ...data,
109
- _id: Date.now().toString() + Math.random().toString(36).substr(2, 5),
110
- createTime: data.createTime || new Date().toISOString()
111
- };
112
- this.store.push(newItem);
113
- return newItem;
114
- }
115
-
116
- async findByIdAndUpdate(id, update, options) {
117
- const idx = this.store.findIndex(i => i._id === id);
118
- if (idx === -1) {
119
- if (options?.upsert) {
120
- return this.create({ _id: id, ...update });
121
- }
122
- return null;
123
- }
124
-
125
- // Handle $inc operator
126
- if (update.$inc) {
127
- for (const key in update.$inc) {
128
- // Simple path support
129
- if (key.includes('.')) {
130
- // Try to handle simple "prizes.$.count" logic roughly or ignore
131
- // For this mock, we skip complex array updates via $inc
132
- continue;
133
- }
134
- this.store[idx][key] = (this.store[idx][key] || 0) + update.$inc[key];
135
- }
136
- delete update.$inc;
137
- }
138
-
139
- // Merge other fields
140
- Object.assign(this.store[idx], update);
141
- return this.store[idx];
142
- }
143
-
144
- async findOneAndUpdate(filter, update, options) {
145
- let idx = this.store.findIndex(item => this._matches(item, filter));
146
-
147
- if (idx === -1) {
148
- if (options?.upsert) {
149
- return this.create({ ...filter, ...update });
150
- }
151
- return null;
152
- }
153
-
154
- Object.assign(this.store[idx], update);
155
- return this.store[idx];
156
- }
157
-
158
- async findByIdAndDelete(id) {
159
- const idx = this.store.findIndex(i => i._id === id);
160
- if (idx !== -1) {
161
- const deleted = this.store[idx];
162
- this.store.splice(idx, 1);
163
- return deleted;
164
- }
165
- return null;
166
- }
167
-
168
- async findOneAndDelete(filter) {
169
- const idx = this.store.findIndex(item => this._matches(item, filter));
170
- if (idx !== -1) {
171
- const deleted = this.store[idx];
172
- this.store.splice(idx, 1);
173
- return deleted;
174
- }
175
- return null;
176
- }
177
-
178
- async deleteOne(filter) {
179
- return this.findOneAndDelete(filter);
180
- }
181
-
182
- async deleteMany(filter) {
183
- const initialLen = this.store.length;
184
- this.store = this.store.filter(item => !this._matches(item, filter));
185
- return { deletedCount: initialLen - this.store.length };
186
- }
187
-
188
- async countDocuments(filter) {
189
- return this.store.filter(item => this._matches(item, filter)).length;
190
- }
191
-
192
- async insertMany(docs) {
193
- const newItems = docs.map(d => ({
194
- ...d,
195
- _id: Date.now().toString() + Math.random().toString(36).substr(2, 5),
196
- createTime: d.createTime || new Date().toISOString()
197
- }));
198
- this.store.push(...newItems);
199
- return newItems;
200
- }
201
-
202
- async updateOne(filter, update) {
203
- // Simple update one (mocking basic $inc or $set)
204
- const idx = this.store.findIndex(item => this._matches(item, filter));
205
- if (idx !== -1) {
206
- if (update.$inc) {
207
- for (const k in update.$inc) {
208
- // Very basic nested support for specific app case: "prizes.$.count"
209
- // If filter matched a nested array element, this gets complicated in Mock.
210
- // Fallback: If we can't find the key, ignore.
211
- if (k.includes('.')) continue;
212
- this.store[idx][k] = (this.store[idx][k] || 0) + update.$inc[k];
213
- }
214
- delete update.$inc;
215
- }
216
- Object.assign(this.store[idx], update);
217
- return { modifiedCount: 1 };
218
- }
219
- return { modifiedCount: 0 };
220
- }
221
-
222
- async updateMany(filter, update) {
223
- let count = 0;
224
- this.store.forEach(item => {
225
- if (this._matches(item, filter)) {
226
- Object.assign(item, update);
227
- count++;
228
- }
229
- });
230
- return { modifiedCount: count };
231
- }
232
- }
233
-
234
- // Export Instances
235
  module.exports = {
236
- School: new MockModel('School'),
237
- User: new MockModel('User'),
238
- Student: new MockModel('Student'),
239
- Course: new MockModel('Course'),
240
- Score: new MockModel('Score'),
241
- ClassModel: new MockModel('Class'),
242
- SubjectModel: new MockModel('Subject'),
243
- ExamModel: new MockModel('Exam'),
244
- ScheduleModel: new MockModel('Schedule'),
245
- ConfigModel: new MockModel('Config'),
246
- NotificationModel: new MockModel('Notification'),
247
- GameSessionModel: new MockModel('GameSession'),
248
- StudentRewardModel: new MockModel('StudentReward'),
249
- LuckyDrawConfigModel: new MockModel('LuckyDrawConfig'),
250
- GameMonsterConfigModel: new MockModel('GameMonsterConfig'),
251
- GameZenConfigModel: new MockModel('GameZenConfig'),
252
- AchievementConfigModel: new MockModel('AchievementConfig'),
253
- TeacherExchangeConfigModel: new MockModel('TeacherExchangeConfig'),
254
- StudentAchievementModel: new MockModel('StudentAchievement'),
255
- AttendanceModel: new MockModel('Attendance'),
256
- LeaveRequestModel: new MockModel('LeaveRequest'),
257
- SchoolCalendarModel: new MockModel('SchoolCalendarEntry'),
258
- WishModel: new MockModel('Wish'),
259
- FeedbackModel: new MockModel('Feedback')
260
  };
 
1
 
2
+ const mongoose = require('mongoose');
3
+
4
+ // ... (Previous Models: School, User, Student, Course, Score, ClassModel, SubjectModel, ExamModel, ScheduleModel, ConfigModel, NotificationModel, GameSessionModel, StudentRewardModel, LuckyDrawConfigModel, GameMonsterConfigModel, GameZenConfigModel)
5
+
6
+ const SchoolSchema = new mongoose.Schema({ name: String, code: String });
7
+ const School = mongoose.model('School', SchoolSchema);
8
+
9
+ const UserSchema = new mongoose.Schema({
10
+ username: String,
11
+ password: String,
12
+ trueName: String,
13
+ phone: String,
14
+ email: String,
15
+ schoolId: String,
16
+ role: String,
17
+ status: String,
18
+ avatar: String,
19
+ createTime: Date,
20
+ teachingSubject: String,
21
+ homeroomClass: String,
22
+ studentNo: String,
23
+ parentName: String,
24
+ parentPhone: String,
25
+ address: String,
26
+ gender: String,
27
+ seatNo: String,
28
+ idCard: String,
29
+ classApplication: {
30
+ type: { type: String },
31
+ targetClass: String,
32
+ status: String
33
+ }
34
+ });
35
+ const User = mongoose.model('User', UserSchema);
36
+
37
+ const StudentSchema = new mongoose.Schema({
38
+ schoolId: String,
39
+ studentNo: String,
40
+ seatNo: String,
41
+ name: String,
42
+ gender: String,
43
+ birthday: String,
44
+ idCard: String,
45
+ phone: String,
46
+ className: String,
47
+ status: String,
48
+ parentName: String,
49
+ parentPhone: String,
50
+ address: String,
51
+ teamId: String,
52
+ drawAttempts: { type: Number, default: 0 },
53
+ dailyDrawLog: { date: String, count: { type: Number, default: 0 } },
54
+ flowerBalance: { type: Number, default: 0 }
55
+ });
56
+ const Student = mongoose.model('Student', StudentSchema);
57
+
58
+ const CourseSchema = new mongoose.Schema({
59
+ schoolId: String,
60
+ courseCode: String,
61
+ courseName: String,
62
+ className: String,
63
+ teacherName: String,
64
+ teacherId: String,
65
+ credits: Number,
66
+ capacity: Number,
67
+ enrolled: Number
68
+ });
69
+ CourseSchema.index({ schoolId: 1, className: 1, courseName: 1 }, { unique: true });
70
+ const Course = mongoose.model('Course', CourseSchema);
71
+
72
+ const ScoreSchema = new mongoose.Schema({ schoolId: String, studentName: String, studentNo: String, courseName: String, score: Number, semester: String, type: String, examName: String, status: String });
73
+ const Score = mongoose.model('Score', ScoreSchema);
74
+
75
+ const ClassSchema = new mongoose.Schema({
76
+ schoolId: String,
77
+ grade: String,
78
+ className: String,
79
+ teacherName: String,
80
+ homeroomTeacherIds: [String]
81
+ });
82
+ const ClassModel = mongoose.model('Class', ClassSchema);
83
+
84
+ const SubjectSchema = new mongoose.Schema({ schoolId: String, name: String, code: String, color: String, excellenceThreshold: Number, thresholds: { type: Map, of: Number } });
85
+ const SubjectModel = mongoose.model('Subject', SubjectSchema);
86
+
87
+ const ExamSchema = new mongoose.Schema({ schoolId: String, name: String, date: String, type: String, semester: String });
88
+ const ExamModel = mongoose.model('Exam', ExamSchema);
89
+
90
+ const ScheduleSchema = new mongoose.Schema({ schoolId: String, className: String, teacherName: String, subject: String, dayOfWeek: Number, period: Number });
91
+ const ScheduleModel = mongoose.model('Schedule', ScheduleSchema);
92
+
93
+ const ConfigSchema = new mongoose.Schema({
94
+ key: String,
95
+ systemName: String,
96
+ semester: String,
97
+ semesters: [String],
98
+ allowRegister: Boolean,
99
+ allowAdminRegister: Boolean,
100
+ allowPrincipalRegister: Boolean,
101
+ allowStudentRegister: Boolean,
102
+ maintenanceMode: Boolean,
103
+ emailNotify: Boolean
104
+ });
105
+ const ConfigModel = mongoose.model('Config', ConfigSchema);
106
+
107
+ const NotificationSchema = new mongoose.Schema({ schoolId: String, targetRole: String, targetUserId: String, title: String, content: String, type: String, createTime: { type: Date, default: Date.now } });
108
+ const NotificationModel = mongoose.model('Notification', NotificationSchema);
109
+
110
+ const GameSessionSchema = new mongoose.Schema({ schoolId: String, className: String, isEnabled: Boolean, maxSteps: Number, teams: [{ id: String, name: String, score: Number, avatar: String, color: String, members: [String] }], rewardsConfig: [{ scoreThreshold: Number, rewardType: String, rewardName: String, rewardValue: Number, achievementId: String }] });
111
+ const GameSessionModel = mongoose.model('GameSession', GameSessionSchema);
112
+
113
+ const StudentRewardSchema = new mongoose.Schema({ schoolId: String, studentId: String, studentName: String, rewardType: String, name: String, count: { type: Number, default: 1 }, status: String, source: String, createTime: { type: Date, default: Date.now }, ownerId: String });
114
+ const StudentRewardModel = mongoose.model('StudentReward', StudentRewardSchema);
115
+
116
+ const LuckyDrawConfigSchema = new mongoose.Schema({ schoolId: String, className: String, ownerId: String, prizes: [{ id: String, name: String, probability: Number, count: Number, icon: String }], dailyLimit: Number, cardCount: Number, defaultPrize: String, consolationWeight: { type: Number, default: 0 } });
117
+ const LuckyDrawConfigModel = mongoose.model('LuckyDrawConfig', LuckyDrawConfigSchema);
118
+
119
+ const GameMonsterConfigSchema = new mongoose.Schema({
120
+ schoolId: String,
121
+ className: String,
122
+ ownerId: String,
123
+ duration: { type: Number, default: 300 },
124
+ sensitivity: { type: Number, default: 25 },
125
+ difficulty: { type: Number, default: 5 },
126
+ useKeyboardMode: { type: Boolean, default: false },
127
+ rewardConfig: {
128
+ enabled: Boolean,
129
+ type: { type: String },
130
+ val: String,
131
+ count: Number
132
+ }
133
+ });
134
+ const GameMonsterConfigModel = mongoose.model('GameMonsterConfig', GameMonsterConfigSchema);
135
+
136
+ const GameZenConfigSchema = new mongoose.Schema({
137
+ schoolId: String,
138
+ className: String,
139
+ ownerId: String,
140
+ durationMinutes: { type: Number, default: 40 },
141
+ threshold: { type: Number, default: 30 },
142
+ passRate: { type: Number, default: 90 },
143
+ rewardConfig: {
144
+ enabled: Boolean,
145
+ type: { type: String },
146
+ val: String,
147
+ count: Number
148
+ }
149
+ });
150
+ const GameZenConfigModel = mongoose.model('GameZenConfig', GameZenConfigSchema);
151
+
152
+ // Updated Achievement Schema with addedBy
153
+ const AchievementConfigSchema = new mongoose.Schema({
154
+ schoolId: String,
155
+ className: String,
156
+ achievements: [{
157
+ id: String,
158
+ name: String,
159
+ icon: String,
160
+ points: Number,
161
+ description: String,
162
+ addedBy: String,
163
+ addedByName: String
164
+ }],
165
+ // Legacy support, rules now moving to TeacherExchangeConfig
166
+ exchangeRules: [{ id: String, cost: Number, rewardType: String, rewardName: String, rewardValue: Number }]
167
+ });
168
+ const AchievementConfigModel = mongoose.model('AchievementConfig', AchievementConfigSchema);
169
+
170
+ // NEW: Independent Teacher Exchange Rules
171
+ const TeacherExchangeConfigSchema = new mongoose.Schema({
172
+ schoolId: String,
173
+ teacherId: String,
174
+ teacherName: String,
175
+ rules: [{ id: String, cost: Number, rewardType: String, rewardName: String, rewardValue: Number }]
176
+ });
177
+ const TeacherExchangeConfigModel = mongoose.model('TeacherExchangeConfig', TeacherExchangeConfigSchema);
178
+
179
+ const StudentAchievementSchema = new mongoose.Schema({ schoolId: String, studentId: String, studentName: String, achievementId: String, achievementName: String, achievementIcon: String, semester: String, createTime: { type: Date, default: Date.now } });
180
+ const StudentAchievementModel = mongoose.model('StudentAchievement', StudentAchievementSchema);
181
+
182
+ const AttendanceSchema = new mongoose.Schema({ schoolId: String, studentId: String, studentName: String, className: String, date: String, status: String, checkInTime: Date });
183
+ const AttendanceModel = mongoose.model('Attendance', AttendanceSchema);
184
+
185
+ const LeaveRequestSchema = new mongoose.Schema({ schoolId: String, studentId: String, studentName: String, className: String, reason: String, startDate: String, endDate: String, status: { type: String, default: 'Pending' }, createTime: { type: Date, default: Date.now } });
186
+ const LeaveRequestModel = mongoose.model('LeaveRequest', LeaveRequestSchema);
187
+
188
+ const SchoolCalendarSchema = new mongoose.Schema({ schoolId: String, className: String, type: String, startDate: String, endDate: String, name: String });
189
+ const SchoolCalendarModel = mongoose.model('SchoolCalendar', SchoolCalendarSchema);
190
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
191
  module.exports = {
192
+ School, User, Student, Course, Score, ClassModel, SubjectModel, ExamModel, ScheduleModel,
193
+ ConfigModel, NotificationModel, GameSessionModel, StudentRewardModel, LuckyDrawConfigModel, GameMonsterConfigModel, GameZenConfigModel,
194
+ AchievementConfigModel, TeacherExchangeConfigModel, StudentAchievementModel, AttendanceModel, LeaveRequestModel, SchoolCalendarModel
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
195
  };
pages/CourseList.tsx CHANGED
@@ -169,7 +169,7 @@ export const CourseList: React.FC = () => {
169
  <div className="flex gap-2 pt-2 border-t border-gray-50">
170
  <button onClick={() => {
171
  setFormData({
172
- courseCode: c.courseCode || '',
173
  courseName: c.courseName,
174
  teacherName: c.teacherName,
175
  teacherId: c.teacherId || '',
 
169
  <div className="flex gap-2 pt-2 border-t border-gray-50">
170
  <button onClick={() => {
171
  setFormData({
172
+ courseCode: c.courseCode,
173
  courseName: c.courseName,
174
  teacherName: c.teacherName,
175
  teacherId: c.teacherId || '',
server.js CHANGED
@@ -1,982 +1,849 @@
1
 
 
 
 
 
 
 
 
 
2
  const express = require('express');
3
- // const mongoose = require('mongoose'); // REMOVED: Using In-Memory DB
4
  const cors = require('cors');
5
  const bodyParser = require('body-parser');
6
  const path = require('path');
7
- const compression = require('compression');
8
 
9
- // Import In-Memory Models
10
- const {
11
- School, User, Student, Course, Score, ClassModel, SubjectModel, ExamModel, ScheduleModel,
12
- ConfigModel, NotificationModel, GameSessionModel, StudentRewardModel, LuckyDrawConfigModel, GameMonsterConfigModel, GameZenConfigModel,
13
- AchievementConfigModel, TeacherExchangeConfigModel, StudentAchievementModel, AttendanceModel, LeaveRequestModel, SchoolCalendarModel,
14
- WishModel, FeedbackModel
15
- } = require('./models');
16
 
17
  const app = express();
18
- const PORT = process.env.PORT || 7860;
19
-
20
- // Middleware
21
- app.use(cors());
22
  app.use(compression());
23
- app.use(bodyParser.json({ limit: '10mb' }));
24
- app.use(bodyParser.urlencoded({ extended: true, limit: '10mb' }));
25
-
26
- // --- DATABASE INITIALIZATION (MOCK) ---
27
- // Initialize a default admin user if none exists
28
- const initDB = async () => {
29
- console.log('Using In-Memory Database');
30
- const admin = await User.findOne({ username: 'admin' });
31
- if (!admin) {
32
- await User.create({
33
- username: 'admin',
34
- password: 'admin',
35
- role: 'ADMIN',
36
- status: 'active',
37
- trueName: '系统管理员'
38
- });
39
- console.log('Default Admin Created: admin / admin');
40
  }
 
 
 
 
 
 
 
 
 
 
 
 
41
  };
42
- initDB();
43
-
44
- // --- Helpers ---
45
- const injectSchoolId = (req, data) => {
46
- const schoolId = req.headers['x-school-id'];
47
- if (schoolId) data.schoolId = schoolId;
48
- return data;
 
 
 
 
 
 
 
 
 
 
49
  };
 
50
 
51
- const getQueryFilter = (req) => {
52
- const schoolId = req.headers['x-school-id'];
53
- return schoolId ? { schoolId } : {};
 
 
 
 
 
54
  };
55
 
56
- // --- AUTH ROUTES ---
57
- app.post('/api/auth/login', async (req, res) => {
58
- const { username, password } = req.body;
59
- const user = await User.findOne({ username, password });
60
- if (user) {
61
- if (user.status === 'banned') return res.status(403).json({ error: 'BANNED' });
62
- if (user.status === 'pending') return res.status(403).json({ error: 'PENDING_APPROVAL' });
63
- res.json(user);
64
  } else {
65
- res.status(401).json({ error: 'INVALID_PASSWORD' });
66
- }
67
- });
68
-
69
- app.post('/api/auth/register', async (req, res) => {
70
- try {
71
- if (req.body.role === 'STUDENT' && !req.body.username) {
72
- const count = await User.countDocuments({ role: 'STUDENT' });
73
- const year = new Date().getFullYear();
74
- req.body.username = `${year}${String(count + 1).padStart(4, '0')}`;
75
- req.body.password = '123456';
76
- req.body.studentNo = req.body.username;
77
- }
78
-
79
- const newUser = await User.create({ ...req.body, status: 'pending' });
80
-
81
- if (req.body.role === 'STUDENT') {
82
- await Student.create(injectSchoolId({ headers: { 'x-school-id': req.body.schoolId } }, {
83
- name: req.body.trueName,
84
- studentNo: req.body.username,
85
- className: req.body.homeroomClass,
86
- seatNo: req.body.seatNo,
87
- gender: req.body.gender,
88
- phone: req.body.phone,
89
- parentName: req.body.parentName,
90
- parentPhone: req.body.parentPhone,
91
- address: req.body.address,
92
- idCard: req.body.idCard,
93
- status: 'Enrolled'
94
- }));
95
- }
96
-
97
- res.json(newUser);
98
- } catch (e) {
99
- res.status(400).json({ error: 'CONFLICT', message: e.message });
100
  }
101
- });
102
-
103
- app.get('/api/auth/me', async (req, res) => {
104
- res.json({ message: "Session check passed" });
105
- });
106
-
107
- app.post('/api/auth/update-profile', async (req, res) => {
108
- const { userId, ...updates } = req.body;
109
- if (updates.newPassword) {
110
- const user = await User.findById(userId);
111
- if (user.password !== updates.currentPassword) {
112
- return res.status(400).json({ error: 'INVALID_PASSWORD' });
113
- }
114
- updates.password = updates.newPassword;
115
- delete updates.newPassword;
116
- delete updates.currentPassword;
117
- }
118
- const updated = await User.findByIdAndUpdate(userId, updates, { new: true });
119
- res.json(updated);
120
- });
121
-
122
- // --- SCHOOL ROUTES ---
123
- app.get('/api/schools', async (req, res) => {
124
- const schools = await School.find();
125
- res.json(schools);
126
- });
127
- app.get('/api/public/schools', async (req, res) => {
128
- const schools = await School.find(); // Mock: Return all, simple projection handled by client if needed or mocked
129
- const projected = schools.map(s => ({ name: s.name, code: s.code, _id: s._id }));
130
- res.json(projected);
131
- });
132
- app.post('/api/schools', async (req, res) => {
133
- const s = await School.create(req.body);
134
- res.json(s);
135
- });
136
- app.put('/api/schools/:id', async (req, res) => {
137
- await School.findByIdAndUpdate(req.params.id, req.body);
138
- res.json({ success: true });
139
- });
140
- app.delete('/api/schools/:id', async (req, res) => {
141
- const sid = req.params.id;
142
- await School.findByIdAndDelete(sid);
143
- await User.deleteMany({ schoolId: sid });
144
- await Student.deleteMany({ schoolId: sid });
145
- res.json({ success: true });
146
- });
147
-
148
- // --- PUBLIC CONFIG & META ---
149
- app.get('/api/public/config', async (req, res) => {
150
- const config = await ConfigModel.findOne(getQueryFilter(req));
151
- res.json(config || {});
152
- });
153
- app.get('/api/public/meta', async (req, res) => {
154
- const { schoolId } = req.query;
155
- if (!schoolId) return res.json({});
156
- const [classes, subjects] = await Promise.all([
157
- ClassModel.find({ schoolId }),
158
- SubjectModel.find({ schoolId })
159
- ]);
160
- res.json({ classes, subjects });
161
- });
162
 
163
- // --- CONFIG ---
164
- app.get('/api/config', async (req, res) => {
165
- const config = await ConfigModel.findOne(getQueryFilter(req));
166
- res.json(config || {});
167
- });
168
- app.post('/api/config', async (req, res) => {
169
- const filter = getQueryFilter(req);
170
- const data = injectSchoolId(req, req.body);
171
- await ConfigModel.findOneAndUpdate(filter, data, { upsert: true });
172
- res.json({ success: true });
173
- });
174
 
175
- // --- USERS ---
176
- app.get('/api/users', async (req, res) => {
177
- const { role, global } = req.query;
178
- const filter = global === 'true' ? {} : getQueryFilter(req);
179
- if (role) filter.role = role;
180
- const users = await User.find(filter);
181
- res.json(users);
182
- });
183
- app.put('/api/users/:id', async (req, res) => {
184
- await User.findByIdAndUpdate(req.params.id, req.body);
185
- res.json({ success: true });
186
- });
187
- app.delete('/api/users/:id', async (req, res) => {
188
- await User.findByIdAndDelete(req.params.id);
189
- res.json({ success: true });
190
- });
191
- app.post('/api/users/class-application', async (req, res) => {
192
- const { userId, type, targetClass, action } = req.body;
193
- if (action === 'APPLY') {
194
- await User.findByIdAndUpdate(userId, {
195
- classApplication: { type, targetClass, status: 'PENDING' }
196
- });
197
- } else if (action === 'APPROVE') {
198
- if (type === 'CLAIM') {
199
- await User.findByIdAndUpdate(userId, {
200
- homeroomClass: targetClass,
201
- classApplication: null
202
- });
203
- } else {
204
- await User.findByIdAndUpdate(userId, {
205
- homeroomClass: '',
206
- classApplication: null
207
- });
208
- }
209
- } else {
210
- await User.findByIdAndUpdate(userId, { classApplication: null });
211
- }
212
- res.json({ success: true });
213
- });
214
  app.get('/api/classes/:className/teachers', async (req, res) => {
215
  const { className } = req.params;
216
- // Decode className just in case
217
- const decodedName = decodeURIComponent(className);
218
- const filter = getQueryFilter(req);
219
-
220
- // Manual complex query simulation for Mock DB
221
- // $or logic needs to be handled by the Mock Model if supported, or manually
222
- const teachers = await User.find({
223
- ...filter,
224
- role: 'TEACHER',
225
- $or: [
226
- { homeroomClass: decodedName }
227
- ]
228
- });
229
 
230
- const courses = await Course.find({ className: decodedName, ...filter });
231
- const courseTeacherIds = courses.map(c => c.teacherId).filter(Boolean);
232
- const courseTeachers = await User.find({ _id: { $in: courseTeacherIds } });
233
 
234
- // Dedup manually
235
- const all = [...teachers, ...courseTeachers];
236
- const uniqueMap = new Map();
237
- all.forEach(u => uniqueMap.set(u._id, u));
238
- res.json(Array.from(uniqueMap.values()));
239
- });
240
 
241
- // --- STUDENTS ---
242
- app.get('/api/students', async (req, res) => {
243
- const students = await Student.find(getQueryFilter(req));
244
- res.json(students);
245
- });
246
- app.post('/api/students', async (req, res) => {
247
- const data = injectSchoolId(req, req.body);
248
- if (!data.studentNo) {
249
- const count = await Student.countDocuments(getQueryFilter(req));
250
- data.studentNo = `S${Date.now().toString().slice(-6)}${count}`;
251
- }
252
- const s = await Student.create(data);
253
- res.json(s);
254
- });
255
- app.put('/api/students/:id', async (req, res) => {
256
- await Student.findByIdAndUpdate(req.params.id, req.body);
257
- res.json({ success: true });
258
- });
259
- app.delete('/api/students/:id', async (req, res) => {
260
- await Student.findByIdAndDelete(req.params.id);
261
- res.json({ success: true });
262
- });
263
- app.post('/api/students/promote', async (req, res) => {
264
- const { teacherFollows } = req.body;
265
- const filter = getQueryFilter(req);
266
- const students = await Student.find(filter);
267
- let count = 0;
268
-
269
- const gradeMap = {
270
- '一年级': '二年级', '二年级': '三年级', '三年级': '四年级',
271
- '四年级': '五年级', '五年级': '六年级', '六年级': '毕业',
272
- '初一': '初二', '初二': '初三', '初三': '毕业',
273
- '高一': '高二', '高二': '高三', '高三': '毕业',
274
- '七年级': '八年级', '八年级': '九年级', '九年级': '毕业'
275
- };
276
 
277
- for (const s of students) {
278
- const match = s.className.match(/^(.+?)([\((]?\d+[)\)]?班)$/);
279
- if (match) {
280
- const currentGrade = match[1];
281
- const suffix = match[2];
282
- const nextGrade = gradeMap[currentGrade];
283
-
284
- if (nextGrade) {
285
- if (nextGrade === '毕业') {
286
- s.status = 'Graduated';
287
- } else {
288
- s.className = nextGrade + suffix;
289
- }
290
- // In mock, objects are ref, but lets be safe
291
- await Student.findByIdAndUpdate(s._id, s);
292
- count++;
293
  }
294
  }
295
- }
296
-
297
- if (teacherFollows) {
298
- const teachers = await User.find({ ...filter, role: 'TEACHER' });
299
- for (const t of teachers) {
300
- if (t.homeroomClass) {
301
- const match = t.homeroomClass.match(/^(.+?)([\((]?\d+[)\)]?班)$/);
302
- if (match) {
303
- const nextG = gradeMap[match[1]];
304
- if (nextG && nextG !== '毕业') {
305
- t.homeroomClass = nextG + match[2];
306
- await User.findByIdAndUpdate(t._id, t);
307
- }
308
  }
309
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
310
  }
311
- }
312
 
313
- res.json({ count });
314
- });
315
- app.post('/api/students/transfer', async (req, res) => {
316
- const { studentId, targetClass } = req.body;
317
- await Student.findByIdAndUpdate(studentId, { className: targetClass });
318
- res.json({ success: true });
319
- });
320
 
321
- // --- CLASSES ---
322
- app.get('/api/classes', async (req, res) => {
323
- const classes = await ClassModel.find(getQueryFilter(req));
324
- res.json(classes);
325
- });
326
- app.post('/api/classes', async (req, res) => {
327
- const data = injectSchoolId(req, req.body);
328
- await ClassModel.create(data);
329
- res.json({ success: true });
330
- });
331
- app.delete('/api/classes/:id', async (req, res) => {
332
- await ClassModel.findByIdAndDelete(req.params.id);
333
- res.json({ success: true });
334
- });
335
- app.put('/api/classes/:id', async (req, res) => {
336
- await ClassModel.findByIdAndUpdate(req.params.id, req.body);
337
- res.json({ success: true });
338
  });
339
 
340
- // --- COURSES ---
341
- app.get('/api/courses', async (req, res) => {
342
- const courses = await Course.find(getQueryFilter(req));
343
- res.json(courses);
344
- });
345
- app.post('/api/courses', async (req, res) => {
346
- const data = injectSchoolId(req, req.body);
347
- const exists = await Course.findOne({
348
- schoolId: data.schoolId,
349
- className: data.className,
350
- courseName: data.courseName
351
- });
352
- if (exists) return res.status(400).json({ error: 'DUPLICATE' });
353
- await Course.create(data);
354
- res.json({ success: true });
355
- });
356
- app.put('/api/courses/:id', async (req, res) => {
357
- await Course.findByIdAndUpdate(req.params.id, req.body);
358
- res.json({ success: true });
359
- });
360
- app.delete('/api/courses/:id', async (req, res) => {
361
- await Course.findByIdAndDelete(req.params.id);
362
- res.json({ success: true });
 
 
 
363
  });
364
 
365
- // --- SCORES ---
366
- app.get('/api/scores', async (req, res) => {
367
- const scores = await Score.find(getQueryFilter(req));
368
- res.json(scores);
369
- });
370
- app.post('/api/scores', async (req, res) => {
371
  const data = injectSchoolId(req, req.body);
372
- await Score.create(data);
373
- res.json({ success: true });
374
- });
375
- app.put('/api/scores/:id', async (req, res) => {
376
- await Score.findByIdAndUpdate(req.params.id, req.body);
377
- res.json({ success: true });
378
- });
379
- app.delete('/api/scores/:id', async (req, res) => {
380
- await Score.findByIdAndDelete(req.params.id);
381
  res.json({ success: true });
382
  });
383
 
384
- // --- SUBJECTS ---
385
- app.get('/api/subjects', async (req, res) => {
386
- const subjects = await SubjectModel.find(getQueryFilter(req));
387
- res.json(subjects);
388
- });
389
- app.post('/api/subjects', async (req, res) => {
390
- const data = injectSchoolId(req, req.body);
391
- await SubjectModel.create(data);
392
- res.json({ success: true });
393
- });
394
- app.put('/api/subjects/:id', async (req, res) => {
395
- await SubjectModel.findByIdAndUpdate(req.params.id, req.body);
396
- res.json({ success: true });
397
- });
398
- app.delete('/api/subjects/:id', async (req, res) => {
399
- await SubjectModel.findByIdAndDelete(req.params.id);
400
- res.json({ success: true });
401
- });
 
 
402
 
403
- // --- EXAMS ---
404
- app.get('/api/exams', async (req, res) => {
405
- const exams = await ExamModel.find(getQueryFilter(req));
406
- res.json(exams);
407
  });
408
- app.post('/api/exams', async (req, res) => {
 
409
  const data = injectSchoolId(req, req.body);
410
- await ExamModel.findOneAndUpdate(
411
- { schoolId: data.schoolId, name: data.name },
 
 
 
 
 
 
412
  data,
413
  { upsert: true }
414
  );
415
  res.json({ success: true });
416
  });
417
 
418
- // --- SCHEDULES ---
419
- app.get('/api/schedules', async (req, res) => {
420
- const filter = getQueryFilter(req);
421
- const { className, teacherName, grade } = req.query;
422
- if (className) filter.className = className;
423
- if (teacherName) filter.teacherName = teacherName;
424
- if (grade) filter.className = { $regex: new RegExp(`^${grade}`) };
425
-
426
- const schedules = await ScheduleModel.find(filter);
427
- res.json(schedules);
428
  });
429
- app.post('/api/schedules', async (req, res) => {
430
- const data = injectSchoolId(req, req.body);
431
- await ScheduleModel.findOneAndDelete({
432
- schoolId: data.schoolId,
433
- className: data.className,
434
- dayOfWeek: data.dayOfWeek,
435
- period: data.period
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
436
  });
437
- await ScheduleModel.create(data);
438
- res.json({ success: true });
439
- });
440
- app.delete('/api/schedules', async (req, res) => {
441
- const filter = getQueryFilter(req);
442
- const { className, dayOfWeek, period } = req.query;
443
- await ScheduleModel.deleteOne({ ...filter, className, dayOfWeek: Number(dayOfWeek), period: Number(period) });
444
  res.json({ success: true });
445
  });
446
 
447
- // --- ATTENDANCE ---
448
- app.get('/api/attendance', async (req, res) => {
449
- const filter = getQueryFilter(req);
450
- const { className, date, studentId } = req.query;
451
- if (date) filter.date = date;
452
- if (studentId) filter.studentId = studentId;
453
 
454
- if (className) {
455
- const students = await Student.find({ ...getQueryFilter(req), className });
456
- const ids = students.map(s => s._id.toString());
457
- filter.studentId = { $in: ids };
 
 
 
 
 
 
 
 
 
 
 
458
  }
459
- const list = await AttendanceModel.find(filter);
460
- res.json(list);
461
- });
462
- app.post('/api/attendance/check-in', async (req, res) => {
463
- const { studentId, date } = req.body;
464
- const existing = await AttendanceModel.findOne({ studentId, date });
465
- if (!existing) {
466
- await AttendanceModel.create({
467
- studentId, date, status: 'Present', checkInTime: new Date()
468
- });
469
  }
470
- res.json({ success: true });
471
- });
472
- app.post('/api/attendance/batch', async (req, res) => {
473
- const { className, date } = req.body;
474
- const students = await Student.find({ ...getQueryFilter(req), className });
475
 
476
- const existing = await AttendanceModel.find({
477
- studentId: { $in: students.map(s => s._id) },
478
- date
479
- });
480
- const existingIds = new Set(existing.map(e => e.studentId));
481
 
482
- const toInsert = students
483
- .filter(s => !existingIds.has(s._id.toString()))
484
- .map(s => ({
485
- studentId: s._id,
486
- date,
487
- status: 'Present',
488
- checkInTime: new Date()
489
- }));
 
 
 
 
 
490
 
491
- if (toInsert.length > 0) {
492
- await AttendanceModel.insertMany(toInsert);
 
493
  }
494
- res.json({ success: true });
495
- });
496
- app.put('/api/attendance/update', async (req, res) => {
497
- const { studentId, date, status } = req.body;
498
- await AttendanceModel.findOneAndUpdate(
499
- { studentId, date },
500
- { status, checkInTime: new Date() },
501
- { upsert: true }
502
- );
503
- res.json({ success: true });
504
- });
505
- app.post('/api/leave', async (req, res) => {
506
- const data = injectSchoolId(req, req.body);
507
- await LeaveRequestModel.create(data);
508
  res.json({ success: true });
509
  });
510
 
511
- // --- CALENDAR ---
512
- app.get('/api/attendance/calendar', async (req, res) => {
513
- const filter = getQueryFilter(req);
514
- if (req.query.className) filter.className = req.query.className;
515
- const entries = await SchoolCalendarModel.find(filter);
516
- res.json(entries);
517
- });
518
- app.post('/api/attendance/calendar', async (req, res) => {
519
- const data = injectSchoolId(req, req.body);
520
- await SchoolCalendarModel.create(data);
521
- res.json({ success: true });
522
  });
523
- app.delete('/api/attendance/calendar/:id', async (req, res) => {
524
- await SchoolCalendarModel.findByIdAndDelete(req.params.id);
525
- res.json({ success: true });
 
 
 
 
 
 
 
 
 
 
 
 
 
526
  });
527
-
528
- // --- STATS ---
529
- app.get('/api/stats', async (req, res) => {
530
- const filter = getQueryFilter(req);
531
- const [stus, courses, scores] = await Promise.all([
532
- Student.countDocuments(filter),
533
- Course.countDocuments(filter),
534
- Score.find(filter)
535
- ]);
536
- const avg = scores.length ? (scores.reduce((a,b)=>a+b.score,0)/scores.length).toFixed(1) : 0;
537
- const exc = scores.length ? ((scores.filter(s=>s.score>=90).length/scores.length)*100).toFixed(1)+'%' : '0%';
538
-
539
- res.json({
540
- studentCount: stus,
541
- courseCount: courses,
542
- avgScore: avg,
543
- excellentRate: exc
544
- });
 
 
 
 
 
 
545
  });
546
-
547
- // --- NOTIFICATIONS ---
548
- app.get('/api/notifications', async (req, res) => {
549
- const { userId, role } = req.query;
550
  const filter = getQueryFilter(req);
551
- const notifs = await NotificationModel.find({
552
- ...filter,
553
- $or: [
554
- { targetUserId: userId },
555
- { targetRole: role },
556
- { targetRole: 'ALL' }
557
- ]
558
- }).sort({ createTime: -1 }).limit(20);
559
- res.json(notifs);
560
- });
561
-
562
- // --- GAMES ---
563
- app.get('/api/games/mountain', async (req, res) => {
564
- const sess = await GameSessionModel.findOne({ ...getQueryFilter(req), className: req.query.className });
565
- res.json(sess);
566
- });
567
- app.post('/api/games/mountain', async (req, res) => {
568
- const data = injectSchoolId(req, req.body);
569
- await GameSessionModel.findOneAndUpdate(
570
- { schoolId: data.schoolId, className: data.className },
571
- data,
572
- { upsert: true }
573
- );
574
- res.json({ success: true });
575
- });
576
- app.get('/api/games/lucky-config', async (req, res) => {
577
- const { className, ownerId } = req.query;
578
- const config = await LuckyDrawConfigModel.findOne({ ...getQueryFilter(req), className, ownerId });
579
- res.json(config);
580
  });
581
- app.post('/api/games/lucky-config', async (req, res) => {
582
- const data = injectSchoolId(req, req.body);
583
- await LuckyDrawConfigModel.findOneAndUpdate(
584
- { schoolId: data.schoolId, className: data.className, ownerId: data.ownerId },
585
- data,
586
- { upsert: true }
587
- );
588
- res.json({ success: true });
 
 
 
 
589
  });
590
- app.post('/api/games/lucky-draw', async (req, res) => {
591
- const { studentId } = req.body;
592
- const student = await Student.findById(studentId);
593
- if (!student || (student.drawAttempts || 0) <= 0) {
594
- return res.status(400).json({ error: 'NO_ATTEMPTS' });
 
 
 
 
 
 
 
595
  }
596
-
597
- student.drawAttempts = (student.drawAttempts || 0) - 1;
598
- await Student.findByIdAndUpdate(student._id, student); // Persist manually in mock
599
-
600
- const config = await LuckyDrawConfigModel.findOne({
601
- schoolId: student.schoolId,
602
- className: student.className
603
- });
604
-
605
- let result = { prize: '再接再厉', rewardType: 'CONSOLATION' };
606
-
607
- if (config) {
608
- const rand = Math.random() * 100;
609
- let cumulative = 0;
610
- const totalWeight = config.prizes.reduce((a,b)=>a+b.probability,0) + (config.consolationWeight || 0);
611
- const normalizedRand = rand * (totalWeight / 100);
612
-
613
- for (const p of config.prizes) {
614
- cumulative += p.probability;
615
- if (normalizedRand <= cumulative) {
616
- if (p.count > 0) {
617
- result = { prize: p.name, rewardType: 'ITEM' };
618
- // Decrement count
619
- p.count -= 1;
620
- await LuckyDrawConfigModel.findByIdAndUpdate(config._id, config);
621
-
622
- await StudentRewardModel.create({
623
- schoolId: student.schoolId,
624
- studentId: student._id,
625
- studentName: student.name,
626
- rewardType: 'ITEM',
627
- name: p.name,
628
- status: 'PENDING',
629
- source: '幸运抽奖',
630
- count: 1
631
- });
632
  }
633
- break;
634
  }
635
- }
636
-
637
- if (result.rewardType === 'CONSOLATION') {
638
- result.prize = config.defaultPrize || '再接再厉';
639
- await StudentRewardModel.create({
640
- schoolId: student.schoolId,
641
- studentId: student._id,
642
- studentName: student.name,
643
- rewardType: 'CONSOLATION',
644
- name: result.prize,
645
- status: 'REDEEMED',
646
- source: '幸运抽奖'
647
- });
648
- }
649
  }
650
- res.json(result);
651
  });
652
- app.post('/api/games/grant-reward', async (req, res) => {
653
- const data = injectSchoolId(req, req.body);
654
- const { studentId, count, rewardType } = data;
655
-
656
- const student = await Student.findById(studentId);
657
- if (!student) return res.status(404).json({ error: 'Student not found' });
658
-
659
- if (rewardType === 'DRAW_COUNT') {
660
- student.drawAttempts = (student.drawAttempts || 0) + count;
661
- await Student.findByIdAndUpdate(studentId, student);
662
-
663
- await StudentRewardModel.create({
664
- schoolId: student.schoolId,
665
- studentId,
666
- studentName: student.name,
667
- rewardType: 'DRAW_COUNT',
668
- name: '抽奖券',
669
- count,
670
- status: 'REDEEMED',
671
- source: '老师发放'
672
- });
673
- } else {
674
- await StudentRewardModel.create({
675
- schoolId: student.schoolId,
676
- studentId,
677
- studentName: student.name,
678
- rewardType: 'ITEM',
679
- name: data.name,
680
- count,
681
- status: 'PENDING',
682
- source: '老师发放'
683
- });
684
  }
685
- res.json({ success: true });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
686
  });
687
-
688
- // Monster & Zen Configs
689
  app.get('/api/games/monster-config', async (req, res) => {
690
- const config = await GameMonsterConfigModel.findOne(getQueryFilter(req));
691
- res.json(config);
 
 
 
 
692
  });
693
  app.post('/api/games/monster-config', async (req, res) => {
694
  const data = injectSchoolId(req, req.body);
695
- await GameMonsterConfigModel.findOneAndUpdate(
696
- { schoolId: data.schoolId, className: data.className },
697
- data,
698
- { upsert: true }
699
- );
700
  res.json({ success: true });
701
  });
702
  app.get('/api/games/zen-config', async (req, res) => {
703
- const config = await GameZenConfigModel.findOne(getQueryFilter(req));
704
- res.json(config);
 
 
 
 
705
  });
706
  app.post('/api/games/zen-config', async (req, res) => {
707
  const data = injectSchoolId(req, req.body);
708
- await GameZenConfigModel.findOneAndUpdate(
709
- { schoolId: data.schoolId, className: data.className },
710
- data,
711
- { upsert: true }
712
- );
713
- res.json({ success: true });
714
- });
715
-
716
- // --- ACHIEVEMENTS ---
717
- app.get('/api/achievements/config', async (req, res) => {
718
- const config = await AchievementConfigModel.findOne({ ...getQueryFilter(req), className: req.query.className });
719
- res.json(config);
720
- });
721
- app.post('/api/achievements/config', async (req, res) => {
722
- const data = injectSchoolId(req, req.body);
723
- await AchievementConfigModel.findOneAndUpdate(
724
- { schoolId: data.schoolId, className: data.className },
725
- data,
726
- { upsert: true }
727
- );
728
- res.json({ success: true });
729
- });
730
- app.get('/api/achievements/student', async (req, res) => {
731
- const { studentId, semester } = req.query;
732
- const filter = { studentId };
733
- if (semester) filter.semester = semester;
734
- const list = await StudentAchievementModel.find(filter);
735
- res.json(list);
736
- });
737
- app.post('/api/achievements/grant', async (req, res) => {
738
- const { studentId, achievementId, semester } = req.body;
739
- const student = await Student.findById(studentId);
740
- const config = await AchievementConfigModel.findOne({ schoolId: student.schoolId, className: student.className });
741
- const ach = config.achievements.find(a => a.id === achievementId);
742
-
743
- if (ach) {
744
- await StudentAchievementModel.create({
745
- schoolId: student.schoolId,
746
- studentId,
747
- achievementId,
748
- achievementName: ach.name,
749
- achievementIcon: ach.icon,
750
- points: ach.points,
751
- semester,
752
- createTime: new Date()
753
- });
754
-
755
- student.flowerBalance = (student.flowerBalance || 0) + ach.points;
756
- await Student.findByIdAndUpdate(studentId, student);
757
  }
 
758
  res.json({ success: true });
759
  });
760
- app.get('/api/achievements/teacher-rules', async (req, res) => {
 
 
 
 
761
  const username = req.headers['x-user-username'];
762
- if (username) {
763
  const user = await User.findOne({ username });
764
- if (user) {
765
- const rules = await TeacherExchangeConfigModel.findOne({ teacherId: user._id });
766
- return res.json(rules || { rules: [] });
767
- }
768
- }
769
- if (req.query.teacherIds) {
770
- const ids = req.query.teacherIds.split(',');
771
- const list = await TeacherExchangeConfigModel.find({ teacherId: { $in: ids } });
772
- return res.json(list);
773
  }
774
- res.json({});
 
775
  });
776
- app.post('/api/achievements/teacher-rules', async (req, res) => {
777
- const username = req.headers['x-user-username'];
778
- if (username) {
779
- const user = await User.findOne({ username });
780
- if (user) {
781
- const data = { ...req.body, teacherId: user._id, teacherName: user.trueName || user.username };
782
- await TeacherExchangeConfigModel.findOneAndUpdate({ teacherId: user._id }, data, { upsert: true });
783
- return res.json({ success: true });
784
- }
785
  }
786
- res.status(400).json({ error: 'User not found' });
787
- });
788
- app.post('/api/achievements/exchange', async (req, res) => {
789
- const { studentId, ruleId, teacherId } = req.body;
790
-
791
- // Find Rule from specific Teacher Config
792
- const config = await TeacherExchangeConfigModel.findOne({ teacherId });
793
- const rule = config?.rules.find(r => r.id === ruleId);
794
- const student = await Student.findById(studentId);
795
-
796
- if (!rule || !student) return res.status(404).json({ error: 'Data not found' });
797
- if ((student.flowerBalance || 0) < rule.cost) return res.status(400).json({ error: 'Insufficient balance' });
798
-
799
- // Deduct Balance
800
- student.flowerBalance = (student.flowerBalance || 0) - rule.cost;
801
- await Student.findByIdAndUpdate(studentId, student);
802
-
803
- // Grant Reward
804
- if (rule.rewardType === 'DRAW_COUNT') {
805
- student.drawAttempts = (student.drawAttempts || 0) + rule.rewardValue;
806
- await Student.findByIdAndUpdate(studentId, student);
807
-
808
- await StudentRewardModel.create({
809
- schoolId: student.schoolId,
810
- studentId,
811
- studentName: student.name,
812
- rewardType: 'DRAW_COUNT',
813
- name: rule.rewardName,
814
- count: rule.rewardValue,
815
- status: 'REDEEMED',
816
- source: '小红花兑换'
817
- });
818
- } else {
819
- await StudentRewardModel.create({
820
- schoolId: student.schoolId,
821
- studentId,
822
- studentName: student.name,
823
- rewardType: 'ITEM',
824
- name: rule.rewardName,
825
- count: rule.rewardValue,
826
- status: 'PENDING',
827
- source: '小红花兑换'
828
- });
829
  }
830
- res.json({ success: true });
831
- });
832
-
833
- // --- REWARDS ---
834
- app.get('/api/rewards', async (req, res) => {
835
- const { studentId, scope, className, page = 1, limit = 20 } = req.query;
836
- const filter = getQueryFilter(req);
837
-
838
- if (studentId) filter.studentId = studentId;
839
  if (req.query.excludeType) filter.rewardType = { $ne: req.query.excludeType };
840
-
841
- if (scope === 'class' && className) {
842
- const students = await Student.find({ className, ...getQueryFilter(req) });
843
- const ids = students.map(s => s._id);
844
- filter.studentId = { $in: ids };
845
- }
846
-
847
  const skip = (page - 1) * limit;
848
  const total = await StudentRewardModel.countDocuments(filter);
849
- const list = await StudentRewardModel.find(filter).sort({ createTime: -1 }).skip(skip).limit(Number(limit));
850
- res.json({ list, total });
851
  });
852
- app.post('/api/rewards', async (req, res) => {
853
  const data = injectSchoolId(req, req.body);
854
- await StudentRewardModel.create(data);
855
- res.json({ success: true });
856
- });
857
- app.put('/api/rewards/:id', async (req, res) => {
858
- await StudentRewardModel.findByIdAndUpdate(req.params.id, req.body);
859
- res.json({ success: true });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
860
  });
861
- app.delete('/api/rewards/:id', async (req, res) => {
862
- await StudentRewardModel.findByIdAndDelete(req.params.id);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
863
  res.json({ success: true });
864
  });
865
- app.post('/api/rewards/:id/redeem', async (req, res) => {
866
- await StudentRewardModel.findByIdAndUpdate(req.params.id, { status: 'REDEEMED' });
867
- res.json({ success: true });
 
 
 
 
 
 
 
868
  });
869
-
870
- // --- WISH TREE ROUTES ---
871
- app.get('/api/wishes', async (req, res) => {
872
  const filter = getQueryFilter(req);
873
- const { studentId, teacherId, status } = req.query;
874
- if (studentId) filter.studentId = studentId;
875
- if (teacherId) filter.teacherId = teacherId;
876
- if (status) filter.status = status;
877
-
878
- // Sort: Pending first, then by time
879
- const wishes = await WishModel.find(filter).sort({ status: -1, createTime: -1 });
880
- res.json(wishes);
881
  });
882
-
883
- app.post('/api/wishes', async (req, res) => {
884
  const data = injectSchoolId(req, req.body);
885
- const existing = await WishModel.findOne({
886
- studentId: data.studentId,
887
- status: 'PENDING'
888
- });
889
- if (existing) {
890
- return res.status(400).json({ error: 'LIMIT_REACHED', message: '您还有一个未实现的愿望,请耐心等待实现后再许愿。' });
891
- }
892
- await WishModel.create(data);
893
- res.json({ success: true });
894
  });
895
-
896
- app.post('/api/wishes/:id/fulfill', async (req, res) => {
897
- await WishModel.findByIdAndUpdate(req.params.id, {
898
- status: 'FULFILLED',
899
- fulfillTime: new Date()
900
- });
901
- res.json({ success: true });
 
 
902
  });
903
-
904
- app.post('/api/wishes/random-fulfill', async (req, res) => {
905
- const { teacherId } = req.body;
906
- const pendingWishes = await WishModel.find({
907
- teacherId,
908
- status: 'PENDING',
909
- ...getQueryFilter(req)
910
- });
911
-
912
- if (pendingWishes.length === 0) {
913
- return res.status(404).json({ error: 'NO_WISHES', message: '暂无待实现的愿望' });
 
 
 
 
 
 
 
 
 
 
914
  }
915
-
916
- const randomWish = pendingWishes[Math.floor(Math.random() * pendingWishes.length)];
917
- await WishModel.findByIdAndUpdate(randomWish._id, {
918
- status: 'FULFILLED',
919
- fulfillTime: new Date()
920
- });
921
-
922
- res.json({ success: true, wish: randomWish });
923
  });
924
-
925
- // --- FEEDBACK WALL ROUTES ---
926
- app.get('/api/feedback', async (req, res) => {
 
 
 
 
 
 
 
 
 
 
927
  const filter = getQueryFilter(req);
928
- const { senderId, targetId, role } = req.query;
929
-
930
- if (senderId) {
931
- filter.senderId = senderId;
932
- } else if (targetId) {
933
- filter.targetId = targetId;
934
- } else if (role === 'ADMIN') {
935
- filter.targetId = 'ADMIN';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
936
  }
937
-
938
- const feedbacks = await FeedbackModel.find(filter).sort({ createTime: -1 });
939
- res.json(feedbacks);
940
  });
941
-
942
- app.post('/api/feedback', async (req, res) => {
943
- const data = injectSchoolId(req, req.body);
944
- await FeedbackModel.create(data);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
945
  res.json({ success: true });
946
  });
947
-
948
- app.put('/api/feedback/:id/status', async (req, res) => {
949
- const { status } = req.body;
950
- await FeedbackModel.findByIdAndUpdate(req.params.id, { status });
 
 
 
 
 
 
 
951
  res.json({ success: true });
952
  });
953
-
954
- app.post('/api/feedback/batch-ignore', async (req, res) => {
955
- const { targetId } = req.body;
956
- await FeedbackModel.updateMany(
957
- { targetId, status: 'PENDING', ...getQueryFilter(req) },
958
- { status: 'IGNORED' }
959
- );
960
  res.json({ success: true });
961
  });
962
-
963
- // --- BATCH DELETE UTILITY ---
964
- app.post('/api/batch-delete', async (req, res) => {
965
- const { type, ids } = req.body;
966
- if (!ids || ids.length === 0) return res.json({ count: 0 });
967
-
968
- let result;
969
- if (type === 'student') result = await Student.deleteMany({ _id: { $in: ids } });
970
- if (type === 'score') result = await Score.deleteMany({ _id: { $in: ids } });
971
- if (type === 'user') result = await User.deleteMany({ _id: { $in: ids } });
972
-
973
- res.json({ count: result.deletedCount });
974
  });
975
-
976
- // Serve Frontend
977
- app.use(express.static(path.join(__dirname, 'dist')));
978
- app.get('*', (req, res) => {
979
- res.sendFile(path.join(__dirname, 'dist', 'index.html'));
 
980
  });
981
 
982
- app.listen(PORT, () => console.log(`Server running on port ${PORT} (In-Memory DB Mode)`));
 
 
1
 
2
+ // ... existing imports
3
+ const {
4
+ School, User, Student, Course, Score, ClassModel, SubjectModel, ExamModel, ScheduleModel,
5
+ ConfigModel, NotificationModel, GameSessionModel, StudentRewardModel, LuckyDrawConfigModel, GameMonsterConfigModel, GameZenConfigModel,
6
+ AchievementConfigModel, TeacherExchangeConfigModel, StudentAchievementModel, AttendanceModel, LeaveRequestModel, SchoolCalendarModel
7
+ } = require('./models');
8
+
9
+ // ... (existing setup code, middleware, connectDB, helpers) ...
10
  const express = require('express');
11
+ const mongoose = require('mongoose');
12
  const cors = require('cors');
13
  const bodyParser = require('body-parser');
14
  const path = require('path');
15
+ const compression = require('compression');
16
 
17
+ // ... constants
18
+ const PORT = 7860;
19
+ const MONGO_URI = 'mongodb+srv://dv890a:db8822723@chatpro.gw3v0v7.mongodb.net/chatpro?retryWrites=true&w=majority&appName=chatpro&authSource=admin';
 
 
 
 
20
 
21
  const app = express();
 
 
 
 
22
  app.use(compression());
23
+ app.use(cors());
24
+ app.use(bodyParser.json({ limit: '10mb' }));
25
+ app.use(express.static(path.join(__dirname, 'dist'), {
26
+ setHeaders: (res, filePath) => {
27
+ if (filePath.endsWith('.html')) {
28
+ res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
29
+ } else {
30
+ res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
 
 
 
 
 
 
 
 
 
31
  }
32
+ }
33
+ }));
34
+
35
+ const InMemoryDB = { schools: [], users: [], isFallback: false };
36
+ const connectDB = async () => {
37
+ try {
38
+ await mongoose.connect(MONGO_URI, { serverSelectionTimeoutMS: 30000 });
39
+ console.log('✅ MongoDB 连接成功 (Real Data)');
40
+ } catch (err) {
41
+ console.error('❌ MongoDB 连接失败:', err.message);
42
+ InMemoryDB.isFallback = true;
43
+ }
44
  };
45
+ connectDB();
46
+
47
+ const getQueryFilter = (req) => {
48
+ const s = req.headers['x-school-id'];
49
+ const role = req.headers['x-user-role'];
50
+ if (role === 'PRINCIPAL') {
51
+ if (!s) return { _id: null };
52
+ return { schoolId: s };
53
+ }
54
+ if (!s) return {};
55
+ return {
56
+ $or: [
57
+ { schoolId: s },
58
+ { schoolId: { $exists: false } },
59
+ { schoolId: null }
60
+ ]
61
+ };
62
  };
63
+ const injectSchoolId = (req, b) => ({ ...b, schoolId: req.headers['x-school-id'] });
64
 
65
+ const getGameOwnerFilter = async (req) => {
66
+ const role = req.headers['x-user-role'];
67
+ const username = req.headers['x-user-username'];
68
+ if (role === 'TEACHER') {
69
+ const user = await User.findOne({ username });
70
+ return { ownerId: user ? user._id.toString() : 'unknown' };
71
+ }
72
+ return {};
73
  };
74
 
75
+ const getAutoSemester = () => {
76
+ const now = new Date();
77
+ const month = now.getMonth() + 1;
78
+ const year = now.getFullYear();
79
+ if (month >= 8 || month === 1) {
80
+ const startYear = month === 1 ? year - 1 : year;
81
+ return `${startYear}-${startYear + 1}学年 第一学期`;
 
82
  } else {
83
+ const startYear = year - 1;
84
+ return `${startYear}-${startYear + 1}学年 第二学期`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  }
86
+ };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
 
88
+ const generateStudentNo = async () => {
89
+ const year = new Date().getFullYear();
90
+ const random = Math.floor(100000 + Math.random() * 900000);
91
+ return `${year}${random}`;
92
+ };
 
 
 
 
 
 
93
 
94
+ // ... (Existing Routes: Auth, Users, Students, Classes, etc.) ...
95
+ // Insert helper route for fetching teachers of a class
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  app.get('/api/classes/:className/teachers', async (req, res) => {
97
  const { className } = req.params;
98
+ const schoolId = req.headers['x-school-id'];
 
 
 
 
 
 
 
 
 
 
 
 
99
 
100
+ // Normalize helper: remove all spaces for loose matching
101
+ const normalize = (s) => (s || '').replace(/\s+/g, '');
102
+ const searchName = normalize(decodeURIComponent(className));
103
 
104
+ const teacherIds = new Set();
105
+ const teacherNamesToResolve = new Set();
 
 
 
 
106
 
107
+ try {
108
+ // 1. Get Homeroom Teachers (Iterate to robustly match concatenated grade+class)
109
+ const allClasses = await ClassModel.find({ schoolId });
110
+ const matchedClass = allClasses.find(c => {
111
+ // Match either full "GradeClass" or just "Class" if that's how it's stored
112
+ const full = normalize(c.grade + c.className);
113
+ const sub = normalize(c.className);
114
+ return full === searchName || sub === searchName;
115
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
 
117
+ if (matchedClass) {
118
+ if (matchedClass.homeroomTeacherIds && matchedClass.homeroomTeacherIds.length > 0) {
119
+ matchedClass.homeroomTeacherIds.forEach(id => teacherIds.add(id));
120
+ }
121
+ // Fallback: If only name string exists (legacy data)
122
+ if (matchedClass.teacherName) {
123
+ matchedClass.teacherName.split(/[,,]/).forEach(n => {
124
+ if(n.trim()) teacherNamesToResolve.add(n.trim());
125
+ });
 
 
 
 
 
 
 
126
  }
127
  }
128
+
129
+ // 2. Get Subject Teachers from Courses
130
+ const allCourses = await Course.find({ schoolId });
131
+ allCourses.forEach(c => {
132
+ // Check normalized name
133
+ if (normalize(c.className) === searchName) {
134
+ if (c.teacherId) {
135
+ teacherIds.add(c.teacherId);
136
+ } else if (c.teacherName) {
137
+ // Fallback: If teacherId is missing, assume name is correct
138
+ teacherNamesToResolve.add(c.teacherName);
 
 
139
  }
140
  }
141
+ });
142
+
143
+ // 3. Resolve Teacher Names to IDs (Fallback mechanism)
144
+ if (teacherNamesToResolve.size > 0) {
145
+ const names = Array.from(teacherNamesToResolve);
146
+ // Find users where trueName matches these names
147
+ const users = await User.find({
148
+ schoolId,
149
+ role: 'TEACHER',
150
+ $or: [
151
+ { trueName: { $in: names } },
152
+ { username: { $in: names } } // Just in case name is stored as username
153
+ ]
154
+ });
155
+ users.forEach(u => teacherIds.add(u._id.toString()));
156
  }
 
157
 
158
+ if (teacherIds.size === 0) return res.json([]);
 
 
 
 
 
 
159
 
160
+ // UPDATED: Include teachingSubject in the response
161
+ const teachers = await User.find({ _id: { $in: Array.from(teacherIds) } }, 'trueName username _id teachingSubject');
162
+ res.json(teachers);
163
+ } catch (e) {
164
+ console.error("Error fetching teachers", e);
165
+ res.json([]);
166
+ }
 
 
 
 
 
 
 
 
 
 
167
  });
168
 
169
+ // ... (Rest of the file follows)
170
+ // ...
171
+ app.get('/api/games/lucky-config', async (req, res) => {
172
+ const filter = getQueryFilter(req);
173
+ // If explicit ownerId passed (e.g. from student view), use it.
174
+ // Otherwise if Teacher, force own ID.
175
+ if (req.query.ownerId) {
176
+ filter.ownerId = req.query.ownerId;
177
+ } else {
178
+ const ownerFilter = await getGameOwnerFilter(req);
179
+ Object.assign(filter, ownerFilter);
180
+ }
181
+
182
+ if (req.query.className) filter.className = req.query.className;
183
+
184
+ const config = await LuckyDrawConfigModel.findOne(filter);
185
+ res.json(config || null); // Return null if not found instead of default
186
+ });
187
+ // ... (Rest of games routes unchanged except achievement/exchange below) ...
188
+
189
+ // ACHIEVEMENT ROUTES
190
+ app.get('/api/achievements/config', async (req, res) => {
191
+ const { className } = req.query;
192
+ const filter = getQueryFilter(req);
193
+ if (className) filter.className = className;
194
+ res.json(await AchievementConfigModel.findOne(filter));
195
  });
196
 
197
+ app.post('/api/achievements/config', async (req, res) => {
 
 
 
 
 
198
  const data = injectSchoolId(req, req.body);
199
+ await AchievementConfigModel.findOneAndUpdate(
200
+ { className: data.className, ...getQueryFilter(req) },
201
+ data,
202
+ { upsert: true }
203
+ );
 
 
 
 
204
  res.json({ success: true });
205
  });
206
 
207
+ // NEW: Teacher Exchange Rules Routes
208
+ app.get('/api/achievements/teacher-rules', async (req, res) => {
209
+ const filter = getQueryFilter(req);
210
+ // If getting for specific teacher (e.g. student viewing shop), allow query param
211
+ // If no query param and user is teacher, return their own
212
+ if (req.query.teacherId) {
213
+ filter.teacherId = req.query.teacherId;
214
+ } else if (req.headers['x-user-role'] === 'TEACHER') {
215
+ const user = await User.findOne({ username: req.headers['x-user-username'] });
216
+ if (user) filter.teacherId = user._id.toString();
217
+ }
218
+
219
+ // Support getting MULTIPLE teachers (for student shop view)
220
+ if (req.query.teacherIds) {
221
+ const ids = req.query.teacherIds.split(',');
222
+ delete filter.teacherId;
223
+ filter.teacherId = { $in: ids };
224
+ const configs = await TeacherExchangeConfigModel.find(filter);
225
+ return res.json(configs);
226
+ }
227
 
228
+ const config = await TeacherExchangeConfigModel.findOne(filter);
229
+ res.json(config || { rules: [] });
 
 
230
  });
231
+
232
+ app.post('/api/achievements/teacher-rules', async (req, res) => {
233
  const data = injectSchoolId(req, req.body);
234
+ const user = await User.findOne({ username: req.headers['x-user-username'] });
235
+ if (!user) return res.status(404).json({ error: 'User not found' });
236
+
237
+ data.teacherId = user._id.toString();
238
+ data.teacherName = user.trueName || user.username;
239
+
240
+ await TeacherExchangeConfigModel.findOneAndUpdate(
241
+ { teacherId: data.teacherId, ...getQueryFilter(req) },
242
  data,
243
  { upsert: true }
244
  );
245
  res.json({ success: true });
246
  });
247
 
248
+ app.get('/api/achievements/student', async (req, res) => {
249
+ const { studentId, semester } = req.query;
250
+ const filter = { studentId };
251
+ if (semester) filter.semester = semester;
252
+ res.json(await StudentAchievementModel.find(filter).sort({ createTime: -1 }));
 
 
 
 
 
253
  });
254
+
255
+ app.post('/api/achievements/grant', async (req, res) => {
256
+ const { studentId, achievementId, semester } = req.body;
257
+ const sId = req.headers['x-school-id'];
258
+
259
+ const student = await Student.findById(studentId);
260
+ if (!student) return res.status(404).json({ error: 'Student not found' });
261
+
262
+ const config = await AchievementConfigModel.findOne({ className: student.className, schoolId: sId });
263
+ const achievement = config?.achievements.find(a => a.id === achievementId);
264
+
265
+ if (!achievement) return res.status(404).json({ error: 'Achievement not found' });
266
+
267
+ // Add Record
268
+ await StudentAchievementModel.create({
269
+ schoolId: sId,
270
+ studentId,
271
+ studentName: student.name,
272
+ achievementId: achievement.id,
273
+ achievementName: achievement.name,
274
+ achievementIcon: achievement.icon,
275
+ semester,
276
+ createTime: new Date()
277
  });
278
+
279
+ // Add Points (Flowers)
280
+ await Student.findByIdAndUpdate(studentId, { $inc: { flowerBalance: achievement.points } });
281
+
 
 
 
282
  res.json({ success: true });
283
  });
284
 
285
+ app.post('/api/achievements/exchange', async (req, res) => {
286
+ const { studentId, ruleId, teacherId } = req.body; // Added teacherId to identify which rule set
287
+ const sId = req.headers['x-school-id'];
 
 
 
288
 
289
+ const student = await Student.findById(studentId);
290
+ if (!student) return res.status(404).json({ error: 'Student not found' });
291
+
292
+ // Find rule in Teacher Config first
293
+ let rule = null;
294
+ let ownerId = null; // To track who receives the redemption request
295
+
296
+ if (teacherId) {
297
+ const tConfig = await TeacherExchangeConfigModel.findOne({ teacherId, schoolId: sId });
298
+ rule = tConfig?.rules.find(r => r.id === ruleId);
299
+ ownerId = teacherId;
300
+ } else {
301
+ // Fallback for legacy class-based rules (if any remain)
302
+ const config = await AchievementConfigModel.findOne({ className: student.className, schoolId: sId });
303
+ rule = config?.exchangeRules?.find(r => r.id === ruleId);
304
  }
305
+
306
+ if (!rule) return res.status(404).json({ error: 'Rule not found' });
307
+
308
+ if (student.flowerBalance < rule.cost) {
309
+ return res.status(400).json({ error: 'INSUFFICIENT_FUNDS', message: '小红花余额不足' });
 
 
 
 
 
310
  }
 
 
 
 
 
311
 
312
+ // Deduct Flowers
313
+ await Student.findByIdAndUpdate(studentId, { $inc: { flowerBalance: -rule.cost } });
 
 
 
314
 
315
+ // Add Reward Record
316
+ await StudentRewardModel.create({
317
+ schoolId: sId,
318
+ studentId,
319
+ studentName: student.name,
320
+ rewardType: rule.rewardType, // ITEM or DRAW_COUNT
321
+ name: rule.rewardName,
322
+ count: rule.rewardValue,
323
+ status: rule.rewardType === 'DRAW_COUNT' ? 'REDEEMED' : 'PENDING',
324
+ source: '积分兑换',
325
+ ownerId: ownerId, // Important: Shows up in teacher's reward list
326
+ createTime: new Date()
327
+ });
328
 
329
+ // If Draw Count, add attempts immediately
330
+ if (rule.rewardType === 'DRAW_COUNT') {
331
+ await Student.findByIdAndUpdate(studentId, { $inc: { drawAttempts: rule.rewardValue } });
332
  }
333
+
 
 
 
 
 
 
 
 
 
 
 
 
 
334
  res.json({ success: true });
335
  });
336
 
337
+ // ... (Rest of existing routes from server.js - copy exact content or use existing file)
338
+ app.get('/api/auth/me', async (req, res) => {
339
+ const username = req.headers['x-user-username'];
340
+ if (!username) return res.status(401).json({ error: 'Unauthorized' });
341
+ const user = await User.findOne({ username });
342
+ if (!user) return res.status(404).json({ error: 'User not found' });
343
+ res.json(user);
 
 
 
 
344
  });
345
+ app.post('/api/auth/update-profile', async (req, res) => {
346
+ const { userId, trueName, phone, avatar, currentPassword, newPassword } = req.body;
347
+ try {
348
+ const user = await User.findById(userId);
349
+ if (!user) return res.status(404).json({ error: 'User not found' });
350
+ if (newPassword) {
351
+ if (user.password !== currentPassword) return res.status(401).json({ error: 'INVALID_PASSWORD', message: '旧密码错误' });
352
+ user.password = newPassword;
353
+ }
354
+ if (trueName) user.trueName = trueName;
355
+ if (phone) user.phone = phone;
356
+ if (avatar) user.avatar = avatar;
357
+ await user.save();
358
+ if (user.role === 'STUDENT') await Student.findOneAndUpdate({ studentNo: user.studentNo }, { name: user.trueName || user.username, phone: user.phone });
359
+ res.json({ success: true, user });
360
+ } catch (e) { res.status(500).json({ error: e.message }); }
361
  });
362
+ app.post('/api/auth/register', async (req, res) => {
363
+ const { role, username, password, schoolId, trueName, seatNo } = req.body;
364
+ const className = req.body.className || req.body.homeroomClass;
365
+ try {
366
+ if (role === 'STUDENT') {
367
+ if (!trueName || !className) return res.status(400).json({ error: 'MISSING_FIELDS', message: '姓名和班级不能为空' });
368
+ const cleanName = trueName.trim();
369
+ const cleanClass = className.trim();
370
+ const existingProfile = await Student.findOne({ schoolId, name: { $regex: new RegExp(`^${cleanName}$`, 'i') }, className: cleanClass });
371
+ let finalUsername = '';
372
+ if (existingProfile) {
373
+ if (existingProfile.studentNo && existingProfile.studentNo.length > 5) finalUsername = existingProfile.studentNo;
374
+ else { finalUsername = await generateStudentNo(); existingProfile.studentNo = finalUsername; await existingProfile.save(); }
375
+ const userExists = await User.findOne({ username: finalUsername, schoolId });
376
+ if (userExists) return res.status(409).json({ error: userExists.status === 'active' ? 'ACCOUNT_EXISTS' : 'ACCOUNT_PENDING', message: '账号已存在' });
377
+ } else finalUsername = await generateStudentNo();
378
+ 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() });
379
+ return res.json({ username: finalUsername });
380
+ }
381
+ const existing = await User.findOne({ username });
382
+ if (existing) return res.status(409).json({ error: 'USERNAME_EXISTS', message: '用户名已存在' });
383
+ await User.create({...req.body, status: 'pending', createTime: new Date()});
384
+ res.json({ username });
385
+ } catch(e) { res.status(500).json({ error: e.message }); }
386
  });
387
+ app.get('/api/users', async (req, res) => {
 
 
 
388
  const filter = getQueryFilter(req);
389
+ if (req.headers['x-user-role'] === 'PRINCIPAL') filter.role = { $ne: 'ADMIN' };
390
+ if (req.query.role) filter.role = req.query.role;
391
+ res.json(await User.find(filter).sort({ createTime: -1 }));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
392
  });
393
+ app.put('/api/users/:id', async (req, res) => {
394
+ const userId = req.params.id;
395
+ const updates = req.body;
396
+ try {
397
+ const user = await User.findById(userId);
398
+ if (!user) return res.status(404).json({ error: 'User not found' });
399
+ if (user.status !== 'active' && updates.status === 'active' && user.role === 'STUDENT') {
400
+ 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 });
401
+ }
402
+ await User.findByIdAndUpdate(userId, updates);
403
+ res.json({});
404
+ } catch (e) { res.status(500).json({ error: e.message }); }
405
  });
406
+ app.post('/api/users/class-application', async (req, res) => {
407
+ const { userId, type, targetClass, action } = req.body;
408
+ const userRole = req.headers['x-user-role'];
409
+ const schoolId = req.headers['x-school-id'];
410
+ if (action === 'APPLY') {
411
+ try {
412
+ const user = await User.findById(userId);
413
+ if(!user) return res.status(404).json({error:'User not found'});
414
+ await User.findByIdAndUpdate(userId, { classApplication: { type: type, targetClass: targetClass || '', status: 'PENDING' } });
415
+ await NotificationModel.create({ schoolId, targetRole: 'ADMIN', title: '新的班主任任免申请', content: `${user.trueName || user.username} 申请 ${type === 'CLAIM' ? '任教' : '卸任'},请及时处理。`, type: 'warning' });
416
+ return res.json({ success: true });
417
+ } catch (e) { return res.status(500).json({ error: e.message }); }
418
  }
419
+ if (userRole === 'ADMIN' || userRole === 'PRINCIPAL') {
420
+ const user = await User.findById(userId);
421
+ if (!user || !user.classApplication) return res.status(404).json({ error: 'Application not found' });
422
+ const appType = user.classApplication.type;
423
+ const appTarget = user.classApplication.targetClass;
424
+ if (action === 'APPROVE') {
425
+ const updates = { classApplication: null };
426
+ const classes = await ClassModel.find({ schoolId });
427
+ if (appType === 'CLAIM') {
428
+ updates.homeroomClass = appTarget;
429
+ const matchedClass = classes.find(c => (c.grade + c.className) === appTarget);
430
+ if (matchedClass) {
431
+ const teacherIds = matchedClass.homeroomTeacherIds || [];
432
+ if (!teacherIds.includes(userId)) {
433
+ teacherIds.push(userId);
434
+ const teachers = await User.find({ _id: { $in: teacherIds } });
435
+ const names = teachers.map(t => t.trueName || t.username).join(', ');
436
+ await ClassModel.findByIdAndUpdate(matchedClass._id, { homeroomTeacherIds: teacherIds, teacherName: names });
437
+ }
438
+ }
439
+ } else if (appType === 'RESIGN') {
440
+ updates.homeroomClass = '';
441
+ const matchedClass = classes.find(c => (c.grade + c.className) === user.homeroomClass);
442
+ if (matchedClass) {
443
+ const teacherIds = (matchedClass.homeroomTeacherIds || []).filter(id => id !== userId);
444
+ const teachers = await User.find({ _id: { $in: teacherIds } });
445
+ const names = teachers.map(t => t.trueName || t.username).join(', ');
446
+ await ClassModel.findByIdAndUpdate(matchedClass._id, { homeroomTeacherIds: teacherIds, teacherName: names });
 
 
 
 
 
 
 
 
447
  }
 
448
  }
449
+ await User.findByIdAndUpdate(userId, updates);
450
+ } else await User.findByIdAndUpdate(userId, { 'classApplication.status': 'REJECTED' });
451
+ return res.json({ success: true });
 
 
 
 
 
 
 
 
 
 
 
452
  }
453
+ res.status(403).json({ error: 'Permission denied' });
454
  });
455
+ app.post('/api/students/promote', async (req, res) => {
456
+ const { teacherFollows } = req.body;
457
+ const sId = req.headers['x-school-id'];
458
+ const GRADE_MAP = { '一年级': '二年级', '二年级': '三年级', '三年级': '四年级', '四年级': '五年级', '五年级': '六年级', '六年级': '毕业', '初一': '初二', '七年级': '八年级', '初二': '初三', '八年级': '九年级', '初三': '毕业', '九年级': '毕业', '高一': '高二', '高二': '高三', '高三': '毕业' };
459
+ const classes = await ClassModel.find(getQueryFilter(req));
460
+ let promotedCount = 0;
461
+ for (const cls of classes) {
462
+ const currentGrade = cls.grade;
463
+ const nextGrade = GRADE_MAP[currentGrade] || currentGrade;
464
+ const suffix = cls.className;
465
+ if (nextGrade === '毕业') {
466
+ const oldFullClass = cls.grade + cls.className;
467
+ await Student.updateMany({ className: oldFullClass, ...getQueryFilter(req) }, { status: 'Graduated', className: '已毕业' });
468
+ if (teacherFollows && cls.homeroomTeacherIds?.length) { await User.updateMany({ _id: { $in: cls.homeroomTeacherIds }, schoolId: sId }, { homeroomClass: '' }); await ClassModel.findByIdAndUpdate(cls._id, { teacherName: '', homeroomTeacherIds: [] }); }
469
+ } else {
470
+ const oldFullClass = cls.grade + cls.className;
471
+ const newFullClass = nextGrade + suffix;
472
+ 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 });
473
+ const result = await Student.updateMany({ className: oldFullClass, status: 'Enrolled', ...getQueryFilter(req) }, { className: newFullClass });
474
+ promotedCount += result.modifiedCount;
475
+ 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: [] }); }
476
+ }
 
 
 
 
 
 
 
 
 
 
477
  }
478
+ res.json({ success: true, count: promotedCount });
479
+ });
480
+ app.post('/api/games/lucky-config', async (req, res) => {
481
+ const data = injectSchoolId(req, req.body);
482
+ if (req.headers['x-user-role'] === 'TEACHER') {
483
+ const user = await User.findOne({ username: req.headers['x-user-username'] });
484
+ data.ownerId = user ? user._id.toString() : null;
485
+ }
486
+ await LuckyDrawConfigModel.findOneAndUpdate({ className: data.className, ...getQueryFilter(req), ownerId: data.ownerId }, data, { upsert: true });
487
+ res.json({ success: true });
488
+ });
489
+ app.post('/api/games/lucky-draw', async (req, res) => {
490
+ const { studentId } = req.body;
491
+ const schoolId = req.headers['x-school-id'];
492
+ const userRole = req.headers['x-user-role'];
493
+ try {
494
+ const student = await Student.findById(studentId);
495
+ if (!student) return res.status(404).json({ error: 'Student not found' });
496
+ let configFilter = { className: student.className, schoolId };
497
+ if (userRole === 'TEACHER') {
498
+ const user = await User.findOne({ username: req.headers['x-user-username'] });
499
+ configFilter.ownerId = user ? user._id.toString() : null;
500
+ }
501
+ const config = await LuckyDrawConfigModel.findOne(configFilter);
502
+ const prizes = config?.prizes || [];
503
+ const defaultPrize = config?.defaultPrize || '再接再厉';
504
+ const dailyLimit = config?.dailyLimit || 3;
505
+ const consolationWeight = config?.consolationWeight || 0;
506
+ const availablePrizes = prizes.filter(p => (p.count === undefined || p.count > 0));
507
+ if (availablePrizes.length === 0 && consolationWeight === 0) return res.status(400).json({ error: 'POOL_EMPTY', message: '奖品库存不足' });
508
+ if (userRole === 'STUDENT') {
509
+ if (student.drawAttempts <= 0) return res.status(403).json({ error: '次数不足', message: '您的抽奖次数已用完' });
510
+ const today = new Date().toISOString().split('T')[0];
511
+ let dailyLog = student.dailyDrawLog || { date: today, count: 0 };
512
+ if (dailyLog.date !== today) dailyLog = { date: today, count: 0 };
513
+ if (dailyLog.count >= dailyLimit) return res.status(403).json({ error: 'DAILY_LIMIT_REACHED', message: `今日抽奖次数已达上限 (${dailyLimit}次)` });
514
+ dailyLog.count += 1;
515
+ student.drawAttempts -= 1;
516
+ student.dailyDrawLog = dailyLog;
517
+ await student.save();
518
+ } else {
519
+ if (student.drawAttempts <= 0) return res.status(403).json({ error: '次数不足', message: '该学生抽奖次数已用完' });
520
+ student.drawAttempts -= 1;
521
+ await student.save();
522
+ }
523
+ let totalWeight = consolationWeight;
524
+ availablePrizes.forEach(p => totalWeight += (p.probability || 0));
525
+ let random = Math.random() * totalWeight;
526
+ let selectedPrize = defaultPrize;
527
+ let rewardType = 'CONSOLATION';
528
+ let matchedPrize = null;
529
+ for (const p of availablePrizes) {
530
+ random -= (p.probability || 0);
531
+ if (random <= 0) { matchedPrize = p; break; }
532
+ }
533
+ if (matchedPrize) {
534
+ selectedPrize = matchedPrize.name;
535
+ rewardType = 'ITEM';
536
+ if (config._id) await LuckyDrawConfigModel.updateOne({ _id: config._id, "prizes.id": matchedPrize.id }, { $inc: { "prizes.$.count": -1 } });
537
+ }
538
+ let ownerId = config?.ownerId;
539
+ await StudentRewardModel.create({ schoolId, studentId, studentName: student.name, rewardType, name: selectedPrize, count: 1, status: 'PENDING', source: '幸运大抽奖', ownerId });
540
+ res.json({ prize: selectedPrize, rewardType });
541
+ } catch (e) { res.status(500).json({ error: e.message }); }
542
  });
 
 
543
  app.get('/api/games/monster-config', async (req, res) => {
544
+ const filter = getQueryFilter(req);
545
+ const ownerFilter = await getGameOwnerFilter(req);
546
+ if (req.query.className) filter.className = req.query.className;
547
+ Object.assign(filter, ownerFilter);
548
+ const config = await GameMonsterConfigModel.findOne(filter);
549
+ res.json(config || {});
550
  });
551
  app.post('/api/games/monster-config', async (req, res) => {
552
  const data = injectSchoolId(req, req.body);
553
+ if (req.headers['x-user-role'] === 'TEACHER') {
554
+ const user = await User.findOne({ username: req.headers['x-user-username'] });
555
+ data.ownerId = user ? user._id.toString() : null;
556
+ }
557
+ await GameMonsterConfigModel.findOneAndUpdate({ className: data.className, ...getQueryFilter(req), ownerId: data.ownerId }, data, { upsert: true });
558
  res.json({ success: true });
559
  });
560
  app.get('/api/games/zen-config', async (req, res) => {
561
+ const filter = getQueryFilter(req);
562
+ const ownerFilter = await getGameOwnerFilter(req);
563
+ if (req.query.className) filter.className = req.query.className;
564
+ Object.assign(filter, ownerFilter);
565
+ const config = await GameZenConfigModel.findOne(filter);
566
+ res.json(config || {});
567
  });
568
  app.post('/api/games/zen-config', async (req, res) => {
569
  const data = injectSchoolId(req, req.body);
570
+ if (req.headers['x-user-role'] === 'TEACHER') {
571
+ const user = await User.findOne({ username: req.headers['x-user-username'] });
572
+ data.ownerId = user ? user._id.toString() : null;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
573
  }
574
+ await GameZenConfigModel.findOneAndUpdate({ className: data.className, ...getQueryFilter(req), ownerId: data.ownerId }, data, { upsert: true });
575
  res.json({ success: true });
576
  });
577
+ app.get('/api/games/mountain', async (req, res) => { res.json(await GameSessionModel.findOne({...getQueryFilter(req), className: req.query.className})); });
578
+ app.post('/api/games/mountain', async (req, res) => {
579
+ const { className } = req.body;
580
+ const sId = req.headers['x-school-id'];
581
+ const role = req.headers['x-user-role'];
582
  const username = req.headers['x-user-username'];
583
+ if (role === 'TEACHER') {
584
  const user = await User.findOne({ username });
585
+ const cls = await ClassModel.findOne({ schoolId: sId, $expr: { $eq: [{ $concat: ["$grade", "$className"] }, className] } });
586
+ if (!cls) return res.status(404).json({ error: 'Class not found' });
587
+ const allowedIds = cls.homeroomTeacherIds || [];
588
+ if (!allowedIds.includes(user._id.toString())) return res.status(403).json({ error: 'PERMISSION_DENIED', message: '只有班主任可以操作登峰游戏' });
 
 
 
 
 
589
  }
590
+ await GameSessionModel.findOneAndUpdate({ className, ...getQueryFilter(req) }, injectSchoolId(req, req.body), {upsert:true});
591
+ res.json({});
592
  });
593
+ app.get('/api/rewards', async (req, res) => {
594
+ const filter = getQueryFilter(req);
595
+ if (req.headers['x-user-role'] === 'TEACHER') {
596
+ const user = await User.findOne({ username: req.headers['x-user-username'] });
597
+ if (user) filter.ownerId = user._id.toString();
 
 
 
 
598
  }
599
+ if(req.query.studentId) filter.studentId = req.query.studentId;
600
+ if (req.query.className) {
601
+ const classStudents = await Student.find({ className: req.query.className, ...getQueryFilter(req) }, '_id');
602
+ filter.studentId = { $in: classStudents.map(s => s._id.toString()) };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
603
  }
 
 
 
 
 
 
 
 
 
604
  if (req.query.excludeType) filter.rewardType = { $ne: req.query.excludeType };
605
+ const page = parseInt(req.query.page) || 1;
606
+ const limit = parseInt(req.query.limit) || 20;
 
 
 
 
 
607
  const skip = (page - 1) * limit;
608
  const total = await StudentRewardModel.countDocuments(filter);
609
+ const list = await StudentRewardModel.find(filter).sort({createTime:-1}).skip(skip).limit(limit);
610
+ res.json({ list, total });
611
  });
612
+ app.post('/api/rewards', async (req, res) => {
613
  const data = injectSchoolId(req, req.body);
614
+ if (!data.count) data.count = 1;
615
+ if (req.headers['x-user-role'] === 'TEACHER') {
616
+ const user = await User.findOne({ username: req.headers['x-user-username'] });
617
+ data.ownerId = user ? user._id.toString() : null;
618
+ }
619
+ if(data.rewardType==='DRAW_COUNT') { data.status='REDEEMED'; await Student.findByIdAndUpdate(data.studentId, {$inc:{drawAttempts:data.count}}); }
620
+ await StudentRewardModel.create(data);
621
+ res.json({});
622
+ });
623
+ app.post('/api/games/grant-reward', async (req, res) => {
624
+ const { studentId, count, rewardType, name } = req.body;
625
+ const finalCount = count || 1;
626
+ const finalName = name || (rewardType === 'DRAW_COUNT' ? '抽奖券' : '奖品');
627
+ let ownerId = null;
628
+ if (req.headers['x-user-role'] === 'TEACHER') {
629
+ const user = await User.findOne({ username: req.headers['x-user-username'] });
630
+ ownerId = user ? user._id.toString() : null;
631
+ }
632
+ if (rewardType === 'DRAW_COUNT') await Student.findByIdAndUpdate(studentId, { $inc: { drawAttempts: finalCount } });
633
+ 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 });
634
+ res.json({});
635
  });
636
+ app.put('/api/classes/:id', async (req, res) => {
637
+ const classId = req.params.id;
638
+ const { grade, className, teacherName, homeroomTeacherIds } = req.body;
639
+ const sId = req.headers['x-school-id'];
640
+ const oldClass = await ClassModel.findById(classId);
641
+ if (!oldClass) return res.status(404).json({ error: 'Class not found' });
642
+ const newFullClass = grade + className;
643
+ const oldFullClass = oldClass.grade + oldClass.className;
644
+ const oldTeacherIds = oldClass.homeroomTeacherIds || [];
645
+ const newTeacherIds = homeroomTeacherIds || [];
646
+ const removedIds = oldTeacherIds.filter(id => !newTeacherIds.includes(id));
647
+ if (removedIds.length > 0) await User.updateMany({ _id: { $in: removedIds }, schoolId: sId }, { homeroomClass: '' });
648
+ if (newTeacherIds.length > 0) await User.updateMany({ _id: { $in: newTeacherIds }, schoolId: sId }, { homeroomClass: newFullClass });
649
+ let displayTeacherName = teacherName;
650
+ if (newTeacherIds.length > 0) {
651
+ const teachers = await User.find({ _id: { $in: newTeacherIds } });
652
+ displayTeacherName = teachers.map(t => t.trueName || t.username).join(', ');
653
+ }
654
+ await ClassModel.findByIdAndUpdate(classId, { grade, className, teacherName: displayTeacherName, homeroomTeacherIds: newTeacherIds });
655
+ if (oldFullClass !== newFullClass) {
656
+ await Student.updateMany({ className: oldFullClass, schoolId: sId }, { className: newFullClass });
657
+ await User.updateMany({ homeroomClass: oldFullClass, schoolId: sId }, { homeroomClass: newFullClass });
658
+ }
659
  res.json({ success: true });
660
  });
661
+ app.post('/api/classes', async (req, res) => {
662
+ const data = injectSchoolId(req, req.body);
663
+ const { homeroomTeacherIds } = req.body;
664
+ if (homeroomTeacherIds && homeroomTeacherIds.length > 0) {
665
+ const teachers = await User.find({ _id: { $in: homeroomTeacherIds } });
666
+ data.teacherName = teachers.map(t => t.trueName || t.username).join(', ');
667
+ }
668
+ await ClassModel.create(data);
669
+ if (homeroomTeacherIds && homeroomTeacherIds.length > 0) await User.updateMany({ _id: { $in: homeroomTeacherIds }, schoolId: data.schoolId }, { homeroomClass: data.grade + data.className });
670
+ res.json({});
671
  });
672
+ app.get('/api/courses', async (req, res) => {
 
 
673
  const filter = getQueryFilter(req);
674
+ if (req.query.teacherId) filter.teacherId = req.query.teacherId;
675
+ res.json(await Course.find(filter));
 
 
 
 
 
 
676
  });
677
+ app.post('/api/courses', async (req, res) => {
 
678
  const data = injectSchoolId(req, req.body);
679
+ 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 }); }
 
 
 
 
 
 
 
 
680
  });
681
+ app.get('/api/public/schools', async (req, res) => { res.json(await School.find({}, 'name code _id')); });
682
+ app.get('/api/public/config', async (req, res) => {
683
+ const currentSem = getAutoSemester();
684
+ let config = await ConfigModel.findOne({ key: 'main' });
685
+ if (config) {
686
+ let semesters = config.semesters || [];
687
+ if (!semesters.includes(currentSem)) { semesters.unshift(currentSem); config.semesters = semesters; config.semester = currentSem; await ConfigModel.updateOne({ key: 'main' }, { semesters, semester: currentSem }); }
688
+ } else { config = { key: 'main', allowRegister: true, semester: currentSem, semesters: [currentSem] }; }
689
+ res.json(config);
690
  });
691
+ 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 }) }); });
692
+ app.post('/api/auth/login', async (req, res) => {
693
+ const { username, password } = req.body;
694
+ const user = await User.findOne({ username, password });
695
+ if (!user) return res.status(401).json({ message: 'Error' });
696
+ if (user.status !== 'active') return res.status(403).json({ error: 'PENDING_APPROVAL' });
697
+ res.json(user);
698
+ });
699
+ app.get('/api/schools', async (req, res) => { res.json(await School.find()); });
700
+ app.post('/api/schools', async (req, res) => { res.json(await School.create(req.body)); });
701
+ app.put('/api/schools/:id', async (req, res) => { await School.findByIdAndUpdate(req.params.id, req.body); res.json({}); });
702
+ app.delete('/api/schools/:id', async (req, res) => {
703
+ const schoolId = req.params.id;
704
+ try { await School.findByIdAndDelete(schoolId); await User.deleteMany({ schoolId }); await Student.deleteMany({ schoolId }); await ClassModel.deleteMany({ schoolId }); await SubjectModel.deleteMany({ schoolId }); await Course.deleteMany({ schoolId }); await Score.deleteMany({ schoolId }); await ExamModel.deleteMany({ schoolId }); await ScheduleModel.deleteMany({ schoolId }); await NotificationModel.deleteMany({ schoolId }); await AttendanceModel.deleteMany({ schoolId }); await LeaveRequestModel.deleteMany({ schoolId }); await GameSessionModel.deleteMany({ schoolId }); await StudentRewardModel.deleteMany({ schoolId }); await LuckyDrawConfigModel.deleteMany({ schoolId }); await GameMonsterConfigModel.deleteMany({ schoolId }); await GameZenConfigModel.deleteMany({ schoolId }); await AchievementConfigModel.deleteMany({ schoolId }); await StudentAchievementModel.deleteMany({ schoolId }); await SchoolCalendarModel.deleteMany({ schoolId }); res.json({ success: true }); } catch (e) { res.status(500).json({ error: e.message }); }
705
+ });
706
+ app.delete('/api/users/:id', async (req, res) => {
707
+ const requesterRole = req.headers['x-user-role'];
708
+ if (requesterRole === 'PRINCIPAL') {
709
+ const user = await User.findById(req.params.id);
710
+ if (!user || user.schoolId !== req.headers['x-school-id']) return res.status(403).json({error: 'Permission denied'});
711
+ if (user.role === 'ADMIN') return res.status(403).json({error: 'Cannot delete admin'});
712
  }
713
+ await User.findByIdAndDelete(req.params.id); res.json({});
 
 
 
 
 
 
 
714
  });
715
+ app.get('/api/students', async (req, res) => { res.json(await Student.find(getQueryFilter(req))); });
716
+ app.post('/api/students', async (req, res) => {
717
+ const data = injectSchoolId(req, req.body);
718
+ if (data.studentNo === '') delete data.studentNo;
719
+ try {
720
+ const existing = await Student.findOne({ schoolId: data.schoolId, name: data.name, className: data.className });
721
+ 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); }
722
+ res.json({ success: true });
723
+ } catch (e) { if (e.code === 11000) return res.status(409).json({ error: 'DUPLICATE', message: '保存失败:存在重复的系统ID' }); res.status(500).json({ error: e.message }); }
724
+ });
725
+ app.put('/api/students/:id', async (req, res) => { await Student.findByIdAndUpdate(req.params.id, req.body); res.json({}); });
726
+ app.delete('/api/students/:id', async (req, res) => { await Student.findByIdAndDelete(req.params.id); res.json({}); });
727
+ app.get('/api/classes', async (req, res) => {
728
  const filter = getQueryFilter(req);
729
+ const cls = await ClassModel.find(filter);
730
+ const resData = await Promise.all(cls.map(async c => {
731
+ const count = await Student.countDocuments({ className: c.grade + c.className, status: 'Enrolled', ...filter });
732
+ return { ...c.toObject(), studentCount: count };
733
+ }));
734
+ res.json(resData);
735
+ });
736
+ app.delete('/api/classes/:id', async (req, res) => {
737
+ const cls = await ClassModel.findById(req.params.id);
738
+ if (cls && cls.homeroomTeacherIds) await User.updateMany({ _id: { $in: cls.homeroomTeacherIds }, schoolId: cls.schoolId }, { homeroomClass: '' });
739
+ await ClassModel.findByIdAndDelete(req.params.id); res.json({});
740
+ });
741
+ app.get('/api/subjects', async (req, res) => { res.json(await SubjectModel.find(getQueryFilter(req))); });
742
+ app.post('/api/subjects', async (req, res) => { await SubjectModel.create(injectSchoolId(req, req.body)); res.json({}); });
743
+ app.put('/api/subjects/:id', async (req, res) => { await SubjectModel.findByIdAndUpdate(req.params.id, req.body); res.json({}); });
744
+ app.delete('/api/subjects/:id', async (req, res) => { await SubjectModel.findByIdAndDelete(req.params.id); res.json({}); });
745
+ app.put('/api/courses/:id', async (req, res) => { await Course.findByIdAndUpdate(req.params.id, req.body); res.json({}); });
746
+ app.delete('/api/courses/:id', async (req, res) => { await Course.findByIdAndDelete(req.params.id); res.json({}); });
747
+ app.get('/api/scores', async (req, res) => { res.json(await Score.find(getQueryFilter(req))); });
748
+ app.post('/api/scores', async (req, res) => { await Score.create(injectSchoolId(req, req.body)); res.json({}); });
749
+ app.put('/api/scores/:id', async (req, res) => { await Score.findByIdAndUpdate(req.params.id, req.body); res.json({}); });
750
+ app.delete('/api/scores/:id', async (req, res) => { await Score.findByIdAndDelete(req.params.id); res.json({}); });
751
+ app.get('/api/exams', async (req, res) => { res.json(await ExamModel.find(getQueryFilter(req))); });
752
+ app.post('/api/exams', async (req, res) => { await ExamModel.findOneAndUpdate({name:req.body.name}, injectSchoolId(req, req.body), {upsert:true}); res.json({}); });
753
+ app.get('/api/schedules', async (req, res) => {
754
+ const query = { ...getQueryFilter(req), ...req.query };
755
+ if (query.grade) { query.className = { $regex: '^' + query.grade }; delete query.grade; }
756
+ res.json(await ScheduleModel.find(query));
757
+ });
758
+ app.post('/api/schedules', async (req, res) => {
759
+ const filter = { className:req.body.className, dayOfWeek:req.body.dayOfWeek, period:req.body.period };
760
+ const sId = req.headers['x-school-id'];
761
+ if(sId) filter.schoolId = sId;
762
+ await ScheduleModel.findOneAndUpdate(filter, injectSchoolId(req, req.body), {upsert:true}); res.json({});
763
+ });
764
+ app.delete('/api/schedules', async (req, res) => { await ScheduleModel.deleteOne({...getQueryFilter(req), ...req.query}); res.json({}); });
765
+ app.get('/api/stats', async (req, res) => {
766
+ const filter = getQueryFilter(req);
767
+ const studentCount = await Student.countDocuments(filter);
768
+ const courseCount = await Course.countDocuments(filter);
769
+ const scores = await Score.find({...filter, status: 'Normal'});
770
+ let avgScore = 0; let excellentRate = '0%';
771
+ if (scores.length > 0) {
772
+ const total = scores.reduce((sum, s) => sum + s.score, 0);
773
+ avgScore = parseFloat((total / scores.length).toFixed(1));
774
+ const excellent = scores.filter(s => s.score >= 90).length;
775
+ excellentRate = Math.round((excellent / scores.length) * 100) + '%';
776
  }
777
+ res.json({ studentCount, courseCount, avgScore, excellentRate });
 
 
778
  });
779
+ app.get('/api/config', async (req, res) => {
780
+ const currentSem = getAutoSemester();
781
+ let config = await ConfigModel.findOne({key:'main'});
782
+ 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] }; }
783
+ res.json(config);
784
+ });
785
+ app.post('/api/config', async (req, res) => { await ConfigModel.findOneAndUpdate({key:'main'}, req.body, {upsert:true}); res.json({}); });
786
+ app.put('/api/rewards/:id', async (req, res) => { await StudentRewardModel.findByIdAndUpdate(req.params.id, req.body); res.json({}); });
787
+ app.delete('/api/rewards/:id', async (req, res) => {
788
+ const reward = await StudentRewardModel.findById(req.params.id);
789
+ if (!reward) return res.status(404).json({error: 'Not found'});
790
+ if (reward.rewardType === 'DRAW_COUNT') {
791
+ const student = await Student.findById(reward.studentId);
792
+ if (student && student.drawAttempts < reward.count) return res.status(400).json({ error: 'FAILED_REVOKE', message: '修改失败,次数已被使用' });
793
+ await Student.findByIdAndUpdate(reward.studentId, { $inc: { drawAttempts: -reward.count } });
794
+ }
795
+ await StudentRewardModel.findByIdAndDelete(req.params.id); res.json({});
796
+ });
797
+ app.post('/api/rewards/:id/redeem', async (req, res) => { await StudentRewardModel.findByIdAndUpdate(req.params.id, {status:'REDEEMED'}); res.json({}); });
798
+ app.get('/api/attendance', async (req, res) => {
799
+ const { className, date, studentId } = req.query;
800
+ const filter = getQueryFilter(req);
801
+ if(className) filter.className = className;
802
+ if(date) filter.date = date;
803
+ if(studentId) filter.studentId = studentId;
804
+ res.json(await AttendanceModel.find(filter));
805
+ });
806
+ app.post('/api/attendance/check-in', async (req, res) => {
807
+ const { studentId, date, status } = req.body;
808
+ const exists = await AttendanceModel.findOne({ studentId, date });
809
+ if (exists) return res.status(400).json({ error: 'ALREADY_CHECKED_IN', message: '今日已打卡' });
810
+ const student = await Student.findById(studentId);
811
+ await AttendanceModel.create({ schoolId: req.headers['x-school-id'], studentId, studentName: student.name, className: student.className, date, status: status || 'Present', checkInTime: new Date() });
812
  res.json({ success: true });
813
  });
814
+ app.post('/api/attendance/batch', async (req, res) => {
815
+ const { className, date, status } = req.body;
816
+ const students = await Student.find({ className, ...getQueryFilter(req) });
817
+ 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 } }));
818
+ if (ops.length > 0) await AttendanceModel.bulkWrite(ops);
819
+ res.json({ success: true, count: ops.length });
820
+ });
821
+ app.put('/api/attendance/update', async (req, res) => {
822
+ const { studentId, date, status } = req.body;
823
+ const student = await Student.findById(studentId);
824
+ 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 });
825
  res.json({ success: true });
826
  });
827
+ app.post('/api/leave', async (req, res) => {
828
+ await LeaveRequestModel.create(injectSchoolId(req, req.body));
829
+ const { studentId, startDate } = req.body;
830
+ const student = await Student.findById(studentId);
831
+ 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 });
 
 
832
  res.json({ success: true });
833
  });
834
+ app.get('/api/attendance/calendar', async (req, res) => {
835
+ const { className } = req.query;
836
+ const filter = getQueryFilter(req);
837
+ const query = { $and: [ filter, { $or: [{ className: { $exists: false } }, { className: null }, { className }] } ] };
838
+ res.json(await SchoolCalendarModel.find(query));
 
 
 
 
 
 
 
839
  });
840
+ app.post('/api/attendance/calendar', async (req, res) => { await SchoolCalendarModel.create(injectSchoolId(req, req.body)); res.json({ success: true }); });
841
+ app.delete('/api/attendance/calendar/:id', async (req, res) => { await SchoolCalendarModel.findByIdAndDelete(req.params.id); res.json({}); });
842
+ app.post('/api/batch-delete', async (req, res) => {
843
+ if(req.body.type==='student') await Student.deleteMany({_id:{$in:req.body.ids}});
844
+ if(req.body.type==='score') await Score.deleteMany({_id:{$in:req.body.ids}});
845
+ res.json({});
846
  });
847
 
848
+ app.get('*', (req, res) => { res.sendFile(path.join(__dirname, 'dist', 'index.html')); });
849
+ app.listen(PORT, () => console.log(`🚀 Server running on port ${PORT}`));
services/api.ts CHANGED
@@ -1,7 +1,6 @@
1
 
2
- import { User, ClassInfo, SystemConfig, Subject, School, Schedule, GameSession, StudentReward, LuckyDrawConfig, Attendance, LeaveRequest, AchievementConfig, SchoolCalendarEntry, TeacherExchangeConfig, Wish, Feedback } from '../types';
3
 
4
- // ... (existing getBaseUrl and request functions) ...
5
  const getBaseUrl = () => {
6
  let isProd = false;
7
  try {
@@ -20,7 +19,6 @@ const getBaseUrl = () => {
20
  const API_BASE_URL = getBaseUrl();
21
 
22
  async function request(endpoint: string, options: RequestInit = {}) {
23
- // ... (existing request logic) ...
24
  const headers: any = { 'Content-Type': 'application/json', ...options.headers };
25
 
26
  if (typeof window !== 'undefined') {
@@ -50,18 +48,15 @@ async function request(endpoint: string, options: RequestInit = {}) {
50
  if (errorData.error === 'BANNED') throw new Error('BANNED');
51
  if (errorData.error === 'CONFLICT') throw new Error(errorData.message);
52
  if (errorData.error === 'INVALID_PASSWORD') throw new Error('INVALID_PASSWORD');
53
- if (errorData.error === 'LIMIT_REACHED') throw new Error(errorData.message);
54
  throw new Error(errorMessage);
55
  }
56
  return res.json();
57
  }
58
 
59
  export const api = {
60
- // ... (existing methods: init, auth, schools, users, etc.) ...
61
  init: () => console.log('🔗 API:', API_BASE_URL),
62
 
63
  auth: {
64
- // ... existing auth methods
65
  login: async (username: string, password: string): Promise<User> => {
66
  const user = await request('/auth/login', { method: 'POST', body: JSON.stringify({ username, password }) });
67
  if (typeof window !== 'undefined') {
@@ -109,7 +104,7 @@ export const api = {
109
  getAll: () => request('/schools'),
110
  add: (data: School) => request('/schools', { method: 'POST', body: JSON.stringify(data) }),
111
  update: (id: string, data: Partial<School>) => request(`/schools/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
112
- delete: (id: string) => request(`/schools/${id}`, { method: 'DELETE' })
113
  },
114
 
115
  users: {
@@ -131,6 +126,7 @@ export const api = {
131
  add: (data: any) => request('/students', { method: 'POST', body: JSON.stringify(data) }),
132
  update: (id: string, data: any) => request(`/students/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
133
  delete: (id: string | number) => request(`/students/${id}`, { method: 'DELETE' }),
 
134
  promote: (data: { teacherFollows: boolean }) => request('/students/promote', { method: 'POST', body: JSON.stringify(data) }),
135
  transfer: (data: { studentId: string, targetClass: string }) => request('/students/transfer', { method: 'POST', body: JSON.stringify(data) })
136
  },
@@ -229,6 +225,7 @@ export const api = {
229
  getStudentAchievements: (studentId: string, semester?: string) => request(`/achievements/student?studentId=${studentId}${semester ? `&semester=${semester}` : ''}`),
230
  grant: (data: { studentId: string, achievementId: string, semester: string }) => request('/achievements/grant', { method: 'POST', body: JSON.stringify(data) }),
231
  exchange: (data: { studentId: string, ruleId: string, teacherId?: string }) => request('/achievements/exchange', { method: 'POST', body: JSON.stringify(data) }),
 
232
  getMyRules: () => request('/achievements/teacher-rules'),
233
  saveMyRules: (data: TeacherExchangeConfig) => request('/achievements/teacher-rules', { method: 'POST', body: JSON.stringify(data) }),
234
  getRulesByTeachers: (teacherIds: string[]) => request(`/achievements/teacher-rules?teacherIds=${teacherIds.join(',')}`),
@@ -247,23 +244,6 @@ export const api = {
247
  redeem: (id: string) => request(`/rewards/${id}/redeem`, { method: 'POST' }),
248
  },
249
 
250
- wishes: {
251
- getMyWishes: (studentId: string) => request(`/wishes?studentId=${studentId}`),
252
- getTeacherWishes: (teacherId: string) => request(`/wishes?teacherId=${teacherId}`),
253
- submit: (data: Partial<Wish>) => request('/wishes', { method: 'POST', body: JSON.stringify(data) }),
254
- fulfill: (id: string) => request(`/wishes/${id}/fulfill`, { method: 'POST' }),
255
- randomFulfill: (teacherId: string) => request('/wishes/random-fulfill', { method: 'POST', body: JSON.stringify({ teacherId }) }),
256
- },
257
-
258
- feedback: {
259
- getSent: (senderId: string) => request(`/feedback?senderId=${senderId}`),
260
- getReceived: (targetId: string) => request(`/feedback?targetId=${targetId}`),
261
- getAdmin: () => request('/feedback?role=ADMIN'), // Special param to indicate admin request
262
- submit: (data: Partial<Feedback>) => request('/feedback', { method: 'POST', body: JSON.stringify(data) }),
263
- updateStatus: (id: string, status: string) => request(`/feedback/${id}/status`, { method: 'PUT', body: JSON.stringify({ status }) }),
264
- batchIgnore: (targetId: string) => request('/feedback/batch-ignore', { method: 'POST', body: JSON.stringify({ targetId }) }),
265
- },
266
-
267
  batchDelete: (type: 'student' | 'score' | 'user', ids: string[]) => {
268
  return request('/batch-delete', { method: 'POST', body: JSON.stringify({ type, ids }) });
269
  }
 
1
 
2
+ import { User, ClassInfo, SystemConfig, Subject, School, Schedule, GameSession, StudentReward, LuckyDrawConfig, Attendance, LeaveRequest, AchievementConfig, SchoolCalendarEntry, TeacherExchangeConfig } from '../types';
3
 
 
4
  const getBaseUrl = () => {
5
  let isProd = false;
6
  try {
 
19
  const API_BASE_URL = getBaseUrl();
20
 
21
  async function request(endpoint: string, options: RequestInit = {}) {
 
22
  const headers: any = { 'Content-Type': 'application/json', ...options.headers };
23
 
24
  if (typeof window !== 'undefined') {
 
48
  if (errorData.error === 'BANNED') throw new Error('BANNED');
49
  if (errorData.error === 'CONFLICT') throw new Error(errorData.message);
50
  if (errorData.error === 'INVALID_PASSWORD') throw new Error('INVALID_PASSWORD');
 
51
  throw new Error(errorMessage);
52
  }
53
  return res.json();
54
  }
55
 
56
  export const api = {
 
57
  init: () => console.log('🔗 API:', API_BASE_URL),
58
 
59
  auth: {
 
60
  login: async (username: string, password: string): Promise<User> => {
61
  const user = await request('/auth/login', { method: 'POST', body: JSON.stringify({ username, password }) });
62
  if (typeof window !== 'undefined') {
 
104
  getAll: () => request('/schools'),
105
  add: (data: School) => request('/schools', { method: 'POST', body: JSON.stringify(data) }),
106
  update: (id: string, data: Partial<School>) => request(`/schools/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
107
+ delete: (id: string) => request(`/schools/${id}`, { method: 'DELETE' }) // NEW
108
  },
109
 
110
  users: {
 
126
  add: (data: any) => request('/students', { method: 'POST', body: JSON.stringify(data) }),
127
  update: (id: string, data: any) => request(`/students/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
128
  delete: (id: string | number) => request(`/students/${id}`, { method: 'DELETE' }),
129
+ // NEW: Promote and Transfer
130
  promote: (data: { teacherFollows: boolean }) => request('/students/promote', { method: 'POST', body: JSON.stringify(data) }),
131
  transfer: (data: { studentId: string, targetClass: string }) => request('/students/transfer', { method: 'POST', body: JSON.stringify(data) })
132
  },
 
225
  getStudentAchievements: (studentId: string, semester?: string) => request(`/achievements/student?studentId=${studentId}${semester ? `&semester=${semester}` : ''}`),
226
  grant: (data: { studentId: string, achievementId: string, semester: string }) => request('/achievements/grant', { method: 'POST', body: JSON.stringify(data) }),
227
  exchange: (data: { studentId: string, ruleId: string, teacherId?: string }) => request('/achievements/exchange', { method: 'POST', body: JSON.stringify(data) }),
228
+ // Teacher Exchange Rules
229
  getMyRules: () => request('/achievements/teacher-rules'),
230
  saveMyRules: (data: TeacherExchangeConfig) => request('/achievements/teacher-rules', { method: 'POST', body: JSON.stringify(data) }),
231
  getRulesByTeachers: (teacherIds: string[]) => request(`/achievements/teacher-rules?teacherIds=${teacherIds.join(',')}`),
 
244
  redeem: (id: string) => request(`/rewards/${id}/redeem`, { method: 'POST' }),
245
  },
246
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
247
  batchDelete: (type: 'student' | 'score' | 'user', ids: string[]) => {
248
  return request('/batch-delete', { method: 'POST', body: JSON.stringify({ type, ids }) });
249
  }
types.ts CHANGED
@@ -1,7 +1,49 @@
1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  export enum UserRole {
3
  ADMIN = 'ADMIN',
4
- PRINCIPAL = 'PRINCIPAL',
5
  TEACHER = 'TEACHER',
6
  STUDENT = 'STUDENT',
7
  USER = 'USER'
@@ -13,301 +55,308 @@ export enum UserStatus {
13
  BANNED = 'banned'
14
  }
15
 
 
 
 
 
 
 
 
16
  export interface User {
 
17
  _id?: string;
18
- id?: string | number;
19
  username: string;
20
- password?: string;
21
- role: UserRole;
22
- status: UserStatus;
23
- schoolId?: string;
24
  trueName?: string;
25
  phone?: string;
26
  email?: string;
 
 
 
27
  avatar?: string;
28
- teachingSubject?: string;
29
- homeroomClass?: string;
 
 
30
  classApplication?: {
31
  type: 'CLAIM' | 'RESIGN';
32
- targetClass: string;
33
- status: 'PENDING' | 'APPROVED' | 'REJECTED';
34
  };
 
35
  studentNo?: string;
36
  parentName?: string;
37
  parentPhone?: string;
 
 
 
 
38
  }
39
 
40
- export interface School {
 
41
  _id?: string;
42
- id?: string | number;
 
 
 
 
 
 
 
 
 
 
 
43
  name: string;
44
  code: string;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  }
46
 
47
  export interface Student {
 
48
  _id?: string;
49
- id?: string | number;
50
- studentNo: string;
 
51
  name: string;
52
- gender: 'Male' | 'Female';
 
 
 
53
  className: string;
54
- seatNo?: string;
55
- phone?: string;
56
- birthday?: string;
57
- idCard?: string;
58
- status: 'Enrolled' | 'Graduated' | 'Left';
59
  parentName?: string;
60
  parentPhone?: string;
61
  address?: string;
62
- drawAttempts?: number;
63
- flowerBalance?: number;
 
 
 
 
64
  }
65
 
66
  export interface Course {
 
67
  _id?: string;
68
- id?: string | number;
69
- courseCode?: string;
70
- courseName: string;
71
- teacherId?: string;
72
  teacherName: string;
73
- className: string;
74
  credits: number;
75
  capacity: number;
76
- enrolled?: number;
77
  }
78
 
79
  export type ExamStatus = 'Normal' | 'Absent' | 'Leave' | 'Cheat';
80
 
81
  export interface Score {
 
82
  _id?: string;
83
- id?: string | number;
84
- studentId?: string;
85
  studentName: string;
86
  studentNo: string;
87
  courseName: string;
88
  score: number;
89
  semester: string;
90
- type: string;
91
  examName?: string;
92
  status?: ExamStatus;
93
  }
94
 
95
- export interface ClassInfo {
96
- _id?: string;
97
- id?: string | number;
98
- grade: string;
99
- className: string;
100
- homeroomTeacherIds?: string[];
101
- teacherName?: string;
102
- studentCount?: number;
103
- }
104
-
105
- export interface Subject {
106
  _id?: string;
107
- id?: string | number;
108
  name: string;
109
- code?: string;
110
- color?: string;
111
- excellenceThreshold?: number;
112
- thresholds?: Record<string, number>;
113
  }
114
 
115
  export interface Schedule {
 
116
  _id?: string;
 
117
  className: string;
 
 
118
  dayOfWeek: number;
119
  period: number;
120
- subject: string;
121
- teacherName: string;
122
  }
123
 
124
  export interface Notification {
 
125
  _id?: string;
 
 
 
126
  title: string;
127
  content: string;
128
  type: 'info' | 'success' | 'warning' | 'error';
 
129
  createTime: string;
130
- targetRole?: string;
131
- targetUserId?: string;
132
  }
133
 
134
- export interface SystemConfig {
135
- systemName?: string;
136
- allowRegister?: boolean;
137
- allowAdminRegister?: boolean;
138
- allowStudentRegister?: boolean;
139
- allowPrincipalRegister?: boolean;
140
- maintenanceMode?: boolean;
141
- emailNotify?: boolean;
142
- semester?: string;
143
- semesters?: string[];
144
  }
145
 
146
- export interface Exam {
147
- _id?: string;
148
- name?: string;
149
- date?: string;
150
- type?: string;
151
- }
152
-
153
- export interface GameRewardConfig {
154
- scoreThreshold: number;
155
- rewardType: 'DRAW_COUNT' | 'ITEM' | 'ACHIEVEMENT';
156
- rewardName: string;
157
- rewardValue: number;
158
- achievementId?: string;
159
- }
160
 
161
  export interface GameTeam {
162
  id: string;
163
  name: string;
164
- color: string;
165
- avatar: string;
166
  score: number;
167
- members: string[];
 
 
 
 
 
 
 
 
 
 
168
  }
169
 
170
  export interface GameSession {
171
  _id?: string;
172
  schoolId: string;
173
- className: string;
174
- subject: string;
175
  isEnabled: boolean;
176
- maxSteps: number;
177
  teams: GameTeam[];
178
  rewardsConfig: GameRewardConfig[];
 
179
  }
180
 
181
- export interface LuckyPrize {
182
- id: string;
183
- name: string;
184
- probability: number;
185
- count: number;
186
- }
187
-
188
- export interface LuckyDrawConfig {
189
- _id?: string;
190
- schoolId: string;
191
- className: string;
192
- ownerId?: string;
193
- prizes: LuckyPrize[];
194
- dailyLimit?: number;
195
- cardCount?: number;
196
- defaultPrize?: string;
197
- consolationWeight?: number;
198
  }
199
 
200
  export interface StudentReward {
201
  _id?: string;
202
- schoolId: string;
203
- studentId: string;
204
  studentName: string;
205
- rewardType: 'DRAW_COUNT' | 'ITEM' | 'CONSOLATION';
206
  name: string;
207
- count?: number;
208
  status: 'PENDING' | 'REDEEMED';
209
- source?: string;
210
  createTime: string;
 
211
  }
212
 
213
- export interface Attendance {
214
- _id?: string;
215
- studentId: string;
216
- date: string;
217
- status: 'Present' | 'Leave' | 'Absent';
218
- checkInTime?: string;
219
- }
220
-
221
- export interface LeaveRequest {
222
- _id?: string;
223
- studentId: string;
224
- studentName: string;
225
- className: string;
226
- reason: string;
227
- startDate: string;
228
- endDate: string;
229
- status: 'PENDING' | 'APPROVED' | 'REJECTED';
230
- createTime: string;
231
- }
232
-
233
- export interface SchoolCalendarEntry {
234
- _id?: string;
235
- schoolId: string;
236
- className?: string;
237
- name: string;
238
- startDate: string;
239
- endDate: string;
240
- type: 'HOLIDAY' | 'EXAM' | 'EVENT' | 'OFF';
241
- }
242
-
243
- export interface AchievementItem {
244
  id: string;
245
  name: string;
246
- icon: string;
247
- points: number;
248
- description?: string;
249
- addedBy?: string;
250
- addedByName?: string;
251
  }
252
 
253
- export interface ExchangeRule {
254
- id: string;
255
- cost: number;
256
- rewardType: 'DRAW_COUNT' | 'ITEM';
257
- rewardName: string;
258
- rewardValue: number;
 
 
 
 
259
  }
260
 
261
- export interface AchievementConfig {
262
  _id?: string;
263
  schoolId: string;
264
  className: string;
265
- achievements: AchievementItem[];
266
- exchangeRules: ExchangeRule[];
 
 
 
 
 
 
 
 
 
267
  }
268
 
269
- export interface TeacherExchangeConfig {
270
- _id?: string;
271
- teacherId: string;
272
- teacherName?: string;
273
- rules: ExchangeRule[];
274
- }
275
 
276
  export interface StudentAchievement {
277
  _id?: string;
 
278
  studentId: string;
279
- achievementId: string;
280
- achievementName: string;
 
281
  achievementIcon: string;
282
- points: number;
283
  semester: string;
284
  createTime: string;
285
- grantBy?: string;
286
  }
287
 
288
- export interface Wish {
 
 
 
289
  _id?: string;
290
  schoolId: string;
291
  studentId: string;
292
  studentName: string;
293
  className: string;
294
- teacherId: string;
295
- teacherName: string;
296
- content: string;
297
- status: 'PENDING' | 'FULFILLED';
298
- createTime: string;
299
- fulfillTime?: string;
300
  }
301
 
302
- export interface Feedback {
303
  _id?: string;
304
  schoolId: string;
305
- senderId: string;
306
- senderName: string;
307
- senderRole: string;
308
- targetId: string;
309
- targetName: string;
310
- content: string;
311
- status: 'PENDING' | 'ACCEPTED' | 'PROCESSED' | 'IGNORED';
312
  createTime: string;
313
  }
 
 
 
 
 
 
 
 
 
 
 
1
 
2
+ // ... existing imports
3
+
4
+ // ... existing types (User, School, etc.)
5
+
6
+ export interface AchievementItem {
7
+ id: string;
8
+ name: string;
9
+ icon: string;
10
+ points: number;
11
+ description?: string;
12
+ addedBy?: string; // Teacher ID
13
+ addedByName?: string; // Teacher Name
14
+ }
15
+
16
+ export interface ExchangeRule {
17
+ id: string;
18
+ cost: number;
19
+ rewardType: 'ITEM' | 'DRAW_COUNT';
20
+ rewardName: string;
21
+ rewardValue: number;
22
+ }
23
+
24
+ // Class-based Config (Shared Achievement Library)
25
+ export interface AchievementConfig {
26
+ _id?: string;
27
+ schoolId: string;
28
+ className: string;
29
+ achievements: AchievementItem[];
30
+ // exchangeRules is deprecated here, moving to TeacherExchangeConfig
31
+ exchangeRules?: ExchangeRule[];
32
+ }
33
+
34
+ // New: Teacher-based Config (Global Rules for a teacher)
35
+ export interface TeacherExchangeConfig {
36
+ _id?: string;
37
+ schoolId: string;
38
+ teacherId: string;
39
+ teacherName: string;
40
+ rules: ExchangeRule[];
41
+ }
42
+
43
+ // ... rest of the file
44
  export enum UserRole {
45
  ADMIN = 'ADMIN',
46
+ PRINCIPAL = 'PRINCIPAL', // 新增校长角色
47
  TEACHER = 'TEACHER',
48
  STUDENT = 'STUDENT',
49
  USER = 'USER'
 
55
  BANNED = 'banned'
56
  }
57
 
58
+ export interface School {
59
+ id?: number;
60
+ _id?: string;
61
+ name: string;
62
+ code: string;
63
+ }
64
+
65
  export interface User {
66
+ id?: number;
67
  _id?: string;
 
68
  username: string;
 
 
 
 
69
  trueName?: string;
70
  phone?: string;
71
  email?: string;
72
+ schoolId?: string;
73
+ role: UserRole;
74
+ status: UserStatus;
75
  avatar?: string;
76
+ createTime?: string;
77
+ teachingSubject?: string;
78
+ homeroomClass?: string; // 废弃或仅作显示,主要逻辑移至 ClassInfo.homeroomTeacherIds
79
+ // Class Application
80
  classApplication?: {
81
  type: 'CLAIM' | 'RESIGN';
82
+ targetClass?: string;
83
+ status: 'PENDING' | 'REJECTED';
84
  };
85
+ // Student Registration Temp Fields
86
  studentNo?: string;
87
  parentName?: string;
88
  parentPhone?: string;
89
+ address?: string;
90
+ gender?: 'Male' | 'Female';
91
+ seatNo?: string;
92
+ idCard?: string; // NEW
93
  }
94
 
95
+ export interface ClassInfo {
96
+ id?: number;
97
  _id?: string;
98
+ schoolId?: string;
99
+ grade: string;
100
+ className: string;
101
+ teacherName?: string; // Display string (e.g., "张三, 李四")
102
+ homeroomTeacherIds?: string[]; // Actual IDs for logic
103
+ studentCount?: number;
104
+ }
105
+
106
+ export interface Subject {
107
+ id?: number;
108
+ _id?: string;
109
+ schoolId?: string;
110
  name: string;
111
  code: string;
112
+ color: string;
113
+ excellenceThreshold?: number;
114
+ thresholds?: Record<string, number>; // Grade specific overrides e.g. {'一年级': 95}
115
+ }
116
+
117
+ export interface SystemConfig {
118
+ systemName: string;
119
+ semester: string;
120
+ semesters?: string[];
121
+ allowRegister: boolean;
122
+ allowAdminRegister: boolean;
123
+ allowPrincipalRegister?: boolean; // 新增:是否允许校长注册
124
+ allowStudentRegister?: boolean;
125
+ maintenanceMode: boolean;
126
+ emailNotify: boolean;
127
  }
128
 
129
  export interface Student {
130
+ id?: number;
131
  _id?: string;
132
+ schoolId?: string;
133
+ studentNo: string; // System ID (Login ID)
134
+ seatNo?: string; // Class Seat Number (Optional, for sorting/display)
135
  name: string;
136
+ gender: 'Male' | 'Female' | 'Other';
137
+ birthday: string;
138
+ idCard: string;
139
+ phone: string;
140
  className: string;
141
+ status: 'Enrolled' | 'Graduated' | 'Suspended';
 
 
 
 
142
  parentName?: string;
143
  parentPhone?: string;
144
  address?: string;
145
+ // Game related
146
+ teamId?: string;
147
+ drawAttempts?: number;
148
+ dailyDrawLog?: { date: string; count: number };
149
+ // Achievement related
150
+ flowerBalance?: number;
151
  }
152
 
153
  export interface Course {
154
+ id?: number;
155
  _id?: string;
156
+ schoolId?: string;
157
+ courseCode: string;
158
+ courseName: string; // Subject Name
159
+ className: string; // Target Class (e.g. "一年级(1)班")
160
  teacherName: string;
161
+ teacherId?: string; // Optional linkage
162
  credits: number;
163
  capacity: number;
164
+ enrolled: number;
165
  }
166
 
167
  export type ExamStatus = 'Normal' | 'Absent' | 'Leave' | 'Cheat';
168
 
169
  export interface Score {
170
+ id?: number;
171
  _id?: string;
172
+ schoolId?: string;
 
173
  studentName: string;
174
  studentNo: string;
175
  courseName: string;
176
  score: number;
177
  semester: string;
178
+ type: 'Midterm' | 'Final' | 'Quiz';
179
  examName?: string;
180
  status?: ExamStatus;
181
  }
182
 
183
+ export interface Exam {
184
+ id?: number;
 
 
 
 
 
 
 
 
 
185
  _id?: string;
186
+ schoolId?: string;
187
  name: string;
188
+ date: string;
189
+ semester?: string;
190
+ type?: 'Midterm' | 'Final' | 'Quiz' | string;
 
191
  }
192
 
193
  export interface Schedule {
194
+ id?: number;
195
  _id?: string;
196
+ schoolId?: string;
197
  className: string;
198
+ teacherName: string;
199
+ subject: string;
200
  dayOfWeek: number;
201
  period: number;
 
 
202
  }
203
 
204
  export interface Notification {
205
+ id?: number;
206
  _id?: string;
207
+ schoolId?: string;
208
+ targetRole?: UserRole;
209
+ targetUserId?: string;
210
  title: string;
211
  content: string;
212
  type: 'info' | 'success' | 'warning' | 'error';
213
+ isRead?: boolean;
214
  createTime: string;
 
 
215
  }
216
 
217
+ export interface ApiResponse<T> {
218
+ code: number;
219
+ message: string;
220
+ data: T;
221
+ timestamp: number;
 
 
 
 
 
222
  }
223
 
224
+ // --- Game Types ---
 
 
 
 
 
 
 
 
 
 
 
 
 
225
 
226
  export interface GameTeam {
227
  id: string;
228
  name: string;
 
 
229
  score: number;
230
+ avatar: string; // Emoji or URL
231
+ color: string;
232
+ members: string[]; // Student IDs
233
+ }
234
+
235
+ export interface GameRewardConfig {
236
+ scoreThreshold: number;
237
+ rewardType: 'ITEM' | 'DRAW_COUNT' | 'ACHIEVEMENT';
238
+ rewardName: string;
239
+ rewardValue: number;
240
+ achievementId?: string;
241
  }
242
 
243
  export interface GameSession {
244
  _id?: string;
245
  schoolId: string;
246
+ className: string;
247
+ subject: string;
248
  isEnabled: boolean;
 
249
  teams: GameTeam[];
250
  rewardsConfig: GameRewardConfig[];
251
+ maxSteps: number;
252
  }
253
 
254
+ export enum RewardType {
255
+ ITEM = 'ITEM',
256
+ DRAW_COUNT = 'DRAW_COUNT',
257
+ CONSOLATION = 'CONSOLATION',
258
+ ACHIEVEMENT = 'ACHIEVEMENT'
 
 
 
 
 
 
 
 
 
 
 
 
259
  }
260
 
261
  export interface StudentReward {
262
  _id?: string;
263
+ schoolId?: string;
264
+ studentId: string;
265
  studentName: string;
266
+ rewardType: RewardType | string;
267
  name: string;
268
+ count?: number;
269
  status: 'PENDING' | 'REDEEMED';
270
+ source: string;
271
  createTime: string;
272
+ ownerId?: string;
273
  }
274
 
275
+ export interface LuckyPrize {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
276
  id: string;
277
  name: string;
278
+ probability: number; // Treated as Weight
279
+ count: number; // Inventory
280
+ icon?: string;
 
 
281
  }
282
 
283
+ export interface LuckyDrawConfig {
284
+ _id?: string;
285
+ schoolId: string;
286
+ className?: string;
287
+ ownerId?: string; // Isolated by teacher
288
+ prizes: LuckyPrize[];
289
+ dailyLimit: number;
290
+ cardCount?: number;
291
+ defaultPrize: string; // "再接再厉"
292
+ consolationWeight?: number; // Weight for NOT winning a main prize
293
  }
294
 
295
+ export interface GameMonsterConfig {
296
  _id?: string;
297
  schoolId: string;
298
  className: string;
299
+ ownerId?: string; // Isolated by teacher
300
+ duration: number;
301
+ sensitivity: number;
302
+ difficulty: number;
303
+ useKeyboardMode: boolean;
304
+ rewardConfig: {
305
+ enabled: boolean;
306
+ type: 'DRAW_COUNT' | 'ITEM' | 'ACHIEVEMENT';
307
+ val: string;
308
+ count: number;
309
+ };
310
  }
311
 
312
+ // --- Achievement System Types ---
313
+ // (Already defined above with new fields, kept here for structure context if needed)
 
 
 
 
314
 
315
  export interface StudentAchievement {
316
  _id?: string;
317
+ schoolId: string;
318
  studentId: string;
319
+ studentName: string;
320
+ achievementId: string;
321
+ achievementName: string;
322
  achievementIcon: string;
 
323
  semester: string;
324
  createTime: string;
 
325
  }
326
 
327
+ // --- Attendance Types ---
328
+ export type AttendanceStatus = 'Present' | 'Absent' | 'Leave';
329
+
330
+ export interface Attendance {
331
  _id?: string;
332
  schoolId: string;
333
  studentId: string;
334
  studentName: string;
335
  className: string;
336
+ date: string; // YYYY-MM-DD
337
+ status: AttendanceStatus;
338
+ checkInTime?: string;
 
 
 
339
  }
340
 
341
+ export interface LeaveRequest {
342
  _id?: string;
343
  schoolId: string;
344
+ studentId: string;
345
+ studentName: string;
346
+ className: string;
347
+ reason: string;
348
+ startDate: string;
349
+ endDate: string;
350
+ status: 'Pending' | 'Approved' | 'Rejected';
351
  createTime: string;
352
  }
353
+
354
+ export interface SchoolCalendarEntry {
355
+ _id?: string;
356
+ schoolId: string;
357
+ className?: string; // Optional, if set applies to specific class only
358
+ type: 'HOLIDAY' | 'BREAK' | 'OFF';
359
+ startDate: string; // YYYY-MM-DD
360
+ endDate: string; // YYYY-MM-DD
361
+ name: string;
362
+ }