服务端加固: - express.json 上限 2mb → 12mb:表格导入把文件以 base64 放进 JSON(约 4/3 倍), 8MB 的声明上限原先到 1.5MB 就被拦掉;超限现在由 error 中间件映射成 413 IMPORT_TOO_LARGE(中文提示"请拆分后分批导入"),不再落进 500 兜底分支抛英文原文; - loginRateLimiter 同时挂 /auth/login 与 /auth/class-login(班级密码默认 123456, 班级登录曾是唯一没有暴力破解防护的入口),两个路径共享同一计数器; - CORS_ORIGIN=* 时启动告警(不拦截启动,避免挡住教室机 file:// 的合法部署); - 删除已无用的 DEFAULT_STUDENT_PASSWORD;班级 termWeeks 上限 40 → 30, 与 MAX_TERM_WEEK / weekNumberSchema 对齐(原先对外承诺了系统兑现不了的学期长度)。 Web 端: - 作业页 ?date= 深链在同路由再推 query 时也生效(watch route.query.date); - 表格导入对话框适配 8MB 上限与 413 提示。 冒烟与文档: - 新增 scripts/lib/smoke-class.mjs:验证用临时班级,避免探针通知弹到用户自己的客户端与灵动岛; - README 与 AGENTS.md 同步本轮全部行为变更(含 schedule source、学生账号清理、凭据门禁)。
113 lines
4.0 KiB
TypeScript
113 lines
4.0 KiB
TypeScript
import type { DashboardSummary, HomeworkDto, NotificationDto } from '@classhelper/shared';
|
|
import {
|
|
assertClassAccess,
|
|
classScopeIdFilter,
|
|
classScopeWhere,
|
|
isStudent,
|
|
resolveClassScope,
|
|
requireStudentClassId,
|
|
} from '../../lib/access.js';
|
|
import { prisma } from '../../lib/db.js';
|
|
import { MAX_TERM_WEEK } from '../../lib/term.js';
|
|
import type { TokenPayload } from '../../lib/jwt.js';
|
|
import { toHomeworkDto, toNotificationDto } from '../../lib/mappers.js';
|
|
import { personalIdWhere, resolvePersonalIds } from '../../lib/session.js';
|
|
|
|
const courseSelect = { select: { id: true, name: true } } as const;
|
|
const creatorSelect = { select: { id: true, name: true, username: true } } as const;
|
|
|
|
/**
|
|
* 仪表盘汇总。
|
|
* 教师/管理员看到的是"自己管理的班级"的汇总;学生看到自己班级的汇总。
|
|
*/
|
|
/**
|
|
* 本班教学周数(默认 20)。
|
|
* - 学生 / 班级会话:固定为自己所在班级;
|
|
* - 教师 / 管理员:优先用显式 classId,**并且必须通过班级访问校验**(与其他接口同口径,
|
|
* 否则任意登录用户都能拿 classId 探测别班的教学周数);
|
|
* - 都不传:全局默认(MAX_TERM_WEEK 兜底)。
|
|
*/
|
|
export async function resolveTermWeeks(user: TokenPayload, classId?: string): Promise<number> {
|
|
if (classId) await assertClassAccess(user, classId);
|
|
const targetClassId = classId ?? (isStudent(user) ? requireStudentClassId(user) : undefined);
|
|
if (!targetClassId) return MAX_TERM_WEEK;
|
|
const record = await prisma.class.findUnique({
|
|
where: { id: targetClassId },
|
|
select: { termWeeks: true },
|
|
});
|
|
return record?.termWeeks && record.termWeeks > 0 ? record.termWeeks : MAX_TERM_WEEK;
|
|
}
|
|
export async function getDashboardSummary(user: TokenPayload): Promise<DashboardSummary> {
|
|
const student = isStudent(user);
|
|
// 学生强制限定在自己班级,避免越权统计
|
|
const scope = await resolveClassScope(user, student ? requireStudentClassId(user) : undefined);
|
|
// 子表按 classId 过滤;Class 表本身按主键 id 过滤
|
|
const where = classScopeWhere(scope);
|
|
const classWhere = classScopeIdFilter(scope);
|
|
// 班级账号(班级设备)以全班学生为范围;普通学生即自己
|
|
const personalIds = await resolvePersonalIds(user);
|
|
const personalWhere = personalIdWhere(personalIds);
|
|
|
|
const [
|
|
classCount,
|
|
studentCount,
|
|
courseCount,
|
|
scheduleCount,
|
|
homeworkCount,
|
|
notificationCount,
|
|
gradeCount,
|
|
unreadNotificationCount,
|
|
pendingHomeworkCount,
|
|
recentNotifications,
|
|
recentHomeworks,
|
|
] = await Promise.all([
|
|
prisma.class.count({ where: classWhere }),
|
|
prisma.user.count({ where: { ...where, role: 'STUDENT' } }),
|
|
prisma.course.count({ where }),
|
|
prisma.schedule.count({ where }),
|
|
prisma.homework.count({ where }),
|
|
prisma.notification.count({ where }),
|
|
prisma.grade.count({ where }),
|
|
prisma.notification.count({ where: { ...where, reads: { none: personalWhere } } }),
|
|
student
|
|
? prisma.homework.count({
|
|
where: { ...where, statuses: { none: { ...personalWhere, completed: true } } },
|
|
})
|
|
: prisma.homework.count({ where }),
|
|
prisma.notification.findMany({
|
|
where,
|
|
include: { creator: creatorSelect, reads: { where: personalWhere } },
|
|
orderBy: { createdAt: 'desc' },
|
|
take: 5,
|
|
}),
|
|
prisma.homework.findMany({
|
|
where,
|
|
include: {
|
|
course: courseSelect,
|
|
creator: creatorSelect,
|
|
statuses: student ? { where: personalWhere } : true,
|
|
},
|
|
orderBy: { createdAt: 'desc' },
|
|
take: 5,
|
|
}),
|
|
]);
|
|
|
|
return {
|
|
classCount,
|
|
studentCount,
|
|
courseCount,
|
|
scheduleCount,
|
|
homeworkCount,
|
|
notificationCount,
|
|
gradeCount,
|
|
unreadNotificationCount,
|
|
pendingHomeworkCount,
|
|
recentNotifications: recentNotifications.map((item): NotificationDto =>
|
|
toNotificationDto(item, { userIds: personalIds }),
|
|
),
|
|
recentHomeworks: recentHomeworks.map((item): HomeworkDto =>
|
|
toHomeworkDto(item, { userIds: student ? personalIds : [], withStatus: !student }),
|
|
),
|
|
};
|
|
}
|