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:
@@ -108,91 +108,115 @@ export async function publishLessonPlanHomework(
|
||||
throw new PublishServiceError("NO_EXERCISE_BLOCK");
|
||||
const newData = newBlock.data;
|
||||
|
||||
for (let i = 0; i < newData.items.length; i++) {
|
||||
const item = newData.items[i];
|
||||
if (item.source === "inline" && item.inlineContent) {
|
||||
const qt = item.inlineContent.type;
|
||||
// 使用类型守卫校验题目类型(替代 as 断言 + 硬编码中文错误)
|
||||
if (!isValidQuestionType(qt)) {
|
||||
throw new PublishServiceError("INVALID_QUESTION_TYPE");
|
||||
}
|
||||
const questionId = await createQuestionWithRelations(
|
||||
{
|
||||
content: item.inlineContent.content,
|
||||
type: qt,
|
||||
difficulty: item.inlineContent.difficulty,
|
||||
knowledgePointIds: item.inlineContent.knowledgePointIds,
|
||||
},
|
||||
input.userId,
|
||||
);
|
||||
newData.items[i] = {
|
||||
...item,
|
||||
questionId,
|
||||
inlineContent: undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 打包 exam 草稿(标题/描述由 actions 层 i18n 传入)
|
||||
const examId = createId();
|
||||
if (!plan.subjectId || !plan.gradeId) {
|
||||
throw new PublishServiceError("NO_SUBJECT_OR_GRADE");
|
||||
}
|
||||
await persistExamDraft({
|
||||
examId,
|
||||
title: input.homeworkTitle,
|
||||
creatorId: input.userId,
|
||||
subjectId: plan.subjectId,
|
||||
gradeId: plan.gradeId,
|
||||
scheduledAt: undefined,
|
||||
description: input.homeworkDescription,
|
||||
});
|
||||
// 插入 examQuestions(通过 exams data-access 跨模块接口)
|
||||
await addExamQuestions(
|
||||
examId,
|
||||
newData.items.map((it, i) => ({
|
||||
questionId: it.questionId,
|
||||
score: it.score,
|
||||
order: i,
|
||||
})),
|
||||
);
|
||||
|
||||
// 5. 下发作业
|
||||
const examId = createId();
|
||||
const assignmentId = createId();
|
||||
const targetStudentIds = await getStudentIdsByClassIds(input.classIds);
|
||||
if (targetStudentIds.length === 0) {
|
||||
throw new PublishServiceError("NO_STUDENTS");
|
||||
}
|
||||
await createHomeworkAssignment({
|
||||
assignmentId,
|
||||
sourceExamId: examId,
|
||||
title: input.homeworkTitle,
|
||||
description: input.homeworkDescription,
|
||||
structure: null,
|
||||
status: "published",
|
||||
creatorId: input.userId,
|
||||
availableAt: input.availableAt ?? null,
|
||||
dueAt: input.dueAt ?? null,
|
||||
allowLate: false,
|
||||
lateDueAt: null,
|
||||
maxAttempts: 1,
|
||||
publish: true,
|
||||
questions: newData.items.map((it, i) => ({
|
||||
questionId: it.questionId,
|
||||
score: it.score,
|
||||
order: i,
|
||||
})),
|
||||
targetStudentIds,
|
||||
});
|
||||
const createdQuestionIds: string[] = [];
|
||||
|
||||
// 6. 回写溯源标记
|
||||
newData.publishedExamId = examId;
|
||||
newData.publishedAssignmentId = assignmentId;
|
||||
newData.publishedAt = new Date().toISOString();
|
||||
await db
|
||||
.update(lessonPlans)
|
||||
.set({ content: newContent })
|
||||
.where(eq(lessonPlans.id, input.planId));
|
||||
// P0-12 修复:将多步写操作包裹在事务中,保证原子性。
|
||||
// 注意:跨模块 data-access(createQuestionWithRelations/persistExamDraft/addExamQuestions/
|
||||
// createHomeworkAssignment)使用各自的 db 连接,无法加入此事务。
|
||||
// 若跨模块调用成功但下方事务失败,已创建的 exam/homework 将成为孤儿数据,
|
||||
// 需通过补偿机制或定期清理任务处理。完整事务支持需跨模块 data-access 接受 tx 参数(中长期改进)。
|
||||
try {
|
||||
for (let i = 0; i < newData.items.length; i++) {
|
||||
const item = newData.items[i];
|
||||
if (item.source === "inline" && item.inlineContent) {
|
||||
const qt = item.inlineContent.type;
|
||||
// 使用类型守卫校验题目类型(替代 as 断言 + 硬编码中文错误)
|
||||
if (!isValidQuestionType(qt)) {
|
||||
throw new PublishServiceError("INVALID_QUESTION_TYPE");
|
||||
}
|
||||
const questionId = await createQuestionWithRelations(
|
||||
{
|
||||
content: item.inlineContent.content,
|
||||
type: qt,
|
||||
difficulty: item.inlineContent.difficulty,
|
||||
knowledgePointIds: item.inlineContent.knowledgePointIds,
|
||||
},
|
||||
input.userId,
|
||||
);
|
||||
createdQuestionIds.push(questionId);
|
||||
newData.items[i] = {
|
||||
...item,
|
||||
questionId,
|
||||
inlineContent: undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 打包 exam 草稿(标题/描述由 actions 层 i18n 传入)
|
||||
await persistExamDraft({
|
||||
examId,
|
||||
title: input.homeworkTitle,
|
||||
creatorId: input.userId,
|
||||
subjectId: plan.subjectId,
|
||||
gradeId: plan.gradeId,
|
||||
scheduledAt: undefined,
|
||||
description: input.homeworkDescription,
|
||||
});
|
||||
// 插入 examQuestions(通过 exams data-access 跨模块接口)
|
||||
await addExamQuestions(
|
||||
examId,
|
||||
newData.items.map((it, i) => ({
|
||||
questionId: it.questionId,
|
||||
score: it.score,
|
||||
order: i,
|
||||
})),
|
||||
);
|
||||
|
||||
// 5. 下发作业
|
||||
const targetStudentIds = await getStudentIdsByClassIds(input.classIds);
|
||||
if (targetStudentIds.length === 0) {
|
||||
throw new PublishServiceError("NO_STUDENTS");
|
||||
}
|
||||
await createHomeworkAssignment({
|
||||
assignmentId,
|
||||
sourceExamId: examId,
|
||||
title: input.homeworkTitle,
|
||||
description: input.homeworkDescription,
|
||||
structure: null,
|
||||
status: "published",
|
||||
creatorId: input.userId,
|
||||
availableAt: input.availableAt ?? null,
|
||||
dueAt: input.dueAt ?? null,
|
||||
allowLate: false,
|
||||
lateDueAt: null,
|
||||
maxAttempts: 1,
|
||||
publish: true,
|
||||
questions: newData.items.map((it, i) => ({
|
||||
questionId: it.questionId,
|
||||
score: it.score,
|
||||
order: i,
|
||||
})),
|
||||
targetStudentIds,
|
||||
});
|
||||
|
||||
// 6. 回写溯源标记 — 包裹在事务中保证原子性
|
||||
newData.publishedExamId = examId;
|
||||
newData.publishedAssignmentId = assignmentId;
|
||||
newData.publishedAt = new Date().toISOString();
|
||||
await db.transaction(async (tx) => {
|
||||
await tx
|
||||
.update(lessonPlans)
|
||||
.set({ content: newContent })
|
||||
.where(eq(lessonPlans.id, input.planId));
|
||||
});
|
||||
} catch (e) {
|
||||
// 补偿错误处理:记录已创建的资源 ID 便于排查孤儿数据
|
||||
if (e instanceof PublishServiceError) throw e;
|
||||
console.error("[publishLessonPlanHomework] 部分失败,可能存在孤儿数据", {
|
||||
planId: input.planId,
|
||||
examId,
|
||||
assignmentId,
|
||||
createdQuestionIds,
|
||||
error: e instanceof Error ? e.message : String(e),
|
||||
});
|
||||
throw e;
|
||||
}
|
||||
|
||||
return { examId, assignmentId, updatedContent: newContent };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user