avatar

一条在知识海洋的咸鱼

这个家伙很懒,啥也没有留下😋

  • Linux
  • OCJP
  • Java核心技术卷
  • J2EE相关标准
  • 深入理解Java虚拟机
  • NIO与SOcket编程技术指南
  • Java多线程编程核心技术
  • Redis开发与运维
  • Spring Cloud Alibaba 微服务原理与实践
  • DevOps
  • Docker
  • MySQL必知必会
  • AI自学路线
  • Spring Boot 编程思想(核心篇)
  • 首页
主页 17 文档上传
文章

17 文档上传

发表于 最近 更新于 最近
作者 Administrator
38~48 分钟 阅读

文档上传

文档上传的核心不是把文件收到就完事,而是完成文件校验、安全存储、元数据入库和后续解析任务的入口。

[TOC]

1. 学完本节要达到什么程度

前面已经准备了数据库。

现在开始进入 RAG 入库流程的第一步:

上传文档  

上传文档不是简单的:

MultipartFile file  
Files.copy(...)  

还要考虑:

文件是否为空  
文件类型是否允许  
文件大小是否超限  
文件名是否安全  
是否重复上传  
保存路径如何生成  
元数据如何入库  
后续解析任务如何触发  

这一节先做一个稳定的上传入口。

2. 上传流程

建议流程:

前端选择文件  
 -> Java 接收 multipart/form-data -> 校验文件  
 -> 生成 documentId -> 保存原始文件  
 -> 计算 sha256 -> 写入 rag_document -> 返回 documentId  

上传流程图:

graph TD;  
 A["前端选择文件"] --> B["POST multipart/form-data"];  
 B --> C["Java Controller 接收文件"];  
 C --> D{"基础校验通过吗?"};  
 D -- "否" --> D1["返回错误<br/>空文件 / 类型不支持 / 大小超限"];  
 D -- "是" --> E["生成 documentId 和安全文件名"];  
 E --> F["保存原始文件"];  
 F --> G["计算 sha256"]; G --> H["写入 rag_document"]; H --> I["创建解析任务或等待手动触发"];  
 I --> J["返回 documentId"];  

后续再解析。

不要在上传接口里一口气做完所有事。

上传接口先保证文件安全落地。

3. 支持的文件类型

第一版建议只支持:

pdf  
docx  
md  
txt  

不要一开始就支持:

xls  
ppt  
图片  
扫描件  
压缩包  
网页链接  

类型越多,解析难度越高。

先把最常见的四类跑通。

4. Spring Boot 上传配置

可以在 application.yaml 里设置上传限制:

spring:  
 servlet: multipart: max-file-size: 20MB max-request-size: 20MB  

含义:

max-file-size:单个文件最大大小  
max-request-size:整个请求最大大小  

学习阶段 20MB 足够。

正式项目要按业务场景调整。

5. Controller

Java 接口可以这样设计:

import org.springframework.http.MediaType;  
import org.springframework.web.bind.annotation.PostMapping;  
import org.springframework.web.bind.annotation.RequestMapping;  
import org.springframework.web.bind.annotation.RequestPart;  
import org.springframework.web.bind.annotation.RestController;  
import org.springframework.web.multipart.MultipartFile;  
  
@RestController  
@RequestMapping("/api/17/documents")  
public class DocumentUploadController {  
  
 private final DocumentUploadService documentUploadService;  
 public DocumentUploadController(DocumentUploadService documentUploadService) { this.documentUploadService = documentUploadService; }  
 @PostMapping( value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE ) public DocumentUploadResponse upload(@RequestPart("file") MultipartFile file) { return documentUploadService.upload(file); }}  

接口:

POST /api/17/documents/upload  
Content-Type: multipart/form-data  

字段:

file:上传文件  

6. 响应对象

import java.time.LocalDateTime;  
import java.util.UUID;  
  
public record DocumentUploadResponse(  
 UUID documentId, String originalName, long fileSize, String contentType, String status, LocalDateTime uploadedAt) {  
}  

返回示例:

{
  "documentId": "3d24e89e-0c22-4b4b-9e0a-b4fd0f83c739",
  "originalName": "产品手册.pdf",
  "fileSize": 102400,
  "contentType": "application/pdf",
  "status": "UPLOADED",
  "uploadedAt": "2026-06-07T23:30:00"
}

前端拿到 documentId 后,后面可以查询解析状态。

7. 文件校验

最少校验:

文件不能为空  
原始文件名不能为空  
文件大小不能超过限制  
扩展名必须允许  
Content-Type 尽量匹配  

扩展名校验:

private static final Set<String> ALLOWED_EXTENSIONS = Set.of(  
 "pdf", "docx", "md", "txt");  
  
private String getExtension(String filename) {  
 int index = filename.lastIndexOf('.'); if (index < 0 || index == filename.length() - 1) { throw new IllegalArgumentException("文件缺少扩展名");  
 }  
 return filename.substring(index + 1).toLowerCase(Locale.ROOT);}  

校验:

String extension = getExtension(originalName);  
  
if (!ALLOWED_EXTENSIONS.contains(extension)) {  
 throw new IllegalArgumentException("不支持的文件类型:" + extension);  
}  

不要只相信浏览器传来的 Content-Type。

它可以作为参考,但不能作为唯一依据。

8. 安全文件名

不要直接用原始文件名当服务器保存路径。

危险示例:

../../application.yaml  

应该生成自己的文件名:

documentId + "." + extension  

比如:

3d24e89e-0c22-4b4b-9e0a-b4fd0f83c739.pdf  

原始文件名只作为 metadata 保存。

9. 保存文件

Service 示例:

import org.springframework.stereotype.Service;  
import org.springframework.web.multipart.MultipartFile;  
  
import java.io.InputStream;  
import java.nio.file.Files;  
import java.nio.file.Path;  
import java.nio.file.StandardCopyOption;  
import java.time.LocalDateTime;  
import java.util.UUID;  
  
@Service  
public class DocumentUploadService {  
  
 private final Path uploadRoot = Path.of("data", "uploads");  
 public DocumentUploadResponse upload(MultipartFile file) { if (file.isEmpty()) { throw new IllegalArgumentException("上传文件不能为空");  
 }  
 String originalName = file.getOriginalFilename(); if (originalName == null || originalName.isBlank()) { throw new IllegalArgumentException("文件名不能为空");  
 }  
 String extension = getExtension(originalName); UUID documentId = UUID.randomUUID(); String storedName = documentId + "." + extension; Path target = uploadRoot.resolve(storedName).normalize().toAbsolutePath();  
 try { Files.createDirectories(uploadRoot);  
 try (InputStream inputStream = file.getInputStream()) { Files.copy(inputStream, target, StandardCopyOption.REPLACE_EXISTING); } } catch (Exception e) { throw new IllegalStateException("保存上传文件失败", e);  
 }  
 return new DocumentUploadResponse( documentId, originalName, file.getSize(), file.getContentType(), "UPLOADED", LocalDateTime.now() ); }}  

这里还没有入库。

下一步要把 metadata 写到 rag_document。

10. 计算 sha256

sha256 可以用于判断重复文件。

示例:

import java.io.InputStream;  
import java.security.DigestInputStream;  
import java.security.MessageDigest;  
import java.util.HexFormat;  
  
private String sha256(MultipartFile file) {  
 try { MessageDigest digest = MessageDigest.getInstance("SHA-256");  
 try (InputStream inputStream = file.getInputStream(); DigestInputStream digestInputStream = new DigestInputStream(inputStream, digest)) { digestInputStream.transferTo(OutputStream.nullOutputStream()); }  
 return HexFormat.of().formatHex(digest.digest()); } catch (Exception e) { throw new IllegalStateException("计算文件 hash 失败", e);  
 }}  

需要导入:

import java.io.OutputStream;  

注意:

如果先计算 hash,再保存文件,会读取两次 input stream  

学习阶段可以接受。

正式项目可以边保存边计算,或者保存后对文件计算 hash。

11. 元数据入库

上传成功后写入 rag_document。

伪代码:

RagDocument document = new RagDocument(  
 documentId, originalName, target.toString(), file.getContentType(), file.getSize(), sha256, "UPLOADED");  
  
ragDocumentRepository.save(document);  

数据库记录要包含:

id  
original_name  
storage_path  
content_type  
file_size  
sha256  
status  
created_at  
updated_at  

保存文件和写数据库要考虑一致性。

第一版可以:

先保存文件  
再写数据库  
数据库失败时删除文件  

或者:

先写 UPLOADING保存成功后改 UPLOADED  

学习阶段先实现简单版本。

12. 触发解析任务

上传完成后可以选择两种模式。

同步模式:

上传成功  
 -> 立即解析  
 -> 返回解析结果  

异步模式:

上传成功  
 -> 创建解析任务  
 -> 返回 documentId -> 后台解析  

建议第一版:

上传接口只返回 documentId解析接口单独触发  

比如:

POST /api/18/documents/{documentId}/parse  

这样更容易调试。

13. curl 测试

上传文件:

curl -X POST http://127.0.0.1:8080/api/17/documents/upload \ -F "file=@./docs/demo.pdf"```  
  
上传 Markdown:  
  
```bash  
curl -X POST http://127.0.0.1:8080/api/17/documents/upload \ -F "file=@./README.md"```  
  
测试不支持类型:  
  
```bash  
curl -X POST http://127.0.0.1:8080/api/17/documents/upload \ -F "file=@./demo.exe"```  
  
期望返回明确错误。  
  
## 14. 上传链路的工程化验收  
  
上传成功不能只看接口返回 `200`。一个可交付的上传链路,还要同时保证文件、数据库记录和后续任务之间能够对应起来。  
  
### 14.1 一致性边界  
  
建议把上传过程拆成三个明确阶段:接收临时文件、持久化原文件、提交文档记录。任一阶段失败,都要知道前面已经留下了什么。  
  
- 文件已保存但数据库写入失败:记录补偿日志,并由定时任务清理孤儿文件。  
- 数据库已写入但任务创建失败:文档保留 `UPLOADED` 状态,允许重新触发解析。  
- 客户端超时后重复提交:使用文件摘要、业务幂等键或上传请求 ID 判断是否重复。  
  
不要把“删除文件”当作唯一补偿方式。网络抖动、进程退出和对象存储短暂不可用,都可能让同步回滚失败,因此还需要可重试的后台清理机制。  
  
### 14.2 可观测性和审计  
  
每次上传至少应记录 `requestId`、`documentId`、原始文件名、安全文件名、文件大小、SHA-256、耗时和最终状态。日志中不要输出文件正文,也不要记录对象存储的永久访问凭证。  
  
建议关注以下指标:  
  
| 指标 | 用途 |  
| --- | --- |  
| 上传成功率 | 判断入口是否稳定 |  
| P95 上传耗时 | 发现磁盘、网络或对象存储变慢 |  
| 校验失败次数 | 观察大小、类型和空文件问题 |  
| 重复文件命中率 | 评估去重策略是否有效 |  
| 孤儿文件数量 | 检查文件与数据库的一致性 |  
  
### 14.3 验收标准  
  
上线前至少验证正常文件、空文件、超限文件、伪造扩展名、重复上传、中文文件名、同名文件和存储失败等场景。每种失败都应返回稳定的错误码,并能通过 `requestId` 定位完整日志。  
  
验收结果不应依赖人工查看目录。应该能通过数据库状态、接口响应和监控指标判断上传是否真正完成。  
  
## 15. 常见问题  
  
### 15.1 为什么不直接解析文件  
  
上传和解析是两个职责。  
  
上传关注:  
  
```text  
接收  
校验  
保存  
入库  

解析关注:

读取文件  
抽取文本  
保留元数据  
处理失败  

拆开后更容易定位问题。

15.2 文件名要不要保留中文

原始文件名可以保留在数据库。

服务器保存文件名建议使用 UUID。

这样可以避免编码、重名和路径安全问题。

15.3 如何处理重复文件

可以根据 sha256 判断。

第一版可以只记录 hash。

后面再决定:

禁止重复上传  
允许重复但复用解析结果  
同用户去重  
同知识库去重  

15.4 上传到本地磁盘够不够

学习阶段够。

正式项目建议对象存储:

MinIO  
阿里云 OSSAWS S3  

数据库只存对象路径。

16. 练习清单

完成几件事情:

配置 multipart 最大文件大小  
实现 /api/17/documents/upload校验空文件  
校验扩展名  
生成 UUID 文件名  
保存文件到 data/uploads计算 sha256写入 rag_document返回 documentId用 curl 上传 pdf/docx/md/txt用 curl 测试非法类型  

建议目录:

ai-agent-study  
├── java  
│   └── rag-api  
│       ├── DocumentUploadController.java  
│       ├── DocumentUploadService.java  
│       ├── DocumentUploadResponse.java  
│       └── RagDocumentRepository.java  
├── data  
│   └── uploads  
└── docs  
 └── document-upload.md  

17. 小结

本节的结论:

文档上传要先做好校验、安全存储和元数据入库,解析和向量化不要急着塞进同一个接口。

最小链路:

multipart file  
 -> 校验  
 -> UUID 保存  
 -> sha256 -> rag_document -> documentId  

有了稳定的 documentId,后面的解析、清洗、切片、Embedding 才能一步步接上。

参考资料

  • Spring Uploading Files
  • Spring Boot Multipart Properties
AI自学路线
AI
许可协议: 
分享

相关文章

6月 12, 2026

17 文档上传

文档上传 文档上传的核心不是把文件收到就完事,而是完成文件校验、安全存储、元数据入库和后续解析任务的入口。

6月 12, 2026

16_01PostgreSQL 介绍

从 MySQL 到 PostgreSQL:面向 AI / RAG 开发的完整入门 适合已经知道 MySQL 基本增删改查,但没有系统使用过 PostgreSQL 的 Java 后

6月 11, 2026

16 数据库准备

数据库准备 RAG 数据库准备的核心是把原始文档、文档片段、向量和任务状态分开存,让后面的上传、解析、切片、Embedding 和੹

下一篇

上一篇

16_01PostgreSQL 介绍

最近更新

  • 17 文档上传
  • 16_01PostgreSQL 介绍
  • 16 数据库准备
  • 15 RAG 原理
  • 14 阶段交付

热门标签

java基础 微服务 maven Spring Tomcat DDD Linux Linux基础 SQL基础 数据结构算法

目录

©2026 一条在知识海洋的咸鱼. 保留部分权利。

使用 Halo 主题 Chirpy