
装修闭坑分析的输入既有户型图、效果图,也有合同、报价单和用户文字说明。装闭 RenoPit 的完整上传实现可以在 fthux/RenoPit 查看,本篇重点追踪文件从浏览器进入磁盘和数据库的过程。

一、前端如何组织多文件上传
CreateProjectPage 使用 <input type="file" multiple> 接收多个文件,允许扩展名包括 PDF、PNG、JPG、WEBP、TXT、DOCX 和 Markdown。文件先保存在 selectedFiles 状态中,页面显示名称、大小和图片或文档图标。
项目创建成功后,前端把所有文件追加到同一个 FormData:
tsx
const formData = new FormData()
selectedFiles.forEach((file) => formData.append('files', file))
await fetch(`${API}/projects/${projectId}/upload`, {
method: 'POST',
body: formData,
})
项目详情页后续追加素材时也调用同一接口。上传成功后,它并行刷新文件、图片和项目信息,使数量与列表立即更新。
二、后端先按扩展名分流
upload_files() 接收 List[UploadFile],然后逐个读取扩展名。PNG、JPG、JPEG、WEBP、GIF 和 BMP 被视为图片,其余文件进入普通文档分支。
flowchart TD AUploadFile --> B{是否图片扩展名} B -->|是| Csave_upload_file: images C --> Dcompress_image D --> Eget_image_dimensions E --> FProjectImage B -->|否| Gsave_upload_file: files G --> Hextract_text H --> IProjectFile
两条分支最后在同一个数据库事务中提交,并返回本次保存的图片数和文件数。
三、文件如何生成磁盘路径
file_storage.py 不直接使用用户上传的文件名作为磁盘名。generate_storage_filename() 先校验扩展名,再生成 UUID 文件名,避免同名文件互相覆盖。
项目目录按如下形式组织:
text
uploads/
└── projects/
└── {project_id}/
├── images/
│ └── {uuid}.jpg
└── files/
└── {uuid}.pdf
save_upload_file() 读取 UploadFile 内容,计算字节数并写入目标路径,最后返回存储文件名、完整路径和文件大小。数据库中仍保存 original_filename,所以页面展示的是用户原始文件名,下载和分析使用的则是唯一存储路径。
四、图片上传后立即预处理
图片落盘后会调用 compress_image()。函数使用 Pillow 打开图片,读取宽高;如果任一边超过最大尺寸,就按比例缩放,并使用 LANCZOS 重采样。
带透明通道的 RGBA 或调色板图片会先转换为 RGB,最终以指定 JPEG 质量原地保存。函数还会尽量保留 EXIF 数据,避免手机照片方向信息丢失。
压缩完成后,get_image_dimensions() 再读取最终宽高,创建 ProjectImage:
python
img = ProjectImage(
project_id=project_id,
original_filename=file.filename,
storage_path=storage_path,
file_size=file_size,
width=width,
height=height,
)
后续多模态分析会再次读取该路径,把图片转换成 Base64 后发送给 LLM。
五、文档如何提取纯文本
普通文件落盘后交给 extract_text()。它使用一个扩展名到解析函数的映射表:
- TXT、Markdown:按 UTF-8 读取;
- DOCX:通过
python-docx遍历段落; - PDF:通过 PyMuPDF 提取各页文本。
提取结果写入 ProjectFile.extracted_text。如果扩展名不在支持列表,extract_text() 抛出 ValueError,上传接口捕获后将提取文本设为空字符串,文件记录仍然可以保存。
这里处理的是 PDF 中的文本层;PDF 内嵌图片不会自动成为 ProjectImage。因此前端也会提示用户:需要视觉分析的图纸最好直接上传图片格式。
六、预览、下载和删除共用文件记录
项目详情页通过 /files 和 /images 获取元数据。图片预览、图片下载和文件下载分别请求带资源 ID 的接口,后端根据数据库中的 storage_path 返回 FileResponse。
删除单个素材时,接口先检查资源确实属于当前项目,再删除磁盘文件和数据库记录。前端成功后刷新对应列表,image_count 和 file_count 也会由项目响应重新计算。
七、上传链路小结
RenoPit 把上传过程拆成"浏览器收集、扩展名分流、唯一命名落盘、图片压缩或文本提取、数据库登记"五个阶段。最终,图片以磁盘路径和尺寸进入多模态链路,文档以磁盘路径和 extracted_text 进入文本分析链路。
完整实现位于 fthux/RenoPit 的 file_storage.py、image_processor.py、file_parser.py 和项目 API 中。下一篇将从上传后的项目状态出发,分析 FastAPI 如何把耗时 AI 任务交给 Celery Worker。