知识库系统的入口工程

把“上传接口与文档主记录创建”这一块当成整个知识库系统的入口工程来学。它不是单纯把文件传上来,而是完成一件更重要的事:把一份用户上传的原始文件,登记成系统可追踪、可异步处理、可查询状态的文档资产,并投递后续解析任务。

一、先建立整体认知:这个接口在项目里解决什么问题?

在项目里,知识库不是用户上传一个 PDF 就立刻可以问答。真正可问答之前,文档还要经历很多阶段:格式解析、内容抽取、切块策略推荐、用户确认、父子块切分、向量化、写入向量库、写入 Elasticsearch、构建 Neo4j 文档结构图谱等。

所以“上传接口与文档主记录创建”这一块的核心职责不是做完所有事情,而是完成上传阶段的初始化工作。

它主要做六件事:

  1. 接收用户上传的文件和元信息;
  2. 校验文件是否合法;
  3. 把原始文件保存到 MinIO;
  4. 在 MySQL 中创建文档主记录;
  5. 创建第一条异步任务记录和任务日志;
  6. 事务提交后发送 Kafka 消息,启动后续异步处理流水线。

你可以把它理解为:用户上传文件后,系统先把这份文件“收进仓库”,再给它创建一张“身份证”和一张“工单”,最后通知后台流水线开始处理。


二、这块在整体架构中的位置

项目整体链路可以简化成这样:

image-09d6ba6d

现在我们只学到这里:

image-06620593

这部分是整个知识库生命周期的第一环,质量非常关键。因为后面的解析、切块、向量化、检索、问答,都依赖这里创建出来的 documentId 和任务状态。

三、先看接口入口:Controller 只负责接收请求

上传接口在 DocumentManageController 里,大概是这样的:

@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ApiResponse<DocumentUploadVo> upload(
        @RequestPart("file") MultipartFile file,
        @Valid @RequestPart(value = "meta", required = false) DocumentUploadDto dto) {

    return ApiResponse.ok(
        documentManageService.upload(file, dto == null ? new DocumentUploadDto() : dto)
    );
}

这里要重点理解三个点。

第一,接口使用的是 multipart/form-data,不是普通 JSON。因为它同时要上传二进制文件和结构化元信息。

其中:

@RequestPart("file") MultipartFile file

负责接收真实文件,例如 PDF、Word、PPT、Excel、Markdown 等。

而:

@RequestPart(value = "meta", required = false) DocumentUploadDto dto

负责接收文档的业务元信息,例如文档名称、知识范围、业务分类、标签、操作人 ID 等。

第二,meta 是可选的。也就是说,前端可以只传文件,不传元信息。Controller 里做了一个兜底:

dto == null ? new DocumentUploadDto() : dto

这样 Service 层就不用反复判断 dto == null,代码会更干净。

第三,Controller 不做业务逻辑。它只做参数绑定,然后把请求交给 Service。这个设计是很标准的分层思想:

Controller:接收请求、绑定参数、返回响应
Service:编排业务流程
StorageService:处理对象存储
Mapper:处理数据库持久化
KafkaProducer:处理消息投递

上传接口采用 multipart/form-data,同时接收文件二进制和可选元信息。Controller 层只负责参数绑定和空 DTO 兜底,实际的文件校验、对象存储、文档入库、任务创建和 Kafka 投递都下沉到 Service 层完成,保证控制器职责单一。

四、Service 第一阶段:文件校验

Service 进入后,第一步一定是校验文件。

核心代码逻辑是:

if (file == null || file.isEmpty()) {
    throw new SuperAgentFrameException(...);
}

String originalFileName = file.getOriginalFilename();
if (StrUtil.isBlank(originalFileName)) {
    throw new SuperAgentFrameException(...);
}

DocumentFileTypeEnum fileType = DocumentFileTypeEnum.fromFileName(originalFileName);
if (fileType == null) {
    throw new SuperAgentFrameException(...);
}

这里做了三层校验。

第一层是文件内容不能为空:

file == null || file.isEmpty()

这可以拦住两类问题:一种是请求里根本没传文件,另一种是 multipart 字段存在,但文件内容为空。

第二层是原始文件名不能为空。

原始文件名不只是展示用,它还用于后续识别文件类型。例如系统会根据文件名后缀判断是 PDF、DOCX、PPTX、XLSX 还是 Markdown。

第三层是文件类型必须受支持。

DocumentFileTypeEnum.fromFileName(originalFileName)

这里一般会根据后缀名匹配枚举。如果匹配不上,就说明系统暂时不支持这种文件,直接拒绝上传。

这一块的核心思想是:越靠前发现非法输入,系统成本越低。

如果不校验,后面可能已经上传到 MinIO、写入数据库,甚至投递 Kafka 了,才发现文件无法解析,这样清理成本就很高。

五、读取文件字节并生成 documentId

通过校验后,系统会把 MultipartFile 一次性读成字节数组:

byte[] fileBytes = getFileBytes(file);
Long documentId = uidGenerator.getUid();

getFileBytes() 里面本质上是:

return file.getBytes();

这样做有两个目的。

第一,后续上传 MinIO 需要文件内容。

第二,文档主记录里要保存文件大小:

document.setFileSize((long) fileBytes.length);

所以先读成 byte[],后面可以复用。

然后生成 documentId。这个 ID 非常重要,它会贯穿整个文档生命周期:

MinIO 对象路径里有 documentId
文档主表主键是 documentId
任务表关联 documentId
任务日志关联 documentId
Kafka 消息携带 documentId
后续解析、切块、索引都靠 documentId 串联

所以这里不是等数据库自增生成 ID,而是服务端提前通过分布式 ID 生成器生成。

在项目描述里,分布式 ID 生成器是基于雪花算法、Lua 和 Redis 实现的。这里文档里提到用 uidGenerator.getUid(),本质目标都是一样的:在上传入口阶段提前生成全局唯一 ID,方便后续跨系统、跨表、跨消息链路追踪。


六、上传原始文件到 MinIO

接下来系统会先把原始文件上传到对象存储:

StoredObjectInfo storedObjectInfo = storageService.uploadOriginalFile(
    documentId,
    originalFileName,
    fileBytes,
    file.getContentType()
);

这里调用的是 MinioDocumentStorageService.uploadOriginalFile()

它会拼接一个对象路径:

String objectName = properties.getMinio().getObjectPrefix() + "/"
    + documentId + "/"
    + System.currentTimeMillis() + "-"
    + originalFileName;

路径大概长这样:

doc-original/123456789/1714000000000-企业制度.pdf

这个路径设计有几个好处。

第一,按 documentId 分目录,方便定位某一份文档的所有相关文件。

第二,前面加时间戳,避免同名文件覆盖。

第三,保留原始文件名,排查问题时更直观。

然后调用真正的上传方法:

minioClient.putObject(...)

上传之前还会检查 bucket 是否存在:

if (!bucketExists()) {
    minioClient.makeBucket(...);
}

这个设计适合新环境部署。比如测试环境第一次启动时,MinIO 里可能还没有对应的 bucket,系统会自动创建,减少人工初始化步骤。

上传成功后,会返回一个 StoredObjectInfo,里面有:

bucketName:桶名
objectName:对象路径
objectUrl:完整访问地址

这三个值后面会保存到文档主表。

这里有一个设计点需要注意:为什么先上传 MinIO,再写数据库?

因为文档主表需要保存文件的存储位置。如果不先上传,就拿不到 bucketNameobjectNameobjectUrl。所以流程上必须先拿到对象存储信息,再构建文档主记录。

不过这样也会带来一个边界问题:如果 MinIO 上传成功,但后面数据库入库失败,MinIO 里可能会留下一个“孤儿文件”。生产项目中一般会通过补偿任务、定时清理或失败回滚清理来处理。

七、构建文档主记录 SuperAgentDocument

文件上传成功后,系统开始构建文档主记录:

SuperAgentDocument document = new SuperAgentDocument();
document.setId(documentId);
document.setDocumentName(...);
document.setOriginalFileName(originalFileName);
document.setFileType(fileType.getCode());
document.setMimeType(file.getContentType());
document.setFileSize((long) fileBytes.length);
document.setStorageType(DocumentStorageTypeEnum.MINIO.getCode());
document.setBucketName(storedObjectInfo.getBucketName());
document.setObjectName(storedObjectInfo.getObjectName());
document.setObjectUrl(storedObjectInfo.getObjectUrl());

这一步是在内存中组装对象,还没有真正写数据库。

文档主记录可以理解为这份文档的“资产档案”。它记录了:

这份文档叫什么?
原始文件名是什么?
文件类型是什么?
MIME 类型是什么?
文件大小是多少?
存储在哪里?
当前解析状态是什么?
后续策略状态是什么?
索引状态是什么?
属于哪个知识范围?
属于哪个业务分类?
有哪些标签?

其中,文档名称的取值逻辑很重要:

document.setDocumentName(
    StrUtil.isNotBlank(dto.getDocumentName())
        ? dto.getDocumentName()
        : originalFileName
);

也就是说,如果前端传了展示名称,就用前端传的;否则用原始文件名。

比如用户上传的文件叫:

report_final_v3_2024_04_01.pdf

但前端传了:

2024 年一季度经营分析报告

那么系统展示时就用后者。这样对用户更友好。

八、三个核心状态:parseStatus、strategyStatus、indexStatus

文档主记录里最关键的是三个状态字段:

document.setParseStatus(DocumentParseStatusEnum.PARSING.getCode());
document.setStrategyStatus(DocumentStrategyStatusEnum.WAIT_RECOMMEND.getCode());
document.setIndexStatus(DocumentIndexStatusEnum.WAIT_BUILD.getCode());

这三个字段代表文档后续处理链路的三个阶段。

1. parseStatus:解析状态

上传成功后设置为:

PARSING

它表示:文档已经进入解析链路,等待异步任务处理。

注意,这里虽然叫 PARSING,但不代表当前接口已经完成了解析。它更准确的含义是:已进入解析中或待解析队列。

2. strategyStatus:切块策略状态

初始值是:

WAIT_RECOMMEND

表示系统还没有推荐切块策略。

后续 Tika 解析完文档文本和结构后,系统才会根据文档类型、结构质量、内容特征推荐切块策略,例如结构化切块、递归切块、语义切块、LLM 智能切块等。

3. indexStatus:索引状态

初始值是:

WAIT_BUILD

表示还没有构建检索索引。

后续用户确认切块策略后,系统才会进行父子块切分、向量化,然后写入 PGVector、Milvus、Elasticsearch、Neo4j 等存储系统。

所以这三个状态其实对应你项目里的异步处理主线:

image-67def917

这几个状态对于前端非常重要。前端可以根据这些状态展示:

上传成功
解析中
等待确认切块策略
索引构建中
处理完成
处理失败

九、业务元信息:知识范围、业务分类、标签

文档主记录还会保存上传时传入的业务信息:

document.setKnowledgeScopeCode(StrUtil.trimToNull(dto.getKnowledgeScopeCode()));
document.setKnowledgeScopeName(StrUtil.trimToNull(dto.getKnowledgeScopeName()));
document.setBusinessCategory(StrUtil.trimToNull(dto.getBusinessCategory()));
document.setDocumentTags(StrUtil.trimToNull(dto.getDocumentTags()));
document.setStatus(BusinessStatus.YES.getCode());

这些字段看起来只是普通元数据,但在项目里很重要。

因为系统设计了三级知识路由漏斗:

领域 → 主题 → 文档

那么文档上传时保存的知识范围、业务分类、标签,就会成为后续路由、筛选、召回和后台管理的重要依据。

比如一个文档属于:

知识范围:人力资源
业务分类:员工制度
标签:考勤、假期、薪酬

后面用户问:

年假怎么计算?

系统就可以更容易把问题路由到“人力资源 / 员工制度”相关文档,而不是全库盲搜。


十、构建任务记录 SuperAgentDocumentTask

文档主记录构建完成后,系统还会创建一条任务记录:

Long taskId = uidGenerator.getUid();

SuperAgentDocumentTask task = new SuperAgentDocumentTask();
task.setId(taskId);
task.setDocumentId(documentId);
task.setTaskType(DocumentTaskTypeEnum.PARSE_ROUTE.getCode());
task.setTaskStatus(DocumentTaskStatusEnum.NEW.getCode());
task.setCurrentStage(DocumentTaskStageEnum.FILE_UPLOAD.getCode());
task.setTriggerSource(resolveTriggerSource(operatorId));
task.setRetryCount(0);
task.setStatus(BusinessStatus.YES.getCode());

这里的任务可以理解为一张“工单”。

文档主记录表示:

这份文档是什么?

任务记录表示:

接下来要对这份文档做什么?

上传完成后创建的第一类任务是:

PARSE_ROUTE

也就是“解析路由任务”。

它负责把文档交给后续异步链路,让消费方决定下一步怎么解析、怎么推荐切块策略、怎么更新状态。

任务初始状态是:

NEW

表示刚创建,还未被消费。

当前阶段是:

FILE_UPLOAD

表示它是从文件上传阶段产生的任务。

触发来源通过 operatorId 判断:

private Integer resolveTriggerSource(Long operatorId) {
    return operatorId == null
        ? DocumentTriggerSourceEnum.SYSTEM.getCode()
        : DocumentTriggerSourceEnum.USER.getCode();
}

如果有操作人 ID,说明是用户主动上传;如果没有,则认为是系统触发。

这里还有一个小设计:

private Long parseOptionalLong(String rawValue) {
    if (StrUtil.isBlank(rawValue)) {
        return null;
    }
    try {
        Long value = Long.valueOf(rawValue.trim());
        return value > 0 ? value : null;
    } catch (NumberFormatException exception) {
        return null;
    }
}

operatorId 是宽松解析的。空字符串、非数字、0、负数都当成 null,不会直接让上传失败。

这个设计体现了一个原则:可选字段不要轻易阻断主流程。


十一、最关键的设计:TransactionTemplate 手动控制事务

接下来是这块最值得学习的地方。

系统没有直接在 upload() 方法上加:

@Transactional

而是用了:

DocumentUploadVo uploadVo = transactionTemplate.execute(status -> {
    documentMapper.insert(document);
    taskMapper.insert(task);

    taskLogService.saveLog(...);

    return new DocumentUploadVo(...);
});

也就是说,只有这几步放在数据库事务里:

插入文档主表
插入任务表
插入任务日志
组装返回结果

Kafka 发送不在事务里,而是在事务提交之后执行。

为什么要这样设计?

因为如果整个 upload() 方法都加 @Transactional,然后在方法最后发送 Kafka,可能出现一个问题:

数据库事务还没提交
Kafka 消息已经发出
消费方马上消费消息
消费方根据 documentId 查询数据库
结果查不到文档记录

这就是典型的“消息先于事务可见”的问题。

所以这里用 TransactionTemplate 精确控制事务边界:

transactionTemplate.execute() 内部:只做数据库写入
execute() 正常返回:说明数据库事务已经提交
事务提交之后:再发送 Kafka

这个设计非常重要,可以作为项目亮点来讲。

上传链路中,没有简单使用方法级 @Transactional,而是通过 TransactionTemplate 手动收窄事务边界。文档主表、任务表和任务日志在同一个事务内提交,事务提交成功后再发送 Kafka 消息,避免消费者收到消息后查询不到数据库记录的问题。

十二、任务日志:为什么上传阶段也要记日志?

在事务里除了插入文档和任务,还会记录一条任务日志:

taskLogService.saveLog(
    taskId,
    documentId,
    DocumentTaskStageEnum.FILE_UPLOAD.getCode(),
    DocumentTaskEventTypeEnum.COMPLETE.getCode(),
    DocumentLogLevelEnum.INFO.getCode(),
    resolveOperatorType(operatorId),
    operatorId,
    "文件上传完成,已进入解析与策略推荐队列。",
    Map.of("originalFileName", originalFileName, "fileSize", fileBytes.length)
);

这条日志记录的是:

哪个任务?
哪个文档?
当前处于哪个阶段?
发生了什么事件?
日志级别是什么?
谁触发的?
附带了哪些上下文?

这对你的项目“全链路可观测”非常重要。

因为文档处理是异步的,用户上传后并不会马上得到最终结果。如果后续解析失败、切块失败、索引失败,排查问题时就需要知道:

文件是否上传成功?
任务是否创建成功?
Kafka 是否投递成功?
消费方是否开始处理?
Tika 是否解析失败?
切块策略是否推荐成功?
索引是否写入成功?

所以从上传阶段就记录任务日志,可以形成完整时间线。

十三、事务提交后发送 Kafka 消息

数据库事务提交成功之后,系统发送 Kafka 消息:

kafkaProducer.sendParseRoute(
    new DocumentParseRouteMessage(documentId, taskId)
);

消息体很简单:

public class DocumentParseRouteMessage {
    private Long documentId;
    private Long taskId;
}

为什么只发 documentIdtaskId

因为详细信息已经保存在数据库了。Kafka 消息只需要告诉消费方:

哪份文档?
哪个任务?

消费方拿到这两个 ID 后,再去数据库查询完整上下文。

这样做有几个好处。

第一,消息体小,不容易因为字段变更导致兼容问题。

第二,数据库是事实来源,消费方读取的是最新状态。

第三,任务状态和日志都可以围绕 taskId 做追踪。

发送时,topic 名称会拼接环境前缀:

SpringUtil.getPrefixDistinctionName() + "-" + properties.getKafka().getParseTopic()

这样 dev、test、prod 不同环境的消息不会混在一起。

消息 key 使用:

String.valueOf(message.getDocumentId())

这样同一份文档相关的消息会尽量进入同一个 partition,有利于保证同一文档任务的顺序性。

底层发送方法中还用了:

kafkaTemplate.send(topic, key, payload).get();

也就是说,它不是“发出去就不管”,而是同步等待 broker 确认。

好处是:如果 Kafka 发送失败,上传接口会直接报错,而不是告诉用户成功但后台根本没启动处理。

不过这里也有一个边界问题:如果数据库事务已经提交,但 Kafka 发送失败,数据库里已经有文档和任务记录,但异步链路没有启动。

所以生产上通常要配合补偿机制,比如:

定时扫描 parseStatus = PARSING 但长时间没有任务进展的文档
重新投递 Kafka 消息
或者将任务状态标记为待重试

这个权衡比“消息发了但数据库还没提交”更容易处理。


十四、最后返回上传结果

接口最后返回:

return uploadVo;

DocumentUploadVo 包含:

private Long documentId;
private Long taskId;
private String documentName;
private Integer parseStatus;
private Integer strategyStatus;
private Integer indexStatus;

它没有返回 MinIO 的 bucketNameobjectNameobjectUrl

这是一个好设计。因为对象存储路径属于服务端内部细节,不应该暴露给前端。前端真正需要的是:

documentId:后续查询文档详情和处理进度
taskId:后续查询任务进度和日志
documentName:展示名称
parseStatus:解析状态
strategyStatus:策略状态
indexStatus:索引状态

也就是说,上传接口返回的是“业务可感知状态”,不是“底层存储细节”。

十五、把完整流程串起来

这一块完整流程可以背成一句话:

上传接口接收 multipart 文件和可选元信息,先进行空文件、原始文件名和文件类型校验,然后读取文件字节并生成全局 documentId;系统将原始文件上传到 MinIO,获得 bucket、objectName 和 objectUrl 后构建文档主记录,同时创建解析路由任务和任务日志;文档主表、任务表和日志表通过 TransactionTemplate 在同一个事务内提交,事务提交成功后再发送 Kafka 消息,触发后续异步解析流水线,最后返回 documentId、taskId 和初始处理状态给前端。

十六、你要重点掌握的设计思想

1. 上传接口不是处理完成,而是异步处理入口

这个接口不会直接完成解析、切块、向量化和索引构建。它只是把文件接入系统,并创建后续任务。

这体现的是异步架构思想:

同步接口做轻量初始化
重任务交给 Kafka 异步消费
状态表记录进度
前端轮询或订阅进度

2. documentId 是全链路主线

documentId贯穿:

对象存储路径
文档主表
任务表
任务日志
Kafka 消息
解析结果
切块结果
向量记录
ES 索引
Neo4j 图谱节点
问答引用来源

所以它必须全局唯一、提前生成、稳定可靠。

3. 文档主表保存“资产状态”

文档主表不是只保存文件路径,它还保存处理状态和业务分类。

它是整个文档生命周期的状态中心。

4. 任务表保存“处理动作”

文档主表表示“文档是什么状态”,任务表表示“当前要执行什么处理”。

这两个表要分开,不要混在一起。

5. TransactionTemplate 的核心价值是控制事务边界

它解决的是:

数据库事务提交
Kafka 消息发送
消费方查询可见性

之间的顺序问题。

6. Kafka 消息只传 ID,不传大对象

详细上下文放数据库,消息只负责触发流程。

这是异步系统里很常见的设计。

7. 任务日志从第一步开始记录

这为后续全链路可观测、失败排查和状态追踪打基础。

十七、这一块可以怎么写进简历或项目介绍

可以这样提炼:

负责设计并实现文档上传与异步解析入口链路,支持 multipart 文件与元信息上传,完成文件合法性校验、MinIO 原始文件存储、文档主记录创建、解析任务创建和任务日志记录。通过 TransactionTemplate 精确控制事务边界,保证文档主表、任务表和日志表事务一致提交,并在事务提交后投递 Kafka 解析路由消息,避免消费方读取未提交数据的问题。上传结果返回 documentId、taskId 和初始处理状态,支撑前端进度追踪和后续异步解析流水线。

如果要突出亮点,可以再加一句:

针对“数据库提交”和“消息投递”之间的一致性问题,采用“先事务提交、后同步发送 Kafka、失败补偿兜底”的方案,提升异步处理链路的可靠性和可观测性。

十八、面试官可能会问的问题

问题 1:为什么不直接在上传接口里解析文档?

因为解析文档可能很慢,尤其是 PDF、PPT、Excel 或大文件。如果同步解析,会导致接口响应时间过长,甚至超时。上传接口只做文件接入和任务创建,后续通过 Kafka 异步处理,可以提升用户体验和系统吞吐量。

问题 2:为什么先上传 MinIO 再写数据库?

因为文档主表需要保存文件的存储位置,包括 bucket、objectName 和 objectUrl。只有文件上传成功后,才能拿到这些信息并写入数据库。

问题 3:如果 MinIO 上传成功,但数据库写入失败怎么办?

会产生孤儿文件。可以通过失败回滚清理、定时扫描无主对象、或者在数据库写入失败时主动删除 MinIO 文件来补偿。

问题 4:为什么不用方法级 @Transactional?

如果方法级事务包住整个上传流程,Kafka 可能在数据库事务提交前就发送出去。消费方收到消息后查询数据库,可能查不到刚插入的文档和任务。使用 TransactionTemplate 可以把事务范围限定在数据库写入部分,确保事务提交后再发 Kafka。

问题 5:如果事务提交成功,但 Kafka 发送失败怎么办?

这种情况数据库里已经有文档和任务,但异步处理没有启动。可以通过补偿任务扫描长时间停留在 PARSINGNEW 状态的记录,重新投递 Kafka 或标记为失败待重试。

问题 6:为什么 Kafka 消息只传 documentId 和 taskId?

因为详细上下文已经在数据库中。消息只负责触发异步流程,消费方通过 ID 查询数据库即可。这样消息更小,也降低了字段变更带来的兼容成本。

问题 7:parseStatus 设置为 PARSING 是否准确?

从严格语义看,它表示文档已经进入解析流程或解析队列,并不代表当前请求线程正在解析。实际项目中也可以命名为 WAIT_PARSEPARSING,关键是状态定义要清晰,前后端和消费方要保持一致。

十九、这一节你需要真正记住的代码主线

你不用死背每一行代码,但要记住主线:

public DocumentUploadVo upload(MultipartFile file, DocumentUploadDto dto) {

    // 1. 校验文件
    checkFile(file);

    // 2. 读取字节,生成 documentId
    byte[] fileBytes = file.getBytes();
    Long documentId = uidGenerator.getUid();

    // 3. 上传 MinIO
    StoredObjectInfo stored = storageService.uploadOriginalFile(
        documentId, originalFileName, fileBytes, contentType
    );

    // 4. 构建文档主记录
    SuperAgentDocument document = buildDocument(...);

    // 5. 构建任务记录
    SuperAgentDocumentTask task = buildTask(...);

    // 6. 数据库事务:插入文档、任务、日志
    DocumentUploadVo vo = transactionTemplate.execute(status -> {
        documentMapper.insert(document);
        taskMapper.insert(task);
        taskLogService.saveLog(...);
        return buildUploadVo(document, task);
    });

    // 7. 事务提交后发送 Kafka
    kafkaProducer.sendParseRoute(new DocumentParseRouteMessage(documentId, taskId));

    // 8. 返回结果
    return vo;
}

你看到任何上传链路源码,都可以按这个模板去拆。


二十、这一块和后续模块的衔接

上传接口完成后,系统已经具备了后续异步处理所需的全部上下文:

数据 作用
documentId 串联文档全生命周期
taskId 串联异步任务执行过程
objectName 定位 MinIO 原始文件
parseStatus 标记解析进度
strategyStatus 标记切块策略推荐进度
indexStatus 标记索引构建进度
taskLog 支持进度追踪和问题排查
Kafka 消息 触发后续解析流水线

所以它后面自然会接到:

Kafka Consumer 消费解析路由消息
根据 documentId 查询文档
从 MinIO 下载原始文件
使用 Apache Tika 解析
更新 parseStatus
推荐切块策略
等待用户确认
触发切块和向量化
构建 ES、向量库、Neo4j 索引

也就是说,“上传接口与文档主记录创建”这一块是后面所有知识库能力的起点。没有它,后面的 RAG、ReAct Agent、知识路由、图谱导航、引用来源都没有稳定的数据基础。

这一节先把入口链路、状态设计、事务边界和 Kafka 投递顺序吃透,这就是这个模块的核心。


企业级项目导航:⬅️ 05-文本转文档结构树 | 06-知识库系统的入口工程 | ➡️ 07-结构节点提取的四阶段流水线