Skip to content

文件上传与解析文档 ​

本文档描述了如何通过文件上传接口实现文件的去重检测、上传以及内容解析,供大模型文问答系统作为上下文信息使用。内容主要分为以下几个部分:

  • API 设计:包括文件上传前的去重检测接口以及文件上传接口的说明。
  • 数据库设计:存储上传文件基本信息的数据库表设计。
  • 文件解析实现:针对不同类型文件的解析实现代码(PDF、Word、Excel、PPT、图片及音视频文件)。
  • 服务实现:如何将文件解析与上传服务整合,包括文件上传服务 ChatUploadService 和 API 请求处理类 ApiChatUploadHandler。

1. API 设计 ​

1.1 文件去重检测接口 ​

  • 接口地址:/api/v1/chat/file
  • 说明:
    前端在上传文件前需计算文件的 MD5 值,通过此接口检测文件是否已经上传过。

1.2 文件上传接口 ​

  • 接口地址:/api/v1/chat/upload
  • 说明:
    通过该接口上传文件。上传成功后,返回一个标识文件的 id(即 file_id)。

支持的文件类型分为三类:

  • 文档类:
    • PDF (.pdf)
    • Word (.docx)
    • Excel (.xlsx)
    • PPT (.pptx)
    • Txt (.txt)
    • Markdown (.md)
  • 图片类:
    • PNG (.png)
    • JPG (.jpg)
    • JPEG (.jpeg)
  • 音视频类:
    • FLAC (.flac)
    • MP3 (.mp3)
    • MP4 (.mp4)
    • MPEG (.mpeg)
    • MPGA (.mpga)
    • M4A (.m4a)
    • OGG (.ogg)
    • WAV (.wav)
    • WEBM (.webm)

请求参数

plaintext
file: binary
category: string

响应参数

json
{
  "data": {
    "name": "7月商品销售表.xlsx",
    "id": "484148184794959872",
    "size": "10257",
    "content": null,
    "targetName": null,
    "md5": "3eb755b2bf70b2c47b273ef00f9e44ea",
    "url": "https://rumiapp.s3.us-west-1.amazonaws.com/chat/484148181326270464.xlsx"
  },
  "msg": null,
  "code": 1,
  "ok": true,
  "error": null
}

1.3 使用文件 ​

在请求/api/v1/chat/ask 使用file_ids字段

file_ids:["id","id"];

文件上传流程: a. 用户选择文件后,前端需要计算文件的MD5值。 b. 调用文件去重检测接口(/api/v1/chat/file)检查文件是否已存在。如果已存在,则可以直接使用已有的file_id;否则,需要上传文件。 c. 上传文件时,调用上传接口(/api/v1/chat/upload),将文件数据和类别(category)作为参数。上传成功后,会返回文件信息,其中包括file_id。

在发送问题(ask)时,将上一步得到的file_id(可能有多个)以数组形式作为参数file_ids传递给后端。

2. 数据库设计 ​

数据库中保存了上传文件的基本信息。表结构如下所示(使用 SQL 语句创建表):

java
drop table if exists chat_upload_file;
CREATE TABLE chat_upload_file (
  id BIGINT primary key,
  md5 VARCHAR(32) NOT NULL,
  name VARCHAR(1024) NOT NULL,
  content text,
  creator VARCHAR(64) DEFAULT '',
  create_time TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updater VARCHAR(64) DEFAULT '',
  update_time TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
  deleted SMALLINT NOT NULL DEFAULT 0,
  tenant_id BIGINT NOT NULL DEFAULT 0
);

说明:

  • 每个文件均有唯一的 id 标识,使用 MD5 值来进行文件去重检测。
  • content 字段保存了文件解析后的文本内容,方便后续进行大模型文问答上下文的构建。

3. 文件解析实现 ​

在文件上传后,需要对文件进行解析以提取文本内容。下面分别介绍各类文件的解析实现代码。

3.1 PDF 文件解析 ​

使用 Apache PDFBox 对 PDF 文件进行解析。示例代码如下:

java
package nexus.io.llm.utils;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;

import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.text.PDFTextStripper;

public class PdfUtils {

  /**
   * 从PDF字节数据中提取文本内容
   * @param pdfBytes PDF文件的字节数组
   * @return 提取的文本内容
   * @throws IOException 如果PDF解析失败
   */
  public static String parseContent(byte[] pdfBytes) throws IOException {
    try (InputStream is = new ByteArrayInputStream(pdfBytes); PDDocument document = PDDocument.load(is)) {
      PDFTextStripper stripper = new PDFTextStripper();
      stripper.setSortByPosition(true); // 按页面布局排序
      stripper.setAddMoreFormatting(true); // 保留更多格式信息
      return stripper.getText(document);
    }
  }
}

3.2 Word 文档解析 ​

利用 Apache POI 读取 docx 文档的段落和表格内容,示例代码如下:

java
package nexus.io.llm.utils;

import java.io.ByteArrayInputStream;
import java.io.IOException;

import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFTable;
import org.apache.poi.xwpf.usermodel.XWPFTableCell;
import org.apache.poi.xwpf.usermodel.XWPFTableRow;

public class DocxUtils {

  public static String parseDocx(byte[] fileData) throws IOException {
    StringBuilder content = new StringBuilder();
    try (XWPFDocument document = new XWPFDocument(new ByteArrayInputStream(fileData))) {
      // 读取段落
      for (XWPFParagraph paragraph : document.getParagraphs()) {
        content.append(paragraph.getText()).append("\n");
      }

      // 读取表格
      for (XWPFTable table : document.getTables()) {
        for (XWPFTableRow row : table.getRows()) {
          for (XWPFTableCell cell : row.getTableCells()) {
            content.append(cell.getText()).append("\t");
          }
          content.append("\n");
        }
      }

    }
    return content.toString().trim();
  }
}

3.3 Excel 文件解析 ​

利用 Apache POI 的 XSSF 对 xlsx 文件进行解析,遍历所有工作表、行和单元格,示例代码如下:

java
package nexus.io.llm.utils;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Sheet;

public class ExcelUtils {

  /**
   * 从xlsx字节数据中提取文本内容
   * @param fileData xlsx文件的字节数组
   * @return 提取的文本内容
   * @throws IOException 如果xlsx解析失败
   */
  public static String parseXlsx(byte[] fileData) throws IOException {
    StringBuilder content = new StringBuilder();
    try (XSSFWorkbook workbook = new XSSFWorkbook(new ByteArrayInputStream(fileData))) {
      int numberOfSheets = workbook.getNumberOfSheets();
      for (int i = 0; i < numberOfSheets; i++) {
        Sheet sheet = workbook.getSheetAt(i);
        content.append("Sheet: ").append(sheet.getSheetName()).append("\n");
        for (Row row : sheet) {
          for (Cell cell : row) {
            content.append(cell.toString()).append("\t");
          }
          content.append("\n");
        }
        content.append("----- End of Sheet -----\n");
      }
    }
    return content.toString().trim();
  }
}

3.4 PPT 文件解析 ​

通过 Apache POI 的 XMLSlideShow 类解析 pptx 文件,将每一页幻灯片中的文本提取出来:

java
package nexus.io.llm.utils;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import org.apache.poi.xslf.usermodel.XMLSlideShow;
import org.apache.poi.xslf.usermodel.XSLFSlide;
import org.apache.poi.xslf.usermodel.XSLFShape;
import org.apache.poi.xslf.usermodel.XSLFTextShape;

public class PptxUtils {

  /**
   * 从pptx字节数据中提取文本内容
   * @param fileData pptx文件的字节数组
   * @return 提取的文本内容
   * @throws IOException 如果pptx解析失败
   */
  public static String parsePptx(byte[] fileData) throws IOException {
    StringBuilder content = new StringBuilder();
    try (XMLSlideShow slideShow = new XMLSlideShow(new ByteArrayInputStream(fileData))) {
      int slideIndex = 1;
      for (XSLFSlide slide : slideShow.getSlides()) {
        content.append("Slide ").append(slideIndex++).append(":\n");
        for (XSLFShape shape : slide.getShapes()) {
          if (shape instanceof XSLFTextShape) {
            XSLFTextShape textShape = (XSLFTextShape) shape;
            content.append(textShape.getText()).append("\n");
          }
        }
        content.append("----- End of Slide -----\n");
      }
    }
    return content.toString().trim();
  }
}

3.5 文件解析逻辑整合 – ChatFileService ​

该服务根据文件类型选择对应的解析方法,同时支持文本类文件(txt、md)直接转换字符串,以及图片类型文件由 OCR 服务解析。代码如下:

java
package nexus.io.llm.service;

import java.io.IOException;
import java.nio.charset.StandardCharsets;

import nexus.io.groq.GropConst;
import nexus.io.groq.GropModel;
import nexus.io.groq.GroqSpeechClient;
import nexus.io.groq.TranscriptionsRequest;
import nexus.io.groq.TranscriptionsResponse;
import nexus.io.jfinal.aop.Aop;
import nexus.io.llm.utils.DocxUtils;
import nexus.io.llm.utils.PdfUtils;
import nexus.io.llm.utils.ExcelUtils;
import nexus.io.llm.utils.PptxUtils;
import nexus.io.model.upload.UploadFile;
import nexus.io.tio.utils.hutool.FilenameUtils;

public class ChatFileService {

  public String parseContent(UploadFile uploadFile) throws IOException {
    String name = uploadFile.getName();
    byte[] data = uploadFile.getData();
    String suffix = FilenameUtils.getSuffix(name).toLowerCase();
    String text = null;

    if ("txt".equals(suffix) || "md".equals(suffix)) {
      text = new String(data, StandardCharsets.UTF_8);
    } else if (GropConst.SUPPORT_LIST.contains(suffix)) {
      TranscriptionsRequest transcriptionsRequest = new TranscriptionsRequest();
      transcriptionsRequest.setModel(GropModel.WHISPER_LARGE_V3_TURBO);
      TranscriptionsResponse transcriptions = GroqSpeechClient.transcriptions(data, name, transcriptionsRequest);
      text = transcriptions.getText();
    } else if ("pdf".equals(suffix)) {
      text = PdfUtils.parseContent(data);
    } else if ("docx".equals(suffix)) {
      text = DocxUtils.parseDocx(data);
    } else if ("xlsx".equals(suffix)) {
      text = ExcelUtils.parseXlsx(data);
    } else if ("pptx".equals(suffix)) {
      text = PptxUtils.parsePptx(data);
    } else if ("jpg".equals(suffix) || "jpeg".equals(suffix) || "png".equals(suffix)) {
      text = Aop.get(LlmOcrService.class).parse(data, name);
    }
    return text;
  }
}

4. 文件上传服务实现 ​

4.1 ChatUploadService ​

该服务负责文件的上传、去重检测、存储记录以及调用文件解析服务将解析结果存入数据库。关键流程如下:

  • 文件上传:调用 uploadBytes 方法上传文件至 AWS S3,生成目标文件名并计算 MD5 值;
  • 去重检测:根据 MD5 值查询数据库,如果文件已存在,则直接返回缓存数据;
  • 解析文件:若未重复,则调用 ChatFileService.parseContent 解析文件内容,并将文件基本信息和解析结果存入数据库;
  • 返回结果:最后返回上传成功后的相关信息(包括文件 id、原始文件名、大小、下载地址和 MD5)。

代码实现如下:

java
package nexus.io.llm.service;

import java.util.ArrayList;
import java.util.List;

import com.jfinal.kit.Kv;
import com.jfinal.kit.StrKit;
import nexus.io.db.TableInput;
import nexus.io.db.TableResult;
import nexus.io.db.activerecord.Db;
import nexus.io.db.activerecord.Row;
import nexus.io.jfinal.aop.Aop;
import nexus.io.llm.consts.AgentTableNames;
import nexus.io.model.body.RespBodyVo;
import nexus.io.table.services.ApiTable;
import nexus.io.tio.boot.admin.costants.TioBootAdminTableNames;
import nexus.io.tio.boot.admin.dao.SystemUploadFileDao;
import nexus.io.tio.boot.admin.services.StorageService;
import nexus.io.tio.boot.admin.services.SystemUploadFileService;
import nexus.io.tio.boot.admin.utils.AwsS3Utils;
import nexus.io.model.upload.UploadResult;
import nexus.io.model.upload.UploadFile;
import nexus.io.tio.utils.crypto.Md5Utils;
import nexus.io.tio.utils.hutool.FilenameUtils;
import nexus.io.tio.utils.snowflake.SnowflakeIdUtils;

import lombok.extern.slf4j.Slf4j;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.PutObjectResponse;

@Slf4j
public class ChatUploadService implements StorageService {
  ChatFileService chatFileService = Aop.get(ChatFileService.class);

  public RespBodyVo upload(String category, UploadFile uploadFile) {
    if (StrKit.isBlank(category)) {
      category = "default";
    }
    UploadResult UploadResult = uploadBytes(category, uploadFile);
    Long id = UploadResult.getId();
    if (!Db.exists(AgentTableNames.chat_upload_file, "id", id)) {
      try {
        String content = chatFileService.parseContent(uploadFile);
        if (content == null) {
          return RespBodyVo.fail("un support file type");
        } else {
          Row row = Row.by("id", id).set("name", uploadFile.getName()).set("content", content).set("md5", UploadResult.getMd5());
          Db.save(AgentTableNames.chat_upload_file, row);
        }
      } catch (Exception e) {
        log.error(e.getMessage(), e);
        return RespBodyVo.fail(e.getMessage());
      }

    }
    return RespBodyVo.ok(UploadResult);
  }

  public UploadResult uploadBytes(String category, UploadFile uploadFile) {
    // 上传文件
    long id = SnowflakeIdUtils.id();
    String suffix = FilenameUtils.getSuffix(uploadFile.getName());
    String newFilename = id + "." + suffix;

    String targetName = category + "/" + newFilename;

    return uploadBytes(id, targetName, uploadFile, suffix);
  }

  /**
   * @param id
   * @param originFilename
   * @param targetName
   * @param fileContent
   * @param size
   * @param suffix
   * @return
   */
  public UploadResult uploadBytes(long id, String targetName, UploadFile uploadFile, String suffix) {
    String originFilename = uploadFile.getName();
    long size = uploadFile.getSize();

    byte[] fileContent = uploadFile.getData();
    String md5 = Md5Utils.digestHex(fileContent);
    Row record = Aop.get(SystemUploadFileDao.class).getFileBasicInfoByMd5(md5);
    if (record != null) {
      log.info("select table reuslt:{}", record.toMap());
      id = record.getLong("id");
      String url = this.getUrl(record.getStr("bucket_name"), record.getStr("target_name"));
      Kv kv = record.toKv();
      kv.remove("target_name");
      kv.remove("bucket_name");
      kv.set("url", url);
      kv.set("md5", md5);
      return new UploadResult(id, uploadFile.getName(), uploadFile.getSize(), url, md5);
    } else {
      log.info("not found from cache table:{}", md5);
    }

    String etag = null;
    try (S3Client client = AwsS3Utils.buildClient();) {
      PutObjectResponse response = AwsS3Utils.upload(client, AwsS3Utils.bucketName, targetName, fileContent, suffix);
      etag = response.eTag();
    } catch (Exception e) {
      e.printStackTrace();
      throw new RuntimeException(e);
    }

    // Log and save to database
    log.info("Uploaded with ETag: {}", etag);

    TableInput kv = TableInput.create().set("name", originFilename).set("size", size).set("md5", md5)
        //
        .set("platform", "aws s3").set("region_name", AwsS3Utils.regionName).set("bucket_name", AwsS3Utils.bucketName)
        //
        .set("target_name", targetName).set("file_id", etag);

    TableResult<Kv> save = ApiTable.save(TioBootAdminTableNames.tio_boot_admin_system_upload_file, kv);
    String downloadUrl = getUrl(AwsS3Utils.bucketName, targetName);

    return new UploadResult(save.getData().getLong("id"), originFilename, Long.valueOf(size), downloadUrl, md5);

  }

  @Override
  public String getUrl(String bucketName, String targetName) {
    return Aop.get(SystemUploadFileService.class).getUrl(bucketName, targetName);
  }

  @Override
  public UploadResult getUrlById(String id) {
    return Aop.get(SystemUploadFileService.class).getUrlById(id);
  }

  @Override
  public UploadResult getUrlById(long id) {
    return Aop.get(SystemUploadFileService.class).getUrlById(id);
  }

  @Override
  public UploadResult getUrlByMd5(String md5) {
    return Aop.get(SystemUploadFileService.class).getUrlByMd5(md5);
  }

  public RespBodyVo file(String md5) {
    boolean exists = Db.exists(AgentTableNames.chat_upload_file, "md5", md5);
    UploadResult UploadResult = Aop.get(SystemUploadFileService.class).getUrlByMd5(md5);

    if (exists && UploadResult != null) {
      return RespBodyVo.ok(UploadResult);
    }
    return RespBodyVo.fail();
  }

  public List<UploadResult> getFileBasicInfoByIds(List<Long> file_ids) {
    List<Row> row = Aop.get(SystemUploadFileDao.class).getFileBasicInfoByIds(file_ids);
    List<UploadResult> files = new ArrayList<>();
    for (Row record : row) {
      Long id = record.getLong("id");
      String url = this.getUrl(record.getStr("bucket_name"), record.getStr("target_name"));
      String originFilename = record.getStr("name");
      String md5 = record.getString("md5");
      Long size = record.getLong("size");
      UploadResult UploadResult = new UploadResult(id, originFilename, size, url, md5);
      Row contentRow = Db.findColumnsById(AgentTableNames.chat_upload_file, "content", id);
      if (row != null) {
        String content = contentRow.getStr("content");
        UploadResult.setContent(content);
        files.add(UploadResult);
      } else {
        log.error("not found content of id:" + id);
      }
    }
    return files;
  }
}

4.2 ApiChatUploadHandler ​

该类用于处理 HTTP 请求,调用 ChatUploadService 完成文件上传和文件去重检测两个 API 接口。代码如下:

java
package nexus.io.llm.handler;

import nexus.io.jfinal.aop.Aop;
import nexus.io.llm.service.ChatUploadService;
import nexus.io.model.body.RespBodyVo;
import nexus.io.tio.boot.http.TioRequestContext;
import nexus.io.tio.http.common.HttpRequest;
import nexus.io.tio.http.common.HttpResponse;
import nexus.io.model.upload.UploadFile;
import nexus.io.tio.http.server.model.HttpCors;
import nexus.io.tio.http.server.util.CORSUtils;
import nexus.io.tio.http.server.util.Resps;

public class ApiChatUploadHandler {
  ChatUploadService chatUploadService = Aop.get(ChatUploadService.class);

  public HttpResponse upload(HttpRequest request) {
    HttpResponse httpResponse = TioRequestContext.getResponse();
    CORSUtils.enableCORS(httpResponse, new HttpCors());
    UploadFile uploadFile = request.getUploadFile("file");
    String category = request.getParam("category");

    if (uploadFile != null) {
      RespBodyVo respBodyVo = chatUploadService.upload(category, uploadFile);
      return Resps.json(httpResponse, respBodyVo);
    }
    return Resps.json(httpResponse, RespBodyVo.ok("Fail"));
  }

  public HttpResponse file(HttpRequest request) {
    HttpResponse httpResponse = TioRequestContext.getResponse();
    CORSUtils.enableCORS(httpResponse, new HttpCors());
    String md5 = request.getParam("md5");

    RespBodyVo vo = chatUploadService.file(md5);
    return Resps.json(httpResponse, vo);
  }
}

5. 依赖说明 ​

为了保证各个依赖版本一致,需要在项目的 pom.xml 文件中增加如下依赖:

xml
    <dependency>
      <groupId>org.apache.pdfbox</groupId>
      <artifactId>pdfbox</artifactId>
      <version>${pdfbox.version}</version>
    </dependency>

    <dependency>
      <groupId>org.apache.poi</groupId>
      <artifactId>poi-ooxml</artifactId>
      <version>${poi-ooxml.version}</version>
    </dependency>

    <dependency>
      <groupId>commons-io</groupId>
      <artifactId>commons-io</artifactId>
      <version>${commons-io.version}</version>
    </dependency>

    <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>easyexcel</artifactId>
      <version>${easyexcel.version}</version>
    </dependency>

总结 ​

本文档详细介绍了文件上传与解析的整体设计与实现流程。通过前端计算文件 MD5 值进行去重检测,上传文件到 AWS S3,再根据文件类型采用相应的解析工具(如 PDFBox、POI 等)提取文件内容,最终将解析结果存入数据库。后续系统可以利用解析后的文本内容作为大模型问答系统的上下文信息,实现更加精准和高效的文问答功能。

MIT Licensed