# 文件模块审计报告 > 审查日期: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 | `` 链接无 `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 在线预览服务打开;视频支持原生 `