在现代AI对话系统中,支持附件上传已成为一项核心功能。本文将深入剖析通意千应——一个基于Spring Boot + Vue.js的AI对话系统中,附件上传功能的完整技术实现方案。通意千应支持多类型文件(PDF、Word、Excel、PPT、图片、文本)的上传、解析和多模态AI交互,具有完整的文件存储、内容提取和AI集成能力。
一、系统架构概览
1.1 通意千应整体架构设计
通意千应的附件上传功能采用分层架构设计:
┌─────────────────────────────────────────────────────────┐
│ 前端层 (Vue.js) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │
│ │ 文件选择组件 │ │ 预览渲染组件 │ │ 上传管理组件 │ │
│ └─────────────┘ └─────────────┘ └─────────────────┘ │
├─────────────────────────────────────────────────────────┤
│ 控制层 (Spring Boot) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │
│ │ ChatController│ │ 文件上传API │ │ 文件下载API │ │
│ └─────────────┘ └─────────────┘ └─────────────────┘ │
├─────────────────────────────────────────────────────────┤
│ 服务层 (Service Layer) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │
│ │ChatFileService│ │FileParseService│ │ QiNiuService │ │
│ └─────────────┘ └─────────────┘ └─────────────────┘ │
├─────────────────────────────────────────────────────────┤
│ 数据层 (Data Layer) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │
│ │ ChatFile │ │ MyBatis │ │ 七牛云OSS │ │
│ │ Entity │ │ Mapper │ │ │ │
│ └─────────────┘ └─────────────┘ └─────────────────┘ │
└─────────────────────────────────────────────────────────┘
1.2 通意千应核心功能模块
- 文件上传管理:支持多文件批量上传,单文件最大100MB
- 文件内容解析:支持PDF、Word、Excel、PPT、TXT等格式的文本提取
- 图片多模态处理:支持图片上传和Base64编码,供通意千应AI视觉分析
- 云存储集成:使用七牛云OSS进行文件存储和CDN加速
- 文件关联管理:文件与消息的关联存储和查询
二、数据模型设计
2.1 文件实体类设计
通意千应的文件实体类设计如下:
@Data
@TableName("chat_file")
public class ChatFile {
@TableId(type = IdType.AUTO)
private Long id; // 文件ID,主键自增
private Long messageId; // 关联的消息ID
private String originalName; // 原始文件名
private String storedName; // 存储文件名(UUID生成)
private String filePath; // 文件存储路径
private String fileUrl; // 七牛云访问URL
private String fileType; // 文件类型(PDF文档、Word文档等)
private Long fileSize; // 文件大小(字节)
private String extractedText; // 提取的文本内容
private LocalDateTime createTime; // 创建时间
}
通意千应设计要点分析:
- UUID存储命名:使用
generateStoredName()方法生成UUID作为存储文件名,避免文件名冲突和安全问题 - 日期路径组织:采用
yyyy/MM/dd的层级目录结构,便于文件管理和清理 - 双存储策略:同时保存本地路径和云存储URL,支持灵活的文件访问方式
- 文本预提取:在文件上传时即完成文本内容提取,避免重复解析,提升通意千应AI响应速度
三、文件上传流程实现
3.1 前端文件选择与预处理
通意千应前端使用Vue.js实现文件选择和预览功能:
// 文件选择处理
handleFileSelect(event) {
const files = Array.from(event.target.files);
const validFiles = [];
files.forEach((file, index) => {
const ext = '.' + file.name.split('.').pop().toLowerCase();
// 1. 文件类型校验
if (!allAllowedExts.includes(ext)) {
errors.push(`${file.name}: 不支持的文件格式`);
return;
}
// 2. 文件大小校验(100MB限制)
if (file.size > this.maxFileSize) {
errors.push(`${file.name}: 文件大小超过100MB限制`);
return;
}
// 3. 图片数量限制(最多3张)
if (fileType === 'image') {
if (currentImageCount + newImageCount >= 3) {
errors.push(`图片识别最多支持3张,已跳过 ${file.name}`);
return;
}
}
// 4. 重复文件检测
const isDuplicate = this.selectedFiles.some(selectedFile =>
selectedFile.name === file.name && selectedFile.size === file.size
);
// 图片类型创建本地预览URL
let previewUrl = null;
if (fileType === 'image') {
previewUrl = URL.createObjectURL(file);
}
validFiles.push({
file: file,
name: file.name,
size: file.size,
type: fileType,
previewUrl: previewUrl,
isUploading: fileType === 'image'
});
});
}
通意千应关键设计决策:
- 即时预览体验:图片文件使用
URL.createObjectURL()创建本地预览,无需等待上传完成 - 多维度校验:类型、大小、数量、重复性四层校验,确保上传质量
- 渐进式上传:图片文件先上传到临时目录,提升用户体验
3.2 后端文件上传接口
通意千应后端文件上传接口实现:
@PostMapping("/api/chat/upload")
@ResponseBody
public Result<List<Map<String, Object>>> uploadFiles(
@RequestParam("files") List<MultipartFile> files) {
// 1. 用户认证检查
User user = userService.getCurrentUser();
if (user == null) {
return Result.error(401, "未登录");
}
// 2. 基础校验
if (files == null || files.isEmpty()) {
return Result.error("请选择要上传的文件");
}
if (files.size() > 10) {
return Result.error("最多只能上传10个文件");
}
// 3. 单文件大小校验
for (MultipartFile file : files) {
if (file.getSize() > 100 * 1024 * 1024) {
return Result.error("文件大小不能超过100MB");
}
}
// 4. 执行上传
List<ChatFile> uploadedFiles = chatFileService.uploadFiles(files);
// 5. 返回文件信息
List<Map<String, Object>> result = new ArrayList<>();
for (ChatFile chatFile : uploadedFiles) {
Map<String, Object> fileInfo = new HashMap<>();
fileInfo.put("id", chatFile.getId());
fileInfo.put("name", chatFile.getOriginalName());
fileInfo.put("type", chatFile.getFileType());
fileInfo.put("size", chatFile.getFileSize());
result.add(fileInfo);
}
return Result.success("上传成功", result);
}
3.3 核心上传服务实现
通意千应的核心上传服务:
@Service
public class ChatFileServiceImpl implements ChatFileService {
@Override
public ChatFile uploadFile(MultipartFile file, Long messageId) {
String originalName = file.getOriginalFilename();
// 1. 文件类型支持性检查
if (!fileParseService.isSupported(originalName)) {
throw new RuntimeException("不支持的文件类型: " + originalName);
}
// 2. 生成存储路径
String storedName = generateStoredName(originalName); // UUID命名
String datePath = getDatePath(); // 日期分层
String fileKey = datePath + "/" + storedName;
// 3. 文件内容解析(关键步骤)
Map<String, Object> parseResult = fileParseService.parseFile(file);
String extractedText = (String) parseResult.get("extractedText");
// 4. 上传到七牛云OSS
String fileUrl = qiNiuService.uploadFile(file, fileKey);
// 5. 保存文件元数据到数据库
ChatFile chatFile = new ChatFile();
chatFile.setMessageId(messageId);
chatFile.setOriginalName(originalName);
chatFile.setStoredName(storedName);
chatFile.setFilePath(fileKey);
chatFile.setFileUrl(fileUrl);
chatFile.setFileType(fileParseService.getFileType(originalName));
chatFile.setFileSize(file.getSize());
chatFile.setExtractedText(extractedText); // 预提取的文本内容
chatFile.setCreateTime(LocalDateTime.now());
chatFileMapper.insert(chatFile);
return chatFile;
}
}
通意千应技术亮点:
- 预解析策略:文件上传时即完成内容解析,将
extractedText保存到数据库,避免通意千应AI对话时的重复解析 - 云存储优先:使用七牛云OSS存储文件,提供CDN加速和可靠的文件访问
- 事务一致性:文件上传、内容解析、数据库存储形成完整事务链
四、多类型文件解析实现
4.1 文件解析服务架构
通意千应支持7种文件类型的解析,采用策略模式设计:
@Service
public class FileParseServiceImpl implements FileParseService {
// 文件类型映射配置
private static final Map<String, String> FILE_TYPE_MAP = new LinkedHashMap<>();
static {
FILE_TYPE_MAP.put("pdf", "PDF文档");
FILE_TYPE_MAP.put("doc", "Word文档");
FILE_TYPE_MAP.put("docx", "Word文档");
FILE_TYPE_MAP.put("xls", "Excel表格");
FILE_TYPE_MAP.put("xlsx", "Excel表格");
FILE_TYPE_MAP.put("ppt", "PowerPoint演示文稿");
FILE_TYPE_MAP.put("pptx", "PowerPoint演示文稿");
FILE_TYPE_MAP.put("png", "图片");
FILE_TYPE_MAP.put("jpg", "图片");
FILE_TYPE_MAP.put("txt", "文本文件");
// ...
}
@Override
public Map<String, Object> parseFile(MultipartFile file) {
String fileName = file.getOriginalFilename();
String fileType = getFileType(fileName);
byte[] content = file.getBytes();
String extractedText = "";
// 根据文件类型选择解析策略
switch (fileType) {
case "PDF文档":
extractedText = parsePdf(content, fileName);
break;
case "Word文档":
extractedText = parseWord(content, fileName);
break;
case "Excel表格":
extractedText = parseExcel(content, fileName);
break;
case "PowerPoint演示文稿":
extractedText = parsePpt(content, fileName);
break;
case "文本文件":
extractedText = parseText(content, fileName);
break;
case "图片":
// 图片不解析文本,通过多模态方式处理
extractedText = "";
break;
}
result.put("extractedText", extractedText);
return result;
}
}
4.2 PDF文档解析
通意千应使用Apache PDFBox库进行PDF文本提取:
@Override
public String parsePdf(byte[] content, String fileName) {
try (PDDocument document = PDDocument.load(new ByteArrayInputStream(content))) {
PDFTextStripper stripper = new PDFTextStripper();
String text = stripper.getText(document);
// 扫描版PDF检测
if (result.isEmpty() && document.getNumberOfPages() > 0) {
result = "[此PDF文件无法提取文本内容,可能是扫描版PDF或图片型PDF]";
}
return result;
}
}
4.3 Office文档解析
通意千应使用Apache POI库处理Word、Excel、PPT:
// Word文档解析(支持doc和docx)
@Override
public String parseWord(byte[] content, String fileName) {
String ext = getExtension(fileName);
if ("docx".equals(ext)) {
// 新版Word格式
try (XWPFDocument document = new XWPFDocument(new ByteArrayInputStream(content))) {
XWPFWordExtractor extractor = new XWPFWordExtractor(document);
return extractor.getText();
}
} else {
// 旧版Word格式
try (HWPFDocument document = new HWPFDocument(new ByteArrayInputStream(content))) {
WordExtractor extractor = new WordExtractor(document);
return extractor.getText();
}
}
}
// Excel解析(保留表格结构)
@Override
public String parseExcel(byte[] content, String fileName) {
Workbook workbook = "xlsx".equals(ext) ?
new XSSFWorkbook(new ByteArrayInputStream(content)) :
new HSSFWorkbook(new ByteArrayInputStream(content));
StringBuilder result = new StringBuilder();
DataFormatter formatter = new DataFormatter();
for (int i = 0; i < workbook.getNumberOfSheets(); i++) {
Sheet sheet = workbook.getSheetAt(i);
result.append("【工作表: ").append(sheet.getSheetName()).append("】\n");
for (Row row : sheet) {
StringBuilder rowText = new StringBuilder();
for (Cell cell : row) {
String cellValue = formatter.formatCellValue(cell);
if (cellValue != null && !cellValue.trim().isEmpty()) {
rowText.append(" | ").append(cellValue);
}
}
result.append(rowText).append("\n");
}
}
return result.toString();
}
4.4 文本文件智能编码检测
通意千应支持多种编码格式的文本文件:
@Override
public String parseText(byte[] content, String fileName) {
// 尝试多种编码方式读取
String[] charsets = {"UTF-8", "GBK", "GB2312", "ISO-8859-1", "UTF-16"};
String text = null;
for (String charset : charsets) {
try {
text = new String(content, charset);
// 乱码检测:检查替换字符比例
int replacementCharCount = 0;
for (int i = 0; i < Math.min(text.length(), 1000); i++) {
if (text.charAt(i) == '\uFFFD') {
replacementCharCount++;
}
}
// 如果替换字符比例超过10%,尝试下一个编码
if (replacementCharCount > Math.min(text.length(), 1000) * 0.1) {
continue;
}
break;
} catch (Exception e) {
continue;
}
}
return text != null ? text.trim() : "[文本解析失败]";
}
五、图片多模态处理
5.1 图片Base64编码
为了实现通意千应AI的视觉理解能力,系统需要将图片转换为Base64编码:
@Override
public String getImageBase64(Long fileId) {
ChatFile chatFile = chatFileMapper.selectById(fileId);
// 类型检查
if (!"图片".equals(chatFile.getFileType())) {
return null;
}
byte[] fileContent = null;
// 优先从七牛云URL下载
if (chatFile.getFileUrl() != null && !chatFile.getFileUrl().isEmpty()) {
String fileUrl = chatFile.getFileUrl();
// HTTP转HTTPS处理
if (fileUrl.startsWith("http://")) {
fileUrl = fileUrl.replace("http://", "https://");
}
// 下载图片内容
java.net.URL url = new java.net.URL(fileUrl);
java.net.HttpURLConnection connection = (java.net.HttpURLConnection) url.openConnection();
connection.setConnectTimeout(10000);
connection.setReadTimeout(30000);
try (InputStream is = connection.getInputStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = is.read(buffer)) != -1) {
baos.write(buffer, 0, bytesRead);
}
fileContent = baos.toByteArray();
}
// 文件头检查,防止下载到错误内容
if (fileContent.length > 10) {
String header = new String(fileContent, 0, Math.min(100, fileContent.length), "UTF-8");
if (header.startsWith("<!DOCTYPE") || header.startsWith("<html")) {
// 下载的是HTML而非图片,尝试本地文件
fileContent = null;
}
}
}
// 本地文件回退(兼容旧数据)
if (fileContent == null) {
Path filePath = Paths.get(uploadPath, chatFile.getFilePath());
fileContent = Files.readAllBytes(filePath);
}
// 转换为Base64 Data URI
String base64 = Base64.getEncoder().encodeToString(fileContent);
String mimeType = getImageMimeType(chatFile.getOriginalName());
return "data:" + mimeType + ";base64," + base64;
}
5.2 图片数量限制策略
通意千应对图片数量进行限制,确保系统稳定性:
// 在ChatController中限制图片数量
List<String> imageBase64List = new ArrayList<>();
if (fileIds != null && !fileIds.isEmpty()) {
int imageCount = 0;
for (Long fileId : fileIds) {
String imageBase64 = chatFileService.getImageBase64(fileId);
if (imageBase64 != null) {
// 限制最多3张图片,避免内存溢出和Token超限
if (imageCount >= 3) {
log.warn("图片数量超过3张,跳过剩余图片");
break;
}
imageBase64List.add(imageBase64);
imageCount++;
}
}
}
六、AI集成与内容传递
6.1 文件内容整合到AI请求
通意千应将文件内容整合到AI请求中:
@PostMapping("/api/chat/send")
@ResponseBody
public Result<Map<String, Object>> sendMessage(@Validated @RequestBody ChatRequest chatRequest) {
// 1. 过滤空值fileId
List<Long> fileIds = chatRequest.getFileIds();
if (fileIds != null) {
fileIds = fileIds.stream()
.filter(id -> id != null)
.collect(Collectors.toList());
}
// 2. 获取图片Base64(用于多模态)
List<String> imageBase64List = new ArrayList<>();
if (fileIds != null && !fileIds.isEmpty()) {
for (Long fileId : fileIds) {
String imageBase64 = chatFileService.getImageBase64(fileId);
if (imageBase64 != null) {
imageBase64List.add(imageBase64);
}
}
}
// 3. 获取文件文本内容
String contentForAI = content;
if (fileIds != null && !fileIds.isEmpty()) {
String fileContent = chatFileService.getFileContentForAI(fileIds);
// 整合文件内容和用户问题
contentForAI = fileContent + "\n\n用户问题:" + content;
}
// 4. 构建AI请求
ChatRequest aiRequest = new ChatRequest();
aiRequest.setContent(contentForAI);
aiRequest.setImageBase64List(imageBase64List); // 多模态图片
aiRequest.setUserQuery(content); // 保存原始问题用于网络搜索
// 5. 调用通意千应AI服务
String aiResponse = chatMessageService.sendMessageToAI(aiRequest, history);
// 6. 关联文件到消息
if (fileIds != null && !fileIds.isEmpty()) {
for (Long fileId : fileIds) {
chatFileService.updateFileMessageId(fileId, userMessage.getId());
}
}
}
6.2 文件内容格式化
通意千应的文件内容格式化服务:
@Override
public String getFileContentForAI(List<Long> fileIds) {
List<ChatFile> files = getByIds(fileIds);
StringBuilder content = new StringBuilder();
content.append("以下是用户上传的文件内容:\n\n");
for (ChatFile file : files) {
content.append("=== 文件: ").append(file.getOriginalName())
.append(" (").append(file.getFileType()).append(") ===\n");
// 图片类型特殊处理
if ("图片".equals(file.getFileType())) {
content.append("[图片文件,将通过视觉能力分析]");
} else {
// 使用预提取的文本内容
String extractedText = file.getExtractedText();
if (extractedText != null && !extractedText.isEmpty()) {
content.append(extractedText);
} else {
content.append("[无法提取文件内容]");
}
}
content.append("\n\n");
}
return content.toString();
}
七、七牛云OSS集成
7.1 云存储服务实现
通意千应使用七牛云OSS进行文件存储:
@Service
public class QiNiuServiceImpl implements QiNiuService {
@Value("${qiniu.access-key}")
private String accessKey;
@Value("${qiniu.secret-key}")
private String secretKey;
@Value("${qiniu.bucket}")
private String bucket;
@Value("${qiniu.domain}")
private String domain;
private UploadManager uploadManager;
private BucketManager bucketManager;
private Auth auth;
@PostConstruct
public void init() {
auth = Auth.create(accessKey, secretKey);
Configuration cfg = new Configuration(getRegion(region));
uploadManager = new UploadManager(cfg);
bucketManager = new BucketManager(auth, cfg);
}
@Override
public String uploadFile(MultipartFile file, String fileName) {
String key = filePrefix + fileName;
String upToken = auth.uploadToken(bucket, key);
Response response = uploadManager.put(
file.getInputStream(), key, upToken, null, null);
DefaultPutRet putRet = response.jsonToObject(DefaultPutRet.class);
return getFileUrl(putRet.key);
}
}
7.2 临时文件管理
通意千应支持临时文件上传(用于图片预览):
// 上传临时图片
@PostMapping("/api/chat/upload/temp")
@ResponseBody
public Result<Map<String, Object>> uploadTempImage(@RequestParam("file") MultipartFile file) {
// 只允许图片格式
List<String> allowedExts = Arrays.asList(".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp");
String tempFileName = UUID.randomUUID().toString().replace("-", "") + ext;
String tempUrl = qiNiuService.uploadTempFile(file, tempFileName);
Map<String, Object> result = new HashMap<>();
result.put("fileName", tempFileName);
result.put("url", tempUrl);
return Result.success("上传成功", result);
}
// 删除临时图片
@DeleteMapping("/api/chat/upload/temp/{fileName}")
@ResponseBody
public Result<Void> deleteTempImage(@PathVariable String fileName) {
boolean deleted = qiNiuService.deleteTempFile(fileName);
return Result.success("删除成功");
}
八、前端文件展示与交互
8.1 消息中的文件展示
通意千应前端消息中的文件展示:
<!-- 用户消息中的文件预览 -->
<div v-if="message.files && message.files.length > 0" class="message-files-preview">
<div v-for="file in message.files" :key="file.id"
class="message-file-item"
:class="{ 'is-image': file.fileType === '图片' }">
<!-- 图片类型:直接显示 -->
<template v-if="file.fileType === '图片'">
<div class="message-image-preview" @click="openMessageImagePreview(file)">
<img :src="'/api/chat/file/' + file.id" class="message-image-thumb">
</div>
</template>
<!-- 非图片类型:显示文件卡片 -->
<template v-else>
<div class="message-file-card" @click="previewFile(file)">
<div class="message-file-card-icon" :class="getFilePreviewClass(file.fileType)">
<i :class="getFileIconByType(file.fileType)"></i>
</div>
<div class="message-file-card-info">
<div class="message-file-card-name">{{ file.originalName }}</div>
<div class="message-file-card-size">{{ formatFileSize(file.fileSize) }}</div>
</div>
</div>
</template>
</div>
</div>
九、性能优化策略
9.1 文本预提取优化
通意千应的文本预提取优化策略:
// 上传时预提取文本,避免重复解析
chatFile.setExtractedText(extractedText);
// AI对话时直接使用预提取内容
String extractedText = file.getExtractedText();
if (extractedText != null && !extractedText.isEmpty()) {
content.append(extractedText);
}
9.2 图片懒加载与压缩
通意千应图片优化策略:
- 图片上传时生成缩略图预览
- 使用七牛云图片处理参数进行动态压缩
- 大图预览时支持缩放和拖拽
9.3 并发控制
// 限制同时处理的文件数量
if (files.size() > 10) {
return Result.error("最多只能上传10个文件");
}
// 限制图片数量,避免AI Token超限
if (imageCount >= 3) {
break;
}
十、安全考虑
10.1 文件类型白名单
通意千应的文件类型白名单机制:
private static final Map<String, String> FILE_TYPE_MAP = new LinkedHashMap<>();
static {
FILE_TYPE_MAP.put("pdf", "PDF文档");
FILE_TYPE_MAP.put("doc", "Word文档");
// ... 只允许特定类型
}
public boolean isSupported(String fileName) {
String ext = getExtension(fileName);
return ext != null && SUPPORTED_EXTENSIONS.contains(ext.toLowerCase());
}
10.2 文件大小限制
// 单文件100MB限制
if (file.getSize() > 100 * 1024 * 1024) {
return Result.error("文件大小不能超过100MB");
}
// 图片单独限制10MB
if (file.getSize() > 10 * 1024 * 1024) {
return Result.error("图片大小不能超过10MB");
}
10.3 用户认证与授权
User user = userService.getCurrentUser();
if (user == null) {
return Result.error(401, "未登录");
}
// 会话所有权验证
ChatSession session = chatSessionService.getSessionById(sessionId, user.getId());
if (session == null) {
return Result.error("会话不存在");
}
十一、总结
本文深入剖析了通意千应——一个完整的AI对话系统附件上传功能的实现方案。通意千应具有以下技术特点:
- 多类型文件支持:支持PDF、Word、Excel、PPT、图片、TXT等常见格式
- 智能内容解析:使用Apache PDFBox、Apache POI等库进行文档解析,支持编码自动检测
- 多模态AI集成:图片文件通过Base64编码支持通意千应AI视觉理解能力
- 云存储架构:使用七牛云OSS提供可靠的文件存储和CDN加速
- 预提取优化:文件上传时即完成内容提取,提升通意千应AI响应速度
- 完整的用户体验:支持文件预览、图片缩放、进度显示等交互功能
通意千应的实现方案可为构建企业级AI对话系统的附件功能提供参考。

评论区 1