feat(lesson-preparation): add AI evaluation, analytics, attachments, calendar, comments, review, substitutes, formative, and version diff

- Add actions-ai-evaluation, actions-analytics, actions-attachments, actions-calendar, actions-comments, actions-formative, actions-questions, actions-review, actions-substitutes

- Add corresponding data-access layers for each new action module

- Add calendar-view, curriculum-map-view, version-diff-viewer components

- Add editor-slice, selection-slice, version-slice hooks for state management

- Add document-diff and scope-check lib utilities

- Add default-question-service and external-questions-bridge services
This commit is contained in:
SpecialX
2026-07-03 10:25:21 +08:00
parent a16f09d3c3
commit 20023e13fd
75 changed files with 5131 additions and 1186 deletions

View File

@@ -59,9 +59,19 @@ export async function createLessonPlanVersion(input: {
userId: string;
isAuto: boolean;
label?: string;
}): Promise<{ versionNo: number }> {
}): Promise<{ versionNo: number } | null> {
// P0 修复max(versionNo)+1 必须在事务内完成,避免并发产生重复版本号
return await db.transaction(async (tx) => {
// 校验 planId 归属
const plan = await tx
.select({ id: lessonPlans.id })
.from(lessonPlans)
.where(
and(eq(lessonPlans.id, input.planId), eq(lessonPlans.creatorId, input.userId)),
)
.limit(1);
if (plan.length === 0) return null;
const maxRow = await tx
.select({ maxNo: max(lessonPlanVersions.versionNo) })
.from(lessonPlanVersions)
@@ -151,8 +161,17 @@ export async function revertToVersion(
export async function pruneAutoVersions(
planId: string,
userId: string,
keep = 50,
): Promise<void> {
): Promise<number> {
// 校验 planId 归属
const plan = await db
.select({ id: lessonPlans.id })
.from(lessonPlans)
.where(and(eq(lessonPlans.id, planId), eq(lessonPlans.creatorId, userId)))
.limit(1);
if (plan.length === 0) return 0;
const rows = await db
.select({
id: lessonPlanVersions.id,
@@ -163,10 +182,10 @@ export async function pruneAutoVersions(
.where(eq(lessonPlanVersions.planId, planId))
.orderBy(desc(lessonPlanVersions.versionNo));
if (rows.length <= keep) return;
if (rows.length <= keep) return 0;
// 保留前 keep 条;超出部分只删 isAuto=true 的
const toDelete = rows.slice(keep).filter((r) => r.isAuto);
if (toDelete.length === 0) return;
if (toDelete.length === 0) return 0;
await db
.delete(lessonPlanVersions)
.where(
@@ -178,4 +197,5 @@ export async function pruneAutoVersions(
),
),
);
return toDelete.length;
}