2026-10-11 11:49:32 +08:00
|
|
|
|
import type { CourseDto } from '@classhelper/shared';
|
2026-09-12 15:02:02 +08:00
|
|
|
|
import { assertCanManageSchedule, classScopeWhere, resolveClassScope } from '../../lib/access.js';
|
2026-09-11 21:57:38 +08:00
|
|
|
|
import { prisma } from '../../lib/db.js';
|
|
|
|
|
|
import { ApiError } from '../../lib/http.js';
|
|
|
|
|
|
import type { TokenPayload } from '../../lib/jwt.js';
|
|
|
|
|
|
import { toCourseDto } from '../../lib/mappers.js';
|
|
|
|
|
|
import type { CreateCourseInput, UpdateCourseInput } from './courses.schemas.js';
|
|
|
|
|
|
|
|
|
|
|
|
const teacherSelect = { select: { id: true, name: true, username: true } } as const;
|
|
|
|
|
|
|
|
|
|
|
|
export async function listCourses(user: TokenPayload, classId?: string): Promise<CourseDto[]> {
|
|
|
|
|
|
const scope = await resolveClassScope(user, classId);
|
|
|
|
|
|
const courses = await prisma.course.findMany({
|
|
|
|
|
|
where: classScopeWhere(scope),
|
|
|
|
|
|
include: { teacher: teacherSelect },
|
|
|
|
|
|
orderBy: [{ classId: 'asc' }, { name: 'asc' }],
|
|
|
|
|
|
});
|
|
|
|
|
|
return courses.map(toCourseDto);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export async function createCourse(user: TokenPayload, input: CreateCourseInput): Promise<CourseDto> {
|
2026-09-12 15:02:02 +08:00
|
|
|
|
await assertCanManageSchedule(user, input.classId);
|
2026-09-11 21:57:38 +08:00
|
|
|
|
|
2026-10-11 11:49:32 +08:00
|
|
|
|
/*
|
|
|
|
|
|
* 同名去重(一个班级里科目名唯一)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 这条不是"友好提示",是**权限护栏**:`course.create` 会把创建者写成这门课的任课老师,
|
|
|
|
|
|
* 于是只要能再建一门同名的「数学」,创建者就凭空成了「数学」的老师 ——
|
|
|
|
|
|
* 后面那道 `assertCanManageSubjectContent` 的科目门随即形同虚设
|
|
|
|
|
|
* (可以布置该科作业、录入该科成绩、删该科内容)。这就是"非该科科任老师
|
|
|
|
|
|
* 能布置其他科作业"的入口。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 处理办法是把**已有**课程的 id 回给前端(409 + details.courseId),让前端复用它;
|
|
|
|
|
|
* 权限判定于是回到"你是不是这门课的任课老师"这个唯一事实上。
|
|
|
|
|
|
*/
|
|
|
|
|
|
const existing = await prisma.course.findFirst({
|
|
|
|
|
|
where: { classId: input.classId, name: input.name },
|
|
|
|
|
|
select: { id: true },
|
|
|
|
|
|
});
|
|
|
|
|
|
if (existing) {
|
|
|
|
|
|
throw new ApiError(409, 'COURSE_EXISTS', `该班级已有名为「${input.name}」的课程`, {
|
|
|
|
|
|
courseId: existing.id,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-11 21:57:38 +08:00
|
|
|
|
const created = await prisma.course.create({
|
|
|
|
|
|
data: { name: input.name, classId: input.classId, teacherId: user.sub },
|
|
|
|
|
|
include: { teacher: teacherSelect },
|
|
|
|
|
|
});
|
|
|
|
|
|
return toCourseDto(created);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export async function updateCourse(
|
|
|
|
|
|
user: TokenPayload,
|
|
|
|
|
|
courseId: string,
|
|
|
|
|
|
input: UpdateCourseInput,
|
|
|
|
|
|
): Promise<CourseDto> {
|
|
|
|
|
|
const course = await prisma.course.findUnique({ where: { id: courseId } });
|
|
|
|
|
|
if (!course) throw ApiError.notFound('课程不存在');
|
2026-09-12 15:02:02 +08:00
|
|
|
|
await assertCanManageSchedule(user, course.classId);
|
2026-09-11 21:57:38 +08:00
|
|
|
|
|
|
|
|
|
|
const updated = await prisma.course.update({
|
|
|
|
|
|
where: { id: courseId },
|
|
|
|
|
|
data: { ...(input.name ? { name: input.name } : {}) },
|
|
|
|
|
|
include: { teacher: teacherSelect },
|
|
|
|
|
|
});
|
|
|
|
|
|
return toCourseDto(updated);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export async function deleteCourse(user: TokenPayload, courseId: string): Promise<void> {
|
|
|
|
|
|
const course = await prisma.course.findUnique({ where: { id: courseId } });
|
|
|
|
|
|
if (!course) throw ApiError.notFound('课程不存在');
|
2026-09-12 15:02:02 +08:00
|
|
|
|
await assertCanManageSchedule(user, course.classId);
|
2026-09-11 21:57:38 +08:00
|
|
|
|
await prisma.course.delete({ where: { id: courseId } });
|
|
|
|
|
|
}
|