refactor(grades,diagnostic): 成绩和学情诊断模块审计修复

P0-1: 10 个页面补充 requirePermission 权限校验
P0-2: diagnostic/data-access-reports.ts 移除直查 users 表,改用 getUserNamesByIds
P0-3: 新增 grade/grades/diagnostic 三组 i18n 翻译文件(zh-CN/en)
P0-4: 新增 /management/grade 重定向页面

P1-2: 抽取 toNumber/normalize/buildScopeClassFilter 到 lib/grade-utils.ts
P1-3: 为 12 个 Action 新增 Zod safeParse 校验(schema.ts +12 查询 schema)
P1-4: 修复 as 断言违规,改用类型守卫函数

P2-2: 移除 diagnostic 组件中 Tailwind 任意值

同步更新架构图文档 004 和 005
This commit is contained in:
SpecialX
2026-06-22 16:23:34 +08:00
parent 20691f53ce
commit 45ee1ae43c
29 changed files with 2276 additions and 186 deletions

View File

@@ -0,0 +1,51 @@
import "server-only"
import { eq, inArray, sql, type SQL } from "drizzle-orm"
import { gradeRecords } from "@/shared/db/schema"
import type { DataScope } from "@/shared/types/permissions"
/**
* Safely convert an unknown value to a finite number.
* Returns 0 when the value is not a finite number.
*
* Used to normalize numeric columns returned by Drizzle (which may be
* string | number depending on the driver) into plain numbers.
*/
export const toNumber = (v: unknown): number => {
const n = typeof v === "number" ? v : Number(v)
return Number.isFinite(n) ? n : 0
}
/**
* Normalize a raw score to a 0-100 scale based on its full score.
* Returns 0 when fullScore is non-positive. Result is rounded to 2 decimals.
*/
export const normalize = (score: number, fullScore: number): number => {
if (fullScore <= 0) return 0
return Math.round((score / fullScore) * 10000) / 100
}
/**
* Build a Drizzle SQL filter that restricts `gradeRecords` rows based on
* the current user's DataScope. Returns `null` when no row-level filter
* is required (e.g. admin / student viewing their own records — the caller
* is expected to add the studentId condition separately for `class_members`).
*/
export const buildScopeClassFilter = (scope: DataScope): SQL | null => {
if (scope.type === "all") return null
if (scope.type === "class_taught") {
return scope.classIds.length > 0
? inArray(gradeRecords.classId, scope.classIds)
: sql`1=0`
}
if (scope.type === "grade_managed") return sql`1=0`
if (scope.type === "class_members") return null
if (scope.type === "children") {
return scope.childrenIds.length > 0
? inArray(gradeRecords.studentId, scope.childrenIds)
: sql`1=0`
}
if (scope.type === "owned") return eq(gradeRecords.studentId, scope.userId)
return sql`1=0`
}