30 KiB
文件模块审计报告
审查日期: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 编排层。
后果:
- 权限校验分散在路由层,无法集中管理;
- 无法在 actions 层统一埋点、审计日志、缓存策略;
- 与项目其他模块(如 announcements、settings、grades 等)的 actions.ts 模式不一致,破坏一致性;
- 无法被其他模块以 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。
后果:
- 中文用户看到全英文界面(与系统其他模块本地化不一致);
- 切换语言无效;
- 翻译协作无法进行;
- 违反项目硬约束。
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 权限点接入;项目已定义该权限点但未消费。
后果:
- 任何登录用户(含无上传权限的角色)均可调用上传接口;
- 与 RBAC 设计意图相悖;
- 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 关联校验。
后果:
- 学生可通过枚举 ID 拉取他人上传的考试附件元数据(含 mimeType、url、uploaderId 等);
- 通过返回的 url 即可访问文件本体(文件存储在
/public/uploads/...,无签名机制); - 任何
FILE_DELETE权限者可删除他人上传的文件; - 安全风险:违反 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) |
原因:路由未补齐标准文件。
后果:
- 无骨架屏 → 用户首次进入白屏时间长;
- 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。
后果:
- 大量恶意 ids 可导致 inArray 查询超长 SQL;
- targetType 可注入任意字符串进入 DB;
- 与项目其他模块(如 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 抽象存在但只在批量删除处使用,单文件上传/删除路径未迁移。
后果:
- 切换到 OSS/S3 时需要修改 3 处代码;
- 4 个调用点对路径解析逻辑重复实现(DRY 违反);
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。
后果:
- 死代码维护负担;
- 误导后续维护者以为有消费者;
- 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。
后果:
- 组件无法独立测试;
- 逻辑无法在多个组件间复用(如
FileUpload的进度逻辑无法被AvatarUpload复用); - 单组件行数膨胀风险。
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 流式渲染模式。
后果:
- 上传失败仅 toast,用户无法定位失败任务重试;
- 整页 await 阻塞流式渲染;
- 网络异常时整页空白。
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 字符串。
后果:
- 头像文件在 DB 中 targetType 字段为 null,无法按类型筛选头像;
- 清理孤立头像文件时无法用
getFileAttachmentsByTarget("user_avatar", ...); - 类型系统未保护跨模块字符串。
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 查询参数。
后果:
- 文件数超过 200 时静默丢失;
- 服务端筛选与客户端筛选逻辑双写、易不一致;
- 无法被搜索引擎或分享链接复用筛选状态。
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 同步项
- 导出函数章节:
- 补充
getFileByUrl函数(用于头像 URL 反查文件记录) - 修正
getFileAttachmentsByOwner→ 实际函数名为getFileAttachmentsByUploader - 补充
FilePreviewDialog组件到组件清单
- 补充
- 依赖关系章节:
- 被依赖方新增
settings(actions-avatar.ts调用getFileByUrl+deleteFileAttachment)
- 被依赖方新增
- 已知问题章节:
- 移除 "⚠️ P2:无 actions.ts" 标记(本轮 P0-1 实施后失效)
- 新增 "i18n 缺失"、"权限校验缺失"、"横向越权"标记(实施后转为 ✅)
5.2 005_architecture_data.json 同步项
modules.files.exports.dataAccess数组补充:{ "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"] }modules.files.exports.actions节点新增(P0-1 实施后):uploadFileAction/deleteFileAction/batchDeleteFilesAction/getFileListAction/getFileStatsAction
modules.files.exports.components数组补充FilePreviewDialogmodules.files.dependencies.usedBy数组新增"settings"modules.files.types中FileTargetType定义更新为"exam" | "textbook" | "question" | "announcement" | "homework" | "user_avatar"(P1-5 实施后)
六、翻译文件结构示例(i18n 就绪)
messages/en/files.json 与 messages/zh-CN/files.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 中长期项按优先级逐步推进。