252 lines
15 KiB
Markdown
252 lines
15 KiB
Markdown
# 个人信息配置和设置模块审计报告 v3
|
||
|
||
> 审查日期:2026-06-22
|
||
> 审查范围:`src/modules/settings/**`、`src/app/(dashboard)/settings/**`、`src/app/(dashboard)/admin/settings/**`、`src/app/(dashboard)/profile/**`
|
||
> 上一版本:`settings-profile-audit-report-v2.md`(v2,12 项已全部完成)
|
||
> 架构图参考:`docs/architecture/004_architecture_impact_map.md` §2.23、`docs/architecture/005_architecture_data.json`
|
||
|
||
---
|
||
|
||
## 一、现有实现概要
|
||
|
||
### 1.1 文件分布
|
||
|
||
| 层 | 路径 | 文件数 | 说明 |
|
||
|----|------|--------|------|
|
||
| 路由层 - 通用设置 | `src/app/(dashboard)/settings/` | `page.tsx` + `error.tsx` + `loading.tsx` | 角色分发到 SettingsView,通过 SettingsServiceProvider 注入服务 |
|
||
| 路由层 - 管理员系统设置 | `src/app/(dashboard)/admin/settings/` | `page.tsx` | 仅 admin 可访问,渲染 AdminSettingsView |
|
||
| 路由层 - 安全设置 | `src/app/(dashboard)/settings/security/` | `page.tsx` + `error.tsx` + `loading.tsx` | 独立密码修改页 |
|
||
| 路由层 - 个人资料 | `src/app/(dashboard)/profile/` | `page.tsx` + `error.tsx` + `loading.tsx` | 个人资料展示页(159 行) |
|
||
| 模块层 - actions | 7 个文件 | 详见下表 | 全部使用 `requirePermission()` |
|
||
| 模块层 - data-access | 3 个文件 | 详见下表 | 全部 `server-only` |
|
||
| 模块层 - types | `types.ts`(75 行) | 1 | AiProvider 类型 + SettingsService 接口 |
|
||
| 模块层 - 组件 | 12 个组件 | 详见下表 | |
|
||
| 模块层 - lib | 3 个纯函数文件 + 3 个测试 | | |
|
||
| 模块层 - config | `role-settings-config.tsx`(84 行) | 1 | 配置驱动角色路由 |
|
||
| i18n | `zh-CN/settings.json` + `en/settings.json` | 2 | 完整翻译 |
|
||
|
||
### 1.2 v1/v2 完成情况回顾
|
||
|
||
v1 报告 13 项 + v2 报告 12 项改进建议已全部完成:
|
||
|
||
- ✅ i18n 命名空间创建(settings.json 中英文)
|
||
- ✅ SettingsService 接口 + Context 注入(消除跨模块 action 直调)
|
||
- ✅ AdminSettingsView 接入真实数据层(system_settings 表)
|
||
- ✅ 配置驱动角色路由(ROLE_SETTINGS_CONFIG)
|
||
- ✅ 分区 Error Boundary + Suspense 骨架屏
|
||
- ✅ Profile 页面拆分(ProfileStudentOverview / ProfileTeacherOverview)
|
||
- ✅ 头像上传 + 旧文件清理
|
||
- ✅ 2FA 完整 TOTP 实现(非占位)
|
||
- ✅ 通知测试按钮接入真实 dispatcher
|
||
- ✅ 会话远程登出
|
||
- ✅ AdminSettingsView dirty 检测
|
||
- ✅ 通知偏好表单 dirty 检测
|
||
- ✅ 单元测试(totp / student-overview-data / security-utils)
|
||
|
||
### 1.3 架构图记录情况
|
||
|
||
`004_architecture_impact_map.md` §2.23 和 `005_architecture_data.json` 的 settings 节点记录完整,包含所有 actions / data-access / components / config / lib / types 的导出、依赖关系和已知问题状态。架构图与实际代码基本一致。
|
||
|
||
---
|
||
|
||
## 二、现存问题与原因分析
|
||
|
||
### 2.1 ProfileStudentOverview / ProfileTeacherOverview 跨模块 data-access 直调(P0)
|
||
|
||
| 位置 | 问题 | 违反规则 |
|
||
|------|------|----------|
|
||
| [profile-student-overview.tsx](file:///e:/Desktop/CICD/src/modules/settings/components/profile-student-overview.tsx) L8-9 | `import { getStudentClasses, getStudentSchedule } from "@/modules/classes/data-access"` / `import { getStudentDashboardGrades, getStudentHomeworkAssignments } from "@/modules/homework/data-access"` | "模块内部组件绝不直接 import 其他业务模块的 actions 或 data-access(只能通过注入的接口调用)" |
|
||
| [profile-teacher-overview.tsx](file:///e:/Desktop/CICD/src/modules/settings/components/profile-teacher-overview.tsx) L6 | `import { getTeacherClasses, getTeacherTeachingSubjects } from "@/modules/classes/data-access"` | 同上 |
|
||
| [profile-student-overview.tsx](file:///e:/Desktop/CICD/src/modules/settings/components/profile-student-overview.tsx) L4-7 | `import { StudentGradesCard } ... from "@/modules/dashboard/components/student-dashboard/*"` | 组件层跨模块直接 import dashboard 组件,耦合度高 |
|
||
|
||
**原因**:Profile 概览组件作为 Server Component 直接编排 classes/homework/dashboard 模块的数据获取和组件渲染,未通过接口抽象。
|
||
|
||
**后果**:classes/homework/dashboard 模块的 data-access 签名变更会直接破坏 settings 组件;settings 模块无法独立测试(mock classes/homework data-access 困难);无法在不修改 settings 组件的前提下替换数据源。
|
||
|
||
### 2.2 profile/page.tsx 角色硬编码(P0)
|
||
|
||
| 位置 | 问题 | 违反规则 |
|
||
|------|------|----------|
|
||
| [profile/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/profile/page.tsx) L37 | `const isStudent = roles.includes("student")` | "前端权限判断统一使用 usePermission().hasPermission(),严禁出现 role === 'xxx' 硬编码" |
|
||
| 同文件 L38 | `const isTeacher = roles.includes("teacher")` | 同上 |
|
||
|
||
**原因**:Profile 页面通过 `roles.includes()` 判断角色来决定渲染学生/教师概览区块,未使用权限点或配置驱动。
|
||
|
||
**后果**:新增角色(如 grade_head)需修改页面代码;角色与概览区块的映射关系不可配置;违反项目硬编码禁令。
|
||
|
||
### 2.3 SecurityCenterCard 超出组件行数上限(P1)
|
||
|
||
| 位置 | 问题 | 违反规则 |
|
||
|------|------|----------|
|
||
| [security-center-card.tsx](file:///e:/Desktop/CICD/src/modules/settings/components/security-center-card.tsx) | 645 行 | "React 组件:建议 ≤ 500 行(复杂表单/大型表格可放宽至 800 行)" |
|
||
|
||
**原因**:单个组件文件混合了 2FA 启用流程、2FA 关闭流程、备份码重新生成流程、最近登录历史列表、远程登出 5 个独立交互区块,以及 3 个 Dialog 的状态管理和 JSX。
|
||
|
||
**后果**:可读性下降,维护困难;难以独立测试各交互区块;修改一个流程容易影响其他流程。
|
||
|
||
**建议**:拆分为 `SecurityTwoFactorSection`(2FA 启用/关闭/备份码)、`SecurityRecentLoginsSection`(登录历史 + 远程登出),主组件仅负责数据加载和组合。
|
||
|
||
### 2.4 ai-provider-settings-card.tsx 超出组件行数上限(P1)
|
||
|
||
| 位置 | 问题 | 违反规则 |
|
||
|------|------|----------|
|
||
| [ai-provider-settings-card.tsx](file:///e:/Desktop/CICD/src/modules/settings/components/ai-provider-settings-card.tsx) | 529 行 | 同上 |
|
||
|
||
**原因**:单个组件文件混合了 Provider 列表选择、表单编辑、测试、保存、删除 5 个交互流程,以及 visibility/isDefault 表单字段。
|
||
|
||
**后果**:可读性下降,维护困难。
|
||
|
||
**建议**:将 Provider 选择器和 keyStatus 显示拆分为 `AiProviderSelector`,表单主体保留在主组件,删除确认 Dialog 拆为 `AiProviderDeleteDialog`。
|
||
|
||
### 2.5 AdminSettingsView 未拆分为子组件(P1)
|
||
|
||
| 位置 | 问题 | 违反规则 |
|
||
|------|------|----------|
|
||
| [admin-settings-view.tsx](file:///e:/Desktop/CICD/src/modules/settings/components/admin-settings-view.tsx) | 444 行,4 个 Card 内联在单文件中 | v2 报告建议拆分为 `SchoolInfoCard` / `SecurityPolicyCard` / `FileUploadCard` / `NotificationConfigCard` |
|
||
|
||
**原因**:v2 报告已建议拆分但未实施。
|
||
|
||
**后果**:4 个 Card 的表单字段和状态更新逻辑混合在主组件中,难以独立测试和复用。
|
||
|
||
### 2.6 settings/page.tsx metadata 硬编码英文(P1)
|
||
|
||
| 位置 | 问题 | 违反规则 |
|
||
|------|------|----------|
|
||
| [settings/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/settings/page.tsx) L19-21 | `metadata = { title: "Settings" }` 硬编码英文 | "所有用户可见文本必须适配 i18n(使用 next-intl),提取翻译键" |
|
||
|
||
**原因**:使用静态 `metadata` 导出而非 `generateMetadata` + `getTranslations`。
|
||
|
||
**后果**:中文环境下浏览器标签页显示英文 "Settings"。
|
||
|
||
### 2.7 i18n 命名空间不一致(P1)
|
||
|
||
| 位置 | 问题 | 违反规则 |
|
||
|------|------|----------|
|
||
| [avatar-upload.tsx](file:///e:/Desktop/CICD/src/modules/settings/components/avatar-upload.tsx) L40 | `useTranslations("settings.profile.avatar")` | 命名空间为 `settings.profile.avatar` |
|
||
| [profile/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/profile/page.tsx) L22 | `getTranslations("settings.profilePage")` | 命名空间为 `settings.profilePage` |
|
||
| [profile-settings-form.tsx](file:///e:/Desktop/CICD/src/modules/settings/components/profile-settings-form.tsx) L39 | `useTranslations("settings.profile")` | 命名空间为 `settings.profile` |
|
||
|
||
**原因**:三处使用了三种不同的 i18n 命名空间根(`settings.profile` / `settings.profilePage` / `settings.profile.avatar`),v2 报告已指出但未统一。
|
||
|
||
**后果**:i18n 命名空间结构混乱,维护时易混淆;翻译键分散在多个命名空间下。
|
||
|
||
**建议**:统一为 `settings.profile.*`(AvatarUpload 改为 `settings.profile.avatar.*`,profile/page.tsx 改为 `settings.profile.*`),将 `profilePage` 命名空间下的键合并到 `profile` 下。
|
||
|
||
### 2.8 缺少 toSettingItem 单元测试(P2)
|
||
|
||
| 位置 | 问题 | 违反规则 |
|
||
|------|------|----------|
|
||
| `src/modules/settings/actions-system-settings.ts` L58-75 | `toSettingItem` 纯函数无单元测试 | v2 报告建议添加 |
|
||
|
||
**原因**:v2 建议未实施。
|
||
|
||
**后果**:值类型转换逻辑(string/number/boolean/json)无回归保障。
|
||
|
||
### 2.9 profile 页 AvatarUpload 未包裹 Error Boundary(P2)
|
||
|
||
| 位置 | 问题 | 违反规则 |
|
||
|------|------|----------|
|
||
| [profile/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/profile/page.tsx) L53-57 | `<AvatarUpload>` 直接渲染,无 Error Boundary 包裹 | "每个独立的数据区块必须用 React Error Boundary 包裹" |
|
||
|
||
**原因**:仅学生/教师概览区块包裹了 Error Boundary,AvatarUpload 区块遗漏。
|
||
|
||
**后果**:头像上传失败(网络异常/文件服务不可用)会导致整页崩溃。
|
||
|
||
### 2.10 settings-view.tsx 直接 import next-auth/react signOut(P2)
|
||
|
||
| 位置 | 问题 | 违反规则 |
|
||
|------|------|----------|
|
||
| [settings-view.tsx](file:///e:/Desktop/CICD/src/modules/settings/components/settings-view.tsx) L8 | `import { signOut } from "next-auth/react"` | 模块内部组件直接耦合认证实现 |
|
||
|
||
**原因**:登出按钮直接调用 next-auth 的客户端 signOut。
|
||
|
||
**后果**:settings 模块耦合 next-auth 实现;如未来更换认证方案需修改 settings 组件。
|
||
|
||
**建议**:将 `signOut` 调用封装为 settings 模块自身的 action 或通过 props 注入。
|
||
|
||
---
|
||
|
||
## 三、行业差距对比
|
||
|
||
### 3.1 与优秀 K12 产品的差距
|
||
|
||
| 维度 | 优秀实践(Google Classroom / PowerSchool / Veracross) | 当前状态 | 差距影响 |
|
||
|------|--------------------------------------------------------|----------|----------|
|
||
| **设置信息架构** | 统一入口,按角色动态显示分组,支持搜索 | ✅ 已统一入口,配置驱动角色路由 | 已达标 |
|
||
| **个人资料** | 头像上传 + 字段级权限可见性 | ✅ 头像上传已实现;字段级权限可见性未实现 | 学生可能看到不该看的字段(如自己的手机号由家长管理) |
|
||
| **安全中心** | 2FA、会话列表、登录历史、密码泄露检测 | ✅ 2FA TOTP + 会话登出 + 登录历史 | 已达标,密码泄露检测(HaveIBeenPwned)未集成 |
|
||
| **通知偏好** | 按事件类型细分,支持渠道矩阵 + 免打扰 + 测试 | ✅ 全部已实现 | 已达标 |
|
||
| **主题/语言** | 主题切换 + 语言切换同页 | ✅ 已集成 | 已达标 |
|
||
| **AI 配置** | 多 Provider + 测试 + 用量统计 | ✅ 多 Provider + 测试,无用量统计 | 教育机构无法监控 AI 成本(中长期计划) |
|
||
| **空状态/骨架屏** | 每个数据区块独立骨架屏 + 空状态 | ✅ 已实现分区 Suspense + 骨架屏 | 已达标 |
|
||
| **设置搜索** | 设置项较多时支持快速搜索 | ❌ 未实现 | 设置项目前 4 个标签页,数量尚可,中长期可考虑 |
|
||
|
||
### 3.2 多角色使用习惯
|
||
|
||
| 角色 | 优秀实践 | 当前状态 |
|
||
|------|----------|----------|
|
||
| **admin** | 系统设置与个人设置在同一入口的不同分组 | ✅ `/settings` 个人设置 + `/admin/settings` 系统设置分离 |
|
||
| **teacher** | 设置页可快速跳转常用教学功能 | ✅ 有 QuickLinksCard |
|
||
| **parent** | 设置页可切换查看不同孩子的通知偏好 | ❌ 仅一套偏好,无法按孩子细分(中长期计划) |
|
||
| **student** | 设置页简洁,无系统配置 | ✅ 简洁 |
|
||
|
||
---
|
||
|
||
## 四、改进优先级建议
|
||
|
||
### P0(紧急,影响架构合规)
|
||
|
||
1. **消除 Profile 概览组件跨模块 data-access 直调**:将 `ProfileStudentOverview` / `ProfileTeacherOverview` 改为通过 props 接收数据,由页面层(app 层)编排 classes/homework data-access 并注入。同时将 dashboard 组件引用改为通过 children/props 传入或抽取为 shared 组件。
|
||
2. **消除 profile/page.tsx 角色硬编码**:将 `roles.includes("student")` / `roles.includes("teacher")` 改为配置驱动或权限点判断。
|
||
|
||
### P1(重要,影响可维护性)
|
||
|
||
3. **拆分 SecurityCenterCard**:645 行 → 拆分为 `SecurityTwoFactorSection` + `SecurityRecentLoginsSection`,主组件负责数据加载和组合。
|
||
4. **拆分 ai-provider-settings-card.tsx**:529 行 → 拆分 Provider 选择器和删除确认 Dialog。
|
||
5. **拆分 AdminSettingsView**:4 个 Card 拆分为独立子组件。
|
||
6. **settings/page.tsx metadata i18n 化**:改为 `generateMetadata` + `getTranslations`。
|
||
7. **统一 i18n 命名空间**:将 `settings.profilePage.*` 合并到 `settings.profile.*`,AvatarUpload 保持 `settings.profile.avatar.*`。
|
||
|
||
### P2(优化,提升质量)
|
||
|
||
8. **添加 toSettingItem 单元测试**。
|
||
9. **profile 页 AvatarUpload 包裹 Error Boundary**。
|
||
10. **封装 signOut 调用**:通过 props 或 action 注入,解耦 next-auth。
|
||
|
||
---
|
||
|
||
## 五、架构图同步说明
|
||
|
||
本次审计发现架构图需补充/修改以下节点:
|
||
|
||
### 5.1 `004_architecture_impact_map.md` §2.23
|
||
|
||
- **修改"已知问题"**:新增 v3 发现的 3 项 P0/P1 问题(Profile 概览跨模块 data-access 直调 / profile 角色硬编码 / SecurityCenterCard 超行数上限)
|
||
- **更新"文件清单"**:新增拆分后的子组件(SecurityTwoFactorSection / SecurityRecentLoginsSection / AiProviderSelector / SchoolInfoCard / SecurityPolicyCard / FileUploadCard / NotificationConfigCard)
|
||
- **更新行数**:SecurityCenterCard 拆分后行数变化
|
||
|
||
### 5.2 `005_architecture_data.json` settings 节点
|
||
|
||
- **`modules.settings.knownIssues`**:新增 v3 问题状态
|
||
- **`modules.settings.exports.components`**:新增拆分后的子组件
|
||
- **`dependencyMatrix`**:settings → classes/homework/dashboard 的依赖类型标注为"组件层直调(待修复)"
|
||
|
||
---
|
||
|
||
## 六、验收标准
|
||
|
||
v3 完成后应满足:
|
||
|
||
1. `npm run lint` 零错误(warnings 可接受)
|
||
2. `npx tsc --noEmit` 零错误
|
||
3. Profile 概览组件不直接 import classes/homework/dashboard 模块
|
||
4. profile/page.tsx 无 `roles.includes()` 硬编码
|
||
5. SecurityCenterCard 拆分后主文件 ≤ 300 行
|
||
6. ai-provider-settings-card.tsx 拆分后 ≤ 400 行
|
||
7. AdminSettingsView 拆分为 4 个子 Card 组件
|
||
8. settings/page.tsx 使用 generateMetadata
|
||
9. i18n 命名空间统一为 `settings.profile.*`
|
||
10. toSettingItem 有单元测试
|
||
11. AvatarUpload 被 Error Boundary 包裹
|
||
12. 架构图 004/005 已同步更新
|