Files
NextEdu/src/modules/lesson-preparation/actions.ts
SpecialX 97e59b95a1 refactor(lesson-preparation): V2 审计深度修复 — Server Actions i18n + 错误码模式 + 类型断言清零 + a11y 深度修复 + Tracker 埋点接入
V2-1: 12 个 Server Action 通过 getTranslations 翻译错误消息;Service/DataAccess 层抛出错误码异常(PublishServiceError/LessonPlanDataError),Actions 层通过 PUBLISH_ERROR_KEY_MAP 翻译为 i18n 消息
V2-2: SYSTEM_TEMPLATES name/title 改为 i18n 键,createLessonPlan 接受 translateTitle 函数在服务端翻译后存储到 DB
V2-3: 8 处 as unknown as 断言替换为显式类型映射函数(mapRowToLessonPlan/mapRowToListItem/mapRowToTemplate/mapRowToVersion)+ 类型守卫(isLessonPlanStatus/isTemplateType/isTemplateScope)
V2-4: MiniMap nodeColor 复用 lib/node-summary.ts 的 getNodeColor
V2-5: a11y 深度修复 — lesson-plan-filters/exercise-block/inline-question-editor 的 select 添加 label htmlFor 关联;exercise-block 题目列表改为 ul/li;node-editor 画布添加 role=application + 键盘导航配置
V2-6: Tracker 埋点接入 — 新增 useLessonPlanTrackerSafe hook,在 create/save/publish/revert/duplicate/archive 6 处调用 tracker.track

同步更新架构图 004 和 005 文档
2026-06-22 18:45:35 +08:00

320 lines
11 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.
"use server";
import { revalidatePath } from "next/cache";
import { getTranslations } from "next-intl/server";
import { requirePermission, PermissionDeniedError } from "@/shared/lib/auth-guard";
import { Permissions } from "@/shared/types/permissions";
import {
getLessonPlans,
getLessonPlanById,
createLessonPlan,
updateLessonPlanContent,
softDeleteLessonPlan,
duplicateLessonPlan,
LessonPlanDataError,
} from "./data-access";
import {
getLessonPlanVersions,
createLessonPlanVersion,
revertToVersion,
pruneAutoVersions,
} from "./data-access-versions";
import {
getLessonPlanTemplates,
saveAsTemplate,
deletePersonalTemplate,
} from "./data-access-templates";
import {
createLessonPlanSchema,
updateLessonPlanContentSchema,
saveVersionSchema,
revertVersionSchema,
saveAsTemplateSchema,
} from "./schema";
import type { ActionState, LessonPlanDocument } from "./types";
// ---- 课案列表 ----
export async function getLessonPlansAction(params: {
query?: string;
textbookId?: string;
chapterId?: string;
subjectId?: string;
status?: string;
}): Promise<
ActionState<{
items: Awaited<ReturnType<typeof getLessonPlans>>;
}>
> {
const t = await getTranslations("lessonPreparation");
try {
const ctx = await requirePermission(Permissions.LESSON_PLAN_READ);
const items = await getLessonPlans(params, ctx.dataScope, ctx.userId);
return { success: true, data: { items } };
} catch (e) {
if (e instanceof PermissionDeniedError)
return { success: false, message: e.message };
return { success: false, message: t("error.getList") };
}
}
// ---- 单课案 ----
export async function getLessonPlanByIdAction(
planId: string,
): Promise<
ActionState<{ plan: Awaited<ReturnType<typeof getLessonPlanById>> }>
> {
const t = await getTranslations("lessonPreparation");
try {
const ctx = await requirePermission(Permissions.LESSON_PLAN_READ);
const plan = await getLessonPlanById(planId, ctx.userId);
if (!plan) return { success: false, message: t("error.notFound") };
return { success: true, data: { plan } };
} catch (e) {
if (e instanceof PermissionDeniedError)
return { success: false, message: e.message };
return { success: false, message: t("error.getOne") };
}
}
// ---- 创建 ----
export async function createLessonPlanAction(
prevState: ActionState | null,
formData: FormData,
): Promise<ActionState<{ planId: string }>> {
const t = await getTranslations("lessonPreparation");
try {
const ctx = await requirePermission(Permissions.LESSON_PLAN_CREATE);
const parsed = createLessonPlanSchema.safeParse({
title: formData.get("title"),
textbookId: formData.get("textbookId") || undefined,
chapterId: formData.get("chapterId") || undefined,
subjectId: formData.get("subjectId") || undefined,
gradeId: formData.get("gradeId") || undefined,
templateId: formData.get("templateId"),
});
if (!parsed.success) {
return { success: false, errors: parsed.error.flatten().fieldErrors };
}
const { planId } = await createLessonPlan({
...parsed.data,
creatorId: ctx.userId,
// V2-2 修复:传入翻译函数,将 SYSTEM_TEMPLATES 中的 i18n 键翻译为实际文本
translateTitle: (key: string) => {
// 支持 "blockType.objective" 和 "template.blocks.tpl_review.1" 两种键
if (key.startsWith("blockType.")) {
const blockKey = key.replace("blockType.", "");
return t(`blockType.${blockKey}`);
}
if (key.startsWith("template.blocks.")) {
const parts = key.split(".");
const templateId = parts[2];
const blockIndex = parts[3];
return t(`template.blocks.${templateId}.${blockIndex}`);
}
return key;
},
});
revalidatePath("/teacher/lesson-plans");
return { success: true, data: { planId } };
} catch (e) {
if (e instanceof PermissionDeniedError)
return { success: false, message: e.message };
if (e instanceof LessonPlanDataError && e.code === "TEMPLATE_NOT_FOUND")
return { success: false, message: t("error.templateNotFound") };
return { success: false, message: t("error.create") };
}
}
// ---- 更新 content自动保存----
export async function updateLessonPlanAction(input: {
planId: string;
title?: string;
content: LessonPlanDocument;
}): Promise<ActionState> {
const t = await getTranslations("lessonPreparation");
try {
const ctx = await requirePermission(Permissions.LESSON_PLAN_UPDATE);
const parsed = updateLessonPlanContentSchema.safeParse(input);
if (!parsed.success)
return { success: false, errors: parsed.error.flatten().fieldErrors };
await updateLessonPlanContent(parsed.data.planId, ctx.userId, {
...(parsed.data.title ? { title: parsed.data.title } : {}),
content: parsed.data.content as LessonPlanDocument,
});
return { success: true };
} catch (e) {
if (e instanceof PermissionDeniedError)
return { success: false, message: e.message };
return { success: false, message: t("error.save") };
}
}
// ---- 手动保存版本 ----
export async function saveLessonPlanVersionAction(input: {
planId: string;
content: LessonPlanDocument;
label?: string;
}): Promise<ActionState<{ versionNo: number }>> {
const t = await getTranslations("lessonPreparation");
try {
const ctx = await requirePermission(Permissions.LESSON_PLAN_UPDATE);
const parsed = saveVersionSchema.safeParse(input);
if (!parsed.success)
return { success: false, errors: parsed.error.flatten().fieldErrors };
const { versionNo } = await createLessonPlanVersion({
planId: parsed.data.planId,
content: input.content,
userId: ctx.userId,
isAuto: false,
label: parsed.data.label,
});
await pruneAutoVersions(parsed.data.planId);
return { success: true, data: { versionNo } };
} catch (e) {
if (e instanceof PermissionDeniedError)
return { success: false, message: e.message };
return { success: false, message: t("error.saveVersion") };
}
}
// ---- 版本列表 ----
export async function getLessonPlanVersionsAction(
planId: string,
): Promise<
ActionState<{
versions: Awaited<ReturnType<typeof getLessonPlanVersions>>;
}>
> {
const t = await getTranslations("lessonPreparation");
try {
const ctx = await requirePermission(Permissions.LESSON_PLAN_READ);
const versions = await getLessonPlanVersions(planId, ctx.userId);
return { success: true, data: { versions } };
} catch (e) {
if (e instanceof PermissionDeniedError)
return { success: false, message: e.message };
return { success: false, message: t("error.getVersions") };
}
}
// ---- 回退版本 ----
export async function revertLessonPlanVersionAction(input: {
planId: string;
versionNo: number;
}): Promise<ActionState<{ newVersionNo: number }>> {
const t = await getTranslations("lessonPreparation");
try {
const ctx = await requirePermission(Permissions.LESSON_PLAN_UPDATE);
const parsed = revertVersionSchema.safeParse(input);
if (!parsed.success)
return { success: false, errors: parsed.error.flatten().fieldErrors };
const result = await revertToVersion(
parsed.data.planId,
parsed.data.versionNo,
ctx.userId,
);
if (!result) return { success: false, message: t("error.versionNotFound") };
revalidatePath(`/teacher/lesson-plans/${parsed.data.planId}/edit`);
return { success: true, data: { newVersionNo: result.newVersionNo } };
} catch (e) {
if (e instanceof PermissionDeniedError)
return { success: false, message: e.message };
return { success: false, message: t("error.revert") };
}
}
// ---- 删除(软删除)----
export async function deleteLessonPlanAction(
planId: string,
): Promise<ActionState> {
const t = await getTranslations("lessonPreparation");
try {
const ctx = await requirePermission(Permissions.LESSON_PLAN_DELETE);
await softDeleteLessonPlan(planId, ctx.userId);
revalidatePath("/teacher/lesson-plans");
return { success: true };
} catch (e) {
if (e instanceof PermissionDeniedError)
return { success: false, message: e.message };
return { success: false, message: t("error.delete") };
}
}
// ---- 复制 ----
export async function duplicateLessonPlanAction(
planId: string,
): Promise<ActionState<{ newPlanId: string }>> {
const t = await getTranslations("lessonPreparation");
try {
const ctx = await requirePermission(Permissions.LESSON_PLAN_CREATE);
const { newPlanId } = await duplicateLessonPlan(planId, ctx.userId);
revalidatePath("/teacher/lesson-plans");
return { success: true, data: { newPlanId } };
} catch (e) {
if (e instanceof PermissionDeniedError)
return { success: false, message: e.message };
if (e instanceof LessonPlanDataError && e.code === "NOT_FOUND")
return { success: false, message: t("error.notFound") };
return { success: false, message: t("error.duplicate") };
}
}
// ---- 模板列表 ----
export async function getLessonPlanTemplatesAction(): Promise<
ActionState<{
templates: Awaited<ReturnType<typeof getLessonPlanTemplates>>;
}>
> {
const t = await getTranslations("lessonPreparation");
try {
const ctx = await requirePermission(Permissions.LESSON_PLAN_READ);
const templates = await getLessonPlanTemplates(ctx.userId);
return { success: true, data: { templates } };
} catch (e) {
if (e instanceof PermissionDeniedError)
return { success: false, message: e.message };
return { success: false, message: t("error.getTemplates") };
}
}
// ---- 另存为模板 ----
export async function saveAsTemplateAction(input: {
sourcePlanId: string;
name: string;
}): Promise<ActionState<{ templateId: string }>> {
const t = await getTranslations("lessonPreparation");
try {
const ctx = await requirePermission(Permissions.LESSON_PLAN_CREATE);
const parsed = saveAsTemplateSchema.safeParse(input);
if (!parsed.success)
return { success: false, errors: parsed.error.flatten().fieldErrors };
const { templateId } = await saveAsTemplate({
...parsed.data,
userId: ctx.userId,
});
return { success: true, data: { templateId } };
} catch (e) {
if (e instanceof PermissionDeniedError)
return { success: false, message: e.message };
if (e instanceof LessonPlanDataError && e.code === "NOT_FOUND")
return { success: false, message: t("error.notFound") };
return { success: false, message: t("error.saveTemplate") };
}
}
// ---- 删除模板 ----
export async function deleteTemplateAction(
templateId: string,
): Promise<ActionState> {
const t = await getTranslations("lessonPreparation");
try {
const ctx = await requirePermission(Permissions.LESSON_PLAN_DELETE);
await deletePersonalTemplate(templateId, ctx.userId);
return { success: true };
} catch (e) {
if (e instanceof PermissionDeniedError)
return { success: false, message: e.message };
return { success: false, message: t("error.deleteTemplate") };
}
}