feat(dashboard): 仪表盘模块审计重构 — 权限校验 + i18n + 逻辑抽离

基于 dashboard-audit-report.md 审计结论,对仪表盘模块进行 P0/P1 级修复:

- 新增 4 个 dashboard 权限点(DASHBOARD_ADMIN/TEACHER/STUDENT/PARENT_READ),补充到 permissions.ts 和角色-权限映射

- 新建 actions.ts:4 个 Server Action 均调用 requirePermission() 校验权限,消除 admin 页面零鉴权、teacher/student/parent 仅 requireAuth 的安全隐患

- 根重定向页 /dashboard 改用 resolvePermissions() + 权限点判断,不再 role === xxx 硬编码

- 新建 lib/dashboard-utils.ts:抽取 toWeekday / countStudentAssignments / sortUpcomingAssignments / filterTodaySchedule / computeTeacherMetrics / getGreetingKey 纯函数,与 UI 分离,便于单测

- 新建 messages/{zh-CN,en}/dashboard.json 翻译文件,i18n request.ts 加载 dashboard 命名空间;所有视图组件接入 useTranslations / getTranslations,消除中英混杂硬编码

- 重构 4 个角色 page.tsx:通过 actions 获取数据,generateMetadata 使用 i18n

- 同步更新架构图 004 / 005 文档(dashboard exports / permissions / 文件清单)
This commit is contained in:
SpecialX
2026-06-22 15:50:56 +08:00
parent 2548f70f40
commit 868ac5f9cf
28 changed files with 1507 additions and 399 deletions

View File

@@ -0,0 +1,146 @@
"use server"
import { cache } from "react"
import { requirePermission } from "@/shared/lib/auth-guard"
import { Permissions } from "@/shared/types/permissions"
import { getClassSchedule, getStudentClasses, getStudentSchedule, getTeacherClasses, getTeacherIdForMutations } from "@/modules/classes/data-access"
import {
getHomeworkAssignments,
getHomeworkSubmissions,
getStudentDashboardGrades,
getStudentHomeworkAssignments,
getTeacherGradeTrends,
} from "@/modules/homework/data-access"
import { getCurrentStudentUser, getUserBasicInfo } from "@/modules/users/data-access"
import { getParentDashboardData } from "@/modules/parent/data-access"
import { getAdminDashboardData } from "./data-access"
import type {
AdminDashboardData,
StudentDashboardProps,
TeacherDashboardData,
} from "./types"
import type { ParentDashboardData } from "@/modules/parent/types"
import {
computeTeacherMetrics,
countStudentAssignments,
sortUpcomingAssignments,
toWeekday,
filterTodaySchedule,
type TeacherDashboardMetrics,
} from "./lib/dashboard-utils"
/**
* 获取管理员仪表盘数据。
* 权限DASHBOARD_ADMIN_READ
*/
export async function getAdminDashboardAction(): Promise<AdminDashboardData> {
const ctx = await requirePermission(Permissions.DASHBOARD_ADMIN_READ)
return getAdminDashboardData(ctx.dataScope)
}
/**
* 获取教师仪表盘数据(含派生指标)。
* 权限DASHBOARD_TEACHER_READ
*/
export async function getTeacherDashboardAction(): Promise<TeacherDashboardData & {
metrics: TeacherDashboardMetrics
}> {
await requirePermission(Permissions.DASHBOARD_TEACHER_READ)
const teacherId = await getTeacherIdForMutations()
const [classes, schedule, assignments, submissions, teacherProfile, gradeTrends] = await Promise.all([
getTeacherClasses({ teacherId }),
getClassSchedule({ teacherId }),
getHomeworkAssignments({ creatorId: teacherId }),
getHomeworkSubmissions({ creatorId: teacherId }),
getUserBasicInfo(teacherId),
getTeacherGradeTrends(teacherId),
])
const metrics = computeTeacherMetrics(
classes,
schedule,
assignments,
submissions,
gradeTrends,
new Date(),
)
return {
classes,
schedule,
assignments,
submissions,
teacherName: teacherProfile?.name ?? "Teacher",
gradeTrends,
metrics,
}
}
/**
* 获取学生仪表盘数据(含派生指标)。
* 权限DASHBOARD_STUDENT_READ
*/
export async function getStudentDashboardAction(): Promise<{
student: { id: string; name: string } | null
dashboardProps: Omit<StudentDashboardProps, "studentName"> | null
}> {
await requirePermission(Permissions.DASHBOARD_STUDENT_READ)
const student = await getCurrentStudentUser()
if (!student) {
return { student: null, dashboardProps: null }
}
const [classes, schedule, assignments, grades] = await Promise.all([
getStudentClasses(student.id),
getStudentSchedule(student.id),
getStudentHomeworkAssignments(student.id),
getStudentDashboardGrades(student.id),
])
const now = new Date()
const stats = countStudentAssignments(assignments, now)
const todayWeekday = toWeekday(now)
const todayScheduleItems = filterTodaySchedule(schedule, todayWeekday)
const upcomingAssignments = sortUpcomingAssignments(assignments, 6)
return {
student: { id: student.id, name: student.name },
dashboardProps: {
enrolledClassCount: classes.length,
dueSoonCount: stats.dueSoonCount,
overdueCount: stats.overdueCount,
gradedCount: stats.gradedCount,
todayScheduleItems,
upcomingAssignments,
grades,
},
}
}
/**
* 获取家长仪表盘数据。
* 权限DASHBOARD_PARENT_READ
*/
export async function getParentDashboardAction(): Promise<{
data: ParentDashboardData | null
hasChildren: boolean
}> {
const ctx = await requirePermission(Permissions.DASHBOARD_PARENT_READ)
// 非 admin 且 dataScope 非 children 类型时,无孩子数据
if (
ctx.dataScope.type !== "all" &&
!(ctx.dataScope.type === "children" && ctx.dataScope.childrenIds.length > 0)
) {
return { data: null, hasChildren: false }
}
const data = await getParentDashboardData(ctx.userId)
return { data, hasChildren: data.children.length > 0 }
}
/** 缓存版本(用于 RSC 直接调用,不走 Server Action 协议) */
export const getCachedAdminDashboard = cache(getAdminDashboardAction)