refactor: RBAC权限系统重构 + UI组件拆分 + 测试修复 + 架构文档
Some checks failed
CI / build-deploy (push) Has been cancelled
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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user