fix(textbooks): 规范核查修复 — 安全漏洞+功能缺失+i18n+类型安全

安全:createPrerequisiteAction 补充 prerequisiteKpId 归属校验;deletePrerequisiteAction 补充双知识点归属校验,防止跨教材越权。

功能:实现图谱添加/删除前置依赖(Dialog + Select 选择知识点 + 调用 Server Action + 自动刷新图谱),替换原 no-op 回调。

i18n:修复 8 处硬编码英文字符串(textbook-reader/chapter-sidebar-list/textbook-card/textbook-form-dialog/textbook-settings-dialog/create-chapter-dialog/teacher-textbook-reader),新增 saveFailed/createFailed/updateFailed/deleteFailed/questionCreatorDefaultContent 等 key。

类型安全:graph-prerequisite-edge.tsx 使用 GraphEdgeData 类型经 unknown 安全转换,替代裸 as 断言。

规范:analytics.tsx 移动 use client 指令到文件第一行;同步架构文档 005 JSON 类型定义(GraphNodeData/GraphEdgeData/MasteryLevel)。

验证:教材模块 lint 零错误、tsc 零错误、193 个单元测试全部通过。
This commit is contained in:
SpecialX
2026-06-23 00:30:14 +08:00
parent 58656da983
commit ec87cd9efa
14 changed files with 388 additions and 104 deletions

View File

@@ -426,7 +426,7 @@ export async function createPrerequisiteAction(
return { success: false, message: t("invalidInput") };
}
// 归属校验
// 归属校验:两个知识点都必须属于当前教材,防止跨教材越权
const kpBelongs = await verifyKnowledgePointBelongsToTextbook(
parsed.data.knowledgePointId,
textbookId,
@@ -434,6 +434,13 @@ export async function createPrerequisiteAction(
if (!kpBelongs) {
return { success: false, message: t("kpNotBelong") };
}
const prereqBelongs = await verifyKnowledgePointBelongsToTextbook(
parsed.data.prerequisiteKpId,
textbookId,
);
if (!prereqBelongs) {
return { success: false, message: t("kpNotBelong") };
}
// 循环检测
const existingEdges = await getPrerequisiteEdgesForTextbook(textbookId);
@@ -475,6 +482,22 @@ export async function deletePrerequisiteAction(
return { success: false, message: t("invalidInput") };
}
// 归属校验:防止跨教材越权删除前置依赖
const kpBelongs = await verifyKnowledgePointBelongsToTextbook(
parsed.data.knowledgePointId,
textbookId,
);
if (!kpBelongs) {
return { success: false, message: t("kpNotBelong") };
}
const prereqBelongs = await verifyKnowledgePointBelongsToTextbook(
parsed.data.prerequisiteKpId,
textbookId,
);
if (!prereqBelongs) {
return { success: false, message: t("kpNotBelong") };
}
await deletePrerequisite(parsed.data);
revalidatePath(`/teacher/textbooks/${textbookId}`);
return { success: true, message: t("prerequisiteDeleted") };