- 新增 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
27 KiB
设置和个人信息模块审计报告
审查日期: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.ts(160 行) |
AI Provider CRUD + test | ✅ 使用 requirePermission(AI_CONFIGURE) |
| 模块层 - actions-password | src/modules/settings/actions-password.ts(87 行) |
修改密码 | ✅ 使用 requirePermission(USER_PROFILE_UPDATE) + Zod + 限流 |
| 模块层 - data-access | src/modules/settings/data-access.ts(158 行) |
AI Provider + 密码 DB 操作 | ✅ server-only |
| 模块层 - types | src/modules/settings/types.ts(16 行) |
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
└─▶ AdminSettingsView(mock,无数据流)
[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.updateUserProfile、notification-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 和 Suspense(P1)
| 位置 | 问题 | 违反规则 |
|---|---|---|
| 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 links(TeacherSettingsView) |
| parent | 设置页可切换查看不同孩子的通知偏好 | 仅一套偏好,无法按孩子细分 |
| student | 设置页简洁,无系统配置 | ✅ 简洁 |
四、改进优先级建议
P0(紧急,影响安全/合规/核心功能)
- 创建 settings i18n 命名空间:新增
zh-CN/settings.json+en/settings.json,覆盖所有设置/个人资料文本;更新i18n/request.ts加载新命名空间。 - 消除跨模块 action 直调:定义
SettingsService接口(含updateProfile/updateNotificationPreferences方法),通过 React Context 注入;ProfileSettingsForm/NotificationPreferencesForm改为消费 Context。 - AdminSettingsView 接入真实数据层:将 4 个 Card(学校信息/安全策略/文件上传/通知配置)接入
school/data-access或新增system-settingsdata-access;移除 mocksetTimeout。
P1(重要,影响可维护性/体验)
- 配置驱动角色路由:新增
settings-config.ts,定义Role → SettingsViewConfig映射(description / backHref / generalExtra),/settings/page.tsx改为查表分发。 - 分区 Error Boundary + Suspense:为每个 TabsContent 内部组件包裹
<ErrorBoundary>+<Suspense fallback={<Skeleton/>}>。 - Profile 页面拆分:将学生概览/教师概览业务逻辑抽为
useStudentProfileOverview/useTeacherProfileOverviewhooks;WEEKDAY_MAP/toWeekday移至shared/lib/utils。 - 移除
as断言:profile-settings-form.tsx的zodResolver(...) as Resolver<...>改为类型兼容写法。
P2(优化,提升完整度)
- 头像上传:profile 页新增头像上传组件(复用
files/data-access)。 - 2FA / 会话管理:security 标签页新增 2FA 开关 + 活跃会话列表。
- 通知测试按钮:通知偏好表单新增"发送测试通知"按钮。
- 语言切换集成:在 Appearance 标签页集成
LocaleSwitcher。 - 埋点接口:在
SettingsService接口预留trackEvent方法。 - a11y 修复:密码显示切换按钮移除
tabIndex={-1};通知偏好表单移除冗余隐藏 checkbox。
五、架构图同步说明
本次审计发现架构图需补充/修改以下节点:
5.1 004_architecture_impact_map.md §2.23 settings 模块
- 修改"已知问题":新增"跨模块 action 直调未修复"(
profile-settings-form→users/actions、notification-preferences-form→messaging/actions) - 修改"已知问题":新增"AdminSettingsView 为 mock 实现,无数据持久化"
- 修改"已知问题":新增"i18n 完全缺失,所有文本硬编码"
- 修改"依赖关系":明确标注
profile-settings-form.tsx依赖users/actions(action 级,非 data-access) - 新增"文件清单":补充
profile/page.tsx(317 行)的归属说明(虽在 app 层,但编排 settings 相关数据)
5.2 005_architecture_data.json settings 节点
modules.settings.knownIssues:新增 3 条(跨模块 action 直调 / AdminSettingsView mock / i18n 缺失)modules.settings.exports:补充SettingsService接口(重构后新增)dependencyMatrix:settings → 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,组件单测无需真实 DBWEEKDAY_MAP/toWeekday移至shared/lib/utils后可独立测试- 学生概览计算逻辑抽为
useStudentProfileOverviewhook,可独立测试
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预留埋点接口