- 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)
200 lines
6.5 KiB
TypeScript
200 lines
6.5 KiB
TypeScript
/**
|
||
* 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));
|
||
}
|