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:
146
src/modules/dashboard/actions.ts
Normal file
146
src/modules/dashboard/actions.ts
Normal 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)
|
||||
Reference in New Issue
Block a user