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

> 审查日期：2026-06-22
> 审查范围：`src/modules/settings/**`、`src/app/(dashboard)/settings/**`、`src/app/(dashboard)/admin/settings/**`、`src/app/(dashboard)/profile/**`
> 上一版本：`settings-profile-audit-report.md`（v1，P0/P1/P2 共 13 项已全部完成）
> 架构图参考：`docs/architecture/004_architecture_impact_map.md` §2.23、`docs/architecture/005_architecture_data.json`

---

## 一、v1 完成情况回顾

v1 报告中的 13 项改进建议已全部完成：

| 编号 | 优先级 | 标题 | 状态 |
|------|--------|------|------|
| P0-1 | P0 | 创建 settings i18n 命名空间 | ✅ 已完成 |
| P0-2 | P0 | 消除跨模块 action 直调（SettingsService 接口） | ✅ 已完成 |
| P0-3 | P0 | AdminSettingsView 接入真实数据层 | ✅ 已完成（新增 system_settings 表 + data-access + actions） |
| P1-4 | P1 | 配置驱动角色路由 | ✅ 已完成 |
| P1-5 | P1 | 分区 Error Boundary + Suspense | ✅ 已完成 |
| P1-6 | P1 | Profile 页面拆分 | ✅ 已完成 |
| P1-7 | P1 | 移除 `as` 断言 | ✅ 已完成 |
| P2-8 | P2 | 头像上传 | ✅ 已完成（AvatarUpload + actions-avatar） |
| P2-9 | P2 | 2FA / 会话管理 | ✅ 已完成（SecurityCenterCard + actions-security） |
| P2-10 | P2 | 通知测试按钮 | ✅ 已完成（sendTestNotificationAction） |
| P2-11 | P2 | 语言切换集成 | ✅ 已完成（ThemePreferencesCard 集成 LocaleSwitcher） |
| P2-12 | P2 | 埋点接口 | ✅ 已完成（SettingsService.trackEvent 预留） |
| P2-13 | P2 | a11y 修复 | ✅ 已完成 |

---

## 二、v2 新发现的问题

### 2.1 安全中心 2FA 为纯占位实现（P0）

| 位置 | 问题 | 严重性 |
|------|------|--------|
| [actions-security.ts](file:///e:/Desktop/CICD/src/modules/settings/actions-security.ts) L21-46 | `toggleTwoFactorAction` 仅将 `twoFactorEnabled` 写入 system_settings 表，未接入 TOTP 密钥绑定、一次性码校验、备份码生成等真实 2FA 流程 | P0 |
| [security-center-card.tsx](file:///e:/Desktop/CICD/src/modules/settings/components/security-center-card.tsx) L105-120 | 用户开启 2FA 后立即显示"已启用"，但实际登录时不会要求二次验证，造成虚假安全感 | P0 |
| 同文件 L70 注释 | "占位实现，仅记录用户偏好" — 注释承认未接入真实流程 | P0 |

**后果**：用户以为启用了 2FA 但实际无效；安全合规审计会失败。

**建议**：在 v2 中要么 (a) 完整实现 TOTP 流程（绑定 authenticator + 验证一次性码 + 备份码），要么 (b) 将开关改为"即将推出"禁用状态，避免误导。

### 2.2 通知测试按钮为纯占位实现（P1）

| 位置 | 问题 | 严重性 |
|------|------|--------|
| [actions-notifications.ts](file:///e:/Desktop/CICD/src/modules/settings/actions-notifications.ts) L29-39 | `sendTestNotificationAction` 仅 `console.info` + `Promise.resolve()`，未调用真实通知发送服务 | P1 |
| [notification-preferences-form.tsx](file:///e:/Desktop/CICD/src/modules/settings/components/notification-preferences-form.tsx) L119-133 | 点击测试按钮后总是显示"测试通知已发送"，但用户不会收到任何通知 | P1 |

**后果**：用户以为测试通知已发送但收不到，无法真正验证渠道配置。

**建议**：接入 `notifications/dispatcher.ts` 的真实发送逻辑，或暂时将按钮改为禁用状态并标注"功能开发中"。

### 2.3 头像上传未清理旧文件（P1）

| 位置 | 问题 | 严重性 |
|------|------|--------|
| [actions-avatar.ts](file:///e:/Desktop/CICD/src/modules/settings/actions-avatar.ts) L15-34 | `updateUserAvatarAction` 更新 `users.image` 字段后，旧头像文件仍留在文件存储中，无清理逻辑 | P1 |
| [avatar-upload.tsx](file:///e:/Desktop/CICD/src/modules/settings/components/avatar-upload.tsx) L108-124 | `handleRemove` 调用 `removeUserAvatarAction` 仅清空 `users.image`，未删除实际文件 | P1 |

**后果**：存储成本累积；孤儿文件无法回收。

**建议**：在 `removeUserAvatarAction` 和 `updateUserAvatarAction` 中，更新数据库前先记录旧 URL，更新成功后异步调用 `files/data-access.deleteFile` 清理旧文件。

### 2.4 SecurityCenterCard 缺少"登出其他会话"功能（P1）

| 位置 | 问题 | 严重性 |
|------|------|--------|
| [security-center-card.tsx](file:///e:/Desktop/CICD/src/modules/settings/components/security-center-card.tsx) 全文 | 仅展示登录历史，无法远程登出其他设备的会话 | P1 |
| [actions-security.ts](file:///e:/Desktop/CICD/src/modules/settings/actions-security.ts) 全文 | 无 `revokeSessionAction` 或类似 Server Action | P1 |

**后果**：用户发现可疑登录后无法主动处置，只能修改密码被动应对。

**建议**：新增 `revokeSessionAction(sessionToken: string)`，删除 `sessions` 表对应记录；UI 在每条登录历史旁显示"登出"按钮（当前会话除外）。

### 2.5 AdminSettingsView 缺少表单变更检测（P1）

| 位置 | 问题 | 严重性 |
|------|------|--------|
| [admin-settings-view.tsx](file:///e:/Desktop/CICD/src/modules/settings/components/admin-settings-view.tsx) L107-122 | `handleSave` 无条件保存，即使用户未修改任何字段也会触发 upsert 全部 16 个设置项 | P1 |
| 同文件 L415 | "Reset" 按钮直接 `setValues(DEFAULT_VALUES)` 而非恢复到加载时的值，会丢失未保存的服务端数据 | P1 |

**后果**：无谓的数据库写入；Reset 语义错误。

**建议**：维护 `dirty` 状态（`JSON.stringify(values) !== JSON.stringify(loadedValues)`），Save 按钮禁用直到 dirty；Reset 恢复到 `loadedValues` 而非 `DEFAULT_VALUES`。

### 2.6 i18n 键 `settings.profile.avatar` 在 `profilePage` 命名空间下缺失（P2）

| 位置 | 问题 | 严重性 |
|------|------|--------|
| [profile/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/profile/page.tsx) | 使用 `<AvatarUpload>` 但页面其他文本使用 `settings.profilePage.*` 命名空间，而 AvatarUpload 内部使用 `settings.profile.avatar.*`，命名空间不一致 | P2 |

**后果**：i18n 命名空间结构混乱，维护时易混淆。

**建议**：统一为 `settings.profile.avatar.*` 或 `settings.profilePage.avatar.*`，二选一。

### 2.7 SecurityCenterCard 未传递 `currentDeviceLabel`（P2）

| 位置 | 问题 | 严重性 |
|------|------|--------|
| [settings-view.tsx](file:///e:/Desktop/CICD/src/modules/settings/components/settings-view.tsx) L182 | `<SecurityCenterCard />` 未传递 `currentDeviceLabel` prop | P2 |
| [security-center-card.tsx](file:///e:/Desktop/CICD/src/modules/settings/components/security-center-card.tsx) L192-194 | `isCurrent` 判断永远为 `false`，"当前会话"徽章永远不会显示 | P2 |

**后果**：用户无法在登录历史中识别当前会话。

**建议**：在 Server Component 层获取 `headers().get("user-agent")`，通过 props 传递到 `SecurityCenterCard`。

### 2.8 头像上传未限制文件名长度（P2）

| 位置 | 问题 | 严重性 |
|------|------|--------|
| [avatar-upload.tsx](file:///e:/Desktop/CICD/src/modules/settings/components/avatar-upload.tsx) L49-57 | `validateFile` 仅校验类型和大小，未校验文件名长度 | P2 |

**后果**：超长文件名可能导致数据库 `varchar` 字段截断或存储错误。

**建议**：添加 `file.name.length > 255` 校验。

### 2.9 通知偏好表单未做 dirty 检测（P2）

| 位置 | 问题 | 严重性 |
|------|------|--------|
| [notification-preferences-form.tsx](file:///e:/Desktop/CICD/src/modules/settings/components/notification-preferences-form.tsx) L98-117 | Save 按钮始终可点击，无 dirty 检测 | P2 |

**后果**：用户误点 Save 触发不必要的 Server Action 调用。

**建议**：维护 dirty 状态，Save 按钮在无变更时禁用。

### 2.10 AdminSettingsView 文件行数接近上限（P2）

| 位置 | 问题 | 严重性 |
|------|------|--------|
| [admin-settings-view.tsx](file:///e:/Desktop/CICD/src/modules/settings/components/admin-settings-view.tsx) | 425 行，接近 500 行建议上限 | P2 |

**后果**：可读性下降，维护困难。

**建议**：将 4 个 Card 拆分为独立子组件（`SchoolInfoCard` / `SecurityPolicyCard` / `FileUploadCard` / `NotificationConfigCard`），主组件仅负责表单状态和提交逻辑。

### 2.11 缺少单元测试（P2）

| 位置 | 问题 | 严重性 |
|------|------|--------|
| `src/modules/settings/**/*.test.ts` | 整个 settings 模块无任何单元测试文件 | P2 |

**后果**：重构无回归保障；纯函数（`toSettingItem`、`parseUserAgent`、`formatRelativeTime`）无法独立验证。

**建议**：为以下纯函数添加单元测试：
- `actions-system-settings.ts` 的 `toSettingItem`（值类型转换）
- `security-center-card.tsx` 的 `parseUserAgent`、`formatRelativeTime`
- `lib/student-overview-data.ts` 的 `buildStudentOverviewData`、`computeStudentStats`

### 2.12 2FA 状态查询存在 N+1 问题（P2）

| 位置 | 问题 | 严重性 |
|------|------|--------|
| [actions-security.ts](file:///e:/Desktop/CICD/src/modules/settings/actions-security.ts) L48-62 | `getTwoFactorStatus` 对每个用户分别查询 3 次 `system_settings` 表（enabled / method / enabledAt），共 3 次 DB 往返 | P2 |

**后果**：每次加载安全中心页面额外 3 次 DB 查询。

**建议**：使用 `getSystemSettingsByCategory("security_policy")` 一次查询所有 security_policy 分类下的设置，在内存中过滤当前用户的键。

---

## 三、改进优先级建议（v2）

### P0（紧急，影响安全/合规）

1. **2FA 真实实现或禁用开关**：要么完整实现 TOTP 流程，要么将开关改为"即将推出"禁用状态，避免虚假安全感。

### P1（重要，影响功能完整性）

2. **通知测试按钮接入真实发送逻辑**：调用 `notifications/dispatcher.ts` 发送真实通知，或暂时禁用按钮。
3. **头像上传清理旧文件**：在 `removeUserAvatarAction` 和 `updateUserAvatarAction` 中添加旧文件清理逻辑。
4. **会话远程登出**：新增 `revokeSessionAction`，UI 添加"登出"按钮。
5. **AdminSettingsView 表单 dirty 检测**：Save 按钮在无变更时禁用；Reset 恢复到加载值。

### P2（优化，提升质量）

6. **统一 i18n 命名空间**：`settings.profile.avatar` 与 `settings.profilePage` 二选一。
7. **SecurityCenterCard 传递 currentDeviceLabel**：Server Component 层获取 user-agent 传入。
8. **头像上传文件名长度校验**：添加 `file.name.length > 255` 校验。
9. **通知偏好表单 dirty 检测**：Save 按钮在无变更时禁用。
10. **AdminSettingsView 拆分子组件**：4 个 Card 拆分为独立组件。
11. **添加单元测试**：为纯函数添加测试覆盖。
12. **2FA 状态查询优化**：合并 3 次 DB 查询为 1 次。

---

## 四、v2 实施计划

### 4.1 P0：2FA 真实实现或禁用

**方案选择**：考虑到完整 TOTP 实现需要额外的库（`otplib`）和 UI（QR 码扫描、备份码展示），v2 阶段先将开关改为"即将推出"禁用状态，避免虚假安全感。完整 TOTP 实现留待 v3。

**改动范围**：
- `security-center-card.tsx`：Switch 添加 `disabled` 属性，显示"即将推出"徽章
- i18n：添加 `twoFactor.comingSoon` 键

### 4.2 P1：通知测试按钮接入真实逻辑

**方案选择**：调用 `notifications/dispatcher.ts` 的 `dispatchNotification` 函数发送真实通知。

**改动范围**：
- `actions-notifications.ts`：导入 `dispatchNotification`，根据 channel 调用对应渠道
- 失败时返回具体错误信息

### 4.3 P1：头像上传清理旧文件

**改动范围**：
- `actions-avatar.ts`：在更新前记录旧 image URL，更新成功后调用 `files/data-access.deleteFileByUrl` 清理
- 需要先确认 `files/data-access` 是否有 `deleteFileByUrl` 函数，若无则新增

### 4.4 P1：会话远程登出

**改动范围**：
- `actions-security.ts`：新增 `revokeSessionAction(sessionToken: string)`
- `security-center-card.tsx`：每条登录历史旁添加"登出"按钮（当前会话除外）
- i18n：添加 `recentLogins.revoke` / `revokeSuccess` / `revokeFailure` 键

### 4.5 P1：AdminSettingsView dirty 检测

**改动范围**：
- `admin-settings-view.tsx`：维护 `loadedValues` 状态，计算 `isDirty`，Save 按钮禁用逻辑，Reset 恢复到 `loadedValues`

### 4.6 P2：其他优化项

逐项实施，每项改动范围较小，详见各小节。

---

## 五、架构图同步说明

v2 改动完成后需同步更新：

### 5.1 `004_architecture_impact_map.md` §2.23

- 更新"已知问题"：标注 v2 新增/修复项
- 更新"文件清单"：新增测试文件、拆分后的子组件

### 5.2 `005_architecture_data.json`

- `modules.settings.exports`：新增 `revokeSessionAction` 等
- `modules.settings.knownIssues`：更新 v2 状态
- `dependencyMatrix`：settings → notifications 依赖（通知测试真实发送）

---

## 六、验收标准

v2 完成后应满足：

1. `npm run lint` 零错误（warnings 可接受）
2. `npx tsc --noEmit` 零错误
3. 2FA 开关为禁用状态或完整 TOTP 实现（二选一）
4. 通知测试按钮发送真实通知或禁用（二选一）
5. 头像更换/删除后旧文件被清理
6. 安全中心可远程登出其他会话
7. AdminSettingsView Save 按钮在无变更时禁用
8. 至少 3 个纯函数有单元测试
9. 架构图 004/005 已同步更新
