Commit 779f5d7b by zhangxingmin

Merge remote-tracking branch 'origin/test' into dev

parents ecd8f9fb 93c23a0c
# 构建阶段 - 创建包含LibreOffice的基础镜像
FROM openjdk:8-jre-slim as base-builder
# ============================================================
# 构建阶段 - 创建包含 LibreOffice 的基础镜像
# ============================================================
FROM eclipse-temurin:8-jre-jammy as base-builder
ENV LANG=zh_CN.UTF-8 \
LANGUAGE=zh_CN:zh \
LC_ALL=zh_CN.UTF-8 \
DEBIAN_FRONTEND=noninteractive
RUN sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list && \
sed -i 's/security.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list
# 替换为阿里云 Ubuntu 源(jammy = 22.04)
RUN sed -i 's|archive.ubuntu.com|mirrors.aliyun.com|g' /etc/apt/sources.list && \
sed -i 's|security.ubuntu.com|mirrors.aliyun.com|g' /etc/apt/sources.list
# 安装字体、LibreOffice 及中文语言包
RUN apt-get update && \
apt-get install -y --no-install-recommends \
fonts-liberation \
......@@ -29,12 +33,15 @@ RUN apt-get update && \
RUN mkdir -p /home/app
WORKDIR /home/app
# ============================================================
# 最终阶段
# ============================================================
FROM base-builder
# 只拷贝jar包
# 拷贝 Spring Boot 可执行 JAR
COPY target/yd-oss-api-1.0-SNAPSHOT-exec.jar /home/app/yd-oss-api.jar
# 执行命令启动jar,并设置JVM内存参数
# 启动命令,设置 JVM 内存参数
ENTRYPOINT ["java", "-Xmx1024m", "-Xms512m", "-jar", "/home/app/yd-oss-api.jar"]
EXPOSE 9106
\ No newline at end of file
package com.yd.oss.api.config;
import org.jodconverter.core.office.OfficeManager;
import org.jodconverter.local.office.LocalOfficeManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
public class OfficeManagerHolder {
private static final Logger logger = LoggerFactory.getLogger(OfficeManagerHolder.class);
private static OfficeManager instance = null;
private static final Object lock = new Object();
public static OfficeManager getInstance(String libreOfficeHome) throws Exception {
if (instance == null) {
synchronized (lock) {
if (instance == null) {
logger.info("正在启动 LibreOffice (home: {}) ...", libreOfficeHome);
instance = LocalOfficeManager.builder()
.officeHome(new File(libreOfficeHome))
.build();
instance.start();
logger.info("LibreOffice 启动成功");
}
}
}
return instance;
}
public static void stop() {
if (instance != null) {
try {
instance.stop();
logger.info("LibreOffice 已停止");
} catch (Exception e) {
logger.error("停止 LibreOffice 失败", e);
}
instance = null;
}
}
}
\ No newline at end of file
package com.yd.oss.api.controller;
import com.yd.common.result.Result;
import com.yd.oss.api.service.ApiChunkedUploadContextService;
import com.yd.oss.feign.client.ApiChunkedUploadContextFeignClient;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.util.Map;
/**
* 分片上传上下文信息
* (一个任务上传文件信息拆分成多个ETag上传存储,任务执行完毕,阿里云合并ETag列表为完整的文件信息)
* @author zxm
* @since 2026-08-05
*/
@Slf4j
@RestController
@RequestMapping("/chunkedUploadContext")
@Validated
public class ApiChunkedUploadContextController implements ApiChunkedUploadContextFeignClient {
@Resource
private ApiChunkedUploadContextService apiChunkedUploadContextService;
/**
* 上传单个分片
* @param chunk
* @param taskId
* @param chunkIndex
* @param projectBizId
* @param source
* @return
*/
@Override
public Result<Void> uploadChunk(MultipartFile chunk, String taskId, Integer chunkIndex, String projectBizId, String source) {
apiChunkedUploadContextService.uploadChunk(taskId, chunkIndex, chunk,projectBizId,source);
return Result.success();
}
/**
* 完成分片上传(合并)
*/
public Result<Map<String, Object>> finishChunks(String taskId,
String projectBizId) {
return apiChunkedUploadContextService.finishChunks(taskId,projectBizId);
}
}
package com.yd.oss.api.controller;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yd.common.result.Result;
import com.yd.oss.api.service.ApiOssService;
import com.yd.oss.feign.client.ApiOssFeignClient;
......@@ -7,9 +8,13 @@ import com.yd.oss.feign.request.ApiUploadFileRequest;
import com.yd.oss.feign.response.ApiBatchUploadResponse;
import com.yd.oss.feign.response.ApiFileMetadataResponse;
import com.yd.oss.feign.response.ApiUploadResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
......@@ -21,6 +26,7 @@ import java.util.List;
* @author zxm
* @since 2025-07-31
*/
@Slf4j
@RestController
@RequestMapping("/oss")
@Validated
......@@ -29,6 +35,9 @@ public class ApiOssController implements ApiOssFeignClient {
@Autowired
private ApiOssService apiOssService;
@Autowired
private ObjectMapper objectMapper;
/**
* 上传文件
* @param file 上传的文件
......@@ -50,6 +59,17 @@ public class ApiOssController implements ApiOssFeignClient {
return apiOssService.uploadFileBody(file,request);
}
public Result<ApiUploadResponse> uploadFileBodyWithJson(MultipartFile file,String requestJson) {
try {
// 反序列化 JSON 为 ApiUploadFileRequest
ApiUploadFileRequest request = objectMapper.readValue(requestJson, ApiUploadFileRequest.class);
return apiOssService.uploadFileBody(file, request);
} catch (Exception e) {
log.error("解析请求参数失败", e);
return Result.fail("请求参数格式错误");
}
}
/**
* 下载文件
* @param fileKey 文件唯一标识
......
package com.yd.oss.api.controller;
import com.yd.oss.api.utils.LocalDocumentConversionService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
/**
* 文档转换接口
* 提供将 Office 文档(Word/Excel/PPT)转换为 PDF/HTML/图片等格式的 HTTP 接口。
* 供其他微服务通过内网调用。
*/
@RestController
@RequestMapping("/convert")
public class DocumentConvertController {
private static final Logger logger = LoggerFactory.getLogger(DocumentConvertController.class);
@Autowired
private LocalDocumentConversionService conversionService;
/**
* 将指定 URL 的文档转换为目标格式,并返回字节流。
*
* @param fileUrl 源文件 URL(支持 HTTP/HTTPS/OSS)
* @param targetFormat 目标格式,如 pdf、html、png、jpg 等,默认 pdf
* @return 转换后的文件字节流,Content-Type 根据格式自动设置
*/
@PostMapping("/file")
public ResponseEntity<byte[]> convert(
@RequestParam("fileUrl") String fileUrl,
@RequestParam(value = "targetFormat", defaultValue = "pdf") String targetFormat) {
logger.info("收到转换请求: fileUrl={}, targetFormat={}", fileUrl, targetFormat);
try {
byte[] result = conversionService.convertToBytes(fileUrl, targetFormat);
HttpHeaders headers = new HttpHeaders();
// 根据目标格式设置 Content-Type
MediaType mediaType = getMediaType(targetFormat);
headers.setContentType(mediaType);
// 可选:设置下载文件名
String filename = extractFileName(fileUrl) + "." + targetFormat;
try {
headers.setContentDispositionFormData("attachment",
URLEncoder.encode(filename, StandardCharsets.UTF_8.name()));
} catch (UnsupportedEncodingException e) {
// 忽略
}
logger.info("转换成功,返回字节数: {}", result.length);
return new ResponseEntity<>(result, headers, HttpStatus.OK);
} catch (Exception e) {
logger.error("文档转换失败: fileUrl={}, targetFormat={}", fileUrl, targetFormat, e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(("转换失败: " + e.getMessage()).getBytes(StandardCharsets.UTF_8));
}
}
private MediaType getMediaType(String format) {
if (format == null) return MediaType.APPLICATION_OCTET_STREAM;
switch (format.toLowerCase()) {
case "pdf":
return MediaType.APPLICATION_PDF;
case "html":
return MediaType.TEXT_HTML;
case "png":
return MediaType.IMAGE_PNG;
case "jpg":
case "jpeg":
return MediaType.IMAGE_JPEG;
default:
return MediaType.APPLICATION_OCTET_STREAM;
}
}
private String extractFileName(String url) {
if (url == null) return "converted";
int lastSlash = url.lastIndexOf('/');
if (lastSlash >= 0 && lastSlash < url.length() - 1) {
String name = url.substring(lastSlash + 1);
int dot = name.lastIndexOf('.');
return dot > 0 ? name.substring(0, dot) : name;
}
return "converted";
}
}
\ No newline at end of file
package com.yd.oss.api.service;
import com.yd.common.result.Result;
import org.springframework.web.multipart.MultipartFile;
import java.util.Map;
public interface ApiChunkedUploadContextService {
void uploadChunk(String taskId, Integer chunkIndex, MultipartFile chunk,String projectBizId,String source);
Result<Map<String, Object>> finishChunks(String taskId,String projectBizId);
}
package com.yd.oss.api.service.impl;
import com.aliyun.oss.OSS;
import com.aliyun.oss.model.*;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yd.common.exception.BusinessException;
import com.yd.common.result.Result;
import com.yd.oss.api.service.ApiChunkedUploadContextService;
import com.yd.oss.service.config.OssClientFactory;
import com.yd.oss.service.dao.ChunkedUploadContextMapper;
import com.yd.oss.service.model.ChunkedUploadContext;
import com.yd.oss.service.model.OssProvider;
import com.yd.oss.service.service.IOssProviderService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.TimeUnit;
@Slf4j
@Service
public class ApiChunkedUploadContextServiceImpl implements ApiChunkedUploadContextService {
@Resource
private ChunkedUploadContextMapper contextMapper;
@Resource
private ObjectMapper objectMapper;
@Resource
private OssClientFactory ossClientFactory;
@Resource
private IOssProviderService ossProviderService;
@Resource
private RedissonClient redissonClient;
private static final String LOCK_KEY_PREFIX = "chunked:upload:lock:";
/**
* 上传单个分片(使用分布式锁保护上下文初始化 + Map存储ETag)
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void uploadChunk(String taskId, Integer chunkIndex, MultipartFile chunk,
String projectBizId, String source) {
log.info("【uploadChunk】开始处理, taskId={}, chunkIndex={}, projectBizId={}, source={}, fileSize={}",
taskId, chunkIndex, projectBizId, source, chunk != null ? chunk.getSize() : 0);
// 获取OSS服务商
OssProvider provider = ossProviderService.getProviderByProjectId(projectBizId);
if (provider == null) {
log.error("【uploadChunk】未找到OSS服务商, projectBizId={}", projectBizId);
throw new BusinessException("未找到对应的OSS服务商配置");
}
OSS ossClient = ossClientFactory.createOssClient(provider);
// 获取或创建上下文(仅初始化时加锁)
ChunkedUploadContext context = getOrCreateContext(taskId, provider, source, ossClient);
// 上传分片并更新ETag(使用Map存储,避免PartETag反序列化问题)
try {
int partNumber = chunkIndex + 1;
log.info("【uploadChunk】开始上传分片, taskId={}, partNumber={}, chunkSize={}",
taskId, partNumber, chunk.getSize());
UploadPartRequest uploadPartRequest = new UploadPartRequest();
uploadPartRequest.setBucketName(provider.getBucketName());
uploadPartRequest.setKey(context.getObjectKey());
uploadPartRequest.setUploadId(context.getUploadId());
uploadPartRequest.setPartNumber(partNumber);
uploadPartRequest.setInputStream(chunk.getInputStream());
uploadPartRequest.setPartSize(chunk.getSize());
UploadPartResult uploadResult = ossClient.uploadPart(uploadPartRequest);
// 读取现有ETag列表(存储为List<Map>)
String json = context.getPartEtagsJson();
List<Map<String, Object>> partInfoList;
if (StringUtils.isBlank(json) || "[]".equals(json)) {
partInfoList = new ArrayList<>();
} else {
partInfoList = objectMapper.readValue(json, new TypeReference<List<Map<String, Object>>>() {});
}
// 添加新分片信息
Map<String, Object> newPart = new HashMap<>();
newPart.put("partNumber", partNumber);
newPart.put("etag", uploadResult.getETag());
partInfoList.add(newPart);
// 按partNumber排序
partInfoList.sort(Comparator.comparingInt(m -> (Integer) m.get("partNumber")));
// 序列化并更新数据库
context.setPartEtagsJson(objectMapper.writeValueAsString(partInfoList));
context.setUpdateTime(LocalDateTime.now());
contextMapper.updateById(context);
log.info("【uploadChunk】分片上传成功, taskId={}, partNumber={}, ETag={}, 当前总分片数={}",
taskId, partNumber, uploadResult.getETag(), partInfoList.size());
} catch (Exception e) {
log.error("【uploadChunk】分片上传或更新ETag失败, taskId={}, chunkIndex={}", taskId, chunkIndex, e);
throw new BusinessException("分片上传失败: " + e.getMessage());
}
}
/**
* 获取或创建上传上下文(使用分布式锁保证唯一性)
*/
private ChunkedUploadContext getOrCreateContext(String taskId, OssProvider provider,
String source, OSS ossClient) {
String lockKey = LOCK_KEY_PREFIX + taskId;
RLock lock = redissonClient.getLock(lockKey);
boolean locked = false;
try {
locked = lock.tryLock(5, 10, TimeUnit.SECONDS);
if (!locked) {
throw new BusinessException("系统繁忙,请稍后重试");
}
ChunkedUploadContext context = contextMapper.selectOne(
new LambdaQueryWrapper<ChunkedUploadContext>()
.eq(ChunkedUploadContext::getTaskId, taskId)
);
if (context == null) {
log.info("【getOrCreateContext】上下文不存在,初始化新上传, taskId={}", taskId);
context = new ChunkedUploadContext();
context.setTaskId(taskId);
String objectKey = String.format("sharding/" + source + "/%tY/%tm/%s_%d.webm",
new Date(), new Date(), taskId, System.currentTimeMillis());
context.setObjectKey(objectKey);
InitiateMultipartUploadRequest initRequest =
new InitiateMultipartUploadRequest(provider.getBucketName(), objectKey);
InitiateMultipartUploadResult initResult = ossClient.initiateMultipartUpload(initRequest);
context.setUploadId(initResult.getUploadId());
context.setPartEtagsJson("[]"); // 初始为空数组
context.setStatus(1); // 上传中
int insertResult = contextMapper.insert(context);
log.info("【getOrCreateContext】初始化分片上传并入库, taskId={}, uploadId={}, objectKey={}, insertResult={}",
taskId, context.getUploadId(), objectKey, insertResult);
} else {
log.info("【getOrCreateContext】找到已存在的上下文, taskId={}, uploadId={}, status={}",
taskId, context.getUploadId(), context.getStatus());
}
return context;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("【getOrCreateContext】获取锁被中断, taskId={}", taskId, e);
throw new BusinessException("系统中断,请重试");
} finally {
if (locked && lock.isHeldByCurrentThread()) {
lock.unlock();
log.debug("【getOrCreateContext】释放锁, taskId={}", taskId);
}
}
}
/**
* 完成分片上传(合并)
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Result<Map<String, Object>> finishChunks(String taskId, String projectBizId) {
log.info("【finishChunks】开始完成分片上传, taskId={}, projectBizId={}", taskId, projectBizId);
// 1. 查询上下文
ChunkedUploadContext context = contextMapper.selectOne(
new LambdaQueryWrapper<ChunkedUploadContext>()
.eq(ChunkedUploadContext::getTaskId, taskId)
);
if (context == null) {
log.error("【finishChunks】未找到分片上传上下文, taskId={}", taskId);
throw new BusinessException("未找到分片上传上下文,请检查 taskId 是否正确");
}
log.info("【finishChunks】找到上下文, taskId={}, uploadId={}, status={}, objectKey={}",
taskId, context.getUploadId(), context.getStatus(), context.getObjectKey());
// 2. 解析分片列表(从Map列表转为PartETag列表)
String json = context.getPartEtagsJson();
List<Map<String, Object>> partInfoList;
try {
partInfoList = objectMapper.readValue(json, new TypeReference<List<Map<String, Object>>>() {});
} catch (Exception e) {
log.error("【finishChunks】解析分片数据失败, taskId={}, json={}", taskId, json, e);
throw new BusinessException("数据异常,无法合并");
}
if (partInfoList.isEmpty()) {
log.error("【finishChunks】没有分片数据, taskId={}", taskId);
throw new BusinessException("没有分片数据,无法合并");
}
List<PartETag> partETags = new ArrayList<>();
for (Map<String, Object> map : partInfoList) {
Integer partNumber = (Integer) map.get("partNumber");
String etag = (String) map.get("etag");
if (partNumber == null || etag == null) {
log.warn("【finishChunks】分片数据不完整,跳过: {}", map);
continue;
}
partETags.add(new PartETag(partNumber, etag));
}
if (partETags.isEmpty()) {
throw new BusinessException("没有有效的分片数据,无法合并");
}
log.info("【finishChunks】解析到 {} 个分片, taskId={}", partETags.size(), taskId);
// 3. 获取OSS服务商
OssProvider provider = ossProviderService.getProviderByProjectId(projectBizId);
if (provider == null) {
log.error("【finishChunks】未找到OSS服务商, projectBizId={}", projectBizId);
throw new BusinessException("未找到对应的OSS服务商配置");
}
OSS ossClient = ossClientFactory.createOssClient(provider);
// 4. 执行合并
CompleteMultipartUploadRequest completeRequest = new CompleteMultipartUploadRequest(
provider.getBucketName(),
context.getObjectKey(),
context.getUploadId(),
partETags
);
CompleteMultipartUploadResult completeResult = ossClient.completeMultipartUpload(completeRequest);
log.info("【finishChunks】合并成功, taskId={}, location={}", taskId, completeResult.getLocation());
// 5. 构建文件URL和大小
String fileUrl = String.format("https://%s.%s/%s",
provider.getBucketName(),
provider.getEndpoint().replace("https://", ""),
context.getObjectKey()
);
ObjectMetadata metadata = ossClient.getObjectMetadata(provider.getBucketName(), context.getObjectKey());
long fileSize = metadata.getContentLength();
// 6. 更新上下文状态为已完成
context.setStatus(2);
contextMapper.updateById(context);
log.info("【finishChunks】上下文状态更新为已完成, taskId={}", taskId);
// 7. 返回结果
Map<String, Object> result = new HashMap<>();
result.put("taskId", taskId);
result.put("fileUrl", fileUrl);
result.put("fileKey", context.getObjectKey());
result.put("fileSize", fileSize);
log.info("【finishChunks】完成, taskId={}, fileUrl={}", taskId, fileUrl);
return Result.success(result);
}
}
\ No newline at end of file
package com.yd.oss.api.utils;
import com.yd.oss.api.config.OfficeManagerHolder;
import org.jodconverter.core.DocumentConverter;
import org.jodconverter.core.office.OfficeException;
import org.jodconverter.local.LocalConverter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.io.InputStream;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.UUID;
@Service
public class LocalDocumentConversionService {
private static final Logger logger = LoggerFactory.getLogger(LocalDocumentConversionService.class);
@Value("${jodconverter.libreoffice.home:}")
private String libreOfficeHome;
private DocumentConverter converter;
@PostConstruct
public void init() {
if (libreOfficeHome == null || libreOfficeHome.trim().isEmpty()) {
libreOfficeHome = System.getenv("LIBREOFFICE_HOME");
}
if (libreOfficeHome == null || libreOfficeHome.trim().isEmpty()) {
String os = System.getProperty("os.name").toLowerCase();
if (os.contains("win")) {
libreOfficeHome = "D:/LibreOffice";
} else if (os.contains("linux")) {
libreOfficeHome = "/opt/libreoffice_standalone/libreoffice7.4";
} else if (os.contains("mac")) {
libreOfficeHome = "/Applications/LibreOffice.app/Contents";
} else {
throw new RuntimeException("无法确定 LibreOffice 安装路径,请通过配置或环境变量设置");
}
logger.warn("未配置 libreOfficeHome,使用默认路径: {}", libreOfficeHome);
}
try {
OfficeManagerHolder.getInstance(libreOfficeHome);
converter = LocalConverter.builder()
.officeManager(OfficeManagerHolder.getInstance(libreOfficeHome))
.build();
logger.info("JodConverter 初始化成功,LibreOffice 路径: {}", libreOfficeHome);
} catch (Exception e) {
logger.error("JodConverter 初始化失败,请检查 LibreOffice 安装", e);
throw new RuntimeException("JodConverter 初始化失败", e);
}
}
public byte[] convertToPdfBytes(String fileUrl) throws Exception {
return convertToBytes(fileUrl, "pdf");
}
public byte[] convertToBytes(String fileUrl, String targetExtension) throws Exception {
if (targetExtension.startsWith(".")) {
targetExtension = targetExtension.substring(1);
}
if (targetExtension == null || targetExtension.trim().isEmpty()) {
throw new IllegalArgumentException("目标扩展名不能为空");
}
logger.info("开始转换文档: {} -> 格式: {}", fileUrl, targetExtension);
Path tempInput = Files.createTempFile("convert_", "_" + UUID.randomUUID());
try (InputStream in = new URL(fileUrl).openStream()) {
Files.copy(in, tempInput, StandardCopyOption.REPLACE_EXISTING);
}
logger.info("源文件下载到临时路径: {}", tempInput.toAbsolutePath());
Path tempOutput = Files.createTempFile("converted_", "." + targetExtension);
try {
converter.convert(tempInput.toFile())
.to(tempOutput.toFile())
.execute();
logger.info("转换完成,临时输出: {}", tempOutput.toAbsolutePath());
byte[] resultBytes = Files.readAllBytes(tempOutput);
logger.info("转换结果大小: {} bytes", resultBytes.length);
return resultBytes;
} catch (OfficeException e) {
logger.error("转换失败 (格式: {})", targetExtension, e);
throw new Exception("文档转换失败 (格式: " + targetExtension + "): " + e.getMessage(), e);
} finally {
try {
Files.deleteIfExists(tempInput);
Files.deleteIfExists(tempOutput);
} catch (Exception ignored) {
logger.warn("清理临时文件失败");
}
}
}
@PreDestroy
public void destroy() {
OfficeManagerHolder.stop();
}
}
\ No newline at end of file
......@@ -291,5 +291,13 @@
<orderEntry type="library" name="Maven: org.apache.commons:commons-compress:1.19" level="project" />
<orderEntry type="library" name="Maven: com.github.virtuald:curvesapi:1.06" level="project" />
<orderEntry type="library" name="Maven: org.apache.xmlbeans:xmlbeans:3.1.0" level="project" />
<orderEntry type="library" name="Maven: org.jodconverter:jodconverter-core:4.4.7" level="project" />
<orderEntry type="library" name="Maven: org.jodconverter:jodconverter-local:4.4.7" level="project" />
<orderEntry type="library" name="Maven: org.openoffice:juh:4.1.2" level="project" />
<orderEntry type="library" name="Maven: org.openoffice:jurt:4.1.2" level="project" />
<orderEntry type="library" name="Maven: org.openoffice:ridl:4.1.2" level="project" />
<orderEntry type="library" name="Maven: org.openoffice:unoil:4.1.2" level="project" />
<orderEntry type="library" name="Maven: com.itextpdf:itextpdf:5.5.13" level="project" />
<orderEntry type="library" name="Maven: com.itextpdf:itext-asian:5.2.0" level="project" />
</component>
</module>
\ No newline at end of file
package com.yd.oss.feign.client;
import com.yd.common.result.Result;
import com.yd.oss.feign.fallback.ApiChunkedUploadContextFeignFallbackFactory;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.multipart.MultipartFile;
import java.util.Map;
/**
* 分片上传上下文信息Feign客户端
*/
@FeignClient(name = "yd-oss-api", path = "/oss/api/chunkedUploadContext", fallbackFactory = ApiChunkedUploadContextFeignFallbackFactory.class)
public interface ApiChunkedUploadContextFeignClient {
/**
* 上传单个分片
* @param chunk
* @param taskId
* @param chunkIndex
* @param projectBizId
* @param source
* @return
*/
@PostMapping(value = "/uploadChunk", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
Result<Void> uploadChunk(
@RequestPart("chunk") MultipartFile chunk,
@RequestParam("taskId") String taskId,
@RequestParam("chunkIndex") Integer chunkIndex,
@RequestParam("projectBizId") String projectBizId,
@RequestParam("source") String source
);
/**
* 完成分片上传(合并)
*/
@PostMapping("/finishChunks")
Result<Map<String, Object>> finishChunks(
@RequestParam("taskId") String taskId,
@RequestParam("projectBizId") String projectBizId
);
}
......@@ -10,14 +10,14 @@ import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.validation.constraints.NotBlank;
import java.util.List;
/**
* OSS服务信息Feign客户端
*/
@FeignClient(name = "yd-oss-api", fallbackFactory = ApiOssFeignFallbackFactory.class)
@FeignClient(name = "yd-oss-api",
path = "/oss/api/oss",
fallbackFactory = ApiOssFeignFallbackFactory.class)
public interface ApiOssFeignClient {
/**
......@@ -42,7 +42,10 @@ public interface ApiOssFeignClient {
Result<ApiUploadResponse> uploadFileBody(@RequestPart("file") MultipartFile file,
@RequestPart("request") ApiUploadFileRequest request);
@PostMapping(value = "/upload/body/json", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
Result<ApiUploadResponse> uploadFileBodyWithJson(
@RequestPart("file") MultipartFile file,
@RequestPart("request") String requestJson);
/**
* 下载文件
......
package com.yd.oss.feign.fallback;
import com.yd.common.result.Result;
import com.yd.oss.feign.client.ApiChunkedUploadContextFeignClient;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.openfeign.FallbackFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.util.Map;
/**
* 分片上传上下文信息Feign降级处理
*/
@Slf4j
@Component
public class ApiChunkedUploadContextFeignFallbackFactory implements FallbackFactory<ApiChunkedUploadContextFeignClient> {
@Override
public ApiChunkedUploadContextFeignClient create(Throwable cause) {
return new ApiChunkedUploadContextFeignClient() {
@Override
public Result<Void> uploadChunk(MultipartFile chunk, String taskId, Integer chunkIndex, String projectBizId, String source) {
return null;
}
@Override
public Result<Map<String, Object>> finishChunks(String taskId, String projectBizId) {
return null;
}
};
}
}
......@@ -4,7 +4,6 @@ import com.yd.common.result.Result;
import com.yd.oss.feign.client.ApiExcelFeignClient;
import com.yd.oss.feign.dto.ExportResult;
import com.yd.oss.feign.request.ApiExportRequest;
import com.yd.oss.feign.request.ApiOssExcelParseRequest;
import com.yd.oss.feign.request.ApiOssExportAppointmentExcelRequest;
import com.yd.oss.feign.request.MultiSheetExportRequest;
import com.yd.oss.feign.response.ApiOssExcelParseResponse;
......
......@@ -33,6 +33,11 @@ public class ApiOssFeignFallbackFactory implements FallbackFactory<ApiOssFeignCl
}
@Override
public Result<ApiUploadResponse> uploadFileBodyWithJson(MultipartFile file, String requestJson) {
return null;
}
@Override
public Result downloadFile(String fileKey) {
return null;
}
......
......@@ -8,6 +8,11 @@ import javax.validation.constraints.NotBlank;
public class ApiUploadFileRequest {
/**
* 租户唯一标识(业务ID)
*/
private String tenantBizId;
/**
* 项目ID
*/
private String projectBizId;
......
......@@ -61,6 +61,6 @@ public class ApiUploadResponse {
/**
* 创建时间
*/
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime createTime;
}
......@@ -120,5 +120,30 @@
<artifactId>xmlbeans</artifactId>
</dependency>
<!-- JodConverter 文档转换 -->
<dependency>
<groupId>org.jodconverter</groupId>
<artifactId>jodconverter-core</artifactId>
<version>4.4.7</version>
</dependency>
<dependency>
<groupId>org.jodconverter</groupId>
<artifactId>jodconverter-local</artifactId>
<version>4.4.7</version>
</dependency>
<!-- iText PDF 处理 -->
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itextpdf</artifactId>
<version>5.5.13</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itext-asian</artifactId>
<version>5.2.0</version>
</dependency>
</dependencies>
</project>
package com.yd.oss.service.dao;
import com.yd.oss.service.model.ChunkedUploadContext;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* <p>
* 分片上传上下文表(一个任务上传文件信息拆分成多个ETag上传存储,任务执行完毕,阿里云合并ETag列表为完整的文件信息) Mapper 接口
* </p>
*
* @author zxm
* @since 2026-08-05
*/
public interface ChunkedUploadContextMapper extends BaseMapper<ChunkedUploadContext> {
}
......@@ -15,6 +15,11 @@ import lombok.NoArgsConstructor;
public class OssUploadFileReqDto {
/**
* 租户唯一标识(业务ID)
*/
private String tenantBizId;
/**
* 项目ID
*/
private String projectBizId;
......
package com.yd.oss.service.model;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import java.time.LocalDateTime;
import lombok.Getter;
import lombok.Setter;
/**
* <p>
* 分片上传上下文表(一个任务上传文件信息拆分成多个ETag上传存储,任务执行完毕,阿里云合并ETag列表为完整的文件信息)
* </p>
*
* @author zxm
* @since 2026-08-05
*/
@Getter
@Setter
@TableName("chunked_upload_context")
public class ChunkedUploadContext implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 任务ID
*/
@TableField("task_id")
private String taskId;
/**
* OSS 分片上传 ID(由 OSS 初始化时返回)
*/
@TableField("upload_id")
private String uploadId;
/**
* 文件在 OSS 中的完整路径(不含 Bucket 名称)
*/
@TableField("object_key")
private String objectKey;
/**
* 已上传分片的 ETag 列表(JSON 数组格式,如 [{"partNumber":1,"eTag":"xxx"}])
*/
@TableField("part_etags_json")
private String partEtagsJson;
/**
* 状态:0-初始化,1-上传中,2-已完成,3-已取消
*/
@TableField("status")
private Integer status;
/**
* 通用备注
*/
@TableField("remark")
private String remark;
/**
* 删除标识: 0-正常, 1-删除
*/
@TableField("is_deleted")
private Integer isDeleted;
/**
* 创建人ID
*/
@TableField("creator_id")
private String creatorId;
/**
* 更新人ID
*/
@TableField("updater_id")
private String updaterId;
/**
* 创建时间
*/
@TableField("create_time")
private LocalDateTime createTime;
/**
* 更新时间
*/
@TableField("update_time")
private LocalDateTime updateTime;
}
package com.yd.oss.service.service;
import com.yd.oss.service.model.ChunkedUploadContext;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* <p>
* 分片上传上下文表(一个任务上传文件信息拆分成多个ETag上传存储,任务执行完毕,阿里云合并ETag列表为完整的文件信息) 服务类
* </p>
*
* @author zxm
* @since 2026-08-05
*/
public interface IChunkedUploadContextService extends IService<ChunkedUploadContext> {
}
package com.yd.oss.service.service.impl;
import com.alibaba.fastjson2.JSON;
import com.aliyun.oss.OSS;
import com.aliyun.oss.model.CannedAccessControlList;
import com.aliyun.oss.model.OSSObject;
......@@ -83,7 +84,7 @@ public class AliYunOssServiceImpl implements OssService {
OSS ossClient = null;
try {
ossClient = ossClientFactory.createOssClient(provider);
return doUploadFile(ossClient, provider, inputStream, fileName, bucketName, uploadUser, type, null, null, null, null, null, null);
return doUploadFile(ossClient, provider, inputStream, fileName, bucketName, uploadUser, type, null, null, null, null, null, null,null,null);
} finally {
if (ossClient != null) {
ossClient.shutdown();
......@@ -115,7 +116,8 @@ public class AliYunOssServiceImpl implements OssService {
FileUtil.getFileType(file.getOriginalFilename()),
reqDto.getObjectBizId(), reqDto.getObjectName(),
reqDto.getObjectTableName(), reqDto.getObjectType(),
reqDto.getObjectBizId(), reqDto.getObjectName()); // 这里需要根据实际情况调整,原代码中存储了object信息
reqDto.getObjectBizId(), reqDto.getObjectName(),
reqDto.getTenantBizId(),reqDto.getProjectBizId()); // 这里需要根据实际情况调整,原代码中存储了object信息
} catch (IOException e) {
log.error("文件输入流解析异常", e);
throw new BusinessException("文件输入流解析异常");
......@@ -142,7 +144,7 @@ public class AliYunOssServiceImpl implements OssService {
try {
ossClient = ossClientFactory.createOssClient(defaultProvider);
OssUploadFileResDto result = doUploadFile(ossClient, defaultProvider, inputStream, fileName,
defaultProvider.getBucketName(), uploadUser, null, null, null, null, null, null, null);
defaultProvider.getBucketName(), uploadUser, null, null, null, null, null, null, null,null,null);
return result.getUrl();
} finally {
if (ossClient != null) {
......@@ -159,7 +161,8 @@ public class AliYunOssServiceImpl implements OssService {
String bucketName, String uploadUser, String type,
String objectBizId, String objectName,
String objectTableName, String objectType,
String creatorId, String creatorName) {
String creatorId, String creatorName,
String tenantBizId,String projectBizId) {
LocalDateTime now = LocalDateTime.now();
long startTime = System.currentTimeMillis();
String operationResult = "success";
......@@ -203,8 +206,15 @@ public class AliYunOssServiceImpl implements OssService {
ossFile.setObjectName(objectName);
ossFile.setObjectTableName(objectTableName);
ossFile.setObjectType(objectType);
ossFile.setTenantBizId(tenantBizId);
ossFile.setProjectBizId(projectBizId);
// 获取当前登录用户信息(如果存在)
AuthUserDto authUserDto = SecurityUtil.getCurrentLoginUser();
AuthUserDto authUserDto = null;
try {
authUserDto = SecurityUtil.getCurrentLoginUser();
}catch (Exception e){
log.info("获取当前登录用户信息失败:{}", e.getMessage());
}
if (authUserDto != null) {
ossFile.setCreatorId(authUserDto.getUserBizId());
ossFile.setCreatorName(authUserDto.getRealName());
......
package com.yd.oss.service.service.impl;
import com.yd.oss.service.model.ChunkedUploadContext;
import com.yd.oss.service.dao.ChunkedUploadContextMapper;
import com.yd.oss.service.service.IChunkedUploadContextService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
/**
* <p>
* 分片上传上下文表(一个任务上传文件信息拆分成多个ETag上传存储,任务执行完毕,阿里云合并ETag列表为完整的文件信息) 服务实现类
* </p>
*
* @author zxm
* @since 2026-08-05
*/
@Service
public class ChunkedUploadContextServiceImpl extends ServiceImpl<ChunkedUploadContextMapper, ChunkedUploadContext> implements IChunkedUploadContextService {
}
......@@ -21,7 +21,7 @@ public class MyBatisPlusCodeGenerator {
})
.strategyConfig(builder -> {
builder.addInclude(
"material","rel_object_material"
"chunked_upload_context"
)
.entityBuilder()
.enableLombok()
......
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.yd.oss.service.dao.ChunkedUploadContextMapper">
</mapper>
......@@ -287,5 +287,13 @@
<orderEntry type="library" name="Maven: org.apache.commons:commons-compress:1.19" level="project" />
<orderEntry type="library" name="Maven: com.github.virtuald:curvesapi:1.06" level="project" />
<orderEntry type="library" name="Maven: org.apache.xmlbeans:xmlbeans:3.1.0" level="project" />
<orderEntry type="library" name="Maven: org.jodconverter:jodconverter-core:4.4.7" level="project" />
<orderEntry type="library" name="Maven: org.jodconverter:jodconverter-local:4.4.7" level="project" />
<orderEntry type="library" name="Maven: org.openoffice:juh:4.1.2" level="project" />
<orderEntry type="library" name="Maven: org.openoffice:jurt:4.1.2" level="project" />
<orderEntry type="library" name="Maven: org.openoffice:ridl:4.1.2" level="project" />
<orderEntry type="library" name="Maven: org.openoffice:unoil:4.1.2" level="project" />
<orderEntry type="library" name="Maven: com.itextpdf:itextpdf:5.5.13" level="project" />
<orderEntry type="library" name="Maven: com.itextpdf:itext-asian:5.2.0" level="project" />
</component>
</module>
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment