refactor: RBAC权限系统重构 + UI组件拆分 + 测试修复 + 架构文档
Some checks failed
CI / build-deploy (push) Has been cancelled

- RBAC: 新增30个权限点、DataScope行级权限、requirePermission守卫,所有57+ Server Action接入权限校验
- UI拆分: exam-form(1623行→11文件)、textbook-reader(744行→7文件),均降至300行以内
- 测试: 新增5个单元测试文件(19用例),修复4个集成测试文件(38用例全部通过)
- 架构文档: 新增架构影响地图(004/005)、标准功能清单(006)、差距审计报告(007)
- 项目规则: 架构图优先规则,改码必同步图
- 安全: rehype-sanitize净化、AES加密API Key、权限路由守卫
- 无障碍: skip-link、aria-label、prefers-reduced-motion
- 性能: next/font优化、next/image、代码分割
This commit is contained in:
SpecialX
2026-06-16 23:38:33 +08:00
parent 99f116cb64
commit 125f7ec54c
75 changed files with 9480 additions and 3289 deletions

View File

@@ -1,10 +1,11 @@
import { db } from "@/shared/db"
import { exams, examQuestions, questions, subjects, grades } from "@/shared/db/schema"
import { eq, desc, like, and, or } from "drizzle-orm"
import { exams, examQuestions, questions, subjects, grades, classes } from "@/shared/db/schema"
import { eq, desc, like, and, or, inArray } from "drizzle-orm"
import { cache } from "react"
import type { Exam, ExamDifficulty, ExamStatus } from "./types"
import type { AiGeneratedQuestion, AiGeneratedStructureNode } from "./ai-pipeline"
import type { DataScope } from "@/shared/types/permissions"
export type GetExamsParams = {
q?: string
@@ -49,7 +50,7 @@ const toExamDifficulty = (n: number | undefined): ExamDifficulty => {
}
export const getExams = cache(async (params: GetExamsParams) => {
export const getExams = cache(async (params: GetExamsParams & { scope: DataScope }) => {
const conditions = []
if (params.q) {
@@ -61,7 +62,28 @@ export const getExams = cache(async (params: GetExamsParams) => {
conditions.push(eq(exams.status, params.status))
}
// Note: Difficulty is stored in JSON description field in current schema,
// Data scope filtering
if (params.scope.type === "owned") {
conditions.push(eq(exams.creatorId, params.scope.userId))
}
if (params.scope.type === "class_taught" && params.scope.classIds.length > 0) {
// Teacher can see exams for grades their classes belong to
const teacherGradeIds = await db
.selectDistinct({ gradeId: classes.gradeId })
.from(classes)
.where(inArray(classes.id, params.scope.classIds))
const gradeIds = teacherGradeIds.map(g => g.gradeId).filter(Boolean) as string[]
if (gradeIds.length > 0) {
conditions.push(inArray(exams.gradeId, gradeIds))
}
}
if (params.scope.type === "grade_managed" && params.scope.gradeIds.length > 0) {
conditions.push(inArray(exams.gradeId, params.scope.gradeIds))
}
// "all" type: no filtering
// "class_members": student sees published exams for their grade (would need student's gradeId)
// Note: Difficulty is stored in JSON description field in current schema,
// so we might need to filter in memory or adjust schema.
// For now, let's fetch and filter in memory if difficulty is needed,
// or just ignore strict DB filtering for JSON fields to keep it simple.
@@ -104,7 +126,7 @@ export const getExams = cache(async (params: GetExamsParams) => {
return result
})
export const getExamById = cache(async (id: string) => {
export const getExamById = cache(async (id: string, scope?: DataScope) => {
const exam = await db.query.exams.findFirst({
where: eq(exams.id, id),
with: {
@@ -121,6 +143,26 @@ export const getExamById = cache(async (id: string) => {
if (!exam) return null
// Data scope verification for single-item fetch
if (scope && scope.type !== "all") {
if (scope.type === "owned" && exam.creatorId !== scope.userId) {
return null
}
if (scope.type === "grade_managed" && scope.gradeIds.length > 0 && !scope.gradeIds.includes(exam.gradeId ?? "")) {
return null
}
if (scope.type === "class_taught" && scope.classIds.length > 0) {
const teacherGradeIds = await db
.selectDistinct({ gradeId: classes.gradeId })
.from(classes)
.where(inArray(classes.id, scope.classIds))
const gradeIds = teacherGradeIds.map(g => g.gradeId).filter(Boolean) as string[]
if (gradeIds.length > 0 && !gradeIds.includes(exam.gradeId ?? "")) {
return null
}
}
}
const meta = parseExamMeta(exam.description || null)
return {