跳到正文
Joeplover
项目实战·2026-01-11·约 7 分钟阅读

文档解析模块:U3W-AI 的 AI 驱动文档解析系统

在 U3W-AI 实习期间独立开发的文档解析模块,Spring Boot 后端 + Vue 3 前端 + 腾讯元器工作流

Document with text being scanned and digitized

文档解析模块:U3W-AI 项目的 AI 驱动文档解析系统

项目背景

这是我在 U3W-AI 实习期间独立开发的一个文档解析功能模块。用户上传文档(PDF/Word/TXT),后端配合腾讯元器智能体工作流自动解析文档内容,最终返回结构化结果。整个过程异步进行:上传后立即返回,AI 解析完成后通过回调通知。

我同时负责了后端(Spring Boot + MyBatis-Plus)和前端(Vue 3 + Element Plus)的开发,以及腾讯元器工作流的配置。

系统架构

前端上传文档 → 后端接收 → 调用内部 /upload 接口存储文件
  → 调用腾讯元器 API 提交工作流任务 → 立即返回(异步)
  → 元器工作流处理完成后 HTTP 回调后端 → 更新解析结果
  → 前端轮询 /status 接口获取解析状态

后端实现

后端代码位于 WxFbsir-business/src/main/java/com/wx/fbsir/business/documentparse/,包含标准四层结构:

controller/DocumentParseController.java   # API 接口层
service/IDocumentParseService.java         # 服务接口
service/impl/DocumentParseServiceImpl.java # 业务实现
mapper/DocumentParseMapper.java            # 数据库访问
domain/DocumentParse.java                  # 实体类

核心业务流程图

uploadAndParse()
  ├─ 1. 参数校验(文件非空、提示词非空)
  ├─ 2. 生成文档ID(DOC-XXXXXXXXXX 格式)
  ├─ 3. 调用内部 /upload 接口上传文件
  ├─ 4. 替换文件 URL 域名(localhost:8080 → 内网穿透域名)
  ├─ 5. 创建 DocumentParse 记录(状态=0 处理中)
  ├─ 6. 调用 triggerDocumentParse()(@Async 异步触发)
  └─ 7. 立即返回记录 ID,不等待 AI 结果

updateParsedContent()  ← 腾讯元器工作流回调
  ├─ 1. 根据 documentId 查找记录
  ├─ 2. 保存 parsedContent
  └─ 3. 更新 processStatus=1(已完成)

/status/{id}  ← 前端轮询
  └─ 返回轻量级状态(不含大字段内容)

关键设计:异步处理架构

// Controller 层触发异步,解决 @Async 同类调用失效问题
@PostMapping("/uploadAndParse")
public AjaxResult uploadAndParse(@RequestParam("file") MultipartFile file, ...) {
    // ... 参数校验、上传文件 ...
    
    // 同步创建记录
    DocumentParse documentParse = documentParseService.createDocumentParseAndProcess(...);
    
    // 从 Controller 调用 @Async 方法(确保 AOP 代理生效)
    documentParseService.triggerDocumentParse(...);  // @Async 在外部调用时生效
    
    // 立即返回
    return success(result);
}

这是一个很容易踩坑的细节:Spring 的 @Async 注解基于 AOP 代理实现。如果在同一个 Service 类内部调用 this.triggerDocumentParse(),AOP 代理不会拦截,方法会同步执行。必须从 Controller(或另一个 Service)调用才能让 @Async 生效。代码注释里特意标注了这一点。

状态管理

public class DocumentParse {
    private Long id;               // 主键
    private Long userId;           // 用户ID
    private String documentId;     // 文档ID(自动生成:DOC-XXXXXXXXXX)
    private String documentName;   // 文档名称
    private String prompt;         // 用户提交的提示词
    private String parsedContent;  // 解析后的内容(由工作流回调写入)
    private String agentTaskId;    // 腾讯元器任务ID
    private Integer processStatus; // 0-处理中, 1-已完成, 2-失败
    private String errorMessage;   // 错误信息
}

状态流转:

处理中(0) → 已完成(1)  ← 工作流回调
         → 失败(2)    ← 异常或超时

超时保护

启动一个5 分钟超时检查定时器:

// 提交工作流任务后,启动延迟 5 分钟的检查
scheduler.schedule(() -> {
    DocumentParse doc = documentParseMapper.selectDocumentParseById(documentParseId);
    if (doc != null && doc.getProcessStatus() == 0) {
        // 5 分钟了还没收到回调,标记为失败
        updateDocumentParseStatus(documentParseId, 2, "工作流回调超时", null);
    }
}, 5, TimeUnit.MINUTES);

这个设计防止了工作流回调丢失时记录永远卡在"处理中"状态。

前端实现

前端代码位于 WxFbsir-ui/src/views/business/content/documentparse/index.vue(1163 行),API 层在 WxFbsir-ui/src/api/business/content/documentparse/documentParse.js。

UI 布局

┌──────────────────────┬──────────────────────┐
│    文档解析助手        │    解析内容             │
│  ┌──────────────┐    │                      │
│  │  文件拖拽上传  │    │  (包含解析内容和)      │
│  │  [提示词输入]  │    │  (提示词显示)         │
│  │  [上传并解析]  │    │                      │
│  └──────────────┘    │                      │
│  ┌──────────────────┐│                      │
│  │ 解析记录列表      ││                      │
│  │ 记录1 已完成      ││                      │
│  │ 记录2 处理中      ││                      │
│  │ 记录3 失败       ││                      │
│  └──────────────────┘│                      │
└──────────────────────┴──────────────────────┘

轮询机制

前端在提交解析任务后启动每 5 秒轮询,检查解析状态:

const startPollingParse = (parseId, documentId) => {
  let pollCount = 0
  const maxPolls = 60  // 最多轮询 5 分钟

  const pollInterval = setInterval(async () => {
    pollCount++
    await loadParses()  // 刷新列表

    const parse = parseList.value.find(p => p.id === parseId)
    if (parse.processStatus === 1) {
      clearInterval(pollInterval)
      ElNotification({ title: '解析完成', type: 'success' })
    } else if (parse.processStatus === 2) {
      clearInterval(pollInterval)
      ElNotification({ title: '解析失败', type: 'error' })
    }
    // status=0 继续轮询
  }, 5000)  // 每 5 秒
}

智能体配置界面

用户在首次使用前需要配置腾讯元器智能体。配置通过加密的方式存储:

const MASKED_VALUE = '***已加密***'
// 配置自动加密,回显时显示已加密标记

配置项包括:

  • 智能体 ID(appid)
  • 智能体名称
  • API 密钥(appkey)
  • API 端点

腾讯元器工作流

工作流配置从 F:\wxfb\U3W-AI\docs\workflows\export-文档解析工作流.zip 中导出,包含完整的节点定义。

工作流流程:

开始 → 文档解析节点 → 大模型处理 → HTTP回调 → 结束

大模型配置

使用 lke-deepseek-v3 模型,温度 0.6,最大 4096 tokens。提示词设计确保了输出质量:

你是一个专业的文档分析助手。你将收到两个输入参数:
1. {{context}} - 解析后的文档内容
2. {{prompt}} - 用户提交的任务提示词

重要要求:
- ✅ 必须基于文档内容回答,不要编造信息
- ✅ 严格按照用户提示词的要求执行
- ✅ 给出完整、准确、可直接使用的结果
- ❌ 不要说"让我先分析"、"接下来我将"等过程性表述
- ❌ 不要询问用户是否需要修改或补充

设计思路是让 LLM 一次性完成所有任务,避免多轮对话增加延迟和 token 消耗。

文件处理流程

工作流中的文件处理策略:

  1. 后端上传文件到服务器
  2. 将文件 URL(域名替换为内网穿透地址)传递给腾讯元器
  3. 工作流中先做文档内容解析(将 PDF/Word 转为文本)
  4. 解析后的文本作为 context 参数传入 LLM

踩坑记录

1. @Async 同类调用失效

这是最隐蔽的问题。Spring 的 @Async 通过 AOP 代理实现,同类内部 this.method() 调用不经过代理。必须在 Controller 层或另一个 Service 中调用。

2. 域名替换

文件上传到本地后获取的 URL 是 http://localhost:8080/...,但腾讯元器工作流需要通过外网访问文件。需要把 URL 中的 localhost:8080 替换为内网穿透域名。

3. 5 分钟超时保护

工作流回调如果因为网络问题丢失,记录会永远卡在"处理中"状态。通过定时器在 5 分钟后检查状态,超过时间仍未完成则标记为失败,用户看到后可以重试。

4. 前端轮询 vs WebSocket

当前使用轮询(每 5 秒查询状态),对于 demo 阶段够用。如果是高并发生产环境,应该改为 WebSocket 推送——后端解析完成后主动通知前端,减少不必要的轮询请求。

5. 配置加密

API 密钥在数据库中加密存储,前端显示时用 ***已加密*** 标记,防止密钥泄露。这是一个基本但重要的安全实践。

总结

这个文档解析模块是我在 U3W-AI 实习期间独立完成的完整功能模块,涵盖了:后端(Spring Boot CRUD + @Async + 工作流回调)、前端(Vue 3 + Element Plus 上传 + 轮询 + 配置管理)、AI 编排(腾讯元器工作流 + DeepSeek V3 LLM)。从产品设计到前后端实现到运维配置,完整的交付经验。