FormatService.doFormatAsync()
异步执行排版任务,处理排版全流程:该方法被标记为 @Async("notificationExecutor"),表示它会在单独的线程中异步执行,避免阻塞主线程;包括文件下载、排版处理、文件上传、状态更新等完整流程。
业务流程:初始化与前置校验 -- 文件保存到临时目录 -- 调用外部排版服务 -- 清理"排版中"状态 -- 上传排版结果 -- 保存结果到 Redis -- 更新数据库状态
java
@Async("notificationExecutor")
public void doFormatAsync(Integer styleId, Integer formId, String sourceNodeId, String fileId, String fileName, Long userId, String taskKey) {
// 临时文件路径,用于最后清理
String tempFilePath = null;
try {
// 1. 初始化与前置校验: 校验原文件是否存在且未被删除。若校验失败,则保存失败状态并清理任务状态。
KbNode sourceNode = kbNodeMapper.selectById(sourceNodeId);
if (sourceNode == null || sourceNode.getDeleted() == 1) {
log.error("FormatService##doFormatAsync-0,排版任务失败,原文件节点不存在,sourceNodeId={}", sourceNodeId);
saveFailedResultItem(sourceNodeId, fileName, userId);
stringRedisTemplate.delete(taskKey);
return;
}
// 如果原文件有父节点则使用父节点,否则说明在根目录,使用原文件所属的根节点(通过查询获取)
String folderNodeId = StringUtils.hasText(sourceNode.getParentId()) ? sourceNode.getParentId(): sourceNodeId; // 根目录下的文件,暂时使用原nodeId,实际部署时可能需要调整
FileBean fileBean = fileBeanService.getById(fileId);// 从file表(db2)查询文件信息
if (fileBean == null || !StringUtils.hasText(fileBean.getFileUrl())) {
log.info("FormatService##doFormatAsync-1,排版任务失败,文件不存在,fileId={}", fileId);
// 保存失败状态的结果项
saveFailedResultItem(sourceNodeId, fileName, userId);
stringRedisTemplate.delete(taskKey);
return;
}
// 2. 文件下载与临时存储:将原始文件从存储系统下载到本地临时目录。
// 使用 saveFileToTemp 方法构建临时路径并复制文件。若下载失败,则记录失败状态并退出.
tempFilePath = saveFileToTemp(fileBean.getFileUrl(), fileName);
if (tempFilePath == null) {
log.error("FormatService##doFormatAsync-2,排版任务失败,文件保存到临时目录失败,fileUrl={}", fileBean.getFileUrl());
saveFailedResultItem(sourceNodeId, fileName, userId);
stringRedisTemplate.delete(taskKey);
return;
}
// 3. 调用外部排版服务:调用公文排版服务生成排版后的文件(PDF 和 Word)。
// callExternalFormatService中:读取临时文件内容。根据styleId选择模板。调用 govdocFormatterService.format() 执行排版。将排版结果保存到临时目录。
FormatCallbackResult callbackResult = callExternalFormatService(styleId, tempFilePath);
if (callbackResult == null || !callbackResult.isSuccess()) {
log.error("FormatService##doFormatAsync-4,排版任务失败,外部排版接口调用失败,sourceNodeId={}", sourceNodeId);
saveFailedResultItem(sourceNodeId, fileName, userId);
stringRedisTemplate.delete(taskKey);
return;
}
// 4. 删除"排版中"状态的临时结果项:清除任务启动时插入的"排版中"状态项,排版完成或失败时调用。
removeFormattingResultItem(sourceNodeId, userId);
// 5. 上传排版结果文件:将排版后的文件上传至文件服务器,并生成新的 nodeId,并保存结果。
// 分别处理PDF和Word文件、调用uploadSingleFile方法上传文件、FormatResultItem对象保存结果信息
List<FormatResultItem> resultItems = uploadFormattedFiles(callbackResult, sourceNodeId, fileName, userId, folderNodeId);
// 6. 保存每个结果项到 Redis,将排版结果持久化到 Redis 中,供前端查询。
// 使用 FORMAT_ITEM_KEY_PREFIX 保存结果项详情。建立用户、原文件与结果文件之间的映射关系.
for (FormatResultItem item : resultItems) {
saveResultItem(item, userId, sourceNodeId);
}
// 7. 删除任务状态标识并清理临时文件目录(表示任务完成)
stringRedisTemplate.delete(taskKey);
// 捕获所有异常(包括Error和Exception),确保任务失败时能正确记录状态并释放资源。
} catch (Throwable e) {
log.error("FormatService##doFormatAsync-e,排版任务异常,userId={}, sourceNodeId={}", userId, sourceNodeId, e);
// 删除"排版中"的临时结果项
removeFormattingResultItem(sourceNodeId, userId);
saveFailedResultItem(sourceNodeId, fileName, userId);
stringRedisTemplate.delete(taskKey);
} finally {
// 7. 清理临时文件目录(无论成功或失败都要清理)
cleanupTempDirectory(tempFilePath);
}
}