Files
NextEdu/docs/architecture/audit/settings-profile-audit-report.md
SpecialX 5d42495480 feat(settings): 设置与个人信息模块审计重构 — i18n + 服务注入解耦 + Error Boundary + 流式渲染
- 新增 SettingsService 接口 + Context 注入,组件层不再直接 import users/messaging actions

- 新增 resolveRoleSettingsConfig 配置驱动角色路由,删除 parent/student/teacher-settings-view 冗余文件

- 新增 SettingsSectionErrorBoundary,每个 TabsContent + profile 角色概览区块均包裹

- 新增 ProfileStudentOverview/ProfileTeacherOverview 异步 Server Component + 骨架屏,支持流式渲染

- 抽取 buildStudentOverviewData 等纯函数到 lib/student-overview-data.ts,便于单元测试

- 新增 settings.json 翻译文件(zh-CN + en),所有组件改用 useTranslations/getTranslations

- 重构 profile/page.tsx:i18n 适配 + Suspense 分区加载 + 业务逻辑抽离

- 同步更新架构图 004/005
2026-06-22 16:15:36 +08:00

27 KiB
Raw Blame History

设置和个人信息模块审计报告

审查日期2026-06-22 审查范围:src/modules/settings/**src/app/(dashboard)/settings/**src/app/(dashboard)/admin/settings/**src/app/(dashboard)/profile/** 架构图参考:docs/architecture/004_architecture_impact_map.md §2.23、docs/architecture/005_architecture_data.json


一、现有实现概要

1.1 文件分布

路径 文件数 说明
路由层 - 通用设置 src/app/(dashboard)/settings/ 1 个 page.tsx + error.tsx + loading.tsx 角色分发到 4 个 SettingsView
路由层 - 管理员系统设置 src/app/(dashboard)/admin/settings/ 1 个 page.tsx 仅 admin 可访问,渲染 AdminSettingsView
路由层 - 安全设置 src/app/(dashboard)/settings/security/ 1 个 page.tsx + error.tsx + loading.tsx 独立密码修改页
路由层 - 个人资料 src/app/(dashboard)/profile/ 1 个 page.tsx + error.tsx + loading.tsx 个人资料展示页317 行)
模块层 - actions src/modules/settings/actions.ts160 行) AI Provider CRUD + test 使用 requirePermission(AI_CONFIGURE)
模块层 - actions-password src/modules/settings/actions-password.ts87 行) 修改密码 使用 requirePermission(USER_PROFILE_UPDATE) + Zod + 限流
模块层 - data-access src/modules/settings/data-access.ts158 行) AI Provider + 密码 DB 操作 server-only
模块层 - types src/modules/settings/types.ts16 行) AI Provider 类型
模块层 - 组件 src/modules/settings/components/ 10 个组件 见下表
i18n 缺失 0 settings.json / profile.json 翻译文件

组件清单

组件 行数 职责
settings-view.tsx 179 统一设置页布局5 标签页 + 角色差异 props 注入 + Tab URL 持久化)
admin-settings-view.tsx 185 mock 实现4 个 Card学校信息/安全策略/文件上传/通知配置),setTimeout 模拟保存
ai-provider-settings-card.tsx 357 AI Provider 管理(选择/新建/测试/保存)
notification-preferences-form.tsx 326 通知偏好(渠道/类别/免打扰时段)
password-change-form.tsx 169 修改密码(强度指示器 + 显示切换)
profile-settings-form.tsx 146 个人资料编辑表单
theme-preferences-card.tsx 55 主题切换system/light/dark
parent-settings-view.tsx 60 家长设置视图(复用 SettingsView + 快捷链接)
teacher-settings-view.tsx 66 教师设置视图(同上)
student-settings-view.tsx 54 学生设置视图(同上)

1.2 数据流

[Route] /settings/page.tsx
   ├─▶ users/data-access.getUserProfile          (跨模块 data-access类型导入)
   ├─▶ notifications/preferences.getNotificationPreferences  (跨模块 data-access)
   └─▶ 按 roles.includes("admin"|"student"|"parent") 分发
         ├─ admin    → SettingsView无 generalExtra
         ├─ student  → StudentSettingsView → SettingsView
         ├─ parent   → ParentSettingsView  → SettingsView
         └─ teacher  → TeacherSettingsView → SettingsView

[Route] /admin/settings/page.tsx
   └─▶ AdminSettingsViewmock无数据流

[Route] /settings/security/page.tsx
   └─▶ PasswordChangeForm → settings/actions-password.changePasswordAction

[Route] /profile/page.tsx
   ├─▶ users/data-access.getUserProfile
   ├─▶ classes/data-access.getStudentClasses / getStudentSchedule       (学生分支)
   ├─▶ homework/data-access.getStudentHomeworkAssignments / getStudentDashboardGrades  (学生分支)
   ├─▶ classes/data-access.getTeacherClasses / getTeacherTeachingSubjects  (教师分支)
   └─▶ 页面层内联 80+ 行业务计算weekday 转换、作业状态统计、排序切片)

[Component] ProfileSettingsForm
   └─▶ users/actions.updateUserProfile  ❌ 跨模块 action 直调

[Component] NotificationPreferencesForm
   └─▶ messaging/actions.updateNotificationPreferencesAction  ❌ 跨模块 action 直调

[Component] AiProviderSettingsCard
   └─▶ settings/actions.getAiProviderSummaries / upsertAiProviderAction / testAiProviderAction  ✅ 模块内

1.3 架构图记录情况

004_architecture_impact_map.md §2.23 记录了 settings 模块的基本结构,但存在以下遗漏和不一致:

  • 未记录 profile/page.tsx 的数据流profile 页面编排了 users/classes/homework 三个模块的 data-access但架构图未记录
  • 未记录跨模块 action 直调问题profile-settings-form.tsx 直调 users/actions.updateUserProfilenotification-preferences-form.tsx 直调 messaging/actions.updateNotificationPreferencesAction,架构图标注为"已修复"但实际仍存在
  • 未记录 AdminSettingsView 是 mock 实现:架构图描述其有 4 个 Card 但未说明无真实数据持久化
  • 未记录 i18n 缺失:架构图未标注 settings 模块所有文本均为硬编码
  • 通知偏好归属不一致:架构图 §2.17 称通知偏好已迁移至 notifications 模块,但 notification-preferences-form.tsx 仍从 messaging/actions 导入 action

二、现存问题与原因分析

2.1 国际化完全缺失P0

位置 问题 违反规则
settings-view.tsx L96-104 "Settings"、"Back to dashboard" 等硬编码英文 "所有用户可见文本必须适配 i18n使用 next-intl提取翻译键"
admin-settings-view.tsx 全文 "系统设置"、"学校信息"、"安全策略" 等硬编码中文 同上
profile-settings-form.tsx L80-82 "Profile Information"、"Update your personal information." 硬编码 同上
notification-preferences-form.tsx L47-99 CHANNELS/CATEGORIES 数组中 label/description 全部硬编码 同上
password-change-form.tsx L72-76 "Change Password"、"Choose a strong password..." 硬编码 同上
theme-preferences-card.tsx L24-28 "Theme"、"Choose how the admin console looks..." 硬编码(且写死 "admin console" 同上
ai-provider-settings-card.tsx L251-257 "AI Providers"、"Manage AI vendors..." 硬编码 同上
profile/page.tsx 全文 "Profile"、"Personal Information"、"Account Information" 等硬编码 同上
settings/loading.tsx 等错误页 "页面加载失败" 中文硬编码,与英文页面不统一 同上
src/shared/i18n/messages/{zh-CN,en}/ 无 settings.json / profile.json i18n 命名空间缺失
src/i18n/request.ts 未加载 settings/profile 命名空间 同上

原因settings 模块在历次重构中未纳入 i18n 改造范围,i18n/request.ts 只加载 6 个命名空间common/auth/onboarding/classes/errors/dashboard

后果无法支持中英文切换admin 端中文、其他端英文,体验割裂;新增语言需逐文件修改。

2.2 跨模块 Action 直调违反解耦原则P0

位置 问题 违反规则
profile-settings-form.tsx L16 import { updateUserProfile } from "@/modules/users/actions" "模块内部组件绝不直接 import 其他业务模块的 actions 或 data-access只能通过注入的接口调用"
notification-preferences-form.tsx L16 import { updateNotificationPreferencesAction } from "@/modules/messaging/actions" 同上
settings-view.tsx L28 import { UserProfile } from "@/modules/users/data-access" 类型导入,语法允许但耦合类型定义
settings-view.tsx L29 import type { NotificationPreferences } from "@/modules/notifications/types" 类型导入,可接受

原因settings 组件直接消费 users/messaging 模块的 Server Action未通过接口抽象 + Context 注入。

后果settings 模块无法独立测试mock users/messaging action 困难users/messaging action 签名变更会直接破坏 settings 组件;无法在不修改 settings 组件的前提下替换数据源。

2.3 AdminSettingsView 是 mock 实现P0

位置 问题 违反规则
admin-settings-view.tsx L20-25 await new Promise((r) => setTimeout(r, 800)) 模拟保存,无 Server Action 调用 "app/ 只能调用 modules/ 的 Server Actions 和 data-access不直接访问数据库" — 这里连 action 都没调
同文件 L23 toast.success("设置已保存") 撒谎,实际未保存 用户体验问题
同文件全文 4 个 Card学校信息/安全策略/文件上传/通知配置)的输入框无 name 属性、无表单提交逻辑 表单不可用
admin/settings/page.tsx /settings 页面割裂admin 用户有两个设置入口 信息架构混乱

原因:初版占位实现,后续未接入真实数据层。

后果admin 调整的安全策略/文件上传限制/通知配置均不生效;与 /settings 页面功能重叠但行为不一致。

2.4 角色路由硬编码非配置驱动P1

位置 问题 违反规则
settings/page.tsx L28-44 if (roles.includes("admin")) ... if (roles.includes("student")) ... 4 分支硬编码 "采用配置驱动设计,例如通过角色配置决定该模块渲染哪些 Widget/子模块"
profile/page.tsx L48-49 const isStudent = roles.includes("student") 同上

原因:角色分发逻辑写在页面层,未抽取为配置。

后果:新增角色(如 grade_head需修改页面代码角色与设置视图的映射关系不可配置。

2.5 缺少分区 Error Boundary 和 SuspenseP1

位置 问题 违反规则
settings-view.tsx L132-184 5 个 TabsContent 内部组件ProfileSettingsForm / NotificationPreferencesForm / ThemePreferencesCard / PasswordChangeForm / AiProviderSettingsCard无独立 Error Boundary "每个独立的数据区块必须用 React Error Boundary 包裹"
同上 AiProviderSettingsCard 在 useEffect 中异步加载 providers无 Suspense 包裹 "异步数据使用 React Suspense + 骨架屏"
profile/page.tsx L229-314 学生概览 / 教师概览区块无独立 Error Boundary 同上

原因:仅依赖页面级 error.tsx / loading.tsx,未做分区隔离。

后果AI Provider 加载失败会导致整个 Security 标签页崩溃ProfileSettingsForm 提交失败不会优雅降级。

2.6 Profile 页面职责臃肿P1

位置 问题 违反规则
profile/page.tsx L37-317 单文件 317 行,混合:用户基本信息展示 + 学生作业统计 + 课表筛选 + 教师班级展示 "页面组件" 建议 ≤ 500 行(虽未超限,但职责过多)
同文件 L51-110 学生分支内联 60 行业务计算dueSoonCount / overdueCount / gradedCount / upcomingAssignments 排序) "数据获取、计算、格式化等纯逻辑全部放入纯函数或 hooks与 UI 分离"
同文件 L27-35 WEEKDAY_MAP / toWeekday 日期工具函数定义在页面文件内 同上

原因profile 页面直接编排了 dashboard 模块的学生概览组件,未通过 service 层。

后果:业务逻辑不可测试、不可复用;学生/教师概览与 dashboard 模块重复。

2.7 类型安全与表单规范问题P2

位置 问题 违反规则
profile-settings-form.tsx L44 zodResolver(profileFormSchema) as Resolver<ProfileFormValues> 使用 as 断言 "禁止 as 断言(除非从 unknown 转换或测试中)"
notification-preferences-form.tsx L121 useActionState(updateNotificationPreferencesAction, null) 第二参数 null 类型不安全 应为 ActionState<null> 初值
admin-settings-view.tsx L22 await new Promise((r) => setTimeout(r, 800)) 参数 r 隐式 any "禁止 any"

2.8 可访问性缺失P2

位置 问题 违反规则
settings-view.tsx L106-130 Tabs 组件虽有 Radix 内置 a11y但 TabsTrigger 仅有图标+文字,无 aria-label "可访问性a11y语义化标签、ARIA 属性、键盘导航"
notification-preferences-form.tsx L198-205 隐藏 checkbox + Switch 双控件模式,屏幕阅读器可能重复朗读 同上
password-change-form.tsx L90-99 密码显示切换按钮 tabIndex={-1},键盘用户无法触达 "键盘导航"

2.9 监控埋点缺失P2

位置 问题 违反规则
全模块 无任何埋点接口预留密码修改成功率、AI Provider 测试通过率、通知偏好变更频率等) "监控:方案中预留关键操作埋点接口"

2.10 行业差距安全功能单薄P2

缺失功能 影响
头像上传 用户无法个性化头像profile 页只能显示文字 fallback
两步验证2FA/MFA K12 系统涉及学生隐私,仅密码保护不够
活跃会话管理 用户无法查看/远程登出其他设备会话
登录历史查看 非管理员用户无法查看自己的登录记录
账号数据导出/注销 不符合 GDPR-like 合规要求
通知预览 通知偏好表单无"发送测试通知"功能
设置搜索 设置项较多时无快速定位

三、行业差距对比

3.1 与优秀 K12 产品的差距

维度 优秀实践Google Classroom / PowerSchool / Veracross 当前状态 差距影响
设置信息架构 统一入口,按角色动态显示分组,支持搜索 admin 有两个入口(/admin/settings mock + /settings),其他角色统一 admin 体验割裂,功能不可用
个人资料 头像上传 + 字段级权限可见性(学生看不到自己手机号,家长可见) 无头像上传,所有字段对本人可见 个性化缺失,字段级权限未实现
安全中心 2FA、会话列表、登录历史、密码泄露检测 仅密码修改 K12 数据安全合规风险
通知偏好 按事件类型细分(作业/成绩/考勤/公告/消息),支持渠道矩阵 + 免打扰 已有基础,但无"测试通知"按钮 功能完整度尚可,交互反馈缺失
主题/语言 主题切换 + 语言切换同页 主题有,语言切换在 shared 但未集成到设置页 用户需到别处找语言切换
AI 配置 多 Provider + 测试 + 用量统计 多 Provider + 测试,无用量统计 教育机构无法监控 AI 成本
空状态/骨架屏 每个数据区块独立骨架屏 + 空状态 仅页面级 loading.tsx 局部加载失败时整页白屏

3.2 多角色使用习惯差距

角色 优秀实践 当前状态
admin 系统设置(学校信息/策略)与个人设置在同一入口的不同分组 两套页面割裂,系统设置是 mock
teacher 设置页可快速跳转常用教学功能 有 Quick linksTeacherSettingsView
parent 设置页可切换查看不同孩子的通知偏好 仅一套偏好,无法按孩子细分
student 设置页简洁,无系统配置 简洁

四、改进优先级建议

P0紧急影响安全/合规/核心功能)

  1. 创建 settings i18n 命名空间:新增 zh-CN/settings.json + en/settings.json,覆盖所有设置/个人资料文本;更新 i18n/request.ts 加载新命名空间。
  2. 消除跨模块 action 直调:定义 SettingsService 接口(含 updateProfile / updateNotificationPreferences 方法),通过 React Context 注入;ProfileSettingsForm / NotificationPreferencesForm 改为消费 Context。
  3. AdminSettingsView 接入真实数据层:将 4 个 Card学校信息/安全策略/文件上传/通知配置)接入 school/data-access 或新增 system-settings data-access移除 mock setTimeout

P1重要影响可维护性/体验)

  1. 配置驱动角色路由:新增 settings-config.ts,定义 Role → SettingsViewConfig 映射description / backHref / generalExtra/settings/page.tsx 改为查表分发。
  2. 分区 Error Boundary + Suspense:为每个 TabsContent 内部组件包裹 <ErrorBoundary> + <Suspense fallback={<Skeleton/>}>
  3. Profile 页面拆分:将学生概览/教师概览业务逻辑抽为 useStudentProfileOverview / useTeacherProfileOverview hooksWEEKDAY_MAP/toWeekday 移至 shared/lib/utils
  4. 移除 as 断言profile-settings-form.tsxzodResolver(...) as Resolver<...> 改为类型兼容写法。

P2优化提升完整度

  1. 头像上传profile 页新增头像上传组件(复用 files/data-access)。
  2. 2FA / 会话管理security 标签页新增 2FA 开关 + 活跃会话列表。
  3. 通知测试按钮:通知偏好表单新增"发送测试通知"按钮。
  4. 语言切换集成:在 Appearance 标签页集成 LocaleSwitcher
  5. 埋点接口:在 SettingsService 接口预留 trackEvent 方法。
  6. a11y 修复:密码显示切换按钮移除 tabIndex={-1};通知偏好表单移除冗余隐藏 checkbox。

五、架构图同步说明

本次审计发现架构图需补充/修改以下节点:

5.1 004_architecture_impact_map.md §2.23 settings 模块

  • 修改"已知问题":新增"跨模块 action 直调未修复"profile-settings-formusers/actionsnotification-preferences-formmessaging/actions
  • 修改"已知问题":新增"AdminSettingsView 为 mock 实现,无数据持久化"
  • 修改"已知问题":新增"i18n 完全缺失,所有文本硬编码"
  • 修改"依赖关系":明确标注 profile-settings-form.tsx 依赖 users/actionsaction 级,非 data-access
  • 新增"文件清单":补充 profile/page.tsx317 行)的归属说明(虽在 app 层,但编排 settings 相关数据)

5.2 005_architecture_data.json settings 节点

  • modules.settings.knownIssues:新增 3 条(跨模块 action 直调 / AdminSettingsView mock / i18n 缺失)
  • modules.settings.exports:补充 SettingsService 接口(重构后新增)
  • dependencyMatrixsettings → users 的依赖类型从 data-access 改为 action(标注为待修复)

5.3 004 §2.17 notifications 模块

  • 修正不一致notification-preferences-form.tsx 仍从 messaging/actions 导入 action但架构图称"通知偏好已迁移至 notifications 模块" — 需标注"表单层 action 调用未同步迁移"

六、重构方案设计

6.1 完全解耦SettingsService 接口 + Context 注入

// src/modules/settings/types.ts (新增)
export interface ProfileService {
  getProfile: () => Promise<UserProfile | null>
  updateProfile: (input: UpdateProfileInput) => Promise<ActionState<UserProfile>>
}

export interface NotificationService {
  getPreferences: () => Promise<NotificationPreferences>
  updatePreferences: (input: UpdateNotificationPreferencesInput) => Promise<ActionState<null>>
}

export interface SettingsService {
  profile: ProfileService
  notifications: NotificationService
  trackEvent?: (event: string, payload?: Record<string, unknown>) => void
}
// src/modules/settings/components/settings-service-context.tsx (新增)
const SettingsServiceContext = createContext<SettingsService | null>(null)

export function SettingsServiceProvider({ service, children }: { service: SettingsService; children: ReactNode }) {
  return <SettingsServiceContext.Provider value={service}>{children}</SettingsServiceContext.Provider>
}

export function useSettingsService(): SettingsService {
  const ctx = useContext(SettingsServiceContext)
  if (!ctx) throw new Error("useSettingsService must be used within SettingsServiceProvider")
  return ctx
}

页面层注入实现:

// /settings/page.tsx
const serverService: SettingsService = {
  profile: {
    getProfile: async () => getUserProfile(userId),
    updateProfile: async (input) => updateUserProfile(input),
  },
  notifications: {
    getPreferences: async () => getNotificationPreferences(userId),
    updatePreferences: async (input) => updateNotificationPreferencesAction(null, input),
  },
}
return <SettingsServiceProvider service={serverService}><SettingsView {...} /></SettingsServiceProvider>

6.2 组合优先:角色配置驱动

// src/modules/settings/config/role-settings-config.ts (新增)
export interface RoleSettingsConfig {
  description: string
  backHref: string
  generalExtra?: ReactNode
}

export const ROLE_SETTINGS_CONFIG: Partial<Record<Role, RoleSettingsConfig>> = {
  admin: { description: "settings.admin.description", backHref: "/admin/dashboard" },
  teacher: { description: "settings.teacher.description", backHref: "/teacher/dashboard", generalExtra: <TeacherQuickLinks /> },
  student: { description: "settings.student.description", backHref: "/student/dashboard", generalExtra: <StudentQuickLinks /> },
  parent: { description: "settings.parent.description", backHref: "/parent/dashboard", generalExtra: <ParentQuickLinks /> },
}

6.3 国际化就绪:翻译文件结构

// src/shared/i18n/messages/zh-CN/settings.json
{
  "title": "设置",
  "backToDashboard": "返回仪表盘",
  "tabs": {
    "general": "通用",
    "notifications": "通知",
    "appearance": "外观",
    "security": "安全",
    "ai": "AI"
  },
  "profile": {
    "title": "个人信息",
    "description": "更新您的个人资料",
    "fields": {
      "name": "姓名",
      "email": "邮箱",
      "phone": "电话",
      "address": "地址",
      "gender": "性别",
      "age": "年龄",
      "role": "角色"
    }
  },
  "notifications": {
    "title": "通知偏好",
    "channels": { "push": "推送通知", "email": "邮件", "sms": "短信" },
    "categories": { "messages": "消息", "announcements": "公告", "homework": "作业", "grades": "成绩", "attendance": "考勤" },
    "quietHours": { "title": "免打扰时段", "enable": "启用", "start": "开始时间", "end": "结束时间" }
  },
  "security": {
    "changePassword": { "title": "修改密码", "current": "当前密码", "new": "新密码", "confirm": "确认密码" },
    "session": { "title": "会话", "signOut": "退出登录" }
  },
  "appearance": { "theme": { "title": "主题", "system": "跟随系统", "light": "浅色", "dark": "深色" } },
  "ai": { "providers": { "title": "AI 服务商", "test": "测试", "save": "保存" } }
}

6.4 错误与边界处理

每个 TabsContent 内部组件用 <ErrorBoundary> + <Suspense> 包裹:

<TabsContent value="ai">
  <ErrorBoundary fallback={<SettingsSectionError />}>
    <Suspense fallback={<AiProviderSkeleton />}>
      <AiProviderSettingsCard />
    </Suspense>
  </ErrorBoundary>
</TabsContent>

6.5 可测试性

  • SettingsService 接口可 mock组件单测无需真实 DB
  • WEEKDAY_MAP / toWeekday 移至 shared/lib/utils 后可独立测试
  • 学生概览计算逻辑抽为 useStudentProfileOverview hook可独立测试

6.6 可扩展性

  • 新增角色只需在 ROLE_SETTINGS_CONFIG 添加条目
  • 新增设置标签页只需在 settings-view.tsx 的 tabs 配置添加条目
  • 新增系统设置 Card 只需在 AdminSettingsView 组合新 Card

6.7 企业级补充

  • a11y:密码显示切换按钮移除 tabIndex={-1};通知偏好表单移除冗余隐藏 checkbox仅用 Switch + name 属性
  • 性能SettingsView 保持客户端组件(需 URL searchParams但各标签页内容组件按需加载
  • 安全SettingsService 实现在 Server Action 层调用 requirePermission,组件层不绕过
  • 监控SettingsService.trackEvent 预留埋点接口