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:
@@ -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") };
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user