dvc890 commited on
Commit
5964851
·
verified ·
1 Parent(s): c728eff

Upload 46 files

Browse files
Files changed (7) hide show
  1. App.tsx +6 -2
  2. components/Sidebar.tsx +2 -1
  3. models.js +25 -180
  4. pages/Wishes.tsx +455 -0
  5. server.js +79 -804
  6. services/api.ts +24 -4
  7. types.ts +173 -224
App.tsx CHANGED
@@ -16,6 +16,7 @@ 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
 
20
  import { Login } from './pages/Login';
21
  import { User, UserRole } from './types';
@@ -23,6 +24,7 @@ import { api } from './services/api';
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,6 +125,7 @@ const AppContent: React.FC = () => {
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,7 +143,8 @@ const AppContent: React.FC = () => {
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,7 +165,7 @@ const AppContent: React.FC = () => {
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>
 
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
  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
  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
  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
  <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>
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 } from 'lucide-react';
4
  import { UserRole } from '../types';
5
 
6
  interface SidebarProps {
@@ -19,6 +19,7 @@ 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: '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
 
1
 
2
  import React from 'react';
3
+ import { LayoutDashboard, Users, BookOpen, GraduationCap, Settings, LogOut, FileText, School, UserCog, Palette, X, Building, Gamepad2, CalendarCheck, UserCircle, MessageCircleHeart } 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: 'wishes', label: '许愿 & 反馈', icon: MessageCircleHeart, 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
models.js CHANGED
@@ -1,195 +1,40 @@
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
  };
 
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, AchievementConfigModel, TeacherExchangeConfigModel, StudentAchievementModel, AttendanceModel, LeaveRequestModel, SchoolCalendarModel)
5
 
6
+ // ... existing schemas ...
 
7
 
8
+ const WishSchema = new mongoose.Schema({
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  schoolId: String,
10
+ studentId: String,
11
+ studentName: String,
12
+ className: String,
13
  teacherId: String,
14
  teacherName: String,
15
+ content: String,
16
+ status: { type: String, default: 'PENDING' },
17
+ createTime: { type: Date, default: Date.now },
18
+ fulfillTime: Date
19
  });
20
+ const WishModel = mongoose.model('Wish', WishSchema);
 
 
 
 
 
 
21
 
22
+ const FeedbackSchema = new mongoose.Schema({
23
+ schoolId: String,
24
+ senderId: String,
25
+ senderName: String,
26
+ senderRole: String,
27
+ targetId: String,
28
+ targetName: String,
29
+ content: String,
30
+ status: { type: String, default: 'PENDING' }, // PENDING, ACCEPTED, PROCESSED, IGNORED
31
+ createTime: { type: Date, default: Date.now }
32
+ });
33
+ const FeedbackModel = mongoose.model('Feedback', FeedbackSchema);
34
 
35
  module.exports = {
36
  School, User, Student, Course, Score, ClassModel, SubjectModel, ExamModel, ScheduleModel,
37
  ConfigModel, NotificationModel, GameSessionModel, StudentRewardModel, LuckyDrawConfigModel, GameMonsterConfigModel, GameZenConfigModel,
38
+ AchievementConfigModel, TeacherExchangeConfigModel, StudentAchievementModel, AttendanceModel, LeaveRequestModel, SchoolCalendarModel,
39
+ WishModel, FeedbackModel
40
  };
pages/Wishes.tsx ADDED
@@ -0,0 +1,455 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import React, { useState, useEffect } from 'react';
3
+ import { api } from '../services/api';
4
+ import { Wish, Feedback, User, Student, UserRole } from '../types';
5
+ import { TreePine, MessageSquare, Loader2, Send, CheckCircle, XCircle, Trash2, Heart, RefreshCcw, Filter, User as UserIcon } from 'lucide-react';
6
+ import { Emoji } from '../components/Emoji';
7
+
8
+ const WishCard: React.FC<{ wish: Wish, isTeacher: boolean, onFulfill?: (id: string) => void }> = ({ wish, isTeacher, onFulfill }) => {
9
+ return (
10
+ <div className={`relative p-4 rounded-xl shadow-md border transform transition-all duration-300 hover:scale-105 ${wish.status === 'FULFILLED' ? 'bg-yellow-50 border-yellow-200 grayscale opacity-80' : 'bg-white border-pink-100 hover:border-pink-300'}`}>
11
+ <div className="absolute -top-3 -right-2 text-2xl animate-pulse delay-100"><Emoji symbol="✨"/></div>
12
+ <div className="text-sm font-bold text-gray-700 mb-2 flex justify-between items-center">
13
+ <span>{wish.studentName}</span>
14
+ <span className="text-xs text-gray-400">{new Date(wish.createTime).toLocaleDateString()}</span>
15
+ </div>
16
+ <div className="text-gray-800 font-medium mb-4 min-h-[40px] text-sm break-words">
17
+ {wish.content}
18
+ </div>
19
+ {isTeacher && wish.status === 'PENDING' && onFulfill && (
20
+ <button
21
+ onClick={() => onFulfill(wish._id!)}
22
+ className="w-full py-1.5 bg-pink-500 hover:bg-pink-600 text-white rounded-lg text-xs font-bold transition-colors shadow-sm"
23
+ >
24
+ 实现愿望 🪄
25
+ </button>
26
+ )}
27
+ {wish.status === 'FULFILLED' && (
28
+ <div className="w-full py-1.5 bg-gray-100 text-gray-500 rounded-lg text-xs font-bold text-center">
29
+ 已实现 ✅
30
+ </div>
31
+ )}
32
+ </div>
33
+ );
34
+ };
35
+
36
+ export const Wishes: React.FC = () => {
37
+ const [activeTab, setActiveTab] = useState<'TREE' | 'WALL'>('TREE');
38
+ const [loading, setLoading] = useState(true);
39
+ const [currentUser, setCurrentUser] = useState<User | null>(null);
40
+ const [studentInfo, setStudentInfo] = useState<Student | null>(null);
41
+ const [teachers, setTeachers] = useState<User[]>([]);
42
+
43
+ // Wish State
44
+ const [wishes, setWishes] = useState<Wish[]>([]);
45
+ const [wishContent, setWishContent] = useState('');
46
+ const [targetTeacherId, setTargetTeacherId] = useState('');
47
+ const [wishFilter, setWishFilter] = useState<'PENDING' | 'FULFILLED'>('PENDING');
48
+
49
+ // Feedback State
50
+ const [feedbackList, setFeedbackList] = useState<Feedback[]>([]);
51
+ const [fbContent, setFbContent] = useState('');
52
+ const [fbTarget, setFbTarget] = useState(''); // Teacher ID or 'ADMIN'
53
+ const [fbFilterStatus, setFbFilterStatus] = useState('UNHANDLED'); // UNHANDLED (Pending), HANDLED (Accepted/Processed), ALL
54
+
55
+ const isStudent = currentUser?.role === UserRole.STUDENT;
56
+ const isTeacher = currentUser?.role === UserRole.TEACHER;
57
+ const isAdmin = currentUser?.role === UserRole.ADMIN || currentUser?.role === UserRole.PRINCIPAL;
58
+
59
+ useEffect(() => {
60
+ const init = async () => {
61
+ const user = api.auth.getCurrentUser();
62
+ setCurrentUser(user);
63
+ if (user?.role === UserRole.STUDENT) {
64
+ // Get student info to check className
65
+ const stus = await api.students.getAll();
66
+ const me = stus.find(s => s.studentNo === user.username || s.name === user.trueName);
67
+ if (me) {
68
+ setStudentInfo(me);
69
+ // Fetch available teachers for student
70
+ const ts = await api.users.getTeachersForClass(me.className);
71
+ setTeachers(ts);
72
+ // Default target to first teacher (usually homeroom if sorted correctly by backend, or just first)
73
+ if (ts.length > 0) setTargetTeacherId(ts[0]._id!);
74
+ }
75
+ } else if (user?.role === UserRole.TEACHER) {
76
+ // Teacher might want to send feedback to Admin
77
+ }
78
+ loadData(user);
79
+ };
80
+ init();
81
+ }, []);
82
+
83
+ const loadData = async (user: User | null) => {
84
+ if (!user) return;
85
+ setLoading(true);
86
+ try {
87
+ // Load Wishes
88
+ if (isStudent && studentInfo) {
89
+ const myWishes = await api.wishes.getMyWishes(studentInfo._id || String(studentInfo.id));
90
+ setWishes(myWishes);
91
+ } else if (isTeacher) {
92
+ const classWishes = await api.wishes.getTeacherWishes(user._id!);
93
+ setWishes(classWishes);
94
+ }
95
+
96
+ // Load Feedback
97
+ if (isStudent && studentInfo) {
98
+ const sent = await api.feedback.getSent(studentInfo._id || String(studentInfo.id));
99
+ setFeedbackList(sent);
100
+ } else if (isTeacher) {
101
+ // Teachers see received from students AND sent to Admin
102
+ // We'll prioritize RECEIVED view for now, maybe add toggle?
103
+ const received = await api.feedback.getReceived(user._id!);
104
+ setFeedbackList(received);
105
+ } else if (isAdmin) {
106
+ const adminFb = await api.feedback.getAdmin();
107
+ setFeedbackList(adminFb);
108
+ }
109
+ } catch(e) { console.error(e); }
110
+ finally { setLoading(false); }
111
+ };
112
+
113
+ // --- Wish Handlers ---
114
+ const submitWish = async () => {
115
+ if (!wishContent) return alert('请填写愿望内容');
116
+ if (!targetTeacherId) return alert('请选择许愿对象');
117
+
118
+ // Find teacher name
119
+ const teacher = teachers.find(t => t._id === targetTeacherId);
120
+
121
+ try {
122
+ await api.wishes.submit({
123
+ studentId: studentInfo?._id || String(studentInfo?.id),
124
+ studentName: studentInfo?.name,
125
+ className: studentInfo?.className,
126
+ teacherId: targetTeacherId,
127
+ teacherName: teacher?.trueName || teacher?.username || '老师',
128
+ content: wishContent
129
+ });
130
+ alert('许愿成功!愿望已挂上许愿树 ✨');
131
+ setWishContent('');
132
+ // Reload
133
+ const myWishes = await api.wishes.getMyWishes(studentInfo?._id || String(studentInfo?.id));
134
+ setWishes(myWishes);
135
+ } catch (e: any) { alert(e.message || '许愿失败'); }
136
+ };
137
+
138
+ const fulfillWish = async (id: string) => {
139
+ if (!confirm('确定要实现这个愿望吗?')) return;
140
+ await api.wishes.fulfill(id);
141
+ // Optimistic update
142
+ setWishes(prev => prev.map(w => w._id === id ? { ...w, status: 'FULFILLED' } : w));
143
+ };
144
+
145
+ const fulfillRandom = async () => {
146
+ if (!currentUser) return;
147
+ try {
148
+ const res = await api.wishes.randomFulfill(currentUser._id!);
149
+ if (res.wish) {
150
+ alert(`系统随机选中了 [${res.wish.studentName}] 的愿望:\n"${res.wish.content}"\n已标记为实现!`);
151
+ // Reload
152
+ const classWishes = await api.wishes.getTeacherWishes(currentUser._id!);
153
+ setWishes(classWishes);
154
+ }
155
+ } catch (e: any) { alert(e.message || '操作失败'); }
156
+ };
157
+
158
+ // --- Feedback Handlers ---
159
+ const submitFeedback = async () => {
160
+ if (!fbContent) return alert('请输入反馈内容');
161
+
162
+ let senderId, senderName, finalTargetId, finalTargetName;
163
+
164
+ if (isStudent && studentInfo) {
165
+ if (!fbTarget) return alert('请选择反馈对象');
166
+ senderId = studentInfo._id || String(studentInfo.id);
167
+ senderName = studentInfo.name;
168
+ finalTargetId = fbTarget;
169
+ const t = teachers.find(u => u._id === fbTarget);
170
+ finalTargetName = t ? (t.trueName || t.username) : '老师';
171
+ } else {
172
+ // Teacher/Staff sending to Admin
173
+ senderId = currentUser?._id;
174
+ senderName = currentUser?.trueName || currentUser?.username;
175
+ finalTargetId = 'ADMIN';
176
+ finalTargetName = '系统管理员';
177
+ }
178
+
179
+ try {
180
+ await api.feedback.submit({
181
+ schoolId: currentUser?.schoolId,
182
+ senderId,
183
+ senderName,
184
+ senderRole: currentUser?.role,
185
+ targetId: finalTargetId,
186
+ targetName: finalTargetName,
187
+ content: fbContent
188
+ });
189
+ alert('反馈已提交');
190
+ setFbContent('');
191
+ // Reload
192
+ if (isStudent && studentInfo) {
193
+ const sent = await api.feedback.getSent(senderId!);
194
+ setFeedbackList(sent);
195
+ } else if (isTeacher) {
196
+ // If teacher sends to admin, maybe show Sent tab? For now simplest is alert.
197
+ }
198
+ } catch(e) { alert('提交失败'); }
199
+ };
200
+
201
+ const updateFeedbackStatus = async (id: string, status: string) => {
202
+ await api.feedback.updateStatus(id, status);
203
+ // Update local
204
+ setFeedbackList(prev => prev.map(f => f._id === id ? { ...f, status: status as any } : f));
205
+ };
206
+
207
+ const batchIgnore = async () => {
208
+ if (!confirm('确定要忽略所有未处理的反馈吗?')) return;
209
+ const targetId = isAdmin ? 'ADMIN' : currentUser?._id;
210
+ await api.feedback.batchIgnore(targetId!);
211
+ // Reload
212
+ if (isAdmin) {
213
+ setFeedbackList(await api.feedback.getAdmin());
214
+ } else {
215
+ setFeedbackList(await api.feedback.getReceived(currentUser?._id!));
216
+ }
217
+ };
218
+
219
+ // Display Logic
220
+ const displayWishes = wishes.filter(w => isTeacher ? w.status === wishFilter : true);
221
+
222
+ // Filter logic for Feedback Receiver View
223
+ const displayFeedback = feedbackList.filter(f => {
224
+ if (!isTeacher && !isAdmin) return true; // Sender sees all history
225
+
226
+ // Receiver Filters
227
+ if (fbFilterStatus === 'UNHANDLED') return f.status === 'PENDING';
228
+ if (fbFilterStatus === 'HANDLED') return f.status === 'ACCEPTED' || f.status === 'PROCESSED';
229
+ // 'ALL' usually implies non-ignored or everything. Requirement says "Ignore filter out".
230
+ // Let's make "ALL" show everything EXCEPT Ignored, and maybe add "IGNORED" tab?
231
+ // Simplifying to: Default hides IGNORED.
232
+ if (fbFilterStatus === 'IGNORED') return f.status === 'IGNORED';
233
+
234
+ return f.status !== 'IGNORED';
235
+ });
236
+
237
+ if (loading) return <div className="flex justify-center p-10"><Loader2 className="animate-spin text-blue-600"/></div>;
238
+
239
+ return (
240
+ <div className="flex flex-col h-full space-y-4">
241
+ {/* Nav Tabs */}
242
+ <div className="bg-white p-2 rounded-xl shadow-sm border border-gray-100 flex gap-2 w-fit">
243
+ <button
244
+ onClick={() => setActiveTab('TREE')}
245
+ className={`px-4 py-2 rounded-lg font-bold flex items-center transition-all ${activeTab==='TREE' ? 'bg-pink-100 text-pink-600' : 'text-gray-500 hover:bg-gray-50'}`}
246
+ >
247
+ <TreePine className="mr-2" size={18}/> 许愿树
248
+ </button>
249
+ <button
250
+ onClick={() => setActiveTab('WALL')}
251
+ className={`px-4 py-2 rounded-lg font-bold flex items-center transition-all ${activeTab==='WALL' ? 'bg-blue-100 text-blue-600' : 'text-gray-500 hover:bg-gray-50'}`}
252
+ >
253
+ <MessageSquare className="mr-2" size={18}/> 意见墙
254
+ </button>
255
+ </div>
256
+
257
+ <div className="flex-1 bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden relative">
258
+ {/* --- WISH TREE VIEW --- */}
259
+ {activeTab === 'TREE' && (
260
+ <div className="h-full flex flex-col relative bg-gradient-to-b from-sky-100 to-green-50 overflow-y-auto custom-scrollbar">
261
+ {/* Tree Background Illustration (CSS/SVG) */}
262
+ <div className="absolute inset-0 pointer-events-none flex justify-center items-end opacity-20">
263
+ <svg viewBox="0 0 500 500" className="h-full w-auto">
264
+ <path d="M250 500 L250 400 Q200 350 150 250 Q100 150 250 50 Q400 150 350 250 Q300 350 250 400" fill="#a5d6a7" />
265
+ <rect x="230" y="400" width="40" height="100" fill="#8d6e63" />
266
+ </svg>
267
+ </div>
268
+
269
+ {/* Content Layer */}
270
+ <div className="relative z-10 p-6 flex-1 flex flex-col">
271
+ {/* Header / Controls */}
272
+ <div className="flex justify-between items-center mb-6">
273
+ <h2 className="text-2xl font-black text-green-800 flex items-center drop-shadow-sm">
274
+ <TreePine className="mr-2"/> 班级许愿树
275
+ </h2>
276
+ {isTeacher && (
277
+ <div className="flex gap-2">
278
+ <div className="bg-white/80 p-1 rounded-lg flex text-xs font-bold border border-green-200">
279
+ <button onClick={()=>setWishFilter('PENDING')} className={`px-3 py-1 rounded transition-colors ${wishFilter==='PENDING' ? 'bg-green-500 text-white' : 'text-green-700 hover:bg-green-100'}`}>待实现</button>
280
+ <button onClick={()=>setWishFilter('FULFILLED')} className={`px-3 py-1 rounded transition-colors ${wishFilter==='FULFILLED' ? 'bg-yellow-400 text-white' : 'text-yellow-600 hover:bg-yellow-100'}`}>已实现</button>
281
+ </div>
282
+ {wishFilter === 'PENDING' && (
283
+ <button onClick={fulfillRandom} className="px-4 py-1.5 bg-pink-500 text-white rounded-lg text-xs font-bold hover:bg-pink-600 shadow-md flex items-center">
284
+ <RefreshCcw size={14} className="mr-1"/> 随机摘取愿望
285
+ </button>
286
+ )}
287
+ </div>
288
+ )}
289
+ </div>
290
+
291
+ {/* Wish Input (Student) */}
292
+ {isStudent && (
293
+ <div className="mb-8 max-w-2xl mx-auto w-full">
294
+ {wishes.some(w => w.status === 'PENDING') ? (
295
+ <div className="bg-white/90 p-6 rounded-2xl shadow-lg border-2 border-pink-200 text-center animate-bounce-slow">
296
+ <div className="text-4xl mb-2"><Emoji symbol="🌠"/></div>
297
+ <h3 className="text-lg font-bold text-pink-600 mb-1">愿望已许下</h3>
298
+ <p className="text-gray-500 text-sm">正在等待老师摘取你的心愿卡...</p>
299
+ </div>
300
+ ) : (
301
+ <div className="bg-white/90 p-4 rounded-xl shadow-lg border border-green-200">
302
+ <h3 className="font-bold text-green-800 mb-2 flex items-center"><Heart size={16} className="mr-2 text-pink-500"/> 写下你的心愿</h3>
303
+ <div className="flex gap-2 mb-2">
304
+ <select className="border rounded p-2 text-sm bg-white" value={targetTeacherId} onChange={e=>setTargetTeacherId(e.target.value)}>
305
+ <option value="">选择老师</option>
306
+ {teachers.map(t => <option key={t._id} value={t._id}>{t.trueName || t.username}</option>)}
307
+ </select>
308
+ </div>
309
+ <textarea
310
+ className="w-full border rounded-lg p-3 text-sm focus:ring-2 focus:ring-green-400 outline-none resize-none"
311
+ rows={3}
312
+ placeholder="老师,我想..."
313
+ value={wishContent}
314
+ onChange={e=>setWishContent(e.target.value)}
315
+ />
316
+ <button onClick={submitWish} className="w-full mt-2 bg-green-600 text-white py-2 rounded-lg font-bold hover:bg-green-700 transition-colors">
317
+ 挂上许愿树
318
+ </button>
319
+ </div>
320
+ )}
321
+ </div>
322
+ )}
323
+
324
+ {/* Wish Grid */}
325
+ {displayWishes.length === 0 ? (
326
+ <div className="flex-1 flex items-center justify-center text-green-700/50 font-medium">
327
+ {isTeacher && wishFilter === 'PENDING' ? '树上空空如也,暂无愿望' : '暂无记录'}
328
+ </div>
329
+ ) : (
330
+ <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4">
331
+ {displayWishes.map(wish => (
332
+ <WishCard key={wish._id} wish={wish} isTeacher={isTeacher} onFulfill={fulfillWish} />
333
+ ))}
334
+ </div>
335
+ )}
336
+ </div>
337
+ </div>
338
+ )}
339
+
340
+ {/* --- FEEDBACK WALL VIEW --- */}
341
+ {activeTab === 'WALL' && (
342
+ <div className="h-full flex flex-col p-6 bg-gray-50">
343
+ {/* Header */}
344
+ <div className="flex justify-between items-center mb-6">
345
+ <h2 className="text-xl font-bold text-gray-800 flex items-center">
346
+ <MessageSquare className="mr-2 text-blue-600"/>
347
+ {isStudent ? '意见反馈' : (isAdmin ? '系统反馈管理' : '学生反馈信箱')}
348
+ </h2>
349
+ {(isTeacher || isAdmin) && (
350
+ <div className="flex gap-2">
351
+ <div className="flex items-center bg-white border rounded-lg p-1">
352
+ <Filter size={14} className="text-gray-400 ml-2 mr-1"/>
353
+ <select className="text-xs bg-transparent outline-none p-1 font-bold text-gray-600" value={fbFilterStatus} onChange={e=>setFbFilterStatus(e.target.value)}>
354
+ <option value="UNHANDLED">待处理</option>
355
+ <option value="HANDLED">已处理/接受</option>
356
+ <option value="IGNORED">已忽略</option>
357
+ </select>
358
+ </div>
359
+ {fbFilterStatus === 'UNHANDLED' && (
360
+ <button onClick={batchIgnore} className="text-xs bg-gray-200 text-gray-600 px-3 py-1.5 rounded-lg hover:bg-gray-300 font-bold transition-colors">
361
+ 一键忽略
362
+ </button>
363
+ )}
364
+ </div>
365
+ )}
366
+ </div>
367
+
368
+ <div className="flex gap-6 h-full min-h-0">
369
+ {/* Left: Submit Form (For Students & Staff sending to Admin) */}
370
+ {(!isTeacher || (isTeacher && !isAdmin)) && ( // Teachers see this only if they want to send to Admin? UI space limited, maybe modal? Let's put inline for now for Students.
371
+ <div className={`w-1/3 bg-white p-4 rounded-xl shadow-sm border border-gray-100 flex flex-col ${isTeacher ? 'hidden md:flex' : ''}`}> {/* Teachers hide form usually to focus on inbox, can be toggle */}
372
+ <h3 className="font-bold text-gray-700 mb-4 text-sm">
373
+ {isStudent ? '向老师提意见' : '向管理员反馈问题'}
374
+ </h3>
375
+
376
+ {isStudent && (
377
+ <div className="mb-3">
378
+ <label className="text-xs font-bold text-gray-500 block mb-1">接收老师</label>
379
+ <select className="w-full border rounded p-2 text-sm bg-gray-50" value={fbTarget} onChange={e=>setFbTarget(e.target.value)}>
380
+ <option value="">请选择...</option>
381
+ {teachers.map(t => <option key={t._id} value={t._id}>{t.trueName || t.username}</option>)}
382
+ </select>
383
+ </div>
384
+ )}
385
+
386
+ <textarea
387
+ className="flex-1 border rounded-lg p-3 text-sm resize-none focus:ring-2 focus:ring-blue-500 outline-none bg-gray-50"
388
+ placeholder="请详细描述您的建议或遇到的问题..."
389
+ value={fbContent}
390
+ onChange={e=>setFbContent(e.target.value)}
391
+ />
392
+ <button onClick={submitFeedback} className="mt-4 w-full bg-blue-600 text-white py-2 rounded-lg font-bold hover:bg-blue-700 flex items-center justify-center gap-2">
393
+ <Send size={16}/> 提交反馈
394
+ </button>
395
+ </div>
396
+ )}
397
+
398
+ {/* Right: List */}
399
+ <div className="flex-1 overflow-y-auto custom-scrollbar space-y-3">
400
+ {displayFeedback.length === 0 ? (
401
+ <div className="text-center py-20 text-gray-400">暂无相关反馈</div>
402
+ ) : displayFeedback.map(fb => {
403
+ // Visual Status for Student: IGNORED -> PENDING visually
404
+ const visualStatus = (isStudent && fb.status === 'IGNORED') ? 'PENDING' : fb.status;
405
+
406
+ return (
407
+ <div key={fb._id} className="bg-white p-4 rounded-xl shadow-sm border border-gray-100">
408
+ <div className="flex justify-between items-start mb-2">
409
+ <div className="flex items-center gap-2">
410
+ <div className="w-8 h-8 rounded-full bg-gray-100 flex items-center justify-center text-gray-500">
411
+ <UserIcon size={16}/>
412
+ </div>
413
+ <div>
414
+ <div className="text-sm font-bold text-gray-800">
415
+ {isStudent ? `To: ${fb.targetName}` : `From: ${fb.senderName}`}
416
+ </div>
417
+ <div className="text-xs text-gray-400">{new Date(fb.createTime).toLocaleString()}</div>
418
+ </div>
419
+ </div>
420
+ <div>
421
+ {visualStatus === 'PENDING' && <span className="bg-gray-100 text-gray-500 text-xs px-2 py-1 rounded">待处理</span>}
422
+ {visualStatus === 'ACCEPTED' && <span className="bg-green-100 text-green-700 text-xs px-2 py-1 rounded">已采纳</span>}
423
+ {visualStatus === 'PROCESSED' && <span className="bg-blue-100 text-blue-700 text-xs px-2 py-1 rounded">已处理</span>}
424
+ {visualStatus === 'IGNORED' && <span className="bg-red-50 text-red-400 text-xs px-2 py-1 rounded">已忽略</span>}
425
+ </div>
426
+ </div>
427
+
428
+ <p className="text-sm text-gray-700 bg-gray-50 p-3 rounded-lg mb-2">
429
+ {fb.content}
430
+ </p>
431
+
432
+ {(isTeacher || isAdmin) && visualStatus === 'PENDING' && (
433
+ <div className="flex gap-2 justify-end mt-2">
434
+ <button onClick={()=>updateFeedbackStatus(fb._id!, 'ACCEPTED')} className="flex items-center px-3 py-1 bg-green-50 text-green-600 rounded text-xs font-bold hover:bg-green-100 border border-green-200">
435
+ <CheckCircle size={12} className="mr-1"/> 采纳
436
+ </button>
437
+ <button onClick={()=>updateFeedbackStatus(fb._id!, 'PROCESSED')} className="flex items-center px-3 py-1 bg-blue-50 text-blue-600 rounded text-xs font-bold hover:bg-blue-100 border border-blue-200">
438
+ <CheckCircle size={12} className="mr-1"/> 已处理
439
+ </button>
440
+ <button onClick={()=>updateFeedbackStatus(fb._id!, 'IGNORED')} className="flex items-center px-3 py-1 bg-gray-100 text-gray-500 rounded text-xs font-bold hover:bg-gray-200 border border-gray-200">
441
+ <Trash2 size={12} className="mr-1"/> 忽略
442
+ </button>
443
+ </div>
444
+ )}
445
+ </div>
446
+ );
447
+ })}
448
+ </div>
449
+ </div>
450
+ </div>
451
+ )}
452
+ </div>
453
+ </div>
454
+ );
455
+ };
server.js CHANGED
@@ -3,847 +3,122 @@
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}`));
 
 
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
+ WishModel, FeedbackModel
8
  } = require('./models');
9
 
10
  // ... (existing setup code, middleware, connectDB, helpers) ...
11
  const express = require('express');
12
  const mongoose = require('mongoose');
13
+ // ... (rest of imports)
 
 
 
14
 
15
+ // ... (rest of middleware setup)
 
 
16
 
17
+ // ... (existing helper functions)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
 
19
+ // ... (Existing Routes) ...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
 
21
+ // --- WISH TREE ROUTES ---
22
+ app.get('/api/wishes', async (req, res) => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  const filter = getQueryFilter(req);
24
+ const { studentId, teacherId, status } = req.query;
25
+ if (studentId) filter.studentId = studentId;
26
+ if (teacherId) filter.teacherId = teacherId;
27
+ if (status) filter.status = status;
 
 
 
 
28
 
29
+ // Sort: Pending first, then by time
30
+ const wishes = await WishModel.find(filter).sort({ status: -1, createTime: -1 });
31
+ res.json(wishes);
 
 
 
 
 
 
 
 
32
  });
33
 
34
+ app.post('/api/wishes', async (req, res) => {
35
  const data = injectSchoolId(req, req.body);
36
+ // Check if student already has a pending wish
37
+ const existing = await WishModel.findOne({
38
+ studentId: data.studentId,
39
+ status: 'PENDING'
40
+ });
41
+ if (existing) {
42
+ return res.status(400).json({ error: 'LIMIT_REACHED', message: '您还有一个未实现的愿望,请耐心等待实现后再许愿。' });
43
+ }
44
+ await WishModel.create(data);
 
 
45
  res.json({ success: true });
46
  });
47
 
48
+ app.post('/api/wishes/:id/fulfill', async (req, res) => {
49
+ await WishModel.findByIdAndUpdate(req.params.id, {
50
+ status: 'FULFILLED',
51
+ fulfillTime: new Date()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  });
 
 
 
 
53
  res.json({ success: true });
54
  });
55
 
56
+ app.post('/api/wishes/random-fulfill', async (req, res) => {
57
+ const { teacherId } = req.body;
58
+ // Find all pending wishes for this teacher
59
+ const pendingWishes = await WishModel.find({
60
+ teacherId,
61
+ status: 'PENDING',
62
+ ...getQueryFilter(req)
63
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
 
65
+ if (pendingWishes.length === 0) {
66
+ return res.status(404).json({ error: 'NO_WISHES', message: '暂无待实现的愿望' });
67
  }
68
 
69
+ const randomWish = pendingWishes[Math.floor(Math.random() * pendingWishes.length)];
70
+ await WishModel.findByIdAndUpdate(randomWish._id, {
71
+ status: 'FULFILLED',
72
+ fulfillTime: new Date()
 
 
 
 
 
 
 
 
 
 
 
73
  });
74
 
75
+ res.json({ success: true, wish: randomWish });
 
 
 
 
 
76
  });
77
 
78
+ // --- FEEDBACK WALL ROUTES ---
79
+ app.get('/api/feedback', async (req, res) => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  const filter = getQueryFilter(req);
81
+ const { senderId, targetId, role } = req.query; // role is the requester's role
82
+
83
+ // Sender View
84
+ if (senderId) {
85
+ filter.senderId = senderId;
86
+ }
87
+ // Target View (Teacher checking inbox)
88
+ else if (targetId) {
89
+ filter.targetId = targetId;
90
+ // Teachers shouldn't see 'IGNORED' by default unless requested (UI handles filter usually, but backend can optimize)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  }
92
+ // Admin View (Checking system feedback)
93
+ else if (role === 'ADMIN') {
94
+ filter.targetId = 'ADMIN';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  }
96
+
97
+ const feedbacks = await FeedbackModel.find(filter).sort({ createTime: -1 });
98
+ res.json(feedbacks);
99
  });
100
+
101
+ app.post('/api/feedback', async (req, res) => {
102
  const data = injectSchoolId(req, req.body);
103
+ await FeedbackModel.create(data);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  res.json({ success: true });
105
  });
106
+
107
+ app.put('/api/feedback/:id/status', async (req, res) => {
108
+ const { status } = req.body;
109
+ await FeedbackModel.findByIdAndUpdate(req.params.id, { status });
 
 
 
 
 
 
 
110
  res.json({ success: true });
111
  });
112
+
113
+ app.post('/api/feedback/batch-ignore', async (req, res) => {
114
+ const { targetId } = req.body; // Teacher ID or ADMIN
115
+ await FeedbackModel.updateMany(
116
+ { targetId, status: 'PENDING', ...getQueryFilter(req) },
117
+ { status: 'IGNORED' }
118
+ );
119
  res.json({ success: true });
120
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
121
 
122
+ // ... (Rest of existing routes from server.js)
123
+ app.get('/api/auth/me', async (req, res) => {
124
+ // ...
services/api.ts CHANGED
@@ -1,6 +1,7 @@
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,6 +20,7 @@ const getBaseUrl = () => {
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,15 +50,18 @@ async function request(endpoint: string, options: RequestInit = {}) {
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,7 +109,7 @@ export const api = {
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,7 +131,6 @@ export const api = {
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,7 +229,6 @@ export const api = {
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,6 +247,23 @@ export const api = {
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
  }
 
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
  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
  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
  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
  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
  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
  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
  }
types.ts CHANGED
@@ -1,49 +1,7 @@
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,308 +13,299 @@ export enum UserStatus {
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
  }
 
1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  export enum UserRole {
3
  ADMIN = 'ADMIN',
4
+ PRINCIPAL = 'PRINCIPAL',
5
  TEACHER = 'TEACHER',
6
  STUDENT = 'STUDENT',
7
  USER = 'USER'
 
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
  }
37
 
38
+ export interface School {
 
39
  _id?: string;
40
+ id?: string | number;
41
  name: string;
42
  code: string;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  }
44
 
45
  export interface Student {
 
46
  _id?: string;
47
+ id?: string | number;
48
+ studentNo: string;
 
49
  name: string;
50
+ gender: 'Male' | 'Female';
 
 
 
51
  className: string;
52
+ seatNo?: string;
53
+ phone?: string;
54
+ birthday?: string;
55
+ idCard?: string;
56
+ status: 'Enrolled' | 'Graduated' | 'Left';
57
  parentName?: string;
58
  parentPhone?: string;
59
  address?: string;
60
+ drawAttempts?: number;
61
+ flowerBalance?: number;
 
 
 
 
62
  }
63
 
64
  export interface Course {
 
65
  _id?: string;
66
+ id?: string | number;
67
+ courseCode?: string;
68
+ courseName: string;
69
+ teacherId?: string;
70
  teacherName: string;
71
+ className: string;
72
  credits: number;
73
  capacity: number;
74
+ enrolled?: number;
75
  }
76
 
77
  export type ExamStatus = 'Normal' | 'Absent' | 'Leave' | 'Cheat';
78
 
79
  export interface Score {
 
80
  _id?: string;
81
+ id?: string | number;
82
+ studentId?: string;
83
  studentName: string;
84
  studentNo: string;
85
  courseName: string;
86
  score: number;
87
  semester: string;
88
+ type: string;
89
  examName?: string;
90
  status?: ExamStatus;
91
  }
92
 
93
+ export interface ClassInfo {
 
94
  _id?: string;
95
+ id?: string | number;
96
+ grade: string;
97
+ className: string;
98
+ homeroomTeacherIds?: string[];
99
+ teacherName?: string;
100
+ studentCount?: number;
101
+ }
102
+
103
+ export interface Subject {
104
+ _id?: string;
105
+ id?: string | number;
106
  name: string;
107
+ code?: string;
108
+ color?: string;
109
+ excellenceThreshold?: number;
110
+ thresholds?: Record<string, number>;
111
  }
112
 
113
  export interface Schedule {
 
114
  _id?: string;
 
115
  className: string;
 
 
116
  dayOfWeek: number;
117
  period: number;
118
+ subject: string;
119
+ teacherName: string;
120
  }
121
 
122
  export interface Notification {
 
123
  _id?: string;
 
 
 
124
  title: string;
125
  content: string;
126
  type: 'info' | 'success' | 'warning' | 'error';
 
127
  createTime: string;
128
+ targetRole?: string;
129
+ targetUserId?: string;
130
  }
131
 
132
+ export interface SystemConfig {
133
+ systemName?: string;
134
+ allowRegister?: boolean;
135
+ allowAdminRegister?: boolean;
136
+ allowStudentRegister?: boolean;
137
+ allowPrincipalRegister?: boolean;
138
+ maintenanceMode?: boolean;
139
+ emailNotify?: boolean;
140
+ semester?: string;
141
+ semesters?: string[];
142
  }
143
 
144
+ export interface Exam {
145
+ _id?: string;
146
+ name?: string;
147
+ date?: string;
148
+ type?: string;
149
+ }
150
+
151
+ export interface GameRewardConfig {
152
+ scoreThreshold: number;
153
+ rewardType: 'DRAW_COUNT' | 'ITEM' | 'ACHIEVEMENT';
154
+ rewardName: string;
155
+ rewardValue: number;
156
+ achievementId?: string;
157
+ }
158
 
159
  export interface GameTeam {
160
  id: string;
161
  name: string;
 
 
162
  color: string;
163
+ avatar: string;
164
+ score: number;
165
+ members: string[];
 
 
 
 
 
 
166
  }
167
 
168
  export interface GameSession {
169
  _id?: string;
170
  schoolId: string;
171
+ className: string;
172
+ subject: string;
173
  isEnabled: boolean;
174
+ maxSteps: number;
175
  teams: GameTeam[];
176
  rewardsConfig: GameRewardConfig[];
 
177
  }
178
 
179
+ export interface LuckyPrize {
180
+ id: string;
181
+ name: string;
182
+ probability: number;
183
+ count: number;
184
+ }
185
+
186
+ export interface LuckyDrawConfig {
187
+ _id?: string;
188
+ schoolId: string;
189
+ className: string;
190
+ ownerId?: string;
191
+ prizes: LuckyPrize[];
192
+ dailyLimit?: number;
193
+ cardCount?: number;
194
+ defaultPrize?: string;
195
+ consolationWeight?: number;
196
  }
197
 
198
  export interface StudentReward {
199
  _id?: string;
200
+ schoolId: string;
201
+ studentId: string;
202
  studentName: string;
203
+ rewardType: 'DRAW_COUNT' | 'ITEM' | 'CONSOLATION';
204
  name: string;
205
+ count?: number;
206
  status: 'PENDING' | 'REDEEMED';
207
+ source?: string;
208
  createTime: string;
 
209
  }
210
 
211
+ export interface Attendance {
212
+ _id?: string;
213
+ studentId: string;
214
+ date: string;
215
+ status: 'Present' | 'Leave' | 'Absent';
216
+ checkInTime?: string;
217
  }
218
 
219
+ export interface LeaveRequest {
220
+ _id?: string;
221
+ studentId: string;
222
+ studentName: string;
223
+ className: string;
224
+ reason: string;
225
+ startDate: string;
226
+ endDate: string;
227
+ status: 'PENDING' | 'APPROVED' | 'REJECTED';
228
+ createTime: string;
229
+ }
230
+
231
+ export interface SchoolCalendarEntry {
232
  _id?: string;
233
  schoolId: string;
234
+ className?: string;
235
+ name: string;
236
+ startDate: string;
237
+ endDate: string;
238
+ type: 'HOLIDAY' | 'EXAM' | 'EVENT' | 'OFF';
239
+ }
240
+
241
+ export interface AchievementItem {
242
+ id: string;
243
+ name: string;
244
+ icon: string;
245
+ points: number;
246
+ description?: string;
247
+ addedBy?: string;
248
+ addedByName?: string;
249
  }
250
 
251
+ export interface ExchangeRule {
252
+ id: string;
253
+ cost: number;
254
+ rewardType: 'DRAW_COUNT' | 'ITEM';
255
+ rewardName: string;
256
+ rewardValue: number;
257
+ }
258
+
259
+ export interface AchievementConfig {
260
  _id?: string;
261
  schoolId: string;
262
  className: string;
263
+ achievements: AchievementItem[];
264
+ exchangeRules: ExchangeRule[];
 
 
 
 
 
 
 
 
 
265
  }
266
 
267
+ export interface TeacherExchangeConfig {
268
+ _id?: string;
269
+ teacherId: string;
270
+ teacherName?: string;
271
+ rules: ExchangeRule[];
272
+ }
273
 
274
  export interface StudentAchievement {
275
  _id?: string;
 
276
  studentId: string;
277
+ achievementId: string;
278
+ achievementName: string;
 
279
  achievementIcon: string;
280
+ points: number;
281
  semester: string;
282
  createTime: string;
283
+ grantBy?: string;
284
  }
285
 
286
+ export interface Wish {
 
 
 
287
  _id?: string;
288
  schoolId: string;
289
  studentId: string;
290
  studentName: string;
291
  className: string;
292
+ teacherId: string;
293
+ teacherName: string;
294
+ content: string;
295
+ status: 'PENDING' | 'FULFILLED';
 
 
 
 
 
 
 
 
 
 
 
296
  createTime: string;
297
+ fulfillTime?: string;
298
  }
299
 
300
+ export interface Feedback {
301
  _id?: string;
302
  schoolId: string;
303
+ senderId: string;
304
+ senderName: string;
305
+ senderRole: string;
306
+ targetId: string;
307
+ targetName: string;
308
+ content: string;
309
+ status: 'PENDING' | 'ACCEPTED' | 'PROCESSED' | 'IGNORED';
310
+ createTime: string;
311
  }