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