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 文档
This commit is contained in:
SpecialX
2026-06-22 18:45:35 +08:00
parent 1fe30984b6
commit 97e59b95a1
23 changed files with 668 additions and 135 deletions

View File

@@ -1,6 +1,7 @@
"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 {
@@ -10,6 +11,7 @@ import {
updateLessonPlanContent,
softDeleteLessonPlan,
duplicateLessonPlan,
LessonPlanDataError,
} from "./data-access";
import {
getLessonPlanVersions,
@@ -43,6 +45,7 @@ export async function getLessonPlansAction(params: {
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);
@@ -50,7 +53,7 @@ export async function getLessonPlansAction(params: {
} catch (e) {
if (e instanceof PermissionDeniedError)
return { success: false, message: e.message };
return { success: false, message: "获取课案列表失败" };
return { success: false, message: t("error.getList") };
}
}
@@ -60,15 +63,16 @@ export async function getLessonPlanByIdAction(
): 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: "课案不存在或无权访问" };
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: "获取课案失败" };
return { success: false, message: t("error.getOne") };
}
}
@@ -77,6 +81,7 @@ 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({
@@ -93,13 +98,30 @@ export async function createLessonPlanAction(
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 };
return { success: false, message: "创建课案失败" };
if (e instanceof LessonPlanDataError && e.code === "TEMPLATE_NOT_FOUND")
return { success: false, message: t("error.templateNotFound") };
return { success: false, message: t("error.create") };
}
}
@@ -109,6 +131,7 @@ export async function updateLessonPlanAction(input: {
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);
@@ -122,7 +145,7 @@ export async function updateLessonPlanAction(input: {
} catch (e) {
if (e instanceof PermissionDeniedError)
return { success: false, message: e.message };
return { success: false, message: "保存失败" };
return { success: false, message: t("error.save") };
}
}
@@ -132,6 +155,7 @@ export async function saveLessonPlanVersionAction(input: {
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);
@@ -149,7 +173,7 @@ export async function saveLessonPlanVersionAction(input: {
} catch (e) {
if (e instanceof PermissionDeniedError)
return { success: false, message: e.message };
return { success: false, message: "保存版本失败" };
return { success: false, message: t("error.saveVersion") };
}
}
@@ -161,6 +185,7 @@ export async function getLessonPlanVersionsAction(
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);
@@ -168,7 +193,7 @@ export async function getLessonPlanVersionsAction(
} catch (e) {
if (e instanceof PermissionDeniedError)
return { success: false, message: e.message };
return { success: false, message: "获取版本失败" };
return { success: false, message: t("error.getVersions") };
}
}
@@ -177,6 +202,7 @@ 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);
@@ -187,13 +213,13 @@ export async function revertLessonPlanVersionAction(input: {
parsed.data.versionNo,
ctx.userId,
);
if (!result) return { success: false, message: "版本不存在或无权操作" };
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: "回退失败" };
return { success: false, message: t("error.revert") };
}
}
@@ -201,6 +227,7 @@ export async function revertLessonPlanVersionAction(input: {
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);
@@ -209,7 +236,7 @@ export async function deleteLessonPlanAction(
} catch (e) {
if (e instanceof PermissionDeniedError)
return { success: false, message: e.message };
return { success: false, message: "删除失败" };
return { success: false, message: t("error.delete") };
}
}
@@ -217,6 +244,7 @@ export async function deleteLessonPlanAction(
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);
@@ -225,7 +253,9 @@ export async function duplicateLessonPlanAction(
} catch (e) {
if (e instanceof PermissionDeniedError)
return { success: false, message: e.message };
return { success: false, message: "复制失败" };
if (e instanceof LessonPlanDataError && e.code === "NOT_FOUND")
return { success: false, message: t("error.notFound") };
return { success: false, message: t("error.duplicate") };
}
}
@@ -235,6 +265,7 @@ export async function getLessonPlanTemplatesAction(): Promise<
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);
@@ -242,7 +273,7 @@ export async function getLessonPlanTemplatesAction(): Promise<
} catch (e) {
if (e instanceof PermissionDeniedError)
return { success: false, message: e.message };
return { success: false, message: "获取模板失败" };
return { success: false, message: t("error.getTemplates") };
}
}
@@ -251,6 +282,7 @@ 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);
@@ -264,7 +296,9 @@ export async function saveAsTemplateAction(input: {
} catch (e) {
if (e instanceof PermissionDeniedError)
return { success: false, message: e.message };
return { success: false, message: "保存模板失败" };
if (e instanceof LessonPlanDataError && e.code === "NOT_FOUND")
return { success: false, message: t("error.notFound") };
return { success: false, message: t("error.saveTemplate") };
}
}
@@ -272,6 +306,7 @@ export async function saveAsTemplateAction(input: {
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);
@@ -279,6 +314,6 @@ export async function deleteTemplateAction(
} catch (e) {
if (e instanceof PermissionDeniedError)
return { success: false, message: e.message };
return { success: false, message: "删除模板失败" };
return { success: false, message: t("error.deleteTemplate") };
}
}