558 lines
30 KiB
Markdown
558 lines
30 KiB
Markdown
# 文件模块审计报告
|
||
|
||
> 审查日期:2026-06-25
|
||
> 审查范围:`src/modules/files/**`、`src/app/api/upload/**`、`src/app/api/files/**`、`src/app/(dashboard)/admin/files/**`、`src/modules/settings/actions-avatar.ts`、`src/modules/settings/components/avatar-upload.tsx`
|
||
> 架构图参考:`docs/architecture/004_architecture_impact_map.md` §2.17、`docs/architecture/005_architecture_data.json`
|
||
|
||
---
|
||
|
||
## 一、现有实现概要
|
||
|
||
### 1.1 文件分布
|
||
|
||
| 层 | 路径 | 文件数 | 说明 |
|
||
|----|------|--------|------|
|
||
| 路由层 - 上传 | `src/app/api/upload/route.ts` | 1 | FormData 接收 + 直接写磁盘 + DB 落库 |
|
||
| 路由层 - 单文件 | `src/app/api/files/[id]/route.ts` | 1 | GET 查询单文件、DELETE 删除单文件 |
|
||
| 路由层 - 批量删除 | `src/app/api/files/batch-delete/route.ts` | 1 | POST 批量删除 |
|
||
| 路由层 - 管理页 | `src/app/(dashboard)/admin/files/page.tsx` | 1 | Server Component,调用 data-access 渲染 AdminFilesView |
|
||
| 模块层 - data-access | `src/modules/files/data-access.ts` | 1(305 行) | 11 个函数 + mapRow |
|
||
| 模块层 - types | `src/modules/files/types.ts` | 1(63 行) | 7 个接口/类型 |
|
||
| 模块层 - 组件 | `src/modules/files/components/` | 6 文件 | FileUpload / FileList / FilePreview / FilePreviewDialog / FileIcon / AdminFilesView |
|
||
| i18n | `messages/{en,zh-CN}/files.json` | 2 | 仅 2 个键(title、description) |
|
||
| Schema | `src/shared/db/schema.ts` §12 | - | `file_attachments` 表 + 3 个索引 |
|
||
| 权限点 | `src/shared/types/permissions.ts` | - | `FILE_UPLOAD` / `FILE_READ` / `FILE_DELETE` |
|
||
|
||
### 1.2 数据流
|
||
|
||
```
|
||
浏览器 (AdminFilesView/FileUpload/AvatarUpload)
|
||
│ fetch /api/upload (POST FormData)
|
||
│ fetch /api/files/[id] (DELETE)
|
||
│ fetch /api/files/batch-delete (POST JSON)
|
||
▼
|
||
API 路由层(requireAuth / requirePermission)
|
||
│ 直接调用 files/data-access 函数
|
||
▼
|
||
files/data-access.ts → shared/db → file_attachments 表
|
||
│ 存储抽象:/api/upload 与 /api/files/[id] 直接用 fs/promises
|
||
│ /api/files/batch-delete 用 storageProvider
|
||
```
|
||
|
||
### 1.3 架构图记录情况
|
||
|
||
`004_architecture_impact_map.md` §2.17 与 `005_architecture_data.json` 的 `modules.files` 节点对 data-access 的导出函数签名、依赖关系记录基本完整。但发现以下遗漏(详见 §五):
|
||
|
||
- **`getFileByUrl` 函数未在 005 JSON 的 `dataAccess` 列表中记录**(实际代码存在,被 settings/actions-avatar.ts 使用)。
|
||
- **被依赖方遗漏 settings 模块**:004 §2.17 仅记录 `app/api/upload / app/api/files/[id] / app/api/files/batch-delete / homework`,遗漏 `settings/actions-avatar.ts` 通过 `getFileByUrl` + `deleteFileAttachment` 调用清理头像文件。
|
||
- **`FilePreviewDialog` 组件未在 005 JSON 的 `components` 列表中记录**。
|
||
|
||
---
|
||
|
||
## 二、现存问题与原因分析
|
||
|
||
### 2.1 三层架构违反:缺少 actions.ts 编排层(P0)
|
||
|
||
| 位置 | 问题 | 违反规则 |
|
||
|------|------|----------|
|
||
| `src/modules/files/` 目录 | 无 `actions.ts` 文件 | "每个模块标准结构:`actions.ts`(编排层)" |
|
||
| `src/app/api/upload/route.ts` L13 | `import { createFileAttachment } from "@/modules/files/data-access"` | "app/ 只能调用 modules/ 的 Server Actions 和 data-access" — API 路由虽可直调 data-access,但项目规范要求所有写操作通过 actions 编排 |
|
||
| `src/app/api/files/batch-delete/route.ts` L6-9 | 直接 import 两个 data-access 函数 | 同上 |
|
||
| `src/app/(dashboard)/admin/files/page.tsx` L7-10 | page.tsx 直接调用 data-access 函数 | 同上(应通过 actions 编排权限与数据获取) |
|
||
|
||
**原因**:模块创建时未遵循标准结构,所有数据访问被路由层与页面层直接调用,跳过 actions 编排层。
|
||
|
||
**后果**:
|
||
1. 权限校验分散在路由层,无法集中管理;
|
||
2. 无法在 actions 层统一埋点、审计日志、缓存策略;
|
||
3. 与项目其他模块(如 announcements、settings、grades 等)的 actions.ts 模式不一致,破坏一致性;
|
||
4. 无法被其他模块以 actions 形式复用(settings 模块只能 import data-access)。
|
||
|
||
### 2.2 i18n 严重缺失:UI 文本全部硬编码英文(P0)
|
||
|
||
| 位置 | 问题 | 违反规则 |
|
||
|------|------|----------|
|
||
| `messages/en/files.json`、`messages/zh-CN/files.json` | 仅 `title`、`description` 2 个键 | "所有用户可见文本必须适配 i18n" |
|
||
| `components/file-upload.tsx` L48-51、L188-191、L218、L230 | `"File is empty"` / `"Click to upload or drag and drop"` / `"Uploaded"` 等硬编码 | 同上 |
|
||
| `components/file-list.tsx` L27-28、L37、L40、L84、L104、L117 | `"No files"` / `"File deleted"` / `"Download"` / `"Delete"` 等硬编码 | 同上 |
|
||
| `components/file-preview.tsx` L71、L113-119、L207、L215 | `"Download"` / `"Office file preview not available"` / `"Load preview"` 等硬编码 | 同上 |
|
||
| `components/file-preview-dialog.tsx` L29、L47 | `"Preview"` / `"File preview ·"` 硬编码 | 同上 |
|
||
| `components/admin-files-view.tsx` L33-45、L121、L135-139、L147、L154、L177、L199、L217、L261、L271 | `TYPE_OPTIONS` 标签 + "Files"/"Total Files"/"Total Size"/"Filter by type"/"Search by file name..."/"Delete Selected"/"Deleting..." 等几十处硬编码 | 同上 |
|
||
|
||
**原因**:组件开发时直接用英文字面量,未提取翻译键;i18n 文件仅 placeholder。
|
||
|
||
**后果**:
|
||
1. 中文用户看到全英文界面(与系统其他模块本地化不一致);
|
||
2. 切换语言无效;
|
||
3. 翻译协作无法进行;
|
||
4. 违反项目硬约束。
|
||
|
||
### 2.3 权限校验缺失:上传路由仅 requireAuth 未走 requirePermission(P0)
|
||
|
||
| 位置 | 问题 | 违反规则 |
|
||
|------|------|----------|
|
||
| `src/app/api/upload/route.ts` L31 | `const ctx = await requireAuth()` | "所有 Server Action 必须调用 `requirePermission()` 进行权限校验" |
|
||
| 同文件 L18-24 | 已定义 `FILE_UPLOAD` 权限点(permissions.ts L123),但路由层未使用 | 同上 |
|
||
|
||
**原因**:上传路由创建时仅考虑登录态校验,未将 `FILE_UPLOAD` 权限点接入;项目已定义该权限点但未消费。
|
||
|
||
**后果**:
|
||
1. 任何登录用户(含无上传权限的角色)均可调用上传接口;
|
||
2. 与 RBAC 设计意图相悖;
|
||
3. FILE_UPLOAD 权限点形同虚设。
|
||
|
||
### 2.4 横向越权:GET /api/files/[id] 仅 requireAuth(P0)
|
||
|
||
| 位置 | 问题 | 违反规则 |
|
||
|------|------|----------|
|
||
| `src/app/api/files/[id]/route.ts` L24 | `await requireAuth()` 后直接返回任意 id 的文件元数据 | "Server Action 二次校验" + 数据级别权限过滤要求 |
|
||
| 同文件 L52-58 | DELETE 仅校验 `FILE_DELETE`,未校验调用者是否拥有该文件 | 同上 |
|
||
|
||
**原因**:GET 路由未做权限分级,DELETE 未做所有权或所属 target 关联校验。
|
||
|
||
**后果**:
|
||
1. 学生可通过枚举 ID 拉取他人上传的考试附件元数据(含 mimeType、url、uploaderId 等);
|
||
2. 通过返回的 url 即可访问文件本体(文件存储在 `/public/uploads/...`,无签名机制);
|
||
3. 任何 `FILE_DELETE` 权限者可删除他人上传的文件;
|
||
4. **安全风险:违反 FERPA/GDPR 学生数据保护原则**。
|
||
|
||
### 2.5 缺少 loading.tsx 和 error.tsx(P1)
|
||
|
||
| 位置 | 问题 | 违反规则 |
|
||
|------|------|----------|
|
||
| `src/app/(dashboard)/admin/files/` | 仅 `page.tsx`,缺 `loading.tsx` / `error.tsx` | "All student routes must include loading.tsx and error.tsx for error boundaries"(admin 同样遵循,参考 admin/users、admin/roles) |
|
||
|
||
**原因**:路由未补齐标准文件。
|
||
|
||
**后果**:
|
||
1. 无骨架屏 → 用户首次进入白屏时间长;
|
||
2. data-access 抛错 → 整个 dashboard 布局错误(无 error boundary 隔离)。
|
||
|
||
### 2.6 类型安全:API 路由存在 `as` 断言(P1)
|
||
|
||
| 位置 | 问题 | 违反规则 |
|
||
|------|------|----------|
|
||
| `src/app/api/upload/route.ts` L100-102 | `targetType: isTargetType(...) ? (targetType as string) : null` | "禁止 `as` 断言(除类型收窄外)" |
|
||
| `src/app/api/files/batch-delete/route.ts` L23 | `const body = (await req.json().catch(() => null)) as { ids?: unknown } \| null` | 同上 |
|
||
| `src/app/api/files/batch-delete/route.ts` L23 | `body!.ids` 非空断言 | "可选链后禁止跟非空断言 `!`"(同类违规) |
|
||
|
||
**原因**:缺少 Zod schema 校验,只能用 `as` 转换未验证输入。
|
||
|
||
**后果**:类型不安全的输入直接进入业务逻辑,运行时仍可能爆炸;tsc 严格模式被绕过。
|
||
|
||
### 2.7 缺少 Zod schema 验证(P1)
|
||
|
||
| 位置 | 问题 | 违反规则 |
|
||
|------|------|----------|
|
||
| `src/modules/files/` | 无 `schema.ts` | "模块标准结构:`schema.ts`(Zod 验证)" |
|
||
| `src/app/api/upload/route.ts` L44-47 | `formData.get("file")` / `formData.get("targetType")` 未做 Zod 校验 | "输入使用 Zod 验证,验证失败返回结构化错误" |
|
||
| `src/app/api/files/batch-delete/route.ts` L22 | `body.ids` 仅做 `Array.isArray` + `typeof === "string"` 简单过滤,无长度上限/格式约束 | 同上 |
|
||
|
||
**原因**:路由层手写最小校验,未引入 Zod schema。
|
||
|
||
**后果**:
|
||
1. 大量恶意 ids 可导致 inArray 查询超长 SQL;
|
||
2. targetType 可注入任意字符串进入 DB;
|
||
3. 与项目其他模块(如 announcements、classes)的 schema.ts 模式不一致。
|
||
|
||
### 2.8 存储抽象使用不一致(P1)
|
||
|
||
| 位置 | 问题 |
|
||
|------|------|
|
||
| `src/app/api/upload/route.ts` L3-4、L80-85 | 直接 `import { mkdir, writeFile } from "fs/promises"` + `path.join(process.cwd(), "public", ...)` 写文件 |
|
||
| `src/app/api/files/[id]/route.ts` L2-3、L61-66 | 直接 `import { unlink } from "fs/promises"` 删文件 |
|
||
| `src/app/api/files/batch-delete/route.ts` L5、L37 | 使用 `storageProvider.delete()` 抽象 |
|
||
| `src/modules/settings/actions-avatar.ts` L2-3、L27-34 | 又一次直接 `import { unlink } from "fs/promises"` |
|
||
|
||
**原因**:`storageProvider` 抽象存在但只在批量删除处使用,单文件上传/删除路径未迁移。
|
||
|
||
**后果**:
|
||
1. 切换到 OSS/S3 时需要修改 3 处代码;
|
||
2. 4 个调用点对路径解析逻辑重复实现(DRY 违反);
|
||
3. `actions-avatar.ts` 中的 `path.join(process.cwd(), "public", fileRecord.storagePath)` 与 storageProvider 的 `delete()` 行为重复,且未处理 `storagePath` 以 `/` 开头的情况。
|
||
|
||
### 2.9 FileList 组件死代码(P2)
|
||
|
||
| 位置 | 问题 |
|
||
|------|------|
|
||
| `src/modules/files/components/file-list.tsx`(125 行) | 全代码库无任何文件 import 该组件(grep `from "@/modules/files/components/file-list"` 为 0 结果);admin-files-view.tsx 自行实现列表,未使用 FileList |
|
||
|
||
**原因**:早期创建后未接入;AdminFilesView 复刻了类似 UI。
|
||
|
||
**后果**:
|
||
1. 死代码维护负担;
|
||
2. 误导后续维护者以为有消费者;
|
||
3. 125 行无测试覆盖。
|
||
|
||
### 2.10 缺少自定义 hooks(P1)
|
||
|
||
| 位置 | 问题 | 违反规则 |
|
||
|------|------|----------|
|
||
| `src/modules/files/` | 无 `hooks/` 目录 | "模块标准结构:`hooks/`(可选)" + "逻辑复用一律抽取为自定义 hooks" |
|
||
| `components/file-upload.tsx` L43-159 | 上传逻辑(XHR + 进度 + 状态机)全部内联在组件中 | "逻辑复用一律抽取为自定义 hooks" |
|
||
| `components/file-preview.tsx` L180-233 | `TextPreview` 内部 `load` 逻辑(fetch + 错误处理)内联 | 同上 |
|
||
| `components/admin-files-view.tsx` L49-128 | 客户端筛选、批量选择、批量删除逻辑全部内联 | 同上 |
|
||
|
||
**原因**:未抽取 `useFileUpload` / `useFilePreview` / `useFileBatchOperations` 等 hooks。
|
||
|
||
**后果**:
|
||
1. 组件无法独立测试;
|
||
2. 逻辑无法在多个组件间复用(如 `FileUpload` 的进度逻辑无法被 `AvatarUpload` 复用);
|
||
3. 单组件行数膨胀风险。
|
||
|
||
### 2.11 缺少 React Error Boundary 和 Suspense 骨架屏(P1)
|
||
|
||
| 位置 | 问题 | 违反规则 |
|
||
|------|------|----------|
|
||
| `src/app/(dashboard)/admin/files/page.tsx` | 整页同步 await,无 Suspense 包裹 | "异步数据使用 React Suspense + 骨架屏" |
|
||
| `components/admin-files-view.tsx` | 上传/批量删除错误仅 toast,无 Error Boundary 隔离失败区块 | "每个独立的数据区块必须用 React Error Boundary 包裹" |
|
||
| `components/file-preview.tsx` L188-198 | TextPreview 的 fetch 错误仅展示文本,无重试按钮 | "明确处理空数据、无权限、网络异常等边界状态" |
|
||
|
||
**原因**:未引入 React 19 的 Error Boundary 与 Suspense 流式渲染模式。
|
||
|
||
**后果**:
|
||
1. 上传失败仅 toast,用户无法定位失败任务重试;
|
||
2. 整页 await 阻塞流式渲染;
|
||
3. 网络异常时整页空白。
|
||
|
||
### 2.12 数据级别权限过滤缺失(P1)
|
||
|
||
| 位置 | 问题 | 违反规则 |
|
||
|------|------|----------|
|
||
| `src/modules/files/data-access.ts` L128-143 | `getAllFileAttachments` 不带 scope/uploaderId 过滤,返回全局数据 | "所有敏感数据查询必须在 data-access 层结合当前用户权限过滤" |
|
||
| `src/modules/files/data-access.ts` L83-103 | `getFileAttachmentsByTarget` 不校验调用者是否有权访问该 target 资源 | 同上 |
|
||
| `src/modules/files/data-access.ts` L196-234 | `getFileAttachmentsWithFilters` 不带任何权限维度 | 同上 |
|
||
| `src/app/(dashboard)/admin/files/page.tsx` L25-28 | 仅校验 `FILE_READ` 权限,未结合 scope(如教师只能看自己班级的文件) | 同上 |
|
||
|
||
**原因**:模块未引入 `DataScope` 概念(grades/classes 模块已实现)。
|
||
|
||
**后果**:FILE_READ 持有者可读全部文件元数据,与 K12 多角色数据隔离要求不符。
|
||
|
||
### 2.13 架构图遗漏:getFileByUrl 未记录(P2)
|
||
|
||
| 位置 | 问题 | 违反规则 |
|
||
|------|------|----------|
|
||
| `docs/architecture/005_architecture_data.json` `modules.files.exports.dataAccess` | 列出 10 个函数,但代码中实际有 11 个(缺 `getFileByUrl`) | "如果架构图未覆盖该模块的任何部分,必须优先补全架构图" |
|
||
|
||
**原因**:`getFileByUrl` 在 P2-13 修复时新增,未同步到 JSON。
|
||
|
||
**后果**:AI 友好数据不准确,自动化审计会遗漏该函数的依赖分析。
|
||
|
||
### 2.14 架构图遗漏:settings 模块依赖未记录(P2)
|
||
|
||
| 位置 | 问题 |
|
||
|------|------|
|
||
| `docs/architecture/004_architecture_impact_map.md` §2.17 被依赖 | 仅列 `app/api/upload / app/api/files/[id] / app/api/files/batch-delete / homework`,遗漏 `settings/actions-avatar.ts` 调用 `getFileByUrl` + `deleteFileAttachment` |
|
||
|
||
**原因**:avatar 清理功能上线时未同步架构图。
|
||
|
||
### 2.15 avatar-upload.tsx 使用 "user_avatar" 非枚举 targetType(P1)
|
||
|
||
| 位置 | 问题 | 违反规则 |
|
||
|------|------|----------|
|
||
| `src/modules/settings/components/avatar-upload.tsx` L78 | `formData.append("targetType", "user_avatar")` | "TypeScript 严格模式:禁止 `any`"(类型不一致) |
|
||
| `src/modules/files/types.ts` L2 | `FileTargetType = "exam" \| "textbook" \| "question" \| "announcement" \| "homework"` 不含 `"user_avatar"` | 类型契约违反 |
|
||
| `src/app/api/upload/route.ts` L100-102 | `isTargetType(...)` 返回 false 后 fallback 到 null,导致头像文件 `targetType=null` | 数据完整性受损 |
|
||
|
||
**原因**:头像上传复用 `/api/upload`,但用了未注册的 targetType 字符串。
|
||
|
||
**后果**:
|
||
1. 头像文件在 DB 中 targetType 字段为 null,无法按类型筛选头像;
|
||
2. 清理孤立头像文件时无法用 `getFileAttachmentsByTarget("user_avatar", ...)`;
|
||
3. 类型系统未保护跨模块字符串。
|
||
|
||
### 2.16 客户端二次筛选与硬编码 limit 200(P2)
|
||
|
||
| 位置 | 问题 |
|
||
|------|------|
|
||
| `src/app/(dashboard)/admin/files/page.tsx` L26 | `getFileAttachmentsWithFilters({ limit: 200 })` 硬编码 200,无分页 |
|
||
| `components/admin-files-view.tsx` L54-75 | 客户端 useMemo 重复执行筛选逻辑(与 data-access 的 getFileAttachmentsWithFilters 重复) |
|
||
|
||
**原因**:早期为简化实现,未做服务端分页 + URL 查询参数。
|
||
|
||
**后果**:
|
||
1. 文件数超过 200 时静默丢失;
|
||
2. 服务端筛选与客户端筛选逻辑双写、易不一致;
|
||
3. 无法被搜索引擎或分享链接复用筛选状态。
|
||
|
||
### 2.17 监控埋点缺失(P2)
|
||
|
||
| 位置 | 问题 | 违反规则 |
|
||
|------|------|----------|
|
||
| `src/app/api/upload/route.ts`、`src/app/api/files/[id]/route.ts`、`src/app/api/files/batch-delete/route.ts` | 无 `trackEvent` 调用 | "方案中预留关键操作埋点接口" |
|
||
| `src/modules/files/data-access.ts` 各写函数 | 无审计日志(其他模块如 audit、announcements 已接入 `audit-logger`) | 同上 |
|
||
|
||
**原因**:模块创建时未规划可观测性。
|
||
|
||
**后果**:无法追踪上传/删除异常、容量增长、恶意批量删除等运营事件。
|
||
|
||
### 2.18 可访问性不足(P2)
|
||
|
||
| 位置 | 问题 |
|
||
|------|------|
|
||
| `components/file-list.tsx` L70-78 | `<a>` 链接无 `aria-label`,仅靠 `title` 提供上下文 |
|
||
| `components/file-preview.tsx` L96-101 | PDF iframe 缺 `aria-label`,仅 `title` |
|
||
| `components/admin-files-view.tsx` L226-229 | 全选 Checkbox indeterminate 状态无 ARIA 描述 |
|
||
| `components/file-upload.tsx` L161-204 | 拖拽区 `role="button"` 但缺 `aria-describedby` 关联限制说明 |
|
||
|
||
---
|
||
|
||
## 三、行业差距对比
|
||
|
||
### 3.1 文件预览能力薄弱
|
||
|
||
- **行业实践**(如 PowerSchool、Canvas、阿里云教育):Office 文档通过 Office Online Viewer / WPS 在线预览服务打开;视频支持原生 `<video>` 流式播放;音频支持波形 + 播放器。
|
||
- **当前实现**:仅支持 image / pdf / text 三类,Office 与视频、音频统一回退到下载。
|
||
- **影响**:教师上传 PPT 课件时无法快速浏览;学生作业扫描图(PNG)预览体验勉强可用,但 Office 资料无法即时查阅。
|
||
|
||
### 3.2 缺少文件分类与文件夹概念
|
||
|
||
- **行业实践**:按课程/班级/学科组织文件树;支持文件夹嵌套;提供"我的文件"/"共享文件"/"班级文件"分区。
|
||
- **当前实现**:仅靠 `targetType` + `targetId` 多态关联,无独立文件夹实体;admin/files 是单一平铺列表。
|
||
- **影响**:教师上传大量课件后无法分类组织;学生查找特定学科资料困难。
|
||
|
||
### 3.3 缺少文件版本管理
|
||
|
||
- **行业实践**:同名文件覆盖时保留历史版本;支持版本对比、回滚;显示修改人/修改时间。
|
||
- **当前实现**:每次上传生成新 cuid 文件名,无版本关联;同名文件多次上传产生多个独立记录。
|
||
- **影响**:教师更新课件后旧链接失效;无法回滚误删/误改。
|
||
|
||
### 3.4 缺少配额与存储管理
|
||
|
||
- **行业实践**:按用户/角色/班级分配存储配额;接近上限时提醒;超限拒绝上传;管理员可查看配额使用排行。
|
||
- **当前实现**:仅全局 `getFileStats` 统计总量,无 per-user 配额、无超限拦截。
|
||
- **影响**:单个用户可耗尽服务器磁盘;K12 学校存储成本不可控。
|
||
|
||
### 3.5 缺少文件分享与权限管理
|
||
|
||
- **行业实践**:生成分享链接(含过期时间、密码、查看/下载权限);可按用户/班级精细授权;分享操作有审计日志。
|
||
- **当前实现**:所有文件 url 直接 `/uploads/...` 公开访问,无签名、无过期、无权限校验。
|
||
- **影响**:**严重安全风险**:任何知道 URL 的人(含未登录用户)均可下载;学生隐私材料(成绩单扫描件、家长联系信息)可能泄露。
|
||
|
||
### 3.6 缺少病毒扫描与内容安全
|
||
|
||
- **行业实践**:上传时通过 ClamAV / 云安全服务扫描;图片通过 NSFW 检测;扫描结果记录到 DB。
|
||
- **当前实现**:仅校验 MIME 与大小,无内容安全检测。
|
||
- **影响**:恶意文件可上传到服务器并被其他用户下载;K12 场景下未成年人保护要求更高。
|
||
|
||
### 3.7 缺少图片缩略图与 CDN 加速
|
||
|
||
- **行业实践**:上传图片后异步生成多档缩略图(48/128/512);通过 CDN 分发;列表用小图、预览用大图。
|
||
- **当前实现**:原图直出。
|
||
- **影响**:列表页加载慢;带宽浪费;移动端体验差。
|
||
|
||
### 3.8 缺少文件使用追踪
|
||
|
||
- **行业实践**:DB 记录文件被哪些资源(exam/textbook/homework)引用;删除文件前校验引用计数;提供"孤立文件清理"任务。
|
||
- **当前实现**:仅 `targetType` + `targetId` 字段记录关联,无引用计数;删除 exam 时其附件不会自动清理。
|
||
- **影响**:磁盘孤儿文件累积;删除资源后附件残留。
|
||
|
||
### 3.9 多角色体验差距
|
||
|
||
- **教师**:无"我上传的"快捷筛选;无按班级/学科筛选;批量上传后无"批量设置 target"操作。
|
||
- **学生**:完全无入口查看自己上传的作业扫描图(仅 homework 模块内部使用);无法预览已上传的作业附件。
|
||
- **家长**:无入口查看学校下发的通知附件;仅靠 announcement 模块内嵌预览。
|
||
- **管理员**:无"按上传者筛选"快捷入口;无"近期 7 天"快捷时间筛选;无配额管理面板。
|
||
|
||
### 3.10 缺少批量操作与企业级功能
|
||
|
||
- **行业实践**:批量移动到 target、批量设置权限、批量下载(ZIP 打包)、批量重命名、Excel 导出文件清单。
|
||
- **当前实现**:仅批量删除。
|
||
- **影响**:管理员整理文件效率低。
|
||
|
||
---
|
||
|
||
## 四、改进优先级建议
|
||
|
||
### P0(必须立即修复 — 安全与架构合规)
|
||
|
||
| # | 项 | 改进方向 |
|
||
|---|----|---------|
|
||
| P0-1 | 创建 `src/modules/files/actions.ts` 编排层 | 将 upload / delete / batchDelete / query 包装为 Server Actions(带 `requirePermission` + Zod + 审计日志),路由层改为转发到 actions |
|
||
| P0-2 | 补全 i18n 翻译键(en + zh-CN `files.json`) | 提取约 40 个翻译键,组件全部接入 `useTranslations("files")` |
|
||
| P0-3 | 修复 `/api/upload` 权限校验 | 将 `requireAuth()` 替换为 `requirePermission(Permissions.FILE_UPLOAD)` |
|
||
| P0-4 | 修复横向越权 | `GET /api/files/[id]` 加 `FILE_READ`;`DELETE /api/files/[id]` 校验调用者 ownership 或拥有 `FILE_DELETE`;data-access 新增 `assertFileOwnedBy` 辅助 |
|
||
| P0-5 | 文件 URL 签名保护(中长期) | 改为非 public 路径 + 通过 API 路由签名分发,含过期时间与权限校验 |
|
||
|
||
### P1(本轮实施 — 质量与一致性)
|
||
|
||
| # | 项 | 改进方向 |
|
||
|---|----|---------|
|
||
| P1-1 | 新增 `loading.tsx` + `error.tsx` | 骨架屏 + Error Boundary 隔离 |
|
||
| P1-2 | 新增 `src/modules/files/schema.ts` | Zod 校验 upload/batchDelete/filter 输入,移除 `as` 断言 |
|
||
| P1-3 | 抽取 hooks | `useFileUpload` / `useFileBatchOperations` / `useFilePreview`,组件瘦身 |
|
||
| P1-4 | 统一 storageProvider 调用 | upload / single delete / avatar cleanup 全部改用 `storageProvider` |
|
||
| P1-5 | 修复 user_avatar targetType | 将 `"user_avatar"` 加入 `FileTargetType` 枚举;upload route `VALID_TARGET_TYPES` 同步;avatar-upload.tsx 类型对齐 |
|
||
| P1-6 | 引入 DataScope 过滤 | data-access 的查询函数新增 `scope: DataScope` 与 `currentUserId?: string` 参数,参照 grades 模块实现 |
|
||
| P1-7 | 分区 Error Boundary + Suspense | AdminFilesView 拆为 StatsSection / UploadSection / ListSection,每区独立 Suspense + Boundary |
|
||
| P1-8 | 接入 audit-logger | upload / delete / batchDelete 记录审计日志(参照 announcements 模块) |
|
||
| P1-9 | 接入 trackEvent 埋点 | 关键操作埋点:upload_success / upload_failed / delete / batch_delete |
|
||
|
||
### P2(中长期 — 体验与企业级)
|
||
|
||
| # | 项 | 改进方向 |
|
||
|---|----|---------|
|
||
| P2-1 | 删除 FileList 死代码 | 确认无消费者后删除 125 行 |
|
||
| P2-2 | 同步架构图 | 补全 `getFileByUrl` 节点、`FilePreviewDialog` 组件、settings 依赖关系 |
|
||
| P2-3 | 服务端分页 + URL 查询参数 | 替换硬编码 limit=200,支持 `?page=&size=&type=&search=&uploader=` |
|
||
| P2-4 | a11y 增强 | 给所有链接/iframe/Checkbox 加 ARIA 标签;拖拽区 `aria-describedby` |
|
||
| P2-5 | 文件预览扩展(中长期) | 接入 Office Online Viewer / 视频原生 `<video>` / 音频播放器 |
|
||
| P2-6 | 文件版本管理(中长期) | 新增 `file_versions` 表,同名覆盖时保留历史 |
|
||
| P2-7 | 文件配额(中长期) | 新增 `user_storage_quota` 配置;data-access 校验超限;admin 配额面板 |
|
||
| P2-8 | 文件分享与签名 URL(中长期) | 文件改为非 public 存储 + 签名分发;分享链接生成/吊销 |
|
||
| P2-9 | 病毒扫描(中长期) | 接入 ClamAV 或云扫描服务;扫描结果记录到 DB |
|
||
| P2-10 | 图片缩略图 + CDN(中长期) | 异步生成多档缩略图;CDN 分发 |
|
||
| P2-11 | 引用计数与孤立文件清理(中长期) | 删除资源时级联清理附件;定期清理孤儿文件任务 |
|
||
| P2-12 | 文件夹/分类实体(中长期) | 新增 `folders` 表;支持嵌套与按班级/学科组织 |
|
||
| P2-13 | 批量操作扩展(中长期) | 批量下载 ZIP、批量移动 target、Excel 导出清单 |
|
||
|
||
### 重构设计原则遵循说明
|
||
|
||
| 原则 | 落地方式 |
|
||
|------|---------|
|
||
| 完全解耦 | data-access 仅暴露纯函数;actions 注入权限/审计;组件通过 props 接收数据(无直接 data-access import) |
|
||
| 组合优先 | AdminFilesView 拆为 `<StatsSection />` + `<UploadSection />` + `<ListSection />` 组合;逻辑全入 hooks |
|
||
| 国际化就绪 | 所有可见文本走 `useTranslations("files.*")`;提供翻译文件结构示例 |
|
||
| 最大化复用 | `useFileUpload` hook 同时被 FileUpload 与 AvatarUpload 复用;`FileIcon` 已是复用单元 |
|
||
| 错误与边界处理 | 每区独立 Error Boundary;Suspense + 骨架屏;上传任务列表保留失败项供重试 |
|
||
| 可测试性 | hooks 独立可测;data-access 纯函数;schema 可独立测试 |
|
||
| 可扩展性 | 通过 `FILES_ROLE_CONFIG` 决定各角色可见 Widget;新增角色仅改配置 |
|
||
| 企业级补充 | a11y ARIA 标签、流式 RSC、storageProvider 二次校验、audit-logger + trackEvent |
|
||
|
||
---
|
||
|
||
## 五、架构图同步说明
|
||
|
||
### 5.1 004_architecture_impact_map.md §2.17 同步项
|
||
|
||
1. **导出函数**章节:
|
||
- 补充 `getFileByUrl` 函数(用于头像 URL 反查文件记录)
|
||
- 修正 `getFileAttachmentsByOwner` → 实际函数名为 `getFileAttachmentsByUploader`
|
||
- 补充 `FilePreviewDialog` 组件到组件清单
|
||
2. **依赖关系**章节:
|
||
- 被依赖方新增 `settings`(`actions-avatar.ts` 调用 `getFileByUrl` + `deleteFileAttachment`)
|
||
3. **已知问题**章节:
|
||
- 移除 "⚠️ P2:无 actions.ts" 标记(本轮 P0-1 实施后失效)
|
||
- 新增 "i18n 缺失"、"权限校验缺失"、"横向越权"标记(实施后转为 ✅)
|
||
|
||
### 5.2 005_architecture_data.json 同步项
|
||
|
||
1. `modules.files.exports.dataAccess` 数组补充:
|
||
```json
|
||
{
|
||
"name": "getFileByUrl",
|
||
"signature": "(url: string) => Promise<FileAttachment | null>",
|
||
"file": "data-access.ts",
|
||
"purpose": "按 URL 反查文件附件记录(用于头像等场景的旧文件清理)",
|
||
"deps": ["shared.db", "shared.db.schema.fileAttachments"],
|
||
"usedBy": ["settings/actions-avatar.ts"]
|
||
}
|
||
```
|
||
2. `modules.files.exports.actions` 节点新增(P0-1 实施后):
|
||
- `uploadFileAction` / `deleteFileAction` / `batchDeleteFilesAction` / `getFileListAction` / `getFileStatsAction`
|
||
3. `modules.files.exports.components` 数组补充 `FilePreviewDialog`
|
||
4. `modules.files.dependencies.usedBy` 数组新增 `"settings"`
|
||
5. `modules.files.types` 中 `FileTargetType` 定义更新为 `"exam" | "textbook" | "question" | "announcement" | "homework" | "user_avatar"`(P1-5 实施后)
|
||
|
||
---
|
||
|
||
## 六、翻译文件结构示例(i18n 就绪)
|
||
|
||
`messages/en/files.json` 与 `messages/zh-CN/files.json` 建议结构:
|
||
|
||
```json
|
||
{
|
||
"title": "File Management",
|
||
"description": "View and manage all uploaded files in the system.",
|
||
"nav": { "files": "Files" },
|
||
"upload": {
|
||
"title": "Click to upload or drag and drop",
|
||
"hint": "Images, PDF, Word, Excel, PPT, Text, ZIP / RAR · up to {size}",
|
||
"success": "{name} uploaded",
|
||
"error": "{name}: {message}",
|
||
"empty": "File is empty",
|
||
"tooLarge": "File size exceeds {limit} limit",
|
||
"invalidType": "File type {type} is not allowed",
|
||
"uploaded": "Uploaded",
|
||
"remove": "Remove",
|
||
"networkError": "Network error",
|
||
"invalidResponse": "Invalid response"
|
||
},
|
||
"list": {
|
||
"empty": "No files",
|
||
"emptyDescription": "There are no files yet.",
|
||
"download": "Download",
|
||
"delete": "Delete",
|
||
"deleted": "File deleted",
|
||
"deleteFailed": "Failed to delete file"
|
||
},
|
||
"preview": {
|
||
"trigger": "Preview",
|
||
"title": "File preview",
|
||
"download": "Download",
|
||
"zoomIn": "Zoom in",
|
||
"zoomOut": "Zoom out",
|
||
"office": {
|
||
"title": "Office file preview not available",
|
||
"hint": "Download the file to view its contents in your Office application."
|
||
},
|
||
"other": {
|
||
"title": "Preview not available",
|
||
"hint": "Download the file to view its contents."
|
||
},
|
||
"text": {
|
||
"title": "Text file",
|
||
"hint": "Click below to load the content.",
|
||
"load": "Load preview",
|
||
"loading": "Loading...",
|
||
"error": "Failed to load text: {message}"
|
||
}
|
||
},
|
||
"admin": {
|
||
"title": "Files",
|
||
"subtitle": "Upload and manage all files in the system.",
|
||
"stats": {
|
||
"totalFiles": "Total Files",
|
||
"totalSize": "Total Size"
|
||
},
|
||
"filter": {
|
||
"byType": "Filter by type",
|
||
"search": "Search by file name...",
|
||
"allTypes": "All Types",
|
||
"images": "Images",
|
||
"pdf": "PDF",
|
||
"word": "Word",
|
||
"wordDocx": "Word (docx)",
|
||
"excel": "Excel",
|
||
"excelXlsx": "Excel (xlsx)",
|
||
"powerpoint": "PowerPoint",
|
||
"powerpointPptx": "PowerPoint (pptx)",
|
||
"text": "Text",
|
||
"zip": "ZIP"
|
||
},
|
||
"selection": {
|
||
"selected": "{count} selected",
|
||
"deleteSelected": "Delete Selected",
|
||
"deleting": "Deleting...",
|
||
"deleted": "Deleted {count} file(s)",
|
||
"deleteFailed": "Failed to delete files"
|
||
},
|
||
"empty": {
|
||
"title": "No files found",
|
||
"description": "Try adjusting your filters or upload a new file."
|
||
},
|
||
"columns": {
|
||
"file": "File",
|
||
"size": "Size",
|
||
"type": "Type",
|
||
"uploaded": "Uploaded",
|
||
"actions": "Actions"
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
> 本报告由资深全栈架构师基于 2026-06-25 代码状态生成,所有 P0/P1 项将在本会话内完成实施,P2 中长期项按优先级逐步推进。
|