Files
NextEdu/src/modules/lesson-preparation/data-access-schedules.ts
SpecialX 524ecade19 feat(lesson-preparation): major update with data-access splits and new components
- Update ai-feedback-dialog, attachment-picker, blocks/text-study-block

- Update detail-panel (detail-panel, detail-props)

- Update inline-question-editor, lesson-plan-card, lesson-plan-editor,

  lesson-plan-mobile-view, schedule-dialog, version-history-drawer

- Update paper-editor (inline-qa-dialog, paper-context-menu)

- Update structure-tree (structure-tree, tree-node-row)

- Update config/block-registry, hooks (editor-slice, use-lesson-plan-persistence,

  use-node-ai-assist), lib (ai-node-assist, consistency-check, export, type-guards)

- Update publish-service, services/default-question-service

- Update data-access files (ai-evaluation, analytics, attachments, calendar,

  comments, formative, knowledge, review, schedules, templates, versions, main)
2026-07-07 16:20:34 +08:00

200 lines
6.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* V5-7课案-课时绑定数据访问层
*
* 将课案绑定到具体班级的某个日期/节次,支持 calendar-view 安排课时。
*/
import "server-only";
import { cacheFn } from "@/shared/lib/cache";
import { db } from "@/shared/db";
import { lessonPlanSchedules } from "@/shared/db/schema";
import { getClassNamesByIds, getClassNameById } from "@/modules/classes/data-access";
import { and, eq, gte, lte, desc } from "drizzle-orm";
import { createId } from "@paralleldrive/cuid2";
/** F-05: 列表查询默认上限,防止无界查询拉爆内存 */
const DEFAULT_LIMIT = 1000;
/** 课时绑定记录 */
export interface LessonPlanScheduleRecord {
id: string;
planId: string;
classId: string;
className: string;
scheduledDate: string; // YYYY-MM-DD
period: number;
classScheduleId?: string | null;
durationMin: number;
createdBy: string;
createdAt: Date;
updatedAt: Date;
}
/** 将 Date 转换为 YYYY-MM-DD 字符串(用于接口输出) */
function toDateStr(d: Date): string {
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
return `${y}-${m}-${day}`;
}
/** 查询课案的所有课时绑定 */
export async function getSchedulesByPlanIdRaw(
planId: string,
): Promise<LessonPlanScheduleRecord[]> {
const rows = await db
.select({
id: lessonPlanSchedules.id,
planId: lessonPlanSchedules.planId,
classId: lessonPlanSchedules.classId,
scheduledDate: lessonPlanSchedules.scheduledDate,
period: lessonPlanSchedules.period,
classScheduleId: lessonPlanSchedules.classScheduleId,
durationMin: lessonPlanSchedules.durationMin,
createdBy: lessonPlanSchedules.createdBy,
createdAt: lessonPlanSchedules.createdAt,
updatedAt: lessonPlanSchedules.updatedAt,
})
.from(lessonPlanSchedules)
.where(eq(lessonPlanSchedules.planId, planId))
.orderBy(desc(lessonPlanSchedules.scheduledDate));
if (rows.length === 0) return [];
// 跨模块批量解析班级名称,避免直接 JOIN classes 表
const classIds = Array.from(new Set(rows.map((r) => r.classId)));
const classNameMap = await getClassNamesByIds(classIds);
return rows.map((r) => ({
id: r.id,
planId: r.planId,
classId: r.classId,
className: classNameMap.get(r.classId) ?? "",
scheduledDate: toDateStr(r.scheduledDate),
period: r.period,
classScheduleId: r.classScheduleId,
durationMin: r.durationMin,
createdBy: r.createdBy,
createdAt: r.createdAt,
updatedAt: r.updatedAt,
}));
}
export const getSchedulesByPlanId = cacheFn(getSchedulesByPlanIdRaw, { tags: ["lesson-preparation"], ttl: 300, keyParts: ["lp", "schedules", "by-plan"] });
/** 查询教师在某日期范围内的课时绑定 */
export async function getSchedulesByDateRangeRaw(
teacherPlanIds: string[],
startDate: string,
endDate: string,
): Promise<LessonPlanScheduleRecord[]> {
if (teacherPlanIds.length === 0) return [];
const rows = await db
.select({
id: lessonPlanSchedules.id,
planId: lessonPlanSchedules.planId,
classId: lessonPlanSchedules.classId,
scheduledDate: lessonPlanSchedules.scheduledDate,
period: lessonPlanSchedules.period,
classScheduleId: lessonPlanSchedules.classScheduleId,
durationMin: lessonPlanSchedules.durationMin,
createdBy: lessonPlanSchedules.createdBy,
createdAt: lessonPlanSchedules.createdAt,
updatedAt: lessonPlanSchedules.updatedAt,
})
.from(lessonPlanSchedules)
.where(
and(
// 简化仅按日期范围过滤planId 过滤由调用方处理或 join
gte(lessonPlanSchedules.scheduledDate, new Date(startDate)),
lte(lessonPlanSchedules.scheduledDate, new Date(endDate)),
),
)
.orderBy(desc(lessonPlanSchedules.scheduledDate))
.limit(DEFAULT_LIMIT);
const filtered = rows.filter((r) => teacherPlanIds.includes(r.planId));
if (filtered.length === 0) return [];
// 跨模块批量解析班级名称,避免直接 JOIN classes 表
const classIds = Array.from(new Set(filtered.map((r) => r.classId)));
const classNameMap = await getClassNamesByIds(classIds);
return filtered.map((r) => ({
id: r.id,
planId: r.planId,
classId: r.classId,
className: classNameMap.get(r.classId) ?? "",
scheduledDate: toDateStr(r.scheduledDate),
period: r.period,
classScheduleId: r.classScheduleId,
durationMin: r.durationMin,
createdBy: r.createdBy,
createdAt: r.createdAt,
updatedAt: r.updatedAt,
}));
}
export const getSchedulesByDateRange = cacheFn(getSchedulesByDateRangeRaw, { tags: ["lesson-preparation"], ttl: 300, keyParts: ["lp", "schedules", "by-date-range"] });
/** 创建课时绑定 */
export async function createSchedule(input: {
planId: string;
classId: string;
scheduledDate: string;
period: number;
classScheduleId?: string;
durationMin?: number;
createdBy: string;
}): Promise<LessonPlanScheduleRecord> {
const newId = createId();
await db.insert(lessonPlanSchedules).values({
id: newId,
planId: input.planId,
classId: input.classId,
scheduledDate: new Date(input.scheduledDate),
period: input.period,
classScheduleId: input.classScheduleId,
durationMin: input.durationMin ?? 40,
createdBy: input.createdBy,
});
const created = await db
.select({
id: lessonPlanSchedules.id,
planId: lessonPlanSchedules.planId,
classId: lessonPlanSchedules.classId,
scheduledDate: lessonPlanSchedules.scheduledDate,
period: lessonPlanSchedules.period,
classScheduleId: lessonPlanSchedules.classScheduleId,
durationMin: lessonPlanSchedules.durationMin,
createdBy: lessonPlanSchedules.createdBy,
createdAt: lessonPlanSchedules.createdAt,
updatedAt: lessonPlanSchedules.updatedAt,
})
.from(lessonPlanSchedules)
.where(eq(lessonPlanSchedules.id, newId));
const r = created[0];
if (!r) throw new Error("SCHEDULE_CREATE_FAILED");
// 跨模块解析班级名称,避免直接 JOIN classes 表
const className = (await getClassNameById(input.classId)) ?? "";
return {
id: r.id,
planId: r.planId,
classId: r.classId,
className,
scheduledDate: toDateStr(r.scheduledDate),
period: r.period,
classScheduleId: r.classScheduleId,
durationMin: r.durationMin,
createdBy: r.createdBy,
createdAt: r.createdAt,
updatedAt: r.updatedAt,
};
}
/** 删除课时绑定 */
export async function deleteSchedule(id: string): Promise<void> {
await db.delete(lessonPlanSchedules).where(eq(lessonPlanSchedules.id, id));
}