diff --git a/blade-auth/src/main/java/org/springblade/auth/AuthApplication.java b/blade-auth/src/main/java/org/springblade/auth/AuthApplication.java
index 62627f3..c674021 100644
--- a/blade-auth/src/main/java/org/springblade/auth/AuthApplication.java
+++ b/blade-auth/src/main/java/org/springblade/auth/AuthApplication.java
@@ -29,6 +29,7 @@ package org.springblade.auth;
import org.springblade.core.cloud.client.BladeCloudApplication;
import org.springblade.core.launch.BladeApplication;
import org.springblade.core.launch.constant.AppConstant;
+import org.springframework.context.annotation.ComponentScan;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
/**
@@ -38,6 +39,7 @@ import org.springframework.session.data.redis.config.annotation.web.http.EnableR
*/
@EnableRedisHttpSession
@BladeCloudApplication
+@ComponentScan({"org.springblade.auth", "org.springblade.**.feign"})
public class AuthApplication {
public static void main(String[] args) {
diff --git a/blade-common/pom.xml b/blade-common/pom.xml
index 993b866..6535ccc 100644
--- a/blade-common/pom.xml
+++ b/blade-common/pom.xml
@@ -27,6 +27,18 @@
blade-core-auto
provided
+
+
+ com.huaweicloud
+ esdk-obs-java-bundle
+ 3.24.9
+
+
+ commons-io
+ commons-io
+ 2.11.0
+
+
diff --git a/blade-common/src/main/java/org/springblade/common/utils/ObsUtil.java b/blade-common/src/main/java/org/springblade/common/utils/ObsUtil.java
new file mode 100644
index 0000000..f992aca
--- /dev/null
+++ b/blade-common/src/main/java/org/springblade/common/utils/ObsUtil.java
@@ -0,0 +1,277 @@
+package org.springblade.common.utils;
+
+import com.alibaba.cloud.commons.lang.StringUtils;
+import com.obs.services.ObsClient;
+import com.obs.services.exception.ObsException;
+import com.obs.services.model.*;
+import jakarta.activation.MimetypesFileTypeMap;
+import lombok.SneakyThrows;
+import org.apache.commons.io.FilenameUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import java.io.*;
+import java.net.URL;
+import java.util.List;
+
+/**
+ * @ClassName: OBSUtil
+ * @Description:
+ * @Author: zhaowei
+ * @Date: 2024/8/22
+ */
+@Component
+public class ObsUtil {
+ private final static Logger logger = LoggerFactory.getLogger(ObsUtil.class);
+
+ private static MimetypesFileTypeMap fileTypeMap = new MimetypesFileTypeMap();
+ /**
+ * 华为云OBS
+ */
+ public static String obsEndpoint;
+ public static String obsAccessKeyId;
+ public static String obsAccessKeySecret;
+ public static String obsBucketName;
+ public static String obsRootDirectory;
+
+ @Value("${obs.endpoint}")
+ public void setObsEndpoint(String endpoint) {
+ ObsUtil.obsEndpoint = endpoint;
+ }
+
+ @Value("${obs.access-key-id}")
+ public void setObsAccessKeyId(String accessKeyId) {
+ ObsUtil.obsAccessKeyId = accessKeyId;
+ }
+
+ @Value("${obs.access-key-secret}")
+ public void setObsAccessKeySecret(String accessKeySecret) {
+ ObsUtil.obsAccessKeySecret = accessKeySecret;
+ }
+
+ @Value("${obs.bucket-name}")
+ public void setObsBucketName(String bucketName) {
+ ObsUtil.obsBucketName = bucketName;
+ }
+
+ @Value("${obs.root-directory}")
+ public void setObsRootDirectory(String rootDirectory) {
+ ObsUtil.obsRootDirectory = rootDirectory;
+ }
+
+
+ // 如果Bucket不存在,则创建它。
+ private static void ensureBucket(ObsClient client, String bucketName) throws ObsException {
+
+ if (client.headBucket(bucketName)) {
+ return;
+ }
+ // 创建bucket
+ client.createBucket(bucketName);
+ }
+
+ // 把Bucket设置为所有人可读
+ private static void setBucketPublicReadable(String bucketName) throws ObsException {
+ //华为云OBS
+ ObsClient obsClient = new ObsClient(obsAccessKeyId, obsAccessKeySecret, obsEndpoint);
+ if (obsClient.doesObjectExist(obsBucketName, obsRootDirectory)) {
+
+ } else {
+ obsClient.createBucket(obsBucketName);
+ }
+ obsClient.setBucketAcl(obsBucketName, com.obs.services.model.AccessControlList.REST_CANNED_PUBLIC_READ_WRITE);
+ }
+
+ /**
+ * 上传一个Object
+ *
+ * @param key
+ * @param content
+ * @throws FileNotFoundException
+ */
+ public static String putObject(String key, InputStream content) throws IOException {
+ //华为云OBS
+ ObsClient obsClient = new ObsClient(obsAccessKeyId, obsAccessKeySecret, obsEndpoint);
+ try {
+ obsClient.putObject(obsBucketName, key, content);
+ } catch (Exception e) {
+ logger.error("obs put object error", e);
+ throw new RuntimeException("obs eror" + e.getMessage());
+ } finally {
+ obsClient.close();
+ }
+ return getExpireUrl(key);
+ }
+
+
+ /**
+ * 下载文件
+ * @param objectKey 上传到OBS起的名
+ * @param filename 文件下载到本地保存的路径
+ * @throws ObsException
+ * @throws IOException
+ */
+ public static void downloadFile(String objectKey, String filename) throws ObsException, IOException {
+ //华为云OBS
+ ObsClient obsClient = new ObsClient(obsAccessKeyId, obsAccessKeySecret, obsEndpoint);
+ try {
+ obsClient.getObject(obsBucketName, objectKey);
+ } finally {
+ obsClient.close();
+ }
+ }
+
+ /**
+ * 删除一个Bucket和其中的Objects
+ *
+ * @param bucketName Bucket名
+ * @throws ObsException
+ */
+ private static void deleteBucket(String bucketName) throws ObsException {
+ ObsClient obsClient = new ObsClient(obsAccessKeyId, obsAccessKeySecret, obsEndpoint);
+ com.obs.services.model.ObjectListing objectListing = obsClient.listObjects(bucketName);
+ List listObject = objectListing.getObjects();
+ for (ObsObject o : listObject) {
+ obsClient.deleteObject(bucketName, o.getObjectKey());
+ }
+ obsClient.deleteBucket(bucketName);
+ }
+
+ /**
+ * 批量删除Object
+ * @param objList Object的key(目录路径/文件名)
+ */
+ public static void delelteObjectList(List objList) {
+ ObsClient obsClient = new ObsClient(obsAccessKeyId, obsAccessKeySecret, obsEndpoint);
+ for (String key : objList) {
+ obsClient.deleteObject(obsBucketName, key);
+ }
+ }
+
+
+ /**
+ * 删除Object
+ * @param key 目录路径/文件名
+ */
+ public static void delelteObject(String key) {
+ ObsClient obsClient = new ObsClient(obsAccessKeyId, obsAccessKeySecret, obsEndpoint);
+ obsClient.deleteObject(obsBucketName, key);
+ }
+
+ /**
+ * 通过key获取带签名的url
+ */
+ public static String getExpireUrl(String key) {
+ String fileUrl = "";
+ ObsClient obsClient = new ObsClient(obsAccessKeyId, obsAccessKeySecret, obsEndpoint);
+ // URL有效期,3600秒.5分钟
+ // long expireSeconds = 3600L;
+ // 约等于1年
+ long expireSeconds = 31536000L;
+ TemporarySignatureRequest request = new TemporarySignatureRequest(HttpMethodEnum.GET, expireSeconds);
+ request.setBucketName(obsBucketName);
+ request.setObjectKey(key);
+ TemporarySignatureResponse response = obsClient.createTemporarySignature(request);
+ fileUrl = response.getSignedUrl();
+ return fileUrl;
+ }
+
+ /**
+ * 上传文件至OSS并且返回链接地址
+ * @param url 远程路径
+ * @param ossDir OSS目录
+ * @param fileName 文件名
+ * @return 文件OSS链接
+ * @modify
+ */
+ public static String uploadFile2Oss(URL url, String ossDir, String fileName) {
+ InputStream is;
+ String key = ossDir + fileName;
+ try {
+ is = url.openStream();
+ return uploadFile2OSS(is, key);
+ } catch (Exception ex) {
+ logger.error("上传失败!", ex);
+ }
+ return StringUtils.EMPTY;
+ }
+
+ /**
+ * 上传到OSS服务器 如果同名文件会覆盖服务器上的
+ *
+ * @param inputStream 文件流
+ * @return 返回文件访问路径
+ */
+ @SneakyThrows
+ public static String uploadFile2OSS(InputStream inputStream, String key) {
+ ObjectMetadata objectMetadata = new ObjectMetadata();
+ objectMetadata.setCacheControl("no-cache");
+ String fileName = FilenameUtils.getName(key);
+ objectMetadata.setContentType(fileTypeMap.getContentType(fileName));
+ objectMetadata.addUserMetadata("filename", fileName);
+
+ ObsClient obsClient = new ObsClient(obsAccessKeyId,
+ obsAccessKeySecret, obsEndpoint);
+ try {
+ obsClient.putObject(obsBucketName, obsRootDirectory + key, inputStream, objectMetadata);
+ return obsRootDirectory + "/" + key;
+ } finally {
+ obsClient.close();
+ }
+ }
+
+ @SneakyThrows
+ public Boolean checkObjectExist(String key) {
+ Boolean existsFlag = false;
+ ObsClient obsClient = new ObsClient(obsAccessKeyId, obsAccessKeySecret, obsEndpoint);
+ try {
+ existsFlag = obsClient.doesObjectExist(obsBucketName, key);
+ } finally {
+ obsClient.close();
+ }
+ return existsFlag;
+ }
+
+
+ /**
+ * 下载文件
+ * @param objectKey 上传到OBS起的名
+ * @param filename 文件下载到本地保存的路径
+ * @throws ObsException
+ * @throws IOException
+ */
+ public static void downloadFileNew(String objectKey, String filename){
+ //华为云OBS
+ ObsClient obsClient = new ObsClient(obsAccessKeyId, obsAccessKeySecret, obsEndpoint);
+ try {
+ ObsObject ossObject = obsClient.getObject(obsBucketName, objectKey);
+
+ OutputStream outputStream = null;
+ try {
+ outputStream = new BufferedOutputStream(new FileOutputStream(filename));
+ byte[] buffer = new byte[8192];
+ int bytesRead;
+ while ((bytesRead = ossObject.getObjectContent().read(buffer)) != -1) {
+ outputStream.write(buffer, 0, bytesRead);
+ }
+ } catch (IOException ex) {
+ } finally {
+ if (outputStream != null) {
+ try {
+ outputStream.close();
+ } catch (IOException e) {
+ }
+ }
+ }
+ } finally {
+ if (obsClient != null) {
+ try {
+ obsClient.close();
+ } catch (IOException e) {
+ }
+ }
+ }
+ }
+}
diff --git a/blade-gateway/src/main/resources/application.yml b/blade-gateway/src/main/resources/application.yml
index bbcb4bd..d0c3371 100644
--- a/blade-gateway/src/main/resources/application.yml
+++ b/blade-gateway/src/main/resources/application.yml
@@ -14,6 +14,3 @@ knife4j:
- blade-admin
- blade-gateway
- blade-log
-
-server:
- port: 28080
diff --git a/blade-service-api/blade-file-api/pom.xml b/blade-service-api/blade-file-api/pom.xml
new file mode 100644
index 0000000..d85da74
--- /dev/null
+++ b/blade-service-api/blade-file-api/pom.xml
@@ -0,0 +1,22 @@
+
+
+
+ org.springblade
+ blade-service-api
+ ${revision}
+
+ 4.0.0
+
+ blade-file-api
+ ${project.artifactId}
+ jar
+
+
+
+ org.springblade
+ blade-ocr-api
+
+
+
\ No newline at end of file
diff --git a/blade-service-api/blade-file-api/src/main/java/org/springblade/file/callback/UnzipCallback.java b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/callback/UnzipCallback.java
new file mode 100644
index 0000000..7135272
--- /dev/null
+++ b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/callback/UnzipCallback.java
@@ -0,0 +1,22 @@
+package org.springblade.file.callback;
+
+/**
+ * 解压回调接口
+ * @author bfhuange
+ * @date 2024/11/8
+ */
+public interface UnzipCallback {
+ /**
+ * 解压任务的业务编码
+ * @return
+ */
+ String getBizCode();
+
+ /**
+ * 回调接口
+ * @param bizId 业务id
+ * @param event 事件 org.springblade.file.callback.UnzipEventType name
+ * @return
+ */
+ boolean callback(Long bizId, String event);
+}
diff --git a/blade-service-api/blade-file-api/src/main/java/org/springblade/file/callback/UnzipEventType.java b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/callback/UnzipEventType.java
new file mode 100644
index 0000000..3df651d
--- /dev/null
+++ b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/callback/UnzipEventType.java
@@ -0,0 +1,22 @@
+package org.springblade.file.callback;
+
+/**
+ * 解压事件类型
+ * @author bfhuange
+ * @date 2024/11/15
+ */
+public enum UnzipEventType {
+ /**
+ * 开始解压
+ */
+ START,
+ /**
+ * 解压成功
+ */
+ SUCCESS,
+ /**
+ * 解压失败
+ */
+ FAIL,
+ ;
+}
diff --git a/blade-service-api/blade-file-api/src/main/java/org/springblade/file/config/FeignFileUploadClientConfig.java b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/config/FeignFileUploadClientConfig.java
new file mode 100644
index 0000000..b23ff27
--- /dev/null
+++ b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/config/FeignFileUploadClientConfig.java
@@ -0,0 +1,20 @@
+package org.springblade.file.config;
+
+import feign.codec.Encoder;
+import feign.form.spring.SpringFormEncoder;
+import org.springframework.beans.factory.ObjectFactory;
+import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
+import org.springframework.cloud.openfeign.support.SpringEncoder;
+import org.springframework.context.annotation.Bean;
+
+/**
+ * @author bfhuange
+ * @since 2025/4/9
+ */
+public class FeignFileUploadClientConfig {
+
+ @Bean
+ public Encoder feignFormEncoder(ObjectFactory messageConverters) {
+ return new SpringFormEncoder(new SpringEncoder(messageConverters));
+ }
+}
diff --git a/blade-service-api/blade-file-api/src/main/java/org/springblade/file/feign/IFileClient.java b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/feign/IFileClient.java
new file mode 100644
index 0000000..31876cc
--- /dev/null
+++ b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/feign/IFileClient.java
@@ -0,0 +1,189 @@
+package org.springblade.file.feign;
+
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import org.springblade.core.launch.constant.AppConstant;
+import org.springblade.core.tool.api.FR;
+import org.springblade.file.pojo.dto.*;
+import org.springblade.file.pojo.entity.Attachment;
+import org.springblade.file.pojo.vo.*;
+import org.springblade.thirdparty.ocr.pojo.vo.TransportCertificateVO;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.cloud.openfeign.SpringQueryMap;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestParam;
+
+import java.util.List;
+
+/**
+ * @author bfhuange
+ * @date 2024/10/21
+ */
+@FeignClient(
+ value = AppConstant.APPLICATION_FILE_NAME,
+ fallback = IFileClientFallback.class
+)
+public interface IFileClient {
+
+ String API_PREFIX = "/feign/client/file";
+ String ATTACHMENT = API_PREFIX + "/attachment";
+ String ATTACHMENT_DETAIL = API_PREFIX + "/attachment-detail";
+ String BUSINESS_ATTACHMENT_SAVE_REMOVE = API_PREFIX + "/businessAttachment-save-remove";
+ String BUSINESS_ATTACHMENT_BATCH_SAVE_REMOVE = API_PREFIX + "/businessAttachment-batch-save-remove";
+ String BUSINESS_ATTACHMENT_BATCH_SAVE_REMOVE_BY_PARAM = API_PREFIX + "/businessAttachment-batch-save-remove-by-param";
+ String BUSINESS_ATTACHMENT_BATCH_SAVE_REMOVE_NO_BIZ_ID = API_PREFIX + "/businessAttachment-batch-save-remove-no-bizId";
+ String BUSINESS_ATTACHMENT_REMOVE = API_PREFIX + "/businessAttachment-remove";
+ String BUSINESS_ATTACHMENT_LIST = API_PREFIX + "/businessAttachment-list";
+ String BUSINESS_UNZIP_TASK_SAVE = API_PREFIX + "/businessUnzipTask-save";
+ String BUSINESS_UNZIP_TASK_UPDATE = API_PREFIX + "/businessUnzipTask-update";
+ String BUSINESS_UNZIP_TASK_CANCEL = API_PREFIX + "/businessUnzipTask-cancel";
+ String BUSINESS_UNZIP_TASK_REMOVE = API_PREFIX + "/businessUnzipTask-remove";
+ String BUSINESS_UNZIP_TASK_LIST = API_PREFIX + "/businessUnzipTask-list";
+ String BUSINESS_UNZIP_DETAIL_LIST = API_PREFIX + "/businessUnzipDetail-list";
+ String RECOGNITION_TRANSPORT_CERTIFICATE = API_PREFIX + "/recognitionTransportCertificate";
+ String FILE_URLS = API_PREFIX + "/getFileUrls";
+
+ /**
+ * 获取附件,多个附件id用逗号分割
+ *
+ * @param id
+ * @return List
+ */
+ @GetMapping(ATTACHMENT)
+ FR> getAttachment(@RequestParam("id") String id);
+
+ /**
+ * 获取附件详情
+ *
+ * @param id
+ * @return List
+ */
+ @GetMapping(ATTACHMENT_DETAIL)
+ FR getAttachmentDetail(@RequestParam("id") Long id);
+
+ /**
+ * 保存并删除业务附件,remove为true,先用bizId删除,再新增
+ *
+ * @param param
+ * @return
+ */
+ @PostMapping(BUSINESS_ATTACHMENT_SAVE_REMOVE)
+ FR saveAndRemoveBusinessAttachment(@RequestBody @Validated BusinessAttachmentBatchAddDTO param);
+
+ /**
+ * 批量保存并删除业务附件,先删除附件,再新增,业务id为空,从params中获取bizId,先删除
+ *
+ * @param params 参数为空不新增
+ * @param bizId 业务id
+ * @return
+ */
+ @PostMapping(BUSINESS_ATTACHMENT_BATCH_SAVE_REMOVE)
+ FR batchSaveAndRemoveBusinessAttachment(@RequestBody List<@Valid BusinessAttachmentAddDTO> params, @RequestParam(value = "bizId", required = false) Long bizId);
+
+ /**
+ * 批量保存并删除业务附件,deleteParam为空,从params获取bizId删除,不为空,使用bizIds和bizCodes删除附件,bizCodes为空删除所有bizIds的附件
+ * deleteParam 中 delete 为false,不删除
+ *
+ * @param params 参数为空不新增
+ * @param deleteParam 删除参数,支持只删除业务编码参数的附件
+ * @return
+ */
+ @PostMapping(BUSINESS_ATTACHMENT_BATCH_SAVE_REMOVE_BY_PARAM)
+ FR batchSaveAndRemoveByParamBusinessAttachment(@RequestBody List<@Valid BusinessAttachmentAddDTO> params, @Valid @SpringQueryMap BusinessAttachmentDeleteDTO deleteParam);
+
+ /**
+ * 批量保存并删除业务附件,从参数中提取所有bizId
+ *
+ * @param params 参数
+ * @return
+ */
+ @PostMapping(BUSINESS_ATTACHMENT_BATCH_SAVE_REMOVE_NO_BIZ_ID)
+ FR batchSaveAndRemoveBusinessAttachment(@RequestBody List<@Valid BusinessAttachmentAddDTO> params);
+
+ /**
+ * 根据业务id删除业务附件
+ *
+ * @param bizId
+ * @return
+ */
+ @PostMapping(BUSINESS_ATTACHMENT_REMOVE)
+ FR removeBusinessAttachment(@NotNull(message = "业务id不能为空") @RequestParam(value = "bizId", required = false) Long bizId);
+
+ /**
+ * 查询业务附件列表
+ *
+ * @param param
+ * @return
+ */
+ @PostMapping(BUSINESS_ATTACHMENT_LIST)
+ FR> getBusinessAttachments(@Validated @RequestBody BusinessAttachmentQueryDTO param);
+
+ /**
+ * 保存业务解压任务
+ * @param param
+ * @return
+ */
+ @PostMapping(BUSINESS_UNZIP_TASK_SAVE)
+ FR saveBusinessUnzipTask(@Validated @RequestBody BusinessUnzipTaskAddDTO param);
+
+ /**
+ * 更新业务解压任务(重新上传)
+ * @param param
+ * @return
+ */
+ @PostMapping(BUSINESS_UNZIP_TASK_UPDATE)
+ FR updateBusinessUnzipTask(@Validated @RequestBody BusinessUnzipTaskUpdateDTO param);
+
+ /**
+ * 取消业务解压任务
+ * @param bizId
+ * @return
+ */
+ @PostMapping(BUSINESS_UNZIP_TASK_CANCEL)
+ FR cancelBusinessUnzipTask(@NotNull(message = "业务id不能为空") @RequestParam("bizId") Long bizId);
+
+ /**
+ * 删除业务解压任务
+ * @param bizId
+ * @return
+ */
+ @PostMapping(BUSINESS_UNZIP_TASK_REMOVE)
+ FR removeBusinessUnzipTask(@NotNull(message = "业务id不能为空") @RequestParam("bizId") Long bizId);
+
+ /**
+ * 根据业务id列表查询业务解压任务列表
+ * @param bizIds
+ * @return
+ */
+ @PostMapping(BUSINESS_UNZIP_TASK_LIST)
+ FR> getBusinessUnzipTasksByBizIds(@Validated @NotEmpty(message = "业务id列表不能为空") @RequestBody List bizIds);
+
+ /**
+ * 查询解压明细列表
+ * @param bizId
+ * @return
+ */
+ @GetMapping(BUSINESS_UNZIP_DETAIL_LIST)
+ FR> queryBusinessUnzipDetail(@NotNull(message = "业务id不能为空") @RequestParam("bizId") Long bizId);
+
+ /**
+ * 识别车辆运输凭证
+ *
+ * @param param 同一车牌文件夹下的附件objectKey列表
+ * @return
+ */
+ @PostMapping(RECOGNITION_TRANSPORT_CERTIFICATE)
+ FR recognitionTransportCertificates(@Validated @RequestBody FileCertificateBatchRecognitionDTO param);
+
+ /**
+ * 批量获取附件下载地址
+ * @param param
+ * @return
+ */
+ @PostMapping(FILE_URLS)
+ FR> getFileUrls(@Validated @RequestBody IdsDTO param);
+}
diff --git a/blade-service-api/blade-file-api/src/main/java/org/springblade/file/feign/IFileClientFallback.java b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/feign/IFileClientFallback.java
new file mode 100644
index 0000000..ecf0da5
--- /dev/null
+++ b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/feign/IFileClientFallback.java
@@ -0,0 +1,113 @@
+package org.springblade.file.feign;
+
+import jakarta.validation.Valid;
+import org.springblade.core.tool.api.FR;
+import org.springblade.file.pojo.dto.*;
+import org.springblade.file.pojo.entity.Attachment;
+import org.springblade.file.pojo.vo.*;
+import org.springblade.thirdparty.ocr.pojo.vo.TransportCertificateVO;
+import org.springframework.cloud.openfeign.SpringQueryMap;
+import org.springframework.stereotype.Component;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.util.List;
+
+/**
+ * @author bfhuange
+ * @date 2024/10/21
+ */
+@Component
+public class IFileClientFallback implements IFileClient, IFileUploadClient {
+
+ @Override
+ public FR> getAttachment(String id) {
+ return FR.fail("获取数据失败");
+ }
+
+ @Override
+ public FR getAttachmentDetail(Long id) {
+ return FR.fail("获取数据失败");
+ }
+
+ @Override
+ public FR saveAndRemoveBusinessAttachment(BusinessAttachmentBatchAddDTO param) {
+ return FR.fail("获取数据失败");
+ }
+
+ @Override
+ public FR batchSaveAndRemoveBusinessAttachment(List params, Long bizId) {
+ return FR.fail("获取数据失败");
+ }
+
+ @Override
+ public FR batchSaveAndRemoveByParamBusinessAttachment(List<@Valid BusinessAttachmentAddDTO> params, @Valid @SpringQueryMap BusinessAttachmentDeleteDTO deleteParam) {
+ return FR.fail("获取数据失败");
+ }
+
+ @Override
+ public FR batchSaveAndRemoveBusinessAttachment(List<@Valid BusinessAttachmentAddDTO> params) {
+ return FR.fail("获取数据失败");
+ }
+
+ @Override
+ public FR removeBusinessAttachment(Long bizId) {
+ return FR.fail("获取数据失败");
+ }
+
+ @Override
+ public FR> getBusinessAttachments(BusinessAttachmentQueryDTO param) {
+ return FR.fail("获取数据失败");
+ }
+
+ @Override
+ public FR saveBusinessUnzipTask(BusinessUnzipTaskAddDTO param) {
+ return FR.fail("获取数据失败");
+ }
+
+ @Override
+ public FR updateBusinessUnzipTask(BusinessUnzipTaskUpdateDTO param) {
+ return FR.fail("获取数据失败");
+ }
+
+ @Override
+ public FR cancelBusinessUnzipTask(Long bizId) {
+ return FR.fail("获取数据失败");
+ }
+
+ @Override
+ public FR removeBusinessUnzipTask(Long bizId) {
+ return FR.fail("获取数据失败");
+ }
+
+ @Override
+ public FR> getBusinessUnzipTasksByBizIds(List bizIds) {
+ return FR.fail("获取数据失败");
+ }
+
+ @Override
+ public FR> queryBusinessUnzipDetail(Long bizId) {
+ return FR.fail("获取数据失败");
+ }
+
+ @Override
+ public FR recognitionTransportCertificates(@Validated @RequestBody FileCertificateBatchRecognitionDTO param) {
+ return FR.fail("获取数据失败");
+ }
+
+ @Override
+ public FR> getFileUrls(IdsDTO param) {
+ return FR.fail("获取数据失败");
+ }
+
+ @Override
+ public FR> ossUpload(MultipartFile[] files) {
+ return FR.fail("获取数据失败");
+ }
+
+ @Override
+ public FR ossUploadTemp(MultipartFile file, Integer expires) {
+ return FR.fail("获取数据失败");
+ }
+}
diff --git a/blade-service-api/blade-file-api/src/main/java/org/springblade/file/feign/IFileUploadClient.java b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/feign/IFileUploadClient.java
new file mode 100644
index 0000000..1502b4b
--- /dev/null
+++ b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/feign/IFileUploadClient.java
@@ -0,0 +1,49 @@
+package org.springblade.file.feign;
+
+import jakarta.validation.constraints.NotNull;
+import org.springblade.core.launch.constant.AppConstant;
+import org.springblade.core.tool.api.FR;
+import org.springblade.file.config.FeignFileUploadClientConfig;
+import org.springblade.file.pojo.entity.Attachment;
+import org.springblade.file.pojo.vo.TempFileUrlVO;
+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.List;
+
+/**
+ * @author bfhuange
+ * @date 2024/10/21
+ */
+@FeignClient(
+ value = AppConstant.APPLICATION_FILE_NAME,
+ fallback = IFileClientFallback.class,
+ configuration = FeignFileUploadClientConfig.class
+)
+public interface IFileUploadClient {
+
+ String API_PREFIX = "/feign/client/file";
+ String UPLOAD = API_PREFIX + "/ossUpload";
+ String UPLOAD_TEMP = API_PREFIX + "/ossUploadTemp";
+
+ /**
+ * 文件上传
+ * @param files
+ * @return
+ */
+ @PostMapping(value = UPLOAD, consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
+ FR> ossUpload(@RequestPart("files") MultipartFile[] files);
+
+ /**
+ * 上传临时文件,不保存到附件表,过期后自动删除
+ * @param file
+ * @param expires 过期时间,单位天,过期后自动删除
+ * @return
+ */
+ @PostMapping(value = UPLOAD_TEMP, consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
+ FR ossUploadTemp(@RequestPart("file") MultipartFile file, @NotNull(message = "过期时间不能为空") @RequestParam(name = "expires", required = false) Integer expires);
+}
diff --git a/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/dto/BusinessAttachmentAddDTO.java b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/dto/BusinessAttachmentAddDTO.java
new file mode 100644
index 0000000..6870a20
--- /dev/null
+++ b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/dto/BusinessAttachmentAddDTO.java
@@ -0,0 +1,53 @@
+package org.springblade.file.pojo.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.Builder;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 业务附件信息表 数据传输对象实体类
+ *
+ * @author BladeX
+ * @since 2024-09-04
+ */
+@Data
+@Schema(description = "业务附件新增参数")
+@Builder
+public class BusinessAttachmentAddDTO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 业务id
+ */
+ @NotNull(message = "业务id不能为空")
+ @Schema(description = "业务id")
+ private Long bizId;
+ /**
+ * 业务code
+ */
+ @NotBlank(message = "业务code不能为空")
+ @Schema(description = "业务code")
+ private String bizCode;
+ /**
+ * 附件id
+ */
+ @NotNull(message = "附件id不能为空")
+ @Schema(description = "附件id")
+ private Long attachmentId;
+ /**
+ * 附件描述
+ */
+ @Schema(description = "附件描述")
+ private String desc;
+ /**
+ * 排序
+ */
+ @Schema(description = "排序")
+ private Integer sort;
+}
diff --git a/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/dto/BusinessAttachmentBatchAddDTO.java b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/dto/BusinessAttachmentBatchAddDTO.java
new file mode 100644
index 0000000..6ae7ce0
--- /dev/null
+++ b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/dto/BusinessAttachmentBatchAddDTO.java
@@ -0,0 +1,73 @@
+package org.springblade.file.pojo.dto;
+
+import com.google.common.collect.Lists;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotNull;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 业务附件信息表 数据传输对象实体类
+ *
+ * @author BladeX
+ * @since 2024-09-04
+ */
+@NoArgsConstructor
+@AllArgsConstructor
+@Data
+@Schema(description = "业务附件批量新增参数")
+public class BusinessAttachmentBatchAddDTO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 业务id
+ */
+ @NotNull(message = "业务id不能为空")
+ @Schema(description = "业务id")
+ private Long bizId;
+
+ /**
+ * 明细列表
+ */
+ @Schema(description = "明细列表")
+ private List<@Valid BusinessAttachmentBatchAddDetailDTO> details;
+
+ /**
+ * 是否先删除,默认 是
+ */
+ @Schema(description = "是否先删除,默认 是")
+ private boolean remove = true;
+
+ public BusinessAttachmentBatchAddDTO(Long bizId) {
+ this.bizId = bizId;
+ }
+
+ public BusinessAttachmentBatchAddDTO(Long bizId, String bizCode, Long attachmentId) {
+ this.bizId = bizId;
+ this.details = Lists.newArrayList(new BusinessAttachmentBatchAddDetailDTO(bizCode, attachmentId));
+ }
+
+ public BusinessAttachmentBatchAddDTO addDetail(BusinessAttachmentBatchAddDetailDTO detail) {
+ if (this.details == null) {
+ this.details = new ArrayList<>();
+ }
+ this.details.add(detail);
+ return this;
+ }
+
+ public BusinessAttachmentBatchAddDTO addDetail(String bizCode, Long attachmentId) {
+ return this.addDetail(new BusinessAttachmentBatchAddDetailDTO(bizCode, attachmentId));
+ }
+
+ public BusinessAttachmentBatchAddDTO addDetail(String bizCode, List attachmentIds) {
+ return this.addDetail(new BusinessAttachmentBatchAddDetailDTO(bizCode, attachmentIds));
+ }
+}
diff --git a/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/dto/BusinessAttachmentBatchAddDetailDTO.java b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/dto/BusinessAttachmentBatchAddDetailDTO.java
new file mode 100644
index 0000000..ac5d3a7
--- /dev/null
+++ b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/dto/BusinessAttachmentBatchAddDetailDTO.java
@@ -0,0 +1,48 @@
+package org.springblade.file.pojo.dto;
+
+import com.google.common.collect.Lists;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 业务附件信息表 数据传输对象实体类
+ *
+ * @author BladeX
+ * @since 2024-09-04
+ */
+@NoArgsConstructor
+@AllArgsConstructor
+@Data
+@Schema(description = "业务附件批量新增明细参数")
+@Builder
+public class BusinessAttachmentBatchAddDetailDTO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 业务code
+ */
+ @NotBlank(message = "业务编码不能为空")
+ @Schema(description = "业务code")
+ private String bizCode;
+ /**
+ * 附件id列表
+ */
+ @NotEmpty(message = "附件id列表不能为空")
+ @Schema(description = "附件id列表")
+ private List<@NotNull(message = "附件id不能为空") Long> attachmentIds;
+
+ public BusinessAttachmentBatchAddDetailDTO(String bizCode, Long attachmentId) {
+ this.bizCode = bizCode;
+ this.attachmentIds = Lists.newArrayList(attachmentId);
+ }
+}
diff --git a/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/dto/BusinessAttachmentDeleteDTO.java b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/dto/BusinessAttachmentDeleteDTO.java
new file mode 100644
index 0000000..d4ea148
--- /dev/null
+++ b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/dto/BusinessAttachmentDeleteDTO.java
@@ -0,0 +1,54 @@
+package org.springblade.file.pojo.dto;
+
+import com.google.common.collect.Lists;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.springblade.core.tool.utils.StringUtil;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 业务附件删除参数
+ * @author bfhuange
+ * @since 2024/12/3
+ */
+@NoArgsConstructor
+@AllArgsConstructor
+@Data
+@Schema(description = "业务附件删除参数")
+public class BusinessAttachmentDeleteDTO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 业务id列表
+ */
+ @Schema(description = "业务id列表")
+ private List bizIds;
+ /**
+ * 业务code列表
+ */
+ @Schema(description = "业务code列表")
+ private List bizCodes;
+ /**
+ * 是否删除
+ */
+ @Schema(description = "是否删除")
+ private boolean delete = true;
+
+ public BusinessAttachmentDeleteDTO(Long bizId) {
+ this.bizIds = Lists.newArrayList(bizId);
+ }
+
+ public BusinessAttachmentDeleteDTO(Long bizId, String bizCode) {
+ this.bizIds = Lists.newArrayList(bizId);
+ if (StringUtil.isNotBlank(bizCode)) {
+ this.bizCodes = Lists.newArrayList(bizCode);
+ }
+ }
+
+}
diff --git a/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/dto/BusinessAttachmentQueryDTO.java b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/dto/BusinessAttachmentQueryDTO.java
new file mode 100644
index 0000000..6bd17d0
--- /dev/null
+++ b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/dto/BusinessAttachmentQueryDTO.java
@@ -0,0 +1,62 @@
+package org.springblade.file.pojo.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 业务附件信息表 数据传输对象实体类
+ *
+ * @author BladeX
+ * @since 2024-09-04
+ */
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+@Schema(description = "业务附件查询参数")
+public class BusinessAttachmentQueryDTO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 业务id列表
+ */
+ @NotNull(message = "业务id列表不能为空")
+ @Schema(description = "业务id列表")
+ private List<@NotNull(message = "业务id不能为空") Long> bizIds;
+ /**
+ * 业务code
+ */
+ @Schema(description = "业务code")
+ private String bizCode;
+ /**
+ * 业务code列表
+ */
+ @Schema(description = "业务code列表")
+ private List bizCodes;
+
+ public BusinessAttachmentQueryDTO(Long bizId) {
+ this.bizIds = List.of(bizId);
+ }
+
+ public BusinessAttachmentQueryDTO(List bizIds) {
+ this.bizIds = bizIds;
+ }
+
+ /**
+ * 兼容历史参数
+ * @param bizId
+ */
+ public void setBizId(Long bizId) {
+ if (this.bizIds == null) {
+ this.bizIds = new ArrayList<>();
+ }
+ this.bizIds.add(bizId);
+ }
+}
diff --git a/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/dto/BusinessUnzipTaskAddDTO.java b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/dto/BusinessUnzipTaskAddDTO.java
new file mode 100644
index 0000000..913eafa
--- /dev/null
+++ b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/dto/BusinessUnzipTaskAddDTO.java
@@ -0,0 +1,55 @@
+package org.springblade.file.pojo.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+import org.springblade.file.pojo.enums.UnzipTask;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 业务解压任务新增参数
+ * @author bfhuange
+ * @date 2024/10/28
+ */
+@Data
+@Schema(description = "业务解压任务新增参数")
+public class BusinessUnzipTaskAddDTO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 业务id
+ */
+ @NotNull(message = "业务id不能为空")
+ @Schema(description = "业务id")
+ private Long bizId;
+ /**
+ * 部门名称ID
+ */
+ @Schema(description = "部门名称ID")
+ private Long departmentId;
+ /**
+ * 业务code
+ */
+ @NotBlank(message = "业务code不能为空")
+ @Schema(description = "业务code")
+ private String bizCode = UnzipTask.BizCode.WAYBILL.getCode();
+ /**
+ * 文档编号
+ */
+ @Schema(description = "文档编号")
+ private String docCode;
+ /**
+ * 关联类型,file_task 文件任务,attachment 附件
+ */
+ @Schema(description = "关联类型,file_task 文件任务,attachment 附件")
+ private String relateType = UnzipTask.RelateType.FILE_TASK.getCode();
+ /**
+ * 关联id,文件任务id或附件id
+ */
+ @NotNull(message = "关联id不能为空")
+ @Schema(description = "关联id,文件任务id或附件id")
+ private Long relateId;
+}
diff --git a/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/dto/BusinessUnzipTaskQueryDTO.java b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/dto/BusinessUnzipTaskQueryDTO.java
new file mode 100644
index 0000000..9b66c8c
--- /dev/null
+++ b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/dto/BusinessUnzipTaskQueryDTO.java
@@ -0,0 +1,55 @@
+package org.springblade.file.pojo.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 业务解压任务查询参数
+ * @author bfhuange
+ * @date 2024/10/28
+ */
+@Data
+@Schema(description = "业务解压任务查询参数")
+public class BusinessUnzipTaskQueryDTO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 文档编号(凭证批次号)
+ */
+ @Schema(description = "文档编号(凭证批次号)")
+ private String docCode;
+ /**
+ * 附件名称
+ */
+ @Schema(description = "附件名称")
+ private String attachmentName;
+ /**
+ * 状态 uploading:正在上传 completed:上传完成 failed:上传失败 paused:已暂停
+ */
+ @Schema(description = "状态 uploading:正在上传 completed:上传完成 failed:上传失败 paused:已暂停")
+ private String status;
+ /**
+ * 关联类型,file_task 文件任务,attachment 附件
+ */
+ @Schema(description = "关联类型,file_task 文件任务,attachment 附件", hidden = true)
+ private String relateType;
+ /**
+ * 是否只查询当前用户
+ */
+ @Schema(description = "是否只查询当前用户")
+ private boolean currentUser;
+ /**
+ * 当前登录人id
+ */
+ @Schema(hidden = true)
+ private Long currentUserId;
+ /**
+ * 业务id列表
+ */
+ @Schema(description = "业务id列表", hidden = true)
+ private List bizIds;
+}
diff --git a/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/dto/BusinessUnzipTaskUpdateDTO.java b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/dto/BusinessUnzipTaskUpdateDTO.java
new file mode 100644
index 0000000..e838efc
--- /dev/null
+++ b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/dto/BusinessUnzipTaskUpdateDTO.java
@@ -0,0 +1,42 @@
+package org.springblade.file.pojo.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+import org.springblade.file.pojo.enums.UnzipTask;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 业务解压任务更新参数
+ * @author bfhuange
+ * @date 2024/10/28
+ */
+@Data
+@Schema(description = "业务解压任务更新参数")
+public class BusinessUnzipTaskUpdateDTO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 主键
+ */
+ @Schema(description = "主键")
+ private Long id;
+ /**
+ * 业务id
+ */
+ @Schema(description = "业务id")
+ private Long bizId;
+ /**
+ * 关联id,文件任务id或附件id
+ */
+ @NotNull(message = "关联id不能为空")
+ @Schema(description = "关联id,文件任务id或附件id")
+ private Long relateId;
+ /**
+ * 关联类型,file_task 文件任务,attachment 附件
+ */
+ @Schema(description = "关联类型,file_task 文件任务,attachment 附件", hidden = true)
+ private String relateType = UnzipTask.RelateType.FILE_TASK.getCode();
+}
diff --git a/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/dto/CertificateAttachmentDTO.java b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/dto/CertificateAttachmentDTO.java
new file mode 100644
index 0000000..0e0738a
--- /dev/null
+++ b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/dto/CertificateAttachmentDTO.java
@@ -0,0 +1,44 @@
+package org.springblade.file.pojo.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 凭证识别参数,依次调用单张识别
+ * @author bfhuange
+ * @since 2024/12/13
+ */
+@Data
+@Schema(description = "凭证附件参数")
+public class CertificateAttachmentDTO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 磅单ObjectKey
+ */
+ @Schema(description = "磅单ObjectKey")
+ private String poundObjectKey;
+ /**
+ * 车头ObjectKey
+ */
+ @Schema(description = "车头ObjectKey")
+ private String carFrontObjectKey;
+ /**
+ * 驾驶证ObjectKey
+ */
+ @Schema(description = "驾驶证ObjectKey")
+ private String driverLicenseObjectKey;
+ /**
+ * 行驶证ObjectKey
+ */
+ @Schema(description = "行驶证ObjectKey")
+ private String vehicleLicenseObjectKey;
+ /**
+ * 项目简称
+ */
+ @Schema(description = "项目简称")
+ private String projectAbbreviation;
+}
diff --git a/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/dto/FileCertificateBatchRecognitionDTO.java b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/dto/FileCertificateBatchRecognitionDTO.java
new file mode 100644
index 0000000..92c6a0c
--- /dev/null
+++ b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/dto/FileCertificateBatchRecognitionDTO.java
@@ -0,0 +1,45 @@
+package org.springblade.file.pojo.dto;
+
+import jakarta.validation.constraints.NotEmpty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 凭证批量识别参数,所有图像链接统一识别,不分类
+ * @author bfhuange
+ * @since 2025/3/13
+ */
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+public class FileCertificateBatchRecognitionDTO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 所有图像 obs key列表
+ */
+ @NotEmpty(message = "图像 obs key列表不能为空")
+ private List objectKeys;
+ /**
+ * 项目简称
+ */
+ private String projectAbbreviation;
+ /**
+ * 凭证编码
+ */
+ private String code;
+ /**
+ * 目录名称
+ */
+ private String dirName;
+
+ public FileCertificateBatchRecognitionDTO(List objectKeys) {
+ this.objectKeys = objectKeys;
+ }
+
+}
diff --git a/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/dto/FileTaskCreateDTO.java b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/dto/FileTaskCreateDTO.java
new file mode 100644
index 0000000..4b3acf0
--- /dev/null
+++ b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/dto/FileTaskCreateDTO.java
@@ -0,0 +1,62 @@
+package org.springblade.file.pojo.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 文件任务创建对象
+ * @author bfhuange
+ * @date 2024/10/22
+ */
+@Data
+@Schema(description = "文件任务创建对象")
+public class FileTaskCreateDTO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Md5
+ */
+ @NotBlank(message = "Md5不能为空")
+ @Schema(description = "Md5")
+ private String md5;
+
+ /**
+ * 附件名
+ */
+ @NotBlank(message = "附件名不能为空")
+ @Schema(description = "附件名")
+ private String attachmentName;
+
+ /**
+ * 附件大小(单位:B)
+ */
+ @NotNull(message = "附件大小不能为空")
+ @Schema(description = "附件大小(单位:B)")
+ private Long size;
+
+ /**
+ * 分段大小
+ */
+ @NotNull(message = "分段大小不能为空")
+ @Schema(description = "分段大小")
+ private Integer chunkSize;
+
+ /**
+ * 分段数量
+ */
+ @NotNull(message = "分段数量不能为空")
+ @Schema(description = "分段数量")
+ private Integer chunkTotal;
+
+ /**
+ * 是否强制创建新任务,默认 false,如果存在相同md5的任务,会创建失败,true则新创建一个任务
+ */
+ @Schema(description = "是否强制创建新任务,默认 false,如果存在相同md5的任务,会创建失败,true则新创建一个任务")
+ private boolean force;
+}
diff --git a/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/dto/FileTaskUpdateDTO.java b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/dto/FileTaskUpdateDTO.java
new file mode 100644
index 0000000..9a6d6bf
--- /dev/null
+++ b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/dto/FileTaskUpdateDTO.java
@@ -0,0 +1,41 @@
+package org.springblade.file.pojo.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+import org.springblade.file.pojo.vo.PartEtagVO;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 文件任务更新对象
+ * @author bfhuange
+ * @date 2024/10/22
+ */
+@Data
+@Schema(description = "文件任务更新对象")
+public class FileTaskUpdateDTO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 文件任务id
+ */
+ @NotNull(message = "文件任务id不能为空")
+ @Schema(description = "主键id")
+ private Long id;
+ /**
+ * 段的ETag值。分段的Base64编码的128位MD5摘要,长度为32的字符串
+ */
+ @NotBlank(message = "etag不能为空")
+ @Schema(description = "etag")
+ private String etag;
+ /**
+ * 段号。分段号可以是不连续的。取值范围是[1,10000]的非负整数
+ */
+ @NotNull(message = "段号不能为空")
+ @Schema(description = "段号")
+ private Integer partNumber;
+}
diff --git a/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/dto/FileUploadDTO.java b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/dto/FileUploadDTO.java
new file mode 100644
index 0000000..a53e06c
--- /dev/null
+++ b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/dto/FileUploadDTO.java
@@ -0,0 +1,15 @@
+package org.springblade.file.pojo.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import org.springframework.web.multipart.MultipartFile;
+
+@Data
+@Schema(description = "分段上传文件对象")
+public class FileUploadDTO {
+ private MultipartFile multipartFile; // 文件对象
+ private String md5; // 文件md5值
+ private Long chunkTotal; // 分段总数
+ private Long chunkSize; // 分段大小
+ private Integer index; // 当前分段的位置
+}
\ No newline at end of file
diff --git a/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/dto/IdsDTO.java b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/dto/IdsDTO.java
new file mode 100644
index 0000000..7119e61
--- /dev/null
+++ b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/dto/IdsDTO.java
@@ -0,0 +1,33 @@
+package org.springblade.file.pojo.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * id批量参数
+ * @author bfhuange
+ * @date 2024/10/29
+ */
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+@Schema(description = "id批量参数")
+public class IdsDTO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * id列表
+ */
+ @NotEmpty(message = "id列表不能为空")
+ @Schema(description = "id列表")
+ private @Valid List<@NotNull(message = "id不能为空") Long> ids;
+}
diff --git a/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/entity/Attachment.java b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/entity/Attachment.java
new file mode 100644
index 0000000..a721e52
--- /dev/null
+++ b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/entity/Attachment.java
@@ -0,0 +1,111 @@
+package org.springblade.file.pojo.entity;
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import org.springblade.core.tool.utils.DateUtil;
+import org.springframework.format.annotation.DateTimeFormat;
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * @author zhaowei
+ * @date 2024/8/16
+ */
+
+@Data
+@TableName("blade_attachment")
+@Schema(description = "附件对象")
+public class Attachment implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ @JsonSerialize(using = ToStringSerializer.class)
+ @Schema(description = "主键id")
+ @TableId(value = "id", type = IdType.ASSIGN_ID)
+ private Long id;
+
+ /**
+ * 附件名
+ */
+ @Schema(description = "附件名")
+ private String attachmentName;
+
+ /**
+ * 压缩文件所在压缩包相对路径
+ */
+ @Schema(description = "压缩文件所在压缩包相对路径")
+ private String dir;
+
+ /**
+ * 附件大小(单位:B)
+ */
+ @Schema(description = "附件大小(单位:B)")
+ private long size;
+
+ /**
+ * objectKey
+ */
+ @Schema(description = "华为OBS objectKey")
+ private String objectKey;
+
+ /**
+ * 创建人-用户名称
+ */
+ @JsonSerialize(using = ToStringSerializer.class)
+ @Schema(description = "创建人-用户名称")
+ @TableField(fill = FieldFill.INSERT)
+ private String createUserName;
+
+ /**
+ * 创建人
+ */
+ @JsonSerialize(using = ToStringSerializer.class)
+ @Schema(description = "创建人")
+ @TableField(fill = FieldFill.INSERT)
+ private Long createUser;
+
+ /**
+ * 创建时间
+ */
+ @DateTimeFormat(pattern = DateUtil.PATTERN_DATETIME)
+ @JsonFormat(pattern = DateUtil.PATTERN_DATETIME)
+ @Schema(description = "创建时间")
+ @TableField(fill = FieldFill.INSERT)
+ private Date createTime;
+
+ /**
+ * 更新人-用户名称
+ */
+ @JsonSerialize(using = ToStringSerializer.class)
+ @Schema(description = "创建人-用户名称")
+ @TableField(fill = FieldFill.INSERT_UPDATE)
+ private String updateUserName;
+
+ /**
+ * 更新人
+ */
+ @JsonSerialize(using = ToStringSerializer.class)
+ @Schema(description = "更新人")
+ @TableField(fill = FieldFill.INSERT_UPDATE)
+ private Long updateUser;
+
+ /**
+ * 更新时间
+ */
+ @DateTimeFormat(pattern = DateUtil.PATTERN_DATETIME)
+ @JsonFormat(pattern = DateUtil.PATTERN_DATETIME)
+ @Schema(description = "更新时间")
+ @TableField(fill = FieldFill.INSERT_UPDATE)
+ private Date updateTime;
+
+ /**
+ * 状态[0:未删除,1:删除]
+ */
+ @TableLogic
+ @Schema(description = "是否已删除 [0:未删除,1:删除]")
+ private Integer isDeleted;
+
+}
diff --git a/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/entity/BusinessAttachment.java b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/entity/BusinessAttachment.java
new file mode 100644
index 0000000..a19529f
--- /dev/null
+++ b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/entity/BusinessAttachment.java
@@ -0,0 +1,113 @@
+package org.springblade.file.pojo.entity;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import org.springblade.core.tool.utils.DateUtil;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 业务附件信息表 实体类
+ *
+ * @author BladeX
+ * @since 2024-09-04
+ */
+@Data
+@TableName("blade_business_attachment")
+@Schema(description = "BusinessAttachment对象")
+public class BusinessAttachment implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 业务id
+ */
+ @Schema(description = "业务id")
+ private Long bizId;
+ /**
+ * 业务code
+ */
+ @Schema(description = "业务code")
+ private String bizCode;
+ /**
+ * 附件id
+ */
+ @Schema(description = "附件id")
+ private Long attachmentId;
+ /**
+ * 附件描述
+ */
+ @Schema(description = "附件描述")
+ @TableField(value = "`desc`")
+ private String desc;
+ /**
+ * 排序
+ */
+ @Schema(description = "排序")
+ private Integer sort;
+
+ /**
+ * 租户ID
+ */
+ @Schema(description = "租户ID")
+ @TableField(fill = FieldFill.INSERT)
+ private String tenantId;
+
+ /**
+ * 主键id
+ */
+ @JsonSerialize(using = ToStringSerializer.class)
+ @Schema(description = "主键id")
+ @TableId(value = "id", type = IdType.ASSIGN_ID)
+ private Long id;
+
+ /**
+ * 创建人
+ */
+ @JsonSerialize(using = ToStringSerializer.class)
+ @Schema(description = "创建人", hidden = true)
+ @TableField(fill = FieldFill.INSERT)
+ private Long createUser;
+
+ /**
+ * 创建时间
+ */
+ @DateTimeFormat(pattern = DateUtil.PATTERN_DATETIME)
+ @JsonFormat(pattern = DateUtil.PATTERN_DATETIME)
+ @Schema(description = "创建时间", hidden = true)
+ @TableField(fill = FieldFill.INSERT)
+ private Date createTime;
+
+ /**
+ * 更新人
+ */
+ @JsonSerialize(using = ToStringSerializer.class)
+ @Schema(description = "更新人", hidden = true)
+ @TableField(fill = FieldFill.INSERT_UPDATE)
+ private Long updateUser;
+
+ /**
+ * 更新时间
+ */
+ @DateTimeFormat(pattern = DateUtil.PATTERN_DATETIME)
+ @JsonFormat(pattern = DateUtil.PATTERN_DATETIME)
+ @Schema(description = "更新时间", hidden = true)
+ @TableField(fill = FieldFill.INSERT_UPDATE)
+ private Date updateTime;
+
+ /**
+ * 状态[0:未删除,1:删除]
+ */
+ @TableLogic
+ @Schema(description = "是否已删除", hidden = true)
+ @TableField(fill = FieldFill.INSERT)
+ private Integer isDeleted;
+}
diff --git a/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/entity/BusinessUnzipDetail.java b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/entity/BusinessUnzipDetail.java
new file mode 100644
index 0000000..6d21a9f
--- /dev/null
+++ b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/entity/BusinessUnzipDetail.java
@@ -0,0 +1,85 @@
+package org.springblade.file.pojo.entity;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import org.springblade.core.tool.utils.DateUtil;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ *
+ * 业务解压明细表
+ *
+ *
+ * @author bfhuange
+ * @since 2024-10-30
+ */
+@Data
+@TableName("blade_business_unzip_detail")
+@Schema(description = "BusinessUnzipDetail对象")
+public class BusinessUnzipDetail implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 主键
+ */
+ @JsonSerialize(using = ToStringSerializer.class)
+ @Schema(description = "主键")
+ @TableId(value = "id", type = IdType.ASSIGN_ID)
+ private Long id;
+ /**
+ * 业务解压任务id
+ */
+ @Schema(description = "业务解压任务id")
+ private Long businessUnzipTaskId;
+ /**
+ * 附件id
+ */
+ @Schema(description = "附件id")
+ private Long attachmentId;
+ /**
+ * 创建人
+ */
+ @JsonSerialize(using = ToStringSerializer.class)
+ @Schema(description = "创建人")
+ @TableField(fill = FieldFill.INSERT)
+ private Long createUser;
+ /**
+ * 创建时间
+ */
+ @DateTimeFormat(pattern = DateUtil.PATTERN_DATETIME)
+ @JsonFormat(pattern = DateUtil.PATTERN_DATETIME)
+ @Schema(description = "创建时间")
+ @TableField(fill = FieldFill.INSERT)
+ private Date createTime;
+ /**
+ * 更新人
+ */
+ @JsonSerialize(using = ToStringSerializer.class)
+ @Schema(description = "更新人")
+ @TableField(fill = FieldFill.INSERT_UPDATE)
+ private Long updateUser;
+ /**
+ * 更新时间
+ */
+ @DateTimeFormat(pattern = DateUtil.PATTERN_DATETIME)
+ @JsonFormat(pattern = DateUtil.PATTERN_DATETIME)
+ @Schema(description = "更新时间")
+ @TableField(fill = FieldFill.INSERT_UPDATE)
+ private Date updateTime;
+ /**
+ * 状态[0:未删除,1:删除]
+ */
+ @TableLogic
+ @Schema(description = "是否已删除 [0:未删除,1:删除]")
+ @TableField(fill = FieldFill.INSERT)
+ private Integer isDeleted;
+}
diff --git a/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/entity/BusinessUnzipTask.java b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/entity/BusinessUnzipTask.java
new file mode 100644
index 0000000..8f9d027
--- /dev/null
+++ b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/entity/BusinessUnzipTask.java
@@ -0,0 +1,89 @@
+package org.springblade.file.pojo.entity;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import org.springblade.core.tool.utils.DateUtil;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ *
+ * 业务解压任务表
+ *
+ *
+ * @author bfhuange
+ * @since 2024-10-28
+ */
+@Data
+@TableName("blade_business_unzip_task")
+@Schema(description = "业务解压任务表")
+public class BusinessUnzipTask implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ @TableId(value = "id", type = IdType.ASSIGN_ID)
+ @Schema(description = "主键")
+ private Long id;
+
+ @Schema(description = "业务id")
+ private Long bizId;
+ /**
+ * 部门名称ID
+ */
+ @Schema(description = "部门名称ID")
+ private Long departmentId;
+
+ @Schema(description = "业务code")
+ private String bizCode;
+
+ @Schema(description = "文档编号")
+ private String docCode;
+
+ @Schema(description = "关联类型,file_task 文件任务,attachment 附件")
+ private String relateType;
+
+ @Schema(description = "关联id,文件任务id或附件id")
+ private Long relateId;
+
+ @Schema(description = "状态 uploading:正在上传 unzipping:解压中 finished:已完成")
+ private String status;
+
+ @Schema(description = "租户ID")
+ @TableField(fill = FieldFill.INSERT)
+ private String tenantId;
+
+ @JsonSerialize(using = ToStringSerializer.class)
+ @Schema(description = "创建人", hidden = true)
+ @TableField(fill = FieldFill.INSERT)
+ private Long createUser;
+
+ @DateTimeFormat(pattern = DateUtil.PATTERN_DATETIME)
+ @JsonFormat(pattern = DateUtil.PATTERN_DATETIME)
+ @Schema(description = "创建时间", hidden = true)
+ @TableField(fill = FieldFill.INSERT)
+ private Date createTime;
+
+ @JsonSerialize(using = ToStringSerializer.class)
+ @Schema(description = "更新人", hidden = true)
+ @TableField(fill = FieldFill.INSERT_UPDATE)
+ private Long updateUser;
+
+ @DateTimeFormat(pattern = DateUtil.PATTERN_DATETIME)
+ @JsonFormat(pattern = DateUtil.PATTERN_DATETIME)
+ @Schema(description = "更新时间", hidden = true)
+ @TableField(fill = FieldFill.INSERT_UPDATE)
+ private Date updateTime;
+
+ @Schema(description = "是否已删除", hidden = true)
+ @TableField(fill = FieldFill.INSERT)
+ @TableLogic
+ private Integer isDeleted;
+}
diff --git a/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/entity/FileTask.java b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/entity/FileTask.java
new file mode 100644
index 0000000..36d0ed2
--- /dev/null
+++ b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/entity/FileTask.java
@@ -0,0 +1,152 @@
+package org.springblade.file.pojo.entity;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import org.springblade.core.tool.utils.DateUtil;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * @author zhaowei
+ * @date 2024/8/23
+ */
+
+@Data
+@TableName("blade_file_task")
+@Schema(description = "文件上传任务对象")
+public class FileTask implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ @JsonSerialize(using = ToStringSerializer.class)
+ @Schema(description = "主键id")
+ @TableId(value = "id", type = IdType.ASSIGN_ID)
+ private Long id;
+
+ /**
+ * Md5
+ */
+ @Schema(description = "Md5")
+ private String md5;
+
+ /**
+ * OBS objectKey
+ */
+ @Schema(description = "OBS objectKey")
+ private String objectKey;
+
+ /**
+ * 附件名
+ */
+ @Schema(description = "附件名")
+ private String attachmentName;
+
+ /**
+ * 文件类型
+ */
+ @Schema(description = "文件类型")
+ private String suffix;
+
+ /**
+ * 附件大小(单位:B)
+ */
+ @Schema(description = "附件大小(单位:B)")
+ private Long size;
+
+ /**
+ * 分段大小
+ */
+ @Schema(description = "分段大小")
+ private Integer chunkSize;
+
+ /**
+ * 分段数量
+ */
+ @Schema(description = "分段数量")
+ private Integer chunkTotal;
+
+ /**
+ * 当前分段的位置(已上传完成的分段),从0开始,上传完第1段,为1
+ */
+ @Schema(description = "当前分段的位置(已上传完成的分段),从0开始,上传完第1段,为1")
+ private Integer currentIndex;
+
+ /**
+ * 华为云初始化上传任务生成的uploadId
+ */
+ @Schema(description = "华为云初始化上传任务生成的uploadId")
+ private String uploadId;
+
+ /**
+ * 状态 uploading:正在上传 completed:上传完成 failed:上传失败 paused:已暂停
+ */
+ @Schema(description = "状态 uploading:正在上传 completed:上传完成 failed:上传失败 paused:已暂停")
+ private String status;
+
+ /**
+ * 创建人-用户名称
+ */
+ @JsonSerialize(using = ToStringSerializer.class)
+ @Schema(description = "创建人-用户名称")
+ @TableField(fill = FieldFill.INSERT)
+ private String createUserName;
+
+ /**
+ * 创建人
+ */
+ @JsonSerialize(using = ToStringSerializer.class)
+ @Schema(description = "创建人")
+ @TableField(fill = FieldFill.INSERT)
+ private Long createUser;
+
+ /**
+ * 创建时间
+ */
+ @DateTimeFormat(pattern = DateUtil.PATTERN_DATETIME)
+ @JsonFormat(pattern = DateUtil.PATTERN_DATETIME)
+ @Schema(description = "创建时间")
+ @TableField(fill = FieldFill.INSERT)
+ private Date createTime;
+
+ /**
+ * 更新人-用户名称
+ */
+ @JsonSerialize(using = ToStringSerializer.class)
+ @Schema(description = "创建人-用户名称")
+ @TableField(fill = FieldFill.INSERT_UPDATE)
+ private String updateUserName;
+
+ /**
+ * 更新人
+ */
+ @JsonSerialize(using = ToStringSerializer.class)
+ @Schema(description = "更新人")
+ @TableField(fill = FieldFill.INSERT_UPDATE)
+ private Long updateUser;
+
+ /**
+ * 更新时间
+ */
+ @DateTimeFormat(pattern = DateUtil.PATTERN_DATETIME)
+ @JsonFormat(pattern = DateUtil.PATTERN_DATETIME)
+ @Schema(description = "更新时间")
+ @TableField(fill = FieldFill.INSERT_UPDATE)
+ private Date updateTime;
+
+ /**
+ * 状态[0:未删除,1:删除]
+ */
+ @TableLogic
+ @Schema(description = "是否已删除 [0:未删除,1:删除]")
+ @TableField(fill = FieldFill.INSERT)
+ private Integer isDeleted;
+
+}
diff --git a/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/entity/FileTaskPart.java b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/entity/FileTaskPart.java
new file mode 100644
index 0000000..a69e1a5
--- /dev/null
+++ b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/entity/FileTaskPart.java
@@ -0,0 +1,69 @@
+package org.springblade.file.pojo.entity;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import org.springblade.core.tool.utils.DateUtil;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * @author zhaowei
+ * @date 2024/8/16
+ */
+
+@Data
+@TableName("blade_file_task_part")
+@Schema(description = "文件任务分段表")
+public class FileTaskPart implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ @JsonSerialize(using = ToStringSerializer.class)
+ @Schema(description = "主键id")
+ @TableId(value = "id", type = IdType.ASSIGN_ID)
+ private Long id;
+
+ /**
+ * 文件任务Id
+ */
+ @Schema(description = "文件任务Id")
+ private Long fileTaskId;
+
+ /**
+ * 段号。分段号可以是不连续的。取值范围是[1,10000]的非负整数
+ */
+ @Schema(description = "段号。分段号可以是不连续的。取值范围是[1,10000]的非负整数")
+ private Integer partNumber;
+
+ /**
+ * 段的ETag值。分段的Base64编码的128位MD5摘要,长度为32的字符串
+ */
+ @Schema(description = "段的ETag值。分段的Base64编码的128位MD5摘要,长度为32的字符串")
+ private String etag;
+
+ /**
+ * 创建人
+ */
+ @JsonSerialize(using = ToStringSerializer.class)
+ @Schema(description = "创建人")
+ @TableField(fill = FieldFill.INSERT)
+ private Long createUser;
+
+ /**
+ * 创建时间
+ */
+ @DateTimeFormat(pattern = DateUtil.PATTERN_DATETIME)
+ @JsonFormat(pattern = DateUtil.PATTERN_DATETIME)
+ @Schema(description = "创建时间")
+ @TableField(fill = FieldFill.INSERT)
+ private Date createTime;
+
+}
diff --git a/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/enums/FileTaskStatus.java b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/enums/FileTaskStatus.java
new file mode 100644
index 0000000..b258db8
--- /dev/null
+++ b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/enums/FileTaskStatus.java
@@ -0,0 +1,39 @@
+package org.springblade.file.pojo.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 文件任务状态
+ * @author bfhuange
+ * @date 2024/10/22
+ */
+@AllArgsConstructor
+@Getter
+public enum FileTaskStatus {
+ /**
+ * 正在上传
+ */
+ UPLOADING("uploading", "正在上传"),
+ /**
+ * 上传完成
+ */
+ COMPLETED("completed", "上传完成"),
+ /**
+ * 上传失败
+ */
+ FAILED("failed", "上传失败"),
+ /**
+ * 已暂停
+ */
+ PAUSED("paused", "已暂停"),
+ ;
+ /**
+ * 编码
+ */
+ private final String code;
+ /**
+ * 名称
+ */
+ private final String name;
+}
diff --git a/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/enums/UnzipTask.java b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/enums/UnzipTask.java
new file mode 100644
index 0000000..7ea5d49
--- /dev/null
+++ b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/enums/UnzipTask.java
@@ -0,0 +1,99 @@
+package org.springblade.file.pojo.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 解压任务枚举类
+ * @author bfhuange
+ * @date 2024/10/27
+ */
+public class UnzipTask {
+
+ /**
+ * 业务编码枚举
+ */
+ @AllArgsConstructor
+ @Getter
+ public enum BizCode {
+ /**
+ * 运单
+ */
+ WAYBILL("waybill", "运单"),
+ /**
+ * 基地
+ */
+ WAREHOUSE("warehouse", "基地"),
+ ;
+ /**
+ * 编码
+ */
+ private final String code;
+ /**
+ * 名称
+ */
+ private final String name;
+ }
+
+ /**
+ * 关联类型枚举
+ */
+ @AllArgsConstructor
+ @Getter
+ public enum RelateType {
+ /**
+ * 文件任务
+ */
+ FILE_TASK("file_task", "文件任务"),
+ /**
+ * 附件
+ */
+ ATTACHMENT("attachment", "附件"),
+ ;
+ /**
+ * 编码
+ */
+ private final String code;
+ /**
+ * 名称
+ */
+ private final String name;
+ }
+
+ /**
+ * 状态枚举
+ */
+ @AllArgsConstructor
+ @Getter
+ public enum Status {
+ /**
+ * 正在上传
+ */
+ UPLOADING("uploading", "正在上传"),
+ /**
+ * 上传完成
+ */
+ UPLOADED("uploaded", "上传完成"),
+ /**
+ * 正在解压
+ */
+ UNZIPPING("unzipping", "正在解压"),
+ /**
+ * 解压失败
+ */
+ UNZIP_FAIL("unzip_fail", "解压失败"),
+ /**
+ * 已完成
+ */
+ FINISHED("finished", "已完成"),
+ ;
+ /**
+ * 编码
+ */
+ private final String code;
+ /**
+ * 名称
+ */
+ private final String name;
+ }
+}
diff --git a/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/vo/AttachmentDetailVO.java b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/vo/AttachmentDetailVO.java
new file mode 100644
index 0000000..7059435
--- /dev/null
+++ b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/vo/AttachmentDetailVO.java
@@ -0,0 +1,73 @@
+package org.springblade.file.pojo.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 附件详情对象
+ * @author bfhuange
+ * @date 2024/10/30
+ */
+@Data
+@Schema(description = "附件详情对象")
+public class AttachmentDetailVO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 主键id
+ */
+ @Schema(description = "主键id")
+ private Long id;
+
+ /**
+ * 附件名
+ */
+ @Schema(description = "附件名")
+ private String attachmentName;
+
+ /**
+ * objectKey
+ */
+ @Schema(description = "华为OBS objectKey")
+ private String objectKey;
+
+ /**
+ * 附件大小(单位:B)
+ */
+ @Schema(description = "附件大小(单位:B)")
+ private long size;
+
+ /**
+ * 创建人
+ */
+ @Schema(description = "创建人")
+ private Long createUser;
+
+ /**
+ * 创建时间
+ */
+ @Schema(description = "创建时间")
+ private Date createTime;
+
+ /**
+ * 更新人
+ */
+ @Schema(description = "更新人")
+ private Long updateUser;
+
+ /**
+ * 更新时间
+ */
+ @Schema(description = "更新时间")
+ private Date updateTime;
+
+ /**
+ * 下载链接
+ */
+ @Schema(description = "下载链接")
+ private String url;
+}
diff --git a/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/vo/BusinessAttachmentVO.java b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/vo/BusinessAttachmentVO.java
new file mode 100644
index 0000000..2a76bae
--- /dev/null
+++ b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/vo/BusinessAttachmentVO.java
@@ -0,0 +1,105 @@
+package org.springblade.file.pojo.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.Size;
+import lombok.Data;
+import org.springblade.core.tool.utils.DateUtil;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 业务附件信息表 视图实体类
+ *
+ * @author BladeX
+ * @since 2024-09-04
+ */
+@Data
+@Schema(description = "业务附件响应参数")
+public class BusinessAttachmentVO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 主键id
+ */
+ @JsonSerialize(using = ToStringSerializer.class)
+ @Schema(description = "主键id")
+ private Long id;
+ /**
+ * 业务id
+ */
+ @Schema(description = "业务id")
+ @JsonSerialize(using = ToStringSerializer.class)
+ private Long bizId;
+ /**
+ * 业务code
+ */
+ @Schema(description = "业务code")
+ private String bizCode;
+ /**
+ * 业务code名称
+ */
+ @Schema(description = "业务code名称")
+ private String bizCodeStr;
+ /**
+ * 附件id
+ */
+ @Schema(description = "附件id")
+ @JsonSerialize(using = ToStringSerializer.class)
+ private Long attachmentId;
+ /**
+ * 附件名
+ */
+ @Schema(description = "附件名")
+ private String attachmentName;
+ /**
+ * 附件大小(单位:B)
+ */
+ @Schema(description = "附件大小(单位:B)")
+ private long size;
+ /**
+ * objectKey
+ */
+ @Schema(description = "华为OBS objectKey")
+ private String objectKey;
+ /**
+ * 附件描述
+ */
+ @Schema(description = "附件描述")
+ @Size(max = 100, message = "附件描述长度不能超过100")
+ private String desc;
+ /**
+ * 排序
+ */
+ @Schema(description = "排序")
+ private Integer sort;
+ /**
+ * 上传人id
+ */
+ @Schema(description = "上传人id")
+ @JsonSerialize(using = ToStringSerializer.class)
+ private Long uploadUserId;
+ /**
+ * 上传人姓名
+ */
+ @Schema(description = "上传人姓名")
+ private String uploadUserName;
+ /**
+ * 上传人姓名
+ */
+ @Schema(description = "上传人姓名")
+ private String createUserName;
+ /**
+ * 创建时间
+ */
+ @DateTimeFormat(pattern = DateUtil.PATTERN_DATETIME)
+ @JsonFormat(pattern = DateUtil.PATTERN_TIME_MINI)
+ @Schema(description = "创建时间")
+ private Date createTime;
+}
diff --git a/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/vo/BusinessUnzipDetailListVO.java b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/vo/BusinessUnzipDetailListVO.java
new file mode 100644
index 0000000..779c13e
--- /dev/null
+++ b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/vo/BusinessUnzipDetailListVO.java
@@ -0,0 +1,94 @@
+package org.springblade.file.pojo.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.Size;
+import lombok.Data;
+import org.springblade.core.tool.utils.DateUtil;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 业务解压任务明细对象
+ * @author bfhuange
+ * @date 2024/11/08
+ */
+@Data
+@Schema(description = "业务解压任务明细对象")
+public class BusinessUnzipDetailListVO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 主键
+ */
+ @JsonSerialize(using = ToStringSerializer.class)
+ @Schema(description = "主键")
+ private Long id;
+ /**
+ * 附件id
+ */
+ @Schema(description = "附件id")
+ @JsonSerialize(using = ToStringSerializer.class)
+ private Long attachmentId;
+
+ //==========================附件属性=============================
+ /**
+ * 压缩文件所在压缩包相对路径
+ */
+ @Schema(description = "压缩文件所在压缩包相对路径")
+ private String dir;
+ /**
+ * 附件名
+ */
+ @Schema(description = "附件名")
+ private String attachmentName;
+ /**
+ * 附件大小(单位:B)
+ */
+ @Schema(description = "附件大小(单位:B)")
+ private long size;
+ /**
+ * objectKey
+ */
+ @Schema(description = "华为OBS objectKey")
+ private String objectKey;
+ /**
+ * 附件描述
+ */
+ @Schema(description = "附件描述")
+ @Size(max = 100, message = "附件描述长度不能超过100")
+ private String desc;
+ /**
+ * 排序
+ */
+ @Schema(description = "排序")
+ private Integer sort;
+ /**
+ * 上传人id
+ */
+ @Schema(description = "上传人id")
+ @JsonSerialize(using = ToStringSerializer.class)
+ private Long uploadUserId;
+ /**
+ * 上传人姓名
+ */
+ @Schema(description = "上传人姓名")
+ private String uploadUserName;
+ /**
+ * 上传人姓名
+ */
+ @Schema(description = "上传人姓名")
+ private String createUserName;
+ /**
+ * 创建时间
+ */
+ @DateTimeFormat(pattern = DateUtil.PATTERN_DATETIME)
+ @JsonFormat(pattern = DateUtil.PATTERN_TIME_MINI)
+ @Schema(description = "创建时间")
+ private Date createTime;
+}
diff --git a/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/vo/BusinessUnzipTaskListVO.java b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/vo/BusinessUnzipTaskListVO.java
new file mode 100644
index 0000000..4e1de52
--- /dev/null
+++ b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/vo/BusinessUnzipTaskListVO.java
@@ -0,0 +1,169 @@
+package org.springblade.file.pojo.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import org.springblade.core.tool.utils.DateUtil;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 业务解压任务分页对象
+ * @author bfhuange
+ * @date 2024/10/28
+ */
+@Data
+@Schema(description = "业务解压任务分页对象")
+public class BusinessUnzipTaskListVO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 主键
+ */
+ @JsonSerialize(using = ToStringSerializer.class)
+ @Schema(description = "主键")
+ private Long id;
+ /**
+ * 业务id
+ */
+ @JsonSerialize(using = ToStringSerializer.class)
+ @Schema(description = "业务id")
+ private Long bizId;
+ /**
+ * 业务code
+ */
+ @Schema(description = "业务code")
+ private String bizCode;
+ /**
+ * 文档编号
+ */
+ @Schema(description = "文档编号")
+ private String docCode;
+ /**
+ * 关联类型,file_task 文件任务,attachment 附件
+ */
+ @Schema(description = "关联类型,file_task 文件任务,attachment 附件")
+ private String relateType;
+ /**
+ * 关联id,文件任务id或附件id
+ */
+ @JsonSerialize(using = ToStringSerializer.class)
+ @Schema(description = "关联id,文件任务id或附件id")
+ private Long relateId;
+
+ //==========================文件任务属性=============================
+ /**
+ * Md5
+ */
+ @Schema(description = "Md5")
+ private String md5;
+
+ /**
+ * OBS objectKey
+ */
+ @Schema(description = "OBS objectKey")
+ private String objectKey;
+
+ /**
+ * 附件名
+ */
+ @Schema(description = "附件名")
+ private String attachmentName;
+
+ /**
+ * 文件类型
+ */
+ @Schema(description = "文件类型")
+ private String suffix;
+
+ /**
+ * 附件大小(单位:B)
+ */
+ @Schema(description = "附件大小(单位:B)")
+ private Long size;
+
+ /**
+ * 分段大小
+ */
+ @Schema(description = "分段大小")
+ private Integer chunkSize;
+
+ /**
+ * 分段数量
+ */
+ @Schema(description = "分段数量")
+ private Integer chunkTotal;
+
+ /**
+ * 当前分段的位置(已上传完成的分段),从0开始,上传完第1段,为1
+ */
+ @Schema(description = "当前分段的位置(已上传完成的分段),从0开始,上传完第1段,为1")
+ private Integer currentIndex;
+
+ /**
+ * 华为云初始化上传任务生成的uploadId
+ */
+ @Schema(description = "华为云初始化上传任务生成的uploadId")
+ private String uploadId;
+
+ /**
+ * 状态 uploading:正在上传 completed:上传完成 failed:上传失败 paused:已暂停
+ */
+ @Schema(description = "状态 uploading:正在上传 completed:上传完成 failed:上传失败 paused:已暂停")
+ private String status;
+
+ /**
+ * 状态名称
+ */
+ @Schema(description = "状态名称")
+ private String statusStr;
+
+ /**
+ * 创建人-用户名称
+ */
+ @JsonSerialize(using = ToStringSerializer.class)
+ @Schema(description = "创建人-用户名称")
+ private String createUserName;
+
+ /**
+ * 创建人
+ */
+ @JsonSerialize(using = ToStringSerializer.class)
+ @Schema(description = "创建人")
+ private Long createUser;
+
+ /**
+ * 创建时间
+ */
+ @DateTimeFormat(pattern = DateUtil.PATTERN_DATETIME)
+ @JsonFormat(pattern = DateUtil.PATTERN_DATETIME)
+ @Schema(description = "创建时间")
+ private Date createTime;
+
+ /**
+ * 更新人-用户名称
+ */
+ @JsonSerialize(using = ToStringSerializer.class)
+ @Schema(description = "创建人-用户名称")
+ private String updateUserName;
+
+ /**
+ * 更新人
+ */
+ @JsonSerialize(using = ToStringSerializer.class)
+ @Schema(description = "更新人")
+ private Long updateUser;
+
+ /**
+ * 更新时间
+ */
+ @DateTimeFormat(pattern = DateUtil.PATTERN_DATETIME)
+ @JsonFormat(pattern = DateUtil.PATTERN_DATETIME)
+ @Schema(description = "更新时间")
+ private Date updateTime;
+}
diff --git a/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/vo/FileHandleVO.java b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/vo/FileHandleVO.java
new file mode 100644
index 0000000..c2af5f6
--- /dev/null
+++ b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/vo/FileHandleVO.java
@@ -0,0 +1,36 @@
+package org.springblade.file.pojo.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.File;
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 文件处理结果对象
+ * @author bfhuange
+ * @date 2024/10/30
+ */
+@Data
+@Schema(description = "文件处理结果对象")
+public class FileHandleVO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 下载的压缩文件
+ */
+ @Schema(description = "下载的压缩文件")
+ private File downloadFile;
+ /**
+ * 解压目录
+ */
+ @Schema(description = "解压目录")
+ private File unzippedDir;
+ /**
+ * 解压后的文件列表
+ */
+ @Schema(description = "解压后的文件列表")
+ private List unzippedFiles;
+}
diff --git a/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/vo/FileTaskUpdateVO.java b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/vo/FileTaskUpdateVO.java
new file mode 100644
index 0000000..3bb8b6c
--- /dev/null
+++ b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/vo/FileTaskUpdateVO.java
@@ -0,0 +1,25 @@
+package org.springblade.file.pojo.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serial;
+
+/**
+ * 文件任务更新结果
+ * @author bfhuange
+ * @date 2024/10/28
+ */
+@EqualsAndHashCode(callSuper = true)
+@Data
+@Schema(description = "文件任务更新结果")
+public class FileTaskUpdateVO extends FileTaskVO {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 文档编号(凭证批次号),如果有则返回,没有为空
+ */
+ @Schema(description = "文档编号(凭证批次号),如果有则返回,没有为空")
+ private String docCode;
+}
diff --git a/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/vo/FileTaskVO.java b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/vo/FileTaskVO.java
new file mode 100644
index 0000000..0407ff8
--- /dev/null
+++ b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/vo/FileTaskVO.java
@@ -0,0 +1,133 @@
+package org.springblade.file.pojo.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import org.springblade.core.tool.utils.DateUtil;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 文件任务对象
+ * @author bfhuange
+ * @date 2024/10/22
+ */
+@Data
+@Schema(description = "文件任务对象")
+public class FileTaskVO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ @JsonSerialize(using = ToStringSerializer.class)
+ @Schema(description = "主键id")
+ private Long id;
+
+ /**
+ * Md5
+ */
+ @Schema(description = "Md5")
+ private String md5;
+
+ /**
+ * OBS objectKey
+ */
+ @Schema(description = "OBS objectKey")
+ private String objectKey;
+
+ /**
+ * 附件名
+ */
+ @Schema(description = "附件名")
+ private String attachmentName;
+
+ /**
+ * 文件类型
+ */
+ @Schema(description = "文件类型")
+ private String suffix;
+
+ /**
+ * 附件大小(单位:B)
+ */
+ @Schema(description = "附件大小(单位:B)")
+ private Long size;
+
+ /**
+ * 分段大小
+ */
+ @Schema(description = "分段大小")
+ private Integer chunkSize;
+
+ /**
+ * 分段数量
+ */
+ @Schema(description = "分段数量")
+ private Integer chunkTotal;
+
+ /**
+ * 当前分段的位置(已上传完成的分段),从0开始,上传完第1段,为1
+ */
+ @Schema(description = "当前分段的位置(已上传完成的分段),从0开始,上传完第1段,为1")
+ private Integer currentIndex;
+
+ /**
+ * 华为云初始化上传任务生成的uploadId
+ */
+ @Schema(description = "华为云初始化上传任务生成的uploadId")
+ private String uploadId;
+
+ /**
+ * 状态 uploading:正在上传 completed:上传完成 failed:上传失败 paused:已暂停
+ */
+ @Schema(description = "状态 uploading:正在上传 completed:上传完成 failed:上传失败 paused:已暂停")
+ private String status;
+
+ /**
+ * 创建人-用户名称
+ */
+ @JsonSerialize(using = ToStringSerializer.class)
+ @Schema(description = "创建人-用户名称")
+ private String createUserName;
+
+ /**
+ * 创建人
+ */
+ @JsonSerialize(using = ToStringSerializer.class)
+ @Schema(description = "创建人")
+ private Long createUser;
+
+ /**
+ * 创建时间
+ */
+ @DateTimeFormat(pattern = DateUtil.PATTERN_DATETIME)
+ @JsonFormat(pattern = DateUtil.PATTERN_DATETIME)
+ @Schema(description = "创建时间")
+ private Date createTime;
+
+ /**
+ * 更新人-用户名称
+ */
+ @JsonSerialize(using = ToStringSerializer.class)
+ @Schema(description = "创建人-用户名称")
+ private String updateUserName;
+
+ /**
+ * 更新人
+ */
+ @JsonSerialize(using = ToStringSerializer.class)
+ @Schema(description = "更新人")
+ private Long updateUser;
+
+ /**
+ * 更新时间
+ */
+ @DateTimeFormat(pattern = DateUtil.PATTERN_DATETIME)
+ @JsonFormat(pattern = DateUtil.PATTERN_DATETIME)
+ @Schema(description = "更新时间")
+ private Date updateTime;
+}
diff --git a/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/vo/FileUrlVO.java b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/vo/FileUrlVO.java
new file mode 100644
index 0000000..2d19918
--- /dev/null
+++ b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/vo/FileUrlVO.java
@@ -0,0 +1,33 @@
+package org.springblade.file.pojo.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 文件下载地址对象
+ * @author bfhuange
+ * @since 2025/4/22
+ */
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+@Schema(description = "文件下载地址对象")
+public class FileUrlVO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 主键id
+ */
+ @Schema(description = "主键id")
+ private Long id;
+ /**
+ * 文件下载地址
+ */
+ @Schema(description ="文件下载地址")
+ private String url;
+}
diff --git a/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/vo/FileVO.java b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/vo/FileVO.java
new file mode 100644
index 0000000..446d5a3
--- /dev/null
+++ b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/vo/FileVO.java
@@ -0,0 +1,35 @@
+package org.springblade.file.pojo.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 文件对象
+ * @author bfhuange
+ * @date 2024/11/19
+ */
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+@Schema(description = "文件对象")
+public class FileVO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * OBS objectKey
+ */
+ @Schema(description = "OBS objectKey")
+ private String objectKey;
+
+ /**
+ * Md5
+ */
+ @Schema(description = "Md5")
+ private String md5;
+}
diff --git a/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/vo/PartEtagVO.java b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/vo/PartEtagVO.java
new file mode 100644
index 0000000..281ed29
--- /dev/null
+++ b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/vo/PartEtagVO.java
@@ -0,0 +1,29 @@
+package org.springblade.file.pojo.vo;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 待合并的段
+ * @author bfhuange
+ * @date 2024/10/21
+ */
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+public class PartEtagVO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 段的ETag值。分段的Base64编码的128位MD5摘要,长度为32的字符串
+ */
+ private String etag;
+ /**
+ * 段号。分段号可以是不连续的。取值范围是[1,10000]的非负整数
+ */
+ private Integer partNumber;
+}
diff --git a/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/vo/TempFileUrlVO.java b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/vo/TempFileUrlVO.java
new file mode 100644
index 0000000..6d92f0e
--- /dev/null
+++ b/blade-service-api/blade-file-api/src/main/java/org/springblade/file/pojo/vo/TempFileUrlVO.java
@@ -0,0 +1,33 @@
+package org.springblade.file.pojo.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 文件下载地址对象
+ * @author bfhuange
+ * @since 2025/4/22
+ */
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+@Schema(description = "临时文件下载地址对象")
+public class TempFileUrlVO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * obs key
+ */
+ @Schema(description = "obs key")
+ private String objectKey;
+ /**
+ * 文件下载地址
+ */
+ @Schema(description ="文件下载地址")
+ private String url;
+}
diff --git a/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/entity/AppVersion.java b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/entity/AppVersion.java
new file mode 100644
index 0000000..3387a92
--- /dev/null
+++ b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/entity/AppVersion.java
@@ -0,0 +1,81 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.system.pojo.entity;
+
+import com.baomidou.mybatisplus.annotation.SqlCondition;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.springblade.core.tenant.mp.TenantEntity;
+
+import java.io.Serial;
+
+
+@Data
+@Schema(description = "APP版本管理")
+@TableName("t_app_version")
+@EqualsAndHashCode(callSuper = true)
+public class AppVersion extends TenantEntity {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ @NotEmpty
+ @Schema(description = "更新类型")
+ protected String updateType;
+
+ @TableField(condition = SqlCondition.LIKE)
+ @NotEmpty
+ @Schema(description = "版本号")
+ protected String version;
+
+ @TableField(condition = SqlCondition.LIKE)
+ @NotEmpty
+ @Schema(description = "子版本号")
+ protected String subVersion;
+
+ @NotEmpty
+ @Schema(description = "强制更新(是/否)")
+ protected String isForceUpdate;
+
+ @NotEmpty
+ @Schema(description = "安装方式")
+ protected String installMethod;
+
+ @NotNull
+ @Schema(description = "安装包(附件Id)")
+ protected Long installAttachmentId;
+
+ @TableField(condition = SqlCondition.LIKE)
+ @NotEmpty
+ @Schema(description = "更新说明")
+ protected String updateExplanation;
+
+}
diff --git a/blade-service-api/pom.xml b/blade-service-api/pom.xml
index 4dbf357..42afb44 100644
--- a/blade-service-api/pom.xml
+++ b/blade-service-api/pom.xml
@@ -23,6 +23,7 @@
blade-system-api
blade-user-api
blade-record-api
+ blade-file-api
diff --git a/blade-service/blade-file/Dockerfile b/blade-service/blade-file/Dockerfile
new file mode 100644
index 0000000..939092c
--- /dev/null
+++ b/blade-service/blade-file/Dockerfile
@@ -0,0 +1,19 @@
+FROM bladex/alpine-java:openjdk17_cn_slim
+ARG ACTIVE
+
+LABEL maintainer="bladejava@qq.com"
+
+# 设置时区为上海
+ENV TZ=Asia/Shanghai
+# 设置时区信息
+RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
+
+COPY ./target/blade-file.jar /opt/app.jar
+
+WORKDIR /opt
+
+EXPOSE 8107
+
+ENTRYPOINT ["java", "--add-opens", "java.base/java.lang=ALL-UNNAMED", "--add-opens", "java.base/java.lang.reflect=ALL-UNNAMED", "-Djava.security.egd=file:/dev/./urandom", "-XX:+HeapDumpOnOutOfMemoryError", "-XX:HeapDumpPath=/opt/logs", "-XX:ErrorFile=/opt/logs/hs_err_pid%p.log", "-Xlog:gc*:file=/opt/logs/gc.log:time,uptime,level,tags:filecount=5,filesize=50M", "-XX:MaxDirectMemorySize=512m", "-Xms1024m", "-Xmx4096m", "-jar", "app.jar"]
+
+CMD ["--spring.profiles.active=${ACTIVE}","--logging.path=/opt/logs"]
diff --git a/blade-service/blade-file/Dockerfile-rt b/blade-service/blade-file/Dockerfile-rt
new file mode 100644
index 0000000..e005354
--- /dev/null
+++ b/blade-service/blade-file/Dockerfile-rt
@@ -0,0 +1,19 @@
+#FROM 10.38.16.129/bladex/alpine-java:openjdk17_cn_slim
+#FROM registry.cn-hangzhou.aliyuncs.com/vic_person/alpine-java:openjdk17_cn_slim
+FROM 10.38.16.129:80/bladex/alpine-java:openjdk17_cn_slim
+ARG ACTIVE
+
+LABEL maintainer="bladejava@qq.com"
+
+RUN apk add --no-cache tzdata
+
+# 设置时区为上海
+ENV TZ=Asia/Shanghai
+# 设置时区信息
+RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
+
+COPY ./target/*.jar /opt/app.jar
+
+ENTRYPOINT ["java", "--add-opens", "java.base/java.lang=ALL-UNNAMED", "--add-opens", "java.base/java.lang.reflect=ALL-UNNAMED", "-Djava.security.egd=file:/dev/./urandom", "-jar", "/opt/app.jar"]
+
+CMD ["--spring.profiles.active=${ACTIVE}"]
diff --git a/blade-service/blade-file/Dockerfile-zh-dev b/blade-service/blade-file/Dockerfile-zh-dev
new file mode 100644
index 0000000..66022e9
--- /dev/null
+++ b/blade-service/blade-file/Dockerfile-zh-dev
@@ -0,0 +1,19 @@
+FROM bladex/alpine-java:openjdk17_cn_slim
+ARG ACTIVE
+
+LABEL maintainer="bladejava@qq.com"
+
+# 设置时区为上海
+ENV TZ=Asia/Shanghai
+# 设置时区信息
+RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
+
+COPY ./target/blade-file.jar /opt/app.jar
+
+WORKDIR /opt
+
+EXPOSE 8107
+
+ENTRYPOINT ["java", "--add-opens", "java.base/java.lang=ALL-UNNAMED", "--add-opens", "java.base/java.lang.reflect=ALL-UNNAMED", "-Djava.security.egd=file:/dev/./urandom", "-XX:+HeapDumpOnOutOfMemoryError", "-XX:HeapDumpPath=/opt/logs", "-XX:ErrorFile=/opt/logs/hs_err_pid%p.log", "-Xlog:gc*:file=/opt/logs/gc.log:time,uptime,level,tags:filecount=5,filesize=50M", "-XX:MaxDirectMemorySize=512m", "-Xms1024m", "-Xmx1024m", "-jar", "app.jar"]
+
+CMD ["--spring.profiles.active=${ACTIVE}","--logging.path=/opt/logs"]
diff --git a/blade-service/blade-file/pom.xml b/blade-service/blade-file/pom.xml
new file mode 100644
index 0000000..ce15c44
--- /dev/null
+++ b/blade-service/blade-file/pom.xml
@@ -0,0 +1,139 @@
+
+
+
+ blade-service
+ org.springblade
+ ${revision}
+
+ 4.0.0
+
+ blade-file
+ ${project.artifactId}
+ jar
+
+
+ 1.27.1
+ 1.10
+
+
+ 1.18.30
+ 1.5.5.Final
+ 7.5.5
+
+
+
+
+ org.springblade
+ blade-core-boot
+
+
+ org.springblade
+ blade-starter-swagger
+
+
+
+ org.springblade
+ blade-file-api
+
+
+
+ org.springblade
+ blade-wps-api
+
+
+
+ org.springblade
+ blade-ocr-api
+
+
+
+
+ net.lingala.zip4j
+ zip4j
+ 1.3.1
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+ org.apache.commons
+ commons-compress
+ ${commons-compress.version}
+
+
+ org.tukaani
+ xz
+ ${xz.version}
+
+
+
+ com.github.junrar
+ junrar
+ ${junrar.version}
+
+
+
+ com.alibaba.csp
+ sentinel-datasource-nacos
+
+
+
+ org.mapstruct
+ mapstruct
+ ${org.mapstruct.version}
+
+
+
+
+
+
+
+ io.fabric8
+ docker-maven-plugin
+
+ ${docker.fabric.skip}
+
+
+
+ org.apache.maven.plugins
+ maven-antrun-plugin
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ ${maven.plugin.version}
+
+ ${java.version}
+ ${java.version}
+ UTF-8
+
+ -parameters
+
+
+
+ org.projectlombok
+ lombok
+ ${lombok.version}
+
+
+ org.mapstruct
+ mapstruct-processor
+ ${org.mapstruct.version}
+
+
+
+
+
+
+
+
+
+
diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/FileApplication.java b/blade-service/blade-file/src/main/java/org/springblade/file/FileApplication.java
new file mode 100644
index 0000000..cfd985f
--- /dev/null
+++ b/blade-service/blade-file/src/main/java/org/springblade/file/FileApplication.java
@@ -0,0 +1,47 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.file;
+
+import org.springblade.core.cloud.client.BladeCloudApplication;
+import org.springblade.core.launch.BladeApplication;
+import org.springblade.core.launch.constant.AppConstant;
+import org.springframework.context.annotation.ComponentScan;
+
+/**
+ * Desk启动器
+ *
+ * @author Chill
+ */
+@BladeCloudApplication
+@ComponentScan({"org.springblade.file", "org.springblade.**.feign"})
+public class FileApplication {
+
+ public static void main(String[] args) {
+ BladeApplication.run(AppConstant.APPLICATION_FILE_NAME, FileApplication.class, args);
+ }
+
+}
+
diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/config/FileConfig.java b/blade-service/blade-file/src/main/java/org/springblade/file/config/FileConfig.java
new file mode 100644
index 0000000..0f462c3
--- /dev/null
+++ b/blade-service/blade-file/src/main/java/org/springblade/file/config/FileConfig.java
@@ -0,0 +1,33 @@
+package org.springblade.file.config;
+
+import com.obs.services.ObsClient;
+import org.springblade.common.utils.ObsUtil;
+import org.springblade.file.service.impl.ObsFileServiceImpl;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+
+/**
+ * 文件配置类
+ * @author bfhuange
+ * @date 2024/10/21
+ */
+@Import(ObsUtil.class)
+@Configuration
+public class FileConfig {
+
+ @Bean
+ public ObsProperties obsProperties() {
+ return new ObsProperties();
+ }
+
+ @Bean
+ public ObsClient obsClient(ObsProperties properties) {
+ return new ObsClient(properties.getAccessKeyId(), properties.getAccessKeySecret(), properties.getEndpoint());
+ }
+
+ @Bean
+ public ObsFileServiceImpl fileService(ObsProperties properties, ObsClient obsClient) {
+ return new ObsFileServiceImpl(properties, obsClient);
+ }
+}
diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/config/ObsProperties.java b/blade-service/blade-file/src/main/java/org/springblade/file/config/ObsProperties.java
new file mode 100644
index 0000000..9e42e6e
--- /dev/null
+++ b/blade-service/blade-file/src/main/java/org/springblade/file/config/ObsProperties.java
@@ -0,0 +1,69 @@
+package org.springblade.file.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+import java.io.File;
+import java.time.Duration;
+
+/**
+ * obs 配置
+ * @author bfhuange
+ * @date 2024/10/21
+ */
+@Data
+@ConfigurationProperties(prefix = "obs")
+public class ObsProperties {
+ /**
+ * OBS endpoint
+ */
+ public String endpoint;
+ /**
+ * AK in the access key
+ */
+ public String accessKeyId;
+ /**
+ * SK in the access key
+ */
+ public String accessKeySecret;
+ /**
+ * 桶名
+ */
+ public String bucketName;
+ /**
+ * 根目录
+ */
+ public String rootDirectory;
+ /**
+ * 过期时间,默认1小时
+ */
+ public Long expires = Duration.ofHours(1).toSeconds();
+ /**
+ * 本地临时下载文件目录
+ */
+ public String tmpDownloadDir = "tmp" + File.separator + "download";
+ /**
+ * 本地临时解压文件目录
+ */
+ public String tmpUnzipDir = "tmp" + File.separator + "unzip";
+ /**
+ * obs上压缩文件上传目录
+ */
+ public String zipUploadDir = "part";
+ /**
+ * obs上解压后的文件上传目录
+ */
+ public String unzipUploadDir = "unzip";
+ /**
+ * obs上设置了过期时间自动删除的上传目录
+ */
+ public String tmpUploadDir = "tmp";
+ /**
+ * 下载进度间隔,默认 10m
+ */
+ public Long downloadProgressInterval = 10 * 1024 * 1024L;
+ /**
+ * 上传进度间隔,默认 10m
+ */
+ public Long uploadProgressInterval = 10 * 1024 * 1024L;
+}
diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/controller/BusinessAttachmentController.java b/blade-service/blade-file/src/main/java/org/springblade/file/controller/BusinessAttachmentController.java
new file mode 100644
index 0000000..d698223
--- /dev/null
+++ b/blade-service/blade-file/src/main/java/org/springblade/file/controller/BusinessAttachmentController.java
@@ -0,0 +1,75 @@
+package org.springblade.file.controller;
+
+import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import lombok.AllArgsConstructor;
+import org.springblade.core.tool.api.R;
+import org.springblade.file.pojo.dto.BusinessAttachmentAddDTO;
+import org.springblade.file.pojo.dto.BusinessAttachmentBatchAddDTO;
+import org.springblade.file.pojo.dto.BusinessAttachmentQueryDTO;
+import org.springblade.file.pojo.vo.BusinessAttachmentVO;
+import org.springblade.file.service.IBusinessAttachmentService;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 业务附件信息表 控制器
+ *
+ * @author BladeX
+ * @since 2024-09-04
+ */
+@Valid
+@RestController
+@AllArgsConstructor
+@RequestMapping("businessAttachment")
+@Tag(name = "业务附件信息表", description = "业务附件信息表接口")
+public class BusinessAttachmentController {
+
+ private final IBusinessAttachmentService businessAttachmentService;
+
+ /**
+ * 业务附件信息表 新增
+ */
+ @PostMapping("/batchSave")
+ @ApiOperationSupport(order = 1)
+ @Operation(summary = "批量新增", description = "批量新增")
+ public R batchSave(@Validated @RequestBody List params) {
+ return R.status(businessAttachmentService.saveAndRemove(params, null));
+ }
+
+ /**
+ * 业务附件信息表 新增
+ */
+ @PostMapping("/save")
+ @ApiOperationSupport(order = 2)
+ @Operation(summary = "新增", description = "新增")
+ public R save(@Validated @RequestBody BusinessAttachmentBatchAddDTO param) {
+ return R.status(businessAttachmentService.saveAndRemove(param));
+ }
+
+ /**
+ * 业务附件信息表 删除
+ */
+ @PostMapping("/remove")
+ @ApiOperationSupport(order = 3)
+ @Operation(summary = "逻辑删除", description = "传入bizId")
+ public R remove(@Parameter(description = "bizId", required = true) @RequestParam Long bizId) {
+ return R.status(businessAttachmentService.removeByBizIds(List.of(bizId)));
+ }
+
+ /**
+ * 查询业务附件信息列表
+ */
+ @PostMapping("/queryList")
+ @ApiOperationSupport(order = 4)
+ @Operation(summary = "查询业务附件信息列表", description = "查询业务附件信息列表")
+ public R> queryList(@Validated @RequestBody BusinessAttachmentQueryDTO param) {
+ return R.data(businessAttachmentService.queryList(param));
+ }
+
+}
diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/controller/BusinessUnzipTaskController.java b/blade-service/blade-file/src/main/java/org/springblade/file/controller/BusinessUnzipTaskController.java
new file mode 100644
index 0000000..eab22f5
--- /dev/null
+++ b/blade-service/blade-file/src/main/java/org/springblade/file/controller/BusinessUnzipTaskController.java
@@ -0,0 +1,69 @@
+package org.springblade.file.controller;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.AllArgsConstructor;
+import org.springblade.core.log.annotation.ApiLog;
+import org.springblade.core.log.utils.AssertUtils;
+import org.springblade.core.mp.support.Condition;
+import org.springblade.core.mp.support.Query;
+import org.springblade.core.secure.annotation.PreAuth;
+import org.springblade.core.secure.utils.AuthUtil;
+import org.springblade.core.tool.api.R;
+import org.springblade.file.pojo.dto.BusinessUnzipTaskQueryDTO;
+import org.springblade.file.pojo.dto.BusinessUnzipTaskUpdateDTO;
+import org.springblade.file.pojo.enums.UnzipTask;
+import org.springblade.file.pojo.vo.BusinessUnzipTaskListVO;
+import org.springblade.file.service.IBusinessUnzipTaskService;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * @author bfhuange
+ * @date 2024/10/28
+ */
+@RestController
+@AllArgsConstructor
+@RequestMapping("/businessUnzipTask")
+@Tag(name = "业务文件解压任务", description = "业务文件解压任务")
+public class BusinessUnzipTaskController {
+
+ private final IBusinessUnzipTaskService unzipTaskService;
+
+ /**
+ * 文件任务列表
+ * @param param
+ * @param query
+ */
+ @GetMapping("/list")
+ @Operation(summary = "业务文件解压任务分页查询", description = "业务文件解压任务分页查询")
+ @PreAuth("hasPermission('waybill:uploadTask:list')")
+ public R> list(BusinessUnzipTaskQueryDTO param, Query query) {
+ if (param == null) {
+ param = new BusinessUnzipTaskQueryDTO();
+ }
+ // 只查文件任务类型
+ param.setRelateType(UnzipTask.RelateType.FILE_TASK.getCode());
+ if (param.isCurrentUser()) {
+ // 只查自己的
+ param.setCurrentUserId(AuthUtil.getUserId());
+ }
+ IPage pages = unzipTaskService.selectPage(Condition.getPage(query), param);
+ return R.data(pages);
+ }
+
+ /**
+ * 业务文件解压任务更新(重新上传)
+ * @param param
+ * @return
+ */
+ @ApiLog("上传任务-重新上传")
+ @PostMapping("/update")
+ @Operation(summary = "业务文件解压任务更新(重新上传)", description = "业务文件解压任务更新(重新上传)")
+ @PreAuth("hasPermission('waybill:uploadTask:update')")
+ public R update(@Validated @RequestBody BusinessUnzipTaskUpdateDTO param) {
+ AssertUtils.notNull(param.getId(), "id不能为空");
+ return R.status(unzipTaskService.updateReupload(param));
+ }
+}
diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/controller/FileController.java b/blade-service/blade-file/src/main/java/org/springblade/file/controller/FileController.java
new file mode 100644
index 0000000..417f9e8
--- /dev/null
+++ b/blade-service/blade-file/src/main/java/org/springblade/file/controller/FileController.java
@@ -0,0 +1,168 @@
+package org.springblade.file.controller;
+
+import com.alibaba.csp.sentinel.annotation.SentinelResource;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.AllArgsConstructor;
+import org.springblade.core.boot.ctrl.BladeController;
+import org.springblade.core.log.annotation.ApiLog;
+import org.springblade.core.tenant.annotation.NonDS;
+import org.springblade.core.tool.api.R;
+import org.springblade.core.tool.utils.StringUtil;
+import org.springblade.file.listener.FileEvent;
+import org.springblade.file.pojo.dto.CertificateAttachmentDTO;
+import org.springblade.file.pojo.dto.FileCertificateBatchRecognitionDTO;
+import org.springblade.file.pojo.entity.Attachment;
+import org.springblade.file.service.IAttachmentService;
+import org.springblade.file.service.IFileService;
+import org.springblade.file.service.IOCRConvertService;
+import org.springblade.thirdparty.ocr.pojo.dto.CertificateRecognitionDTO;
+import org.springblade.thirdparty.ocr.pojo.vo.TransportCertificateVO;
+import org.springblade.thirdparty.ocr.service.IOCRService;
+import org.springblade.thirdparty.wps.service.IWpsService;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationEvent;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * @ClassName: fileUploadController
+ * @Author: zhaowei
+ * @Date: 2024/8/22
+ */
+@Valid
+@NonDS
+@RestController
+@AllArgsConstructor
+@RequestMapping("/file")
+@Tag(name = "文件相关管理", description = "文件相关管理")
+public class FileController extends BladeController {
+
+ private final IAttachmentService attachmentService;
+ private final IFileService fileService;
+ private final IWpsService wpsService;
+ private final IOCRService ocrService;
+ private final IOCRConvertService ocrConvertService;
+ private final ApplicationContext applicationContext;
+
+ @ApiLog("附件管理-批量上传")
+ @Operation(summary = "OBS批量上传", description = "OBS批量上传,参数名:files")
+ @PostMapping("/ossUpload")
+ public R ossUpload(MultipartFile[] files, @RequestParam(value = "fileName", required = false) String fileName) {
+ return attachmentService.batchOssUpload(files, fileName);
+ }
+
+ @Operation(summary = "上传", description = "上传,参数名:file")
+ @PostMapping("/upload")
+ public R upload(MultipartFile file) {
+ MultipartFile[] files = {file};
+ R> result = attachmentService.batchOssUpload(files, null);
+ List data = result.getData();
+ if(data != null && !data.isEmpty())return R.data(data.get(0));
+ return R.fail("上传失败");
+ }
+
+ @Operation(summary = "获取附件,多个附件id用逗号分割", description = "获取附件,多个附件id用逗号分割")
+ @GetMapping("/getAttachment")
+ public R getAttachment(@RequestParam(value = "id") String id) {
+ return R.data(attachmentService.getAttachment(id));
+ }
+
+ @Operation(summary = "获取文件url", description = "获取文件url,参数:objectKey")
+ @GetMapping("/getFileUrl")
+ public R getFileUrl(@RequestParam(value = "objectKey") String objectKey, @RequestParam(value = "attachmentName", required = false) String attachmentName) {
+ if (StringUtil.isBlank(attachmentName)) {
+ // 附件名为空,查询附件名
+ Attachment attachment = attachmentService.getOne(Wrappers.lambdaQuery()
+ .eq(Attachment::getObjectKey, objectKey)
+ .last("limit 1")
+ );
+ attachmentName = Optional.ofNullable(attachment)
+ .map(Attachment::getAttachmentName)
+ .orElse(null);
+ if (attachmentName != null) {
+ // 附件名中如果有逗号,则替换为下划线
+ attachmentName = attachmentName.replaceAll(",", "_");
+ }
+ }
+ return R.data(fileService.getFileUrl(objectKey, attachmentName, null));
+ }
+
+ /**
+ * 获取wps文件预览url
+ * @param attachmentId * @param attachmentName
+ * @return
+ */
+ @Operation(summary = "获取wps文件预览url", description = "获取wps文件预览url")
+ @GetMapping("/getWpsFilePreviewUrl")
+ public R getWpsFilePreviewUrl(@NotNull(message = "附件id不能为空") @RequestParam(value = "attachmentId", required = false) String attachmentId,
+ @NotBlank(message = "附件名称不能为空") @RequestParam(value = "attachmentName", required = false) String attachmentName) {
+ return R.data(wpsService.getFilePreviewUrl(attachmentId, attachmentName));
+ }
+
+ /**
+ * 获取wps文件编辑url
+ * @param attachmentId
+ * @param attachmentName
+ * @return
+ */
+ @Operation(summary = "获取wps文件编辑url", description = "获取wps文件编辑url")
+ @GetMapping("/getWpsFileEditUrl")
+ public R getWpsFileEditUrl(@NotNull(message = "附件id不能为空") @RequestParam(value = "attachmentId", required = false) String attachmentId,
+ @NotBlank(message = "附件名称不能为空") @RequestParam(value = "attachmentName", required = false) String attachmentName) {
+ return R.data(wpsService.getWpsFileEditUrl(attachmentId, attachmentName));
+ }
+
+ @Operation(summary = "批量获取文件url", description = "批量获取文件url,参数:objectKey数组")
+ @PostMapping("/getFileUrls")
+ public R getFileUrls(@RequestBody List objectKeys) {
+ return R.data(fileService.getFileUrls(objectKeys, null));
+ }
+
+ @ApiLog("OCR识别-识别身份证")
+ @Operation(summary = "识别身份证信息支持正反面", description = "参数:objectKey")
+ @GetMapping("/recognitionIDCard")
+ public R recognitionIDCard(@NotNull(message = "objectKey不能为空") @RequestParam(value = "objectKey", required = false) String objectKey) {
+ String fileUrl = fileService.getFileUrl(objectKey, null);
+ return R.data(ocrService.recognitionIDCard(List.of(fileUrl)));
+ }
+
+ @SentinelResource("ocr:batchCards")
+ @ApiLog("OCR识别-识别车辆运输凭证(不知道类型)")
+ @Operation(summary = "识别车辆运输凭证,不知道类型", description = "参数:objectKeylist")
+ @PostMapping("/recognitionTransportCertificates")
+ public R recognitionTransportCertificates(@RequestBody List objectKeys, @RequestParam(value = "projectAbbreviation", required = false) String projectAbbreviation, @RequestParam(value = "code", required = false) String code) {
+ return R.data(ocrConvertService.recognitionTransportCertificate(new FileCertificateBatchRecognitionDTO(objectKeys, projectAbbreviation, code, null)));
+ }
+
+ @ApiLog("OCR识别-识别车辆运输凭证(知道类型)")
+ @Operation(summary = "识别车辆运输凭证,知道类型", description = "参数:objectKeylist")
+ @PostMapping("/recognitionTransportCertificatesByType")
+ public R recognitionTransportCertificatesByType(@RequestBody CertificateAttachmentDTO param, @RequestParam(value = "code", required = false) String code) {
+ CertificateRecognitionDTO recognitionParam = new CertificateRecognitionDTO();
+ recognitionParam.setPoundUrl(fileService.getFileUrl(param.getPoundObjectKey(), null));
+ recognitionParam.setCarFrontUrl(fileService.getFileUrl(param.getCarFrontObjectKey(), null));
+ recognitionParam.setDriverLicenseUrl(fileService.getFileUrl(param.getDriverLicenseObjectKey(), null));
+ recognitionParam.setVehicleLicenseUrl(fileService.getFileUrl(param.getVehicleLicenseObjectKey(), null));
+ recognitionParam.setName(param.getProjectAbbreviation());
+ return R.data(ocrService.recognitionTransportCertificate(recognitionParam, code));
+ }
+
+ @Operation(hidden = true)
+ @PostMapping("/publishEvent")
+ public R publishEvent(String code, Long id) {
+ ApplicationEvent event = FileEvent.createEvent(code, id);
+ if (event == null) {
+ return R.fail("事件名称错误");
+ }
+ applicationContext.publishEvent(event);
+ return R.status(true);
+ }
+}
diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/controller/FileTaskController.java b/blade-service/blade-file/src/main/java/org/springblade/file/controller/FileTaskController.java
new file mode 100644
index 0000000..071625d
--- /dev/null
+++ b/blade-service/blade-file/src/main/java/org/springblade/file/controller/FileTaskController.java
@@ -0,0 +1,85 @@
+package org.springblade.file.controller;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.Parameters;
+import io.swagger.v3.oas.annotations.enums.ParameterIn;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotNull;
+import lombok.AllArgsConstructor;
+import org.springblade.core.boot.ctrl.BladeController;
+import org.springblade.core.log.annotation.ApiLog;
+import org.springblade.core.tenant.annotation.NonDS;
+import org.springblade.core.tool.api.R;
+import org.springblade.file.pojo.dto.FileTaskCreateDTO;
+import org.springblade.file.pojo.dto.FileTaskUpdateDTO;
+import org.springblade.file.pojo.dto.IdsDTO;
+import org.springblade.file.pojo.vo.FileTaskUpdateVO;
+import org.springblade.file.service.IFileTaskService;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * @ClassName: FileTaskController
+ * @Author: zhaowei
+ * @Date: 2024/8/28
+ */
+@Valid
+@NonDS
+@RestController
+@AllArgsConstructor
+@RequestMapping("/fileTask")
+@Tag(name = "文件任务", description = "文件任务")
+public class FileTaskController extends BladeController {
+
+ private final IFileTaskService fileTaskService;
+
+ @GetMapping("/queryFileTask")
+ @Operation(summary = "根据文件的MD5值查询文件任务", description = "文件的MD5值检查分段,参数名:md5")
+ public R queryFileTask(@RequestParam(value = "md5") String md5) {
+ FileTaskUpdateVO fileTask = fileTaskService.queryFileTask(null, md5);
+ return R.data(fileTask);
+ }
+
+ @ApiLog("上传任务-创建文件任务")
+ @PostMapping("/createFileTask")
+ @Operation(summary = "创建文件任务", description = "创建文件任务")
+ public R createFileTask(@RequestBody FileTaskCreateDTO param) {
+ Long id = fileTaskService.createFileTask(param);
+ return R.data(fileTaskService.queryFileTask(id, null));
+ }
+
+ @ApiLog("上传任务-更新文件任务进度")
+ @PostMapping("/updateFileTask")
+ @Operation(summary = "更新文件任务进度", description = "更新文件任务进度")
+ public R updateFileTask(@RequestBody FileTaskUpdateDTO param) {
+ return R.data(fileTaskService.updateFileTask(param));
+ }
+
+ @ApiLog("上传任务-修改文件任务状态为失败")
+ @PostMapping("/updateFailed")
+ @Operation(summary = "更新文件任务为失败", description = "更新文件任务为失败")
+ public R updateFailed(@NotNull(message = "文件任务id不能为空") Long id) {
+ return R.data(fileTaskService.updateFailed(id));
+ }
+
+ @ApiLog("上传任务-暂停文件任务")
+ @PostMapping("/updatePaused")
+ @Operation(summary = "更新文件任务为暂停", description = "更新文件任务为暂停")
+ public R updatePaused(@Validated @RequestBody IdsDTO param) {
+ return R.status(fileTaskService.updatePaused(param.getIds()));
+ }
+
+ @GetMapping("/getUploadUrl")
+ @Parameters({
+ @Parameter(name = "id", description = "文件任务id", in = ParameterIn.QUERY, schema = @Schema(type = "string")),
+ @Parameter(name = "partNumber", description = "分段编号", in = ParameterIn.QUERY, schema = @Schema(type = "int"))
+ })
+ @Operation(summary = "根据文件任务id查询上传链接", description = "根据文件任务id查询上传链接")
+ public R getUploadUrl(Long id, Integer partNumber) {
+ String uploadUrl = fileTaskService.getUploadUrl(id, partNumber);
+ return R.data(uploadUrl);
+ }
+}
diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/convert/BusinessAttachmentConvert.java b/blade-service/blade-file/src/main/java/org/springblade/file/convert/BusinessAttachmentConvert.java
new file mode 100644
index 0000000..6a81f10
--- /dev/null
+++ b/blade-service/blade-file/src/main/java/org/springblade/file/convert/BusinessAttachmentConvert.java
@@ -0,0 +1,44 @@
+package org.springblade.file.convert;
+
+import org.mapstruct.Mapper;
+import org.mapstruct.MappingConstants;
+import org.mapstruct.ReportingPolicy;
+import org.springblade.file.pojo.dto.BusinessAttachmentAddDTO;
+import org.springblade.file.pojo.dto.BusinessAttachmentBatchAddDTO;
+import org.springblade.file.pojo.entity.Attachment;
+import org.springblade.file.pojo.entity.BusinessAttachment;
+import org.springblade.file.pojo.vo.AttachmentDetailVO;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * @author bfhuange
+ * @date 2024/9/4
+ */
+@Mapper(componentModel = MappingConstants.ComponentModel.SPRING, unmappedTargetPolicy = ReportingPolicy.IGNORE)
+public interface BusinessAttachmentConvert {
+ BusinessAttachment dto2entity(BusinessAttachmentAddDTO dto);
+
+ List dtos2entities(List dtos);
+
+ AttachmentDetailVO entity2vo(Attachment entity);
+
+ default List dto2entities(BusinessAttachmentBatchAddDTO dto) {
+ if (dto == null || dto.getDetails() == null) {
+ return Collections.emptyList();
+ }
+ AtomicInteger sort = new AtomicInteger(1);
+ return dto.getDetails().stream()
+ .flatMap(detail -> detail.getAttachmentIds().stream().map(attachmentId -> {
+ BusinessAttachment businessAttachment = new BusinessAttachment();
+ businessAttachment.setAttachmentId(attachmentId);
+ businessAttachment.setBizId(dto.getBizId());
+ businessAttachment.setBizCode(detail.getBizCode());
+ businessAttachment.setSort(sort.getAndIncrement());
+ return businessAttachment;
+ })).toList();
+ }
+
+}
diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/convert/FileTaskConvert.java b/blade-service/blade-file/src/main/java/org/springblade/file/convert/FileTaskConvert.java
new file mode 100644
index 0000000..f966920
--- /dev/null
+++ b/blade-service/blade-file/src/main/java/org/springblade/file/convert/FileTaskConvert.java
@@ -0,0 +1,31 @@
+package org.springblade.file.convert;
+
+import org.mapstruct.Mapper;
+import org.mapstruct.MappingConstants;
+import org.mapstruct.ReportingPolicy;
+import org.springblade.file.pojo.dto.BusinessUnzipTaskAddDTO;
+import org.springblade.file.pojo.dto.FileTaskCreateDTO;
+import org.springblade.file.pojo.entity.BusinessUnzipTask;
+import org.springblade.file.pojo.entity.FileTask;
+import org.springblade.file.pojo.entity.FileTaskPart;
+import org.springblade.file.pojo.vo.FileTaskUpdateVO;
+import org.springblade.file.pojo.vo.FileTaskVO;
+import org.springblade.file.pojo.vo.PartEtagVO;
+
+/**
+ * @author bfhuange
+ * @date 2024/10/22
+ */
+@Mapper(componentModel = MappingConstants.ComponentModel.SPRING, unmappedTargetPolicy = ReportingPolicy.IGNORE)
+public interface FileTaskConvert {
+
+ FileTaskVO entity2vo(FileTask entity);
+
+ FileTaskUpdateVO entity2updateVo(FileTask entity);
+
+ FileTask dto2entity(FileTaskCreateDTO dto);
+
+ PartEtagVO entity2vo(FileTaskPart entity);
+
+ BusinessUnzipTask dto2entity(BusinessUnzipTaskAddDTO dto);
+}
diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/convert/OCRConverter.java b/blade-service/blade-file/src/main/java/org/springblade/file/convert/OCRConverter.java
new file mode 100644
index 0000000..bd551bb
--- /dev/null
+++ b/blade-service/blade-file/src/main/java/org/springblade/file/convert/OCRConverter.java
@@ -0,0 +1,17 @@
+package org.springblade.file.convert;
+
+import org.mapstruct.Mapper;
+import org.mapstruct.MappingConstants;
+import org.mapstruct.ReportingPolicy;
+import org.springblade.file.pojo.dto.FileCertificateBatchRecognitionDTO;
+import org.springblade.thirdparty.ocr.pojo.dto.CertificateBatchRecognitionDTO;
+
+/**
+ * @author bfhuange
+ * @since 2025/3/13
+ */
+@Mapper(componentModel = MappingConstants.ComponentModel.SPRING, unmappedTargetPolicy = ReportingPolicy.IGNORE)
+public interface OCRConverter {
+
+ CertificateBatchRecognitionDTO dto2ocr(FileCertificateBatchRecognitionDTO dto);
+}
diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/exception/UnzipFailException.java b/blade-service/blade-file/src/main/java/org/springblade/file/exception/UnzipFailException.java
new file mode 100644
index 0000000..739c41e
--- /dev/null
+++ b/blade-service/blade-file/src/main/java/org/springblade/file/exception/UnzipFailException.java
@@ -0,0 +1,28 @@
+package org.springblade.file.exception;
+
+import lombok.Getter;
+
+import java.io.File;
+
+/**
+ * 文件解压失败异常
+ * @author bfhuange
+ * @date 2024/11/11
+ */
+@Getter
+public class UnzipFailException extends RuntimeException{
+ /**
+ * 解压目录
+ */
+ private final File unzipDir;
+
+ public UnzipFailException(Throwable cause, File unzipDir) {
+ super(cause);
+ this.unzipDir = unzipDir;
+ }
+
+ public UnzipFailException(String message, File unzipDir) {
+ super(message);
+ this.unzipDir = unzipDir;
+ }
+}
diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/feign/FileClient.java b/blade-service/blade-file/src/main/java/org/springblade/file/feign/FileClient.java
new file mode 100644
index 0000000..05e7f86
--- /dev/null
+++ b/blade-service/blade-file/src/main/java/org/springblade/file/feign/FileClient.java
@@ -0,0 +1,157 @@
+package org.springblade.file.feign;
+
+import com.alibaba.csp.sentinel.annotation.SentinelResource;
+import io.swagger.v3.oas.annotations.Hidden;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotNull;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springblade.core.log.utils.AssertUtils;
+import org.springblade.core.tool.api.FR;
+import org.springblade.core.tool.api.R;
+import org.springblade.file.config.ObsProperties;
+import org.springblade.file.pojo.dto.*;
+import org.springblade.file.pojo.entity.Attachment;
+import org.springblade.file.pojo.vo.*;
+import org.springblade.file.service.*;
+import org.springblade.file.util.ObjectKeyUtils;
+import org.springblade.thirdparty.ocr.pojo.vo.TransportCertificateVO;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.util.List;
+
+/**
+ * @author bfhuange
+ * @date 2024/10/21
+ */
+@Slf4j
+@Valid
+@Hidden
+@RestController
+@AllArgsConstructor
+public class FileClient implements IFileClient, IFileUploadClient {
+
+ private final IAttachmentService attachmentService;
+ private final IBusinessAttachmentService businessAttachmentService;
+ private final IBusinessUnzipTaskService businessUnzipTaskService;
+ private final IOCRConvertService ocrConvertService;
+ private final IFileService fileService;
+ private final ObsProperties obsProperties;
+
+ @Override
+ @GetMapping(ATTACHMENT)
+ public FR> getAttachment(String id) {
+ return FR.data(attachmentService.getAttachment(id));
+ }
+
+ @Override
+ public FR getAttachmentDetail(Long id) {
+ return FR.data(attachmentService.getDetail(id));
+ }
+
+ @Override
+ public FR saveAndRemoveBusinessAttachment(@RequestBody @Validated BusinessAttachmentBatchAddDTO param) {
+ return FR.status(businessAttachmentService.saveAndRemove(param));
+ }
+
+ @Override
+ public FR batchSaveAndRemoveBusinessAttachment(@RequestBody List<@Valid BusinessAttachmentAddDTO> params, @RequestParam(value = "bizId", required = false) Long bizId) {
+ return FR.status(businessAttachmentService.saveAndRemove(params, new BusinessAttachmentDeleteDTO(bizId)));
+ }
+
+ @Override
+ public FR batchSaveAndRemoveByParamBusinessAttachment(@RequestBody List<@Valid BusinessAttachmentAddDTO> params, @Valid BusinessAttachmentDeleteDTO deleteParam) {
+ return FR.status(businessAttachmentService.saveAndRemove(params, deleteParam));
+ }
+
+ @Override
+ public FR batchSaveAndRemoveBusinessAttachment(List<@Valid BusinessAttachmentAddDTO> params) {
+ return FR.status(businessAttachmentService.saveAndRemove(params, null));
+ }
+
+ @Override
+ public FR removeBusinessAttachment(@NotNull(message = "业务id不能为空") @RequestParam(value = "bizId", required = false) Long bizId) {
+ businessAttachmentService.removeByBizIds(List.of(bizId));
+ return FR.data(true);
+ }
+
+ @Override
+ public FR> getBusinessAttachments(@Validated @RequestBody BusinessAttachmentQueryDTO param) {
+ return FR.data(businessAttachmentService.queryList(param));
+ }
+
+ @Override
+ public FR saveBusinessUnzipTask(BusinessUnzipTaskAddDTO param) {
+ return FR.status(businessUnzipTaskService.save(param));
+ }
+
+ @Override
+ public FR updateBusinessUnzipTask(BusinessUnzipTaskUpdateDTO param) {
+ AssertUtils.notNull(param.getBizId(), "业务id不能为空");
+ return FR.status(businessUnzipTaskService.updateReupload(param));
+ }
+
+ @Override
+ public FR cancelBusinessUnzipTask(@NotNull(message = "业务id不能为空") Long bizId) {
+ return FR.status(businessUnzipTaskService.cancelBusinessUnzipTask(bizId, true));
+ }
+
+ @Override
+ public FR removeBusinessUnzipTask(Long bizId) {
+ return FR.status(businessUnzipTaskService.cancelBusinessUnzipTask(bizId, false));
+ }
+
+ @Override
+ public FR> getBusinessUnzipTasksByBizIds(List bizIds) {
+ return FR.data(businessUnzipTaskService.getBusinessUnzipTasksByBizIds(bizIds));
+ }
+
+ @Override
+ public FR> queryBusinessUnzipDetail(@NotNull(message = "业务id不能为空") Long bizId) {
+ return FR.data(businessUnzipTaskService.queryBusinessUnzipDetail(bizId));
+ }
+
+ @SentinelResource("ocr:batchCards")
+ @Override
+ public FR recognitionTransportCertificates(@Validated @RequestBody FileCertificateBatchRecognitionDTO param) {
+ return FR.data(ocrConvertService.recognitionTransportCertificate(param));
+ }
+
+ @Override
+ public FR> getFileUrls(IdsDTO param) {
+ List list = attachmentService.listByIds(param.getIds()).stream()
+ .map(attachment -> {
+ String fileUrl = fileService.getFileUrl(attachment.getObjectKey(), attachment.getAttachmentName(), null);
+ return new FileUrlVO(attachment.getId(), fileUrl);
+ })
+ .toList();
+ return FR.data(list);
+ }
+
+ @Override
+ public FR> ossUpload(MultipartFile[] files) {
+ R> r = attachmentService.batchOssUpload(files, null);
+ return R.isNotSuccess(r) ? FR.fail(r.getCode(), r.getMsg()) : FR.data(r.getData(), r.getMsg());
+ }
+
+ @Override
+ public FR ossUploadTemp(MultipartFile file, Integer expires) {
+ if (expires == null) {
+ // 默认1天过期
+ expires = 1;
+ }
+ try {
+ String objectKey = ObjectKeyUtils.generateObjectKey(obsProperties.getTmpUploadDir(), file.getOriginalFilename());
+ String fileUrl = fileService.uploadTempFile(objectKey, file.getInputStream(), expires);
+ return FR.data(new TempFileUrlVO(objectKey, fileUrl));
+ } catch (Exception e) {
+ log.error("上传临时文件异常", e);
+ return FR.fail("上传临时文件异常");
+ }
+ }
+}
diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/listener/FileEvent.java b/blade-service/blade-file/src/main/java/org/springblade/file/listener/FileEvent.java
new file mode 100644
index 0000000..78c9a3a
--- /dev/null
+++ b/blade-service/blade-file/src/main/java/org/springblade/file/listener/FileEvent.java
@@ -0,0 +1,56 @@
+package org.springblade.file.listener;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.springframework.context.ApplicationEvent;
+
+import java.util.function.Function;
+import java.util.stream.Stream;
+
+/**
+ * 文件事件枚举
+ * @author bfhuange
+ * @since 2024/11/22
+ */
+@AllArgsConstructor
+@Getter
+public enum FileEvent {
+ /**
+ * 上传完成事件
+ */
+ UPLOADED(FileUploadedEvent::new),
+ /**
+ * 解压开始事件
+ */
+ UNZIP_START(UnzipStartEvent::new),
+ /**
+ * 解压完成事件
+ */
+ UNZIP_COMPLETED(UnzipCompletedEvent::new),
+ /**
+ * 解压失败事件
+ */
+ UNZIP_FAILED(UnzipFailEvent::new),
+ ;
+ /**
+ * 事件函数
+ */
+ private final Function eventFunction;
+
+ /**
+ * 根据编码创建事件
+ * @param code
+ * @param id
+ * @return
+ */
+ public static ApplicationEvent createEvent(String code, Long id) {
+ if (code == null || code.isBlank()) {
+ return null;
+ }
+ return Stream.of(values())
+ .filter(event -> event.name().equalsIgnoreCase(code))
+ .findFirst()
+ .map(event -> event.getEventFunction().apply(id))
+ .orElse(null);
+ }
+}
diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/listener/FileListener.java b/blade-service/blade-file/src/main/java/org/springblade/file/listener/FileListener.java
new file mode 100644
index 0000000..7ca5a0f
--- /dev/null
+++ b/blade-service/blade-file/src/main/java/org/springblade/file/listener/FileListener.java
@@ -0,0 +1,173 @@
+package org.springblade.file.listener;
+
+import com.alibaba.fastjson2.JSON;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import lombok.extern.slf4j.Slf4j;
+import org.springblade.core.log.exception.ServiceException;
+import org.springblade.core.log.utils.AssertUtils;
+import org.springblade.file.callback.UnzipCallback;
+import org.springblade.file.callback.UnzipEventType;
+import org.springblade.file.pojo.entity.BusinessUnzipTask;
+import org.springblade.file.pojo.enums.UnzipTask;
+import org.springblade.file.pojo.vo.FileVO;
+import org.springblade.file.service.IBusinessUnzipDetailService;
+import org.springblade.file.service.IBusinessUnzipTaskService;
+import org.springframework.context.event.EventListener;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.event.TransactionPhase;
+import org.springframework.transaction.event.TransactionalEventListener;
+
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * 文件监听器
+ * @author bfhuange
+ * @date 2024/11/8
+ */
+@Slf4j
+@Component
+public class FileListener {
+
+ private final IBusinessUnzipTaskService taskService;
+ private final IBusinessUnzipDetailService detailService;
+ private final Map callbackMap;
+
+ public FileListener(IBusinessUnzipTaskService taskService, IBusinessUnzipDetailService detailService, List callbacks) {
+ this.taskService = taskService;
+ this.detailService = detailService;
+ this.callbackMap = callbacks.stream()
+ .collect(Collectors.toMap(UnzipCallback::getBizCode, Function.identity(), (a, b) -> {
+ throw new ServiceException("重复的解压完成回调接口");
+ }));
+ }
+
+ /**
+ * 文件上传完成监听器
+ * @param event
+ */
+ @Async
+ @TransactionalEventListener(value = FileUploadedEvent.class, phase = TransactionPhase.AFTER_COMMIT, fallbackExecution = true)
+ public void fileUploaded(FileUploadedEvent event) {
+ try {
+ Long fileTaskId = event.getFileTaskId();
+ log.info("文件上传任务完成事件 id:{}", fileTaskId);
+ BusinessUnzipTask businessUnzipTask = taskService.getOne(Wrappers.lambdaQuery()
+ .eq(BusinessUnzipTask::getRelateId, fileTaskId)
+ .last("limit 1")
+ );
+ if (businessUnzipTask != null) {
+ // 存在解压任务,更新任务为上传完成
+ BusinessUnzipTask updateParam = new BusinessUnzipTask();
+ updateParam.setId(businessUnzipTask.getId());
+ updateParam.setStatus(UnzipTask.Status.UPLOADED.getCode());
+ taskService.updateById(updateParam);
+ // 解压开始
+ this.unzipStart(new UnzipStartEvent(businessUnzipTask));
+ }
+ } catch (Exception e) {
+ log.error("文件上传任务完成事件 异常", e);
+ }
+ }
+
+ /**
+ * 解压开始
+ * @param event
+ */
+ @Async
+ @TransactionalEventListener(value = UnzipStartEvent.class, phase = TransactionPhase.AFTER_COMMIT, fallbackExecution = true)
+ public void unzipStart(UnzipStartEvent event) {
+ try {
+ Long unzipTaskId = event.getUnzipTaskId();
+ BusinessUnzipTask task = event.getTask();
+ log.info("解压开始事件 id:{} 业务解压任务:{}", unzipTaskId, JSON.toJSONString(task));
+ if (task == null) {
+ // 手动触发时,只有id
+ task = taskService.getById(unzipTaskId);
+ }
+ // 回调接口
+ callback(task, UnzipEventType.START);
+ // 解压并保存
+ detailService.unzipAndSaveAsync(task.getId(), taskService.getFile(task));
+ } catch (Exception e) {
+ log.error("解压开始事件 异常", e);
+ }
+ }
+
+ /**
+ * 解压完成监听器
+ * @param event
+ */
+ @Async
+ @EventListener(UnzipCompletedEvent.class)
+ public void unzipCompleted(UnzipCompletedEvent event) {
+ try {
+ Long unzipTaskId = event.getUnzipTaskId();
+ log.info("解压完成事件 id:{}", unzipTaskId);
+ // 查询解压任务
+ BusinessUnzipTask unzipTask = taskService.getById(unzipTaskId);
+ AssertUtils.notNull(unzipTask, "解压任务不存在");
+ // 更新解压任务状态
+ BusinessUnzipTask updateParam = new BusinessUnzipTask();
+ updateParam.setId(unzipTaskId);
+ updateParam.setStatus(UnzipTask.Status.FINISHED.getCode());
+ taskService.updateById(updateParam);
+ // 回调接口
+ callback(unzipTask, UnzipEventType.SUCCESS);
+ } catch (Exception e) {
+ log.error("解压完成事件 异常", e);
+ }
+ }
+
+ /**
+ * 解压失败监听器
+ * @param event
+ */
+ @Async
+ @EventListener(UnzipFailEvent.class)
+ public void unzipFail(UnzipFailEvent event) {
+ try {
+ Long unzipTaskId = event.getUnzipTaskId();
+ log.info("解压失败事件 id:{}", unzipTaskId);
+ // 查询解压任务
+ BusinessUnzipTask unzipTask = taskService.getById(unzipTaskId);
+ AssertUtils.notNull(unzipTask, "解压任务不存在");
+ // 更新解压任务状态
+ BusinessUnzipTask updateParam = new BusinessUnzipTask();
+ updateParam.setId(unzipTaskId);
+ updateParam.setStatus(UnzipTask.Status.UNZIP_FAIL.getCode());
+ taskService.updateById(updateParam);
+ // 回调接口
+ callback(unzipTask, UnzipEventType.FAIL);
+ } catch (Exception e) {
+ log.error("解压失败事件 异常", e);
+ }
+ }
+
+ /**
+ * 回调接口
+ * @param unzipTask
+ * @param event
+ */
+ private void callback(BusinessUnzipTask unzipTask, UnzipEventType event) {
+ if (unzipTask == null) {
+ log.warn("解压回调接口 解压任务为空");
+ return;
+ }
+ UnzipCallback unzipCallback = callbackMap.get(unzipTask.getBizCode());
+ if (unzipCallback == null) {
+ log.error("未配置业务编码:{}对应的解压回调接口", unzipTask.getBizCode());
+ return;
+ }
+ Boolean result = null;
+ try {
+ result = unzipCallback.callback(unzipTask.getBizId(), event.name());
+ } catch (Exception e) {
+ log.error("解压回调接口异常", e);
+ }
+ log.info("解压回调接口参数:{} 业务编码:{} 事件:{} 结果:{}", unzipTask.getBizId(), unzipTask.getBizCode(), event.name(), result);
+ }
+}
diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/listener/FileUploadedEvent.java b/blade-service/blade-file/src/main/java/org/springblade/file/listener/FileUploadedEvent.java
new file mode 100644
index 0000000..459193e
--- /dev/null
+++ b/blade-service/blade-file/src/main/java/org/springblade/file/listener/FileUploadedEvent.java
@@ -0,0 +1,21 @@
+package org.springblade.file.listener;
+
+import lombok.Getter;
+import org.springframework.context.ApplicationEvent;
+
+/**
+ * @author bfhuange
+ * @date 2024/11/8
+ */
+@Getter
+public class FileUploadedEvent extends ApplicationEvent {
+ /**
+ * 文件任务id
+ */
+ private final Long fileTaskId;
+
+ public FileUploadedEvent(Long fileTaskId) {
+ super(fileTaskId);
+ this.fileTaskId = fileTaskId;
+ }
+}
diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/listener/UnzipCompletedEvent.java b/blade-service/blade-file/src/main/java/org/springblade/file/listener/UnzipCompletedEvent.java
new file mode 100644
index 0000000..32a60ad
--- /dev/null
+++ b/blade-service/blade-file/src/main/java/org/springblade/file/listener/UnzipCompletedEvent.java
@@ -0,0 +1,22 @@
+package org.springblade.file.listener;
+
+import lombok.Getter;
+import org.springframework.context.ApplicationEvent;
+
+/**
+ * 解压完成事件
+ * @author bfhuange
+ * @date 2024/11/8
+ */
+@Getter
+public class UnzipCompletedEvent extends ApplicationEvent {
+ /**
+ * 解压任务id
+ */
+ private final Long unzipTaskId;
+
+ public UnzipCompletedEvent(Long unzipTaskId) {
+ super(unzipTaskId);
+ this.unzipTaskId = unzipTaskId;
+ }
+}
diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/listener/UnzipFailEvent.java b/blade-service/blade-file/src/main/java/org/springblade/file/listener/UnzipFailEvent.java
new file mode 100644
index 0000000..771d464
--- /dev/null
+++ b/blade-service/blade-file/src/main/java/org/springblade/file/listener/UnzipFailEvent.java
@@ -0,0 +1,22 @@
+package org.springblade.file.listener;
+
+import lombok.Getter;
+import org.springframework.context.ApplicationEvent;
+
+/**
+ * 解压失败事件
+ * @author bfhuange
+ * @date 2024/11/8
+ */
+@Getter
+public class UnzipFailEvent extends ApplicationEvent {
+ /**
+ * 解压任务id
+ */
+ private final Long unzipTaskId;
+
+ public UnzipFailEvent(Long unzipTaskId) {
+ super(unzipTaskId);
+ this.unzipTaskId = unzipTaskId;
+ }
+}
diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/listener/UnzipStartEvent.java b/blade-service/blade-file/src/main/java/org/springblade/file/listener/UnzipStartEvent.java
new file mode 100644
index 0000000..a317fc6
--- /dev/null
+++ b/blade-service/blade-file/src/main/java/org/springblade/file/listener/UnzipStartEvent.java
@@ -0,0 +1,35 @@
+package org.springblade.file.listener;
+
+import lombok.Getter;
+import org.springblade.file.pojo.entity.BusinessUnzipTask;
+import org.springblade.file.pojo.vo.FileVO;
+import org.springframework.context.ApplicationEvent;
+
+/**
+ * 解压开始事件
+ * @author bfhuange
+ * @date 2024/11/8
+ */
+@Getter
+public class UnzipStartEvent extends ApplicationEvent {
+ /**
+ * 解压任务id
+ */
+ private final Long unzipTaskId;
+ /**
+ * 解压任务
+ */
+ private final BusinessUnzipTask task;
+
+ public UnzipStartEvent(Long unzipTaskId) {
+ super(unzipTaskId);
+ this.unzipTaskId = unzipTaskId;
+ this.task = null;
+ }
+
+ public UnzipStartEvent(BusinessUnzipTask task) {
+ super(task);
+ this.task = task;
+ this.unzipTaskId = null;
+ }
+}
diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/mapper/AttachmentMapper.java b/blade-service/blade-file/src/main/java/org/springblade/file/mapper/AttachmentMapper.java
new file mode 100644
index 0000000..c607e20
--- /dev/null
+++ b/blade-service/blade-file/src/main/java/org/springblade/file/mapper/AttachmentMapper.java
@@ -0,0 +1,37 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.file.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.springblade.file.pojo.entity.Attachment;
+
+/**
+ * AttachmentMapper 接口
+ *
+ * @author zhaowei
+ */
+public interface AttachmentMapper extends BaseMapper {
+}
diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/mapper/AttachmentMapper.xml b/blade-service/blade-file/src/main/java/org/springblade/file/mapper/AttachmentMapper.xml
new file mode 100644
index 0000000..d5bc4eb
--- /dev/null
+++ b/blade-service/blade-file/src/main/java/org/springblade/file/mapper/AttachmentMapper.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/mapper/BusinessAttachmentMapper.java b/blade-service/blade-file/src/main/java/org/springblade/file/mapper/BusinessAttachmentMapper.java
new file mode 100644
index 0000000..65b5af0
--- /dev/null
+++ b/blade-service/blade-file/src/main/java/org/springblade/file/mapper/BusinessAttachmentMapper.java
@@ -0,0 +1,24 @@
+package org.springblade.file.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.springblade.file.pojo.dto.BusinessAttachmentQueryDTO;
+import org.springblade.file.pojo.entity.BusinessAttachment;
+import org.springblade.file.pojo.vo.BusinessAttachmentVO;
+
+import java.util.List;
+
+/**
+ * 业务附件信息表 Mapper 接口
+ *
+ * @author BladeX
+ * @since 2024-09-04
+ */
+public interface BusinessAttachmentMapper extends BaseMapper {
+
+ /**
+ * 查询业务附件列表
+ * @param param
+ * @return
+ */
+ List queryList(BusinessAttachmentQueryDTO param);
+}
diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/mapper/BusinessAttachmentMapper.xml b/blade-service/blade-file/src/main/java/org/springblade/file/mapper/BusinessAttachmentMapper.xml
new file mode 100644
index 0000000..0d3b4fd
--- /dev/null
+++ b/blade-service/blade-file/src/main/java/org/springblade/file/mapper/BusinessAttachmentMapper.xml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ select
+ bba.id,
+ bba.biz_id,
+ bba.biz_code,
+ bba.attachment_id,
+ bba.`desc`,
+ bba.sort,
+ ba.dir,
+ ba.attachment_name,
+ ba.size,
+ ba.object_key,
+ ba.create_user uploadUserId,
+ ba.create_user_name uploadUserName,
+ ba.create_time,
+ ba.create_user_name
+ from blade_business_attachment bba
+ left join blade_attachment ba on ba.id = bba.attachment_id
+ where bba.is_deleted = 0 and ba.is_deleted = 0 and bba.biz_id in #{item}
+
+ and bba.biz_code = #{bizCode}
+
+
+ and bba.biz_code in #{item}
+
+ order by bba.biz_id, bba.sort, bba.id
+
+
+
diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/mapper/BusinessUnzipDetailMapper.java b/blade-service/blade-file/src/main/java/org/springblade/file/mapper/BusinessUnzipDetailMapper.java
new file mode 100644
index 0000000..8c284af
--- /dev/null
+++ b/blade-service/blade-file/src/main/java/org/springblade/file/mapper/BusinessUnzipDetailMapper.java
@@ -0,0 +1,16 @@
+package org.springblade.file.mapper;
+
+import org.springblade.file.pojo.entity.BusinessUnzipDetail;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+
+/**
+ *
+ * 业务解压明细表 Mapper 接口
+ *
+ *
+ * @author bfhuange
+ * @since 2024-10-30
+ */
+public interface BusinessUnzipDetailMapper extends BaseMapper {
+
+}
diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/mapper/BusinessUnzipDetailMapper.xml b/blade-service/blade-file/src/main/java/org/springblade/file/mapper/BusinessUnzipDetailMapper.xml
new file mode 100644
index 0000000..ff49b0c
--- /dev/null
+++ b/blade-service/blade-file/src/main/java/org/springblade/file/mapper/BusinessUnzipDetailMapper.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ id, business_unzip_task_id, attachment_id, create_user, create_time, update_user, update_time, is_deleted
+
+
+
diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/mapper/BusinessUnzipTaskMapper.java b/blade-service/blade-file/src/main/java/org/springblade/file/mapper/BusinessUnzipTaskMapper.java
new file mode 100644
index 0000000..1b0ddfe
--- /dev/null
+++ b/blade-service/blade-file/src/main/java/org/springblade/file/mapper/BusinessUnzipTaskMapper.java
@@ -0,0 +1,37 @@
+package org.springblade.file.mapper;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import org.apache.ibatis.annotations.Param;
+import org.springblade.file.pojo.dto.BusinessUnzipTaskQueryDTO;
+import org.springblade.file.pojo.entity.BusinessUnzipTask;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.springblade.file.pojo.vo.BusinessUnzipDetailListVO;
+import org.springblade.file.pojo.vo.BusinessUnzipTaskListVO;
+
+import java.util.List;
+
+/**
+ *
+ * 业务解压任务表 Mapper 接口
+ *
+ *
+ * @author bfhuange
+ * @since 2024-10-28
+ */
+public interface BusinessUnzipTaskMapper extends BaseMapper {
+
+ /**
+ * 查询列表
+ * @param page
+ * @param param
+ * @return
+ */
+ List selectTaskList(IPage page, @Param("param") BusinessUnzipTaskQueryDTO param);
+
+ /**
+ * 根据业务id查询解压明细
+ * @param bizId
+ * @return
+ */
+ List queryBusinessUnzipDetail(@Param("bizId") Long bizId);
+}
diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/mapper/BusinessUnzipTaskMapper.xml b/blade-service/blade-file/src/main/java/org/springblade/file/mapper/BusinessUnzipTaskMapper.xml
new file mode 100644
index 0000000..3fb6be6
--- /dev/null
+++ b/blade-service/blade-file/src/main/java/org/springblade/file/mapper/BusinessUnzipTaskMapper.xml
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ id, biz_id, department_id, biz_code, doc_code, relate_type, relate_id, status, tenant_id, create_user, create_time, update_user, update_time, is_deleted
+
+
+ select
+ but.id,
+ but.biz_id,
+ but.biz_code,
+ but.doc_code,
+ but.relate_type,
+ but.relate_id,
+ but.department_id,
+ but.create_user b_create_user,
+ ft.md5,
+ ft.object_key,
+ ft.attachment_name,
+ ft.suffix,
+ ft.size,
+ ft.chunk_size,
+ ft.chunk_total,
+ ft.current_index,
+ ft.upload_id,
+ ft.status,
+ ft.create_user,
+ ft.create_user_name,
+ ft.create_time,
+ ft.update_user,
+ ft.update_user_name,
+ ft.update_time
+ from blade_business_unzip_task but
+ left join blade_file_task ft on ft.id = but.relate_id
+ where but.is_deleted = 0 and ft.is_deleted = 0
+
+ and but.biz_id in #{item}
+
+
+ and but.doc_code like concat('%',#{param.docCode},'%')
+
+
+ and but.relate_type = #{param.relateType}
+
+
+ and but.create_user = #{param.currentUserId}
+
+
+ and ft.attachment_name like concat('%',#{param.attachmentName},'%')
+
+
+ and ft.status = #{param.status}
+
+ order by Field(ft.status, 'uploading','failed', 'completed'), but.id desc
+
+
+ select
+ bbud.id,
+ bbud.attachment_id,
+ ba.dir,
+ ba.attachment_name,
+ ba.size,
+ ba.object_key,
+ ba.create_user uploadUserId,
+ ba.create_user_name uploadUserName,
+ ba.create_time,
+ ba.create_user_name
+ from blade_business_unzip_task bbut
+ left join blade_business_unzip_detail bbud on bbud.business_unzip_task_id = bbut.id
+ left join blade_attachment ba on bbud.attachment_id = ba.id
+ where bbut.is_deleted = 0 and bbud.is_deleted = 0 and ba.is_deleted = 0
+ and bbut.biz_id = #{bizId}
+
+
+
diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/mapper/FileTaskMapper.java b/blade-service/blade-file/src/main/java/org/springblade/file/mapper/FileTaskMapper.java
new file mode 100644
index 0000000..aa70585
--- /dev/null
+++ b/blade-service/blade-file/src/main/java/org/springblade/file/mapper/FileTaskMapper.java
@@ -0,0 +1,38 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.file.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.springblade.file.pojo.entity.FileTask;
+
+/**
+ * FileTaskMapper 接口
+ *
+ * @author zhaowei
+ */
+public interface FileTaskMapper extends BaseMapper {
+
+}
diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/mapper/FileTaskMapper.xml b/blade-service/blade-file/src/main/java/org/springblade/file/mapper/FileTaskMapper.xml
new file mode 100644
index 0000000..3a983c3
--- /dev/null
+++ b/blade-service/blade-file/src/main/java/org/springblade/file/mapper/FileTaskMapper.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/mapper/FileTaskPartMapper.java b/blade-service/blade-file/src/main/java/org/springblade/file/mapper/FileTaskPartMapper.java
new file mode 100644
index 0000000..fc3e278
--- /dev/null
+++ b/blade-service/blade-file/src/main/java/org/springblade/file/mapper/FileTaskPartMapper.java
@@ -0,0 +1,38 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.file.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.springblade.file.pojo.entity.FileTaskPart;
+
+/**
+ * FileUploadRecordMapper 接口
+ *
+ * @author zhaowei
+ */
+public interface FileTaskPartMapper extends BaseMapper {
+
+}
diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/mapper/FileTaskPartMapper.xml b/blade-service/blade-file/src/main/java/org/springblade/file/mapper/FileTaskPartMapper.xml
new file mode 100644
index 0000000..924e84b
--- /dev/null
+++ b/blade-service/blade-file/src/main/java/org/springblade/file/mapper/FileTaskPartMapper.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/service/IAttachmentService.java b/blade-service/blade-file/src/main/java/org/springblade/file/service/IAttachmentService.java
new file mode 100644
index 0000000..0a54e01
--- /dev/null
+++ b/blade-service/blade-file/src/main/java/org/springblade/file/service/IAttachmentService.java
@@ -0,0 +1,89 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.file.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import org.springblade.core.tool.api.R;
+import org.springblade.file.pojo.entity.Attachment;
+import org.springblade.file.pojo.vo.AttachmentDetailVO;
+import org.springblade.file.pojo.vo.FileVO;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.File;
+import java.util.List;
+
+/**
+ * 服务类
+ *
+ * @author zhaowei
+ */
+public interface IAttachmentService extends IService {
+
+ /**
+ * 批量上传
+ *
+ * @param files
+ * @param fileName 文件名,如果 files只有1个,且fileName不为空,设置文件名为 fileName
+ * @return
+ */
+ R> batchOssUpload(MultipartFile[] files, String fileName);
+
+ /**
+ * 获取附件,多个附件id用逗号分割
+ * @param id
+ * @return List
+ */
+ List getAttachment(String id);
+
+ /**
+ * 批量上传解压缩文件并删除文件
+ *
+ * @param files
+ * @param workDir 工作目录,取文件相对于工作目录的相对路径作为前缀
+ * @return
+ */
+ List batchOssUploadUnzipFilesAndDelete(List files, File workDir);
+
+ /**
+ * 新事务批量保存
+ * @param list
+ */
+ void batchSaveWithNewTransaction(List list);
+
+ /**
+ * 获取附件详情
+ * @param id
+ * @return
+ */
+ AttachmentDetailVO getDetail(Long id);
+
+ /**
+ * 解压文件并上传
+ * @param file
+ * @return
+ */
+ List unzipAndUpload(FileVO file);
+}
diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/service/IBusinessAttachmentService.java b/blade-service/blade-file/src/main/java/org/springblade/file/service/IBusinessAttachmentService.java
new file mode 100644
index 0000000..9b46ba8
--- /dev/null
+++ b/blade-service/blade-file/src/main/java/org/springblade/file/service/IBusinessAttachmentService.java
@@ -0,0 +1,51 @@
+package org.springblade.file.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import org.springblade.file.pojo.dto.BusinessAttachmentAddDTO;
+import org.springblade.file.pojo.dto.BusinessAttachmentBatchAddDTO;
+import org.springblade.file.pojo.dto.BusinessAttachmentDeleteDTO;
+import org.springblade.file.pojo.dto.BusinessAttachmentQueryDTO;
+import org.springblade.file.pojo.entity.BusinessAttachment;
+import org.springblade.file.pojo.vo.BusinessAttachmentVO;
+
+import java.util.List;
+
+/**
+ * 业务附件信息表 服务类
+ *
+ * @author BladeX
+ * @since 2024-09-04
+ */
+public interface IBusinessAttachmentService extends IService {
+ /**
+ * 保存
+ * @param param
+ * @return
+ */
+ boolean saveAndRemove(BusinessAttachmentBatchAddDTO param);
+
+ /**
+ * 保存并删除
+ *
+ * @param params
+ * @param deleteParam 业务id不为空,则先删除
+ * @return
+ */
+ boolean saveAndRemove(List params, BusinessAttachmentDeleteDTO deleteParam);
+
+ /**
+ * 根据bizId列表删除
+ *
+ * @param bizIds
+ * @return
+ */
+ boolean removeByBizIds(List bizIds);
+
+ /**
+ * 查询业务附件列表
+ *
+ * @param param
+ * @return
+ */
+ List queryList(BusinessAttachmentQueryDTO param);
+}
diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/service/IBusinessUnzipDetailService.java b/blade-service/blade-file/src/main/java/org/springblade/file/service/IBusinessUnzipDetailService.java
new file mode 100644
index 0000000..eda034a
--- /dev/null
+++ b/blade-service/blade-file/src/main/java/org/springblade/file/service/IBusinessUnzipDetailService.java
@@ -0,0 +1,23 @@
+package org.springblade.file.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import org.springblade.file.pojo.entity.BusinessUnzipDetail;
+import org.springblade.file.pojo.vo.FileVO;
+
+/**
+ *
+ * 业务解压明细表 服务类
+ *
+ *
+ * @author bfhuange
+ * @since 2024-10-30
+ */
+public interface IBusinessUnzipDetailService extends IService {
+
+ /**
+ * 异步解压并保存解压明细
+ * @param unzipTaskId 解压任务id
+ * @param file obs对象存储的key合md5值,md5有可能为空
+ */
+ void unzipAndSaveAsync(Long unzipTaskId, FileVO file);
+}
diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/service/IBusinessUnzipTaskService.java b/blade-service/blade-file/src/main/java/org/springblade/file/service/IBusinessUnzipTaskService.java
new file mode 100644
index 0000000..56f7531
--- /dev/null
+++ b/blade-service/blade-file/src/main/java/org/springblade/file/service/IBusinessUnzipTaskService.java
@@ -0,0 +1,82 @@
+package org.springblade.file.service;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import org.springblade.file.pojo.dto.BusinessUnzipTaskAddDTO;
+import org.springblade.file.pojo.dto.BusinessUnzipTaskQueryDTO;
+import org.springblade.file.pojo.dto.BusinessUnzipTaskUpdateDTO;
+import org.springblade.file.pojo.entity.BusinessUnzipTask;
+import com.baomidou.mybatisplus.extension.service.IService;
+import org.springblade.file.pojo.vo.BusinessUnzipDetailListVO;
+import org.springblade.file.pojo.vo.BusinessUnzipTaskListVO;
+import org.springblade.file.pojo.vo.FileVO;
+
+import java.util.List;
+
+/**
+ *
+ * 业务解压任务表 服务类
+ *
+ *
+ * @author bfhuange
+ * @since 2024-10-28
+ */
+public interface IBusinessUnzipTaskService extends IService {
+
+ /**
+ * 保存业务解压任务
+ * @param param
+ * @return
+ */
+ boolean save(BusinessUnzipTaskAddDTO param);
+
+ /**
+ * 获取obs对象存储的key
+ * @param task
+ * @return
+ */
+ String getObjectKey(BusinessUnzipTask task);
+
+ /**
+ * 获取文件对象
+ * @param task
+ * @return
+ */
+ FileVO getFile(BusinessUnzipTask task);
+
+ /**
+ * 业务解压任务分页查询
+ * @param page
+ * @param param
+ * @return
+ */
+ IPage selectPage(IPage page, BusinessUnzipTaskQueryDTO param);
+
+ /**
+ * 业务文件解压任务更新(重新上传)
+ * @param param
+ * @return
+ */
+ boolean updateReupload(BusinessUnzipTaskUpdateDTO param);
+
+ /**
+ * 根据业务id删除业务文件解压任务
+ * @param bizId
+ * @param cancelFile 是否取消文件,如果取消,上传完成后取消会报错
+ * @return
+ */
+ boolean cancelBusinessUnzipTask(Long bizId, boolean cancelFile);
+
+ /**
+ * 根据业务id查询解压明细
+ * @param bizId
+ * @return
+ */
+ List queryBusinessUnzipDetail(Long bizId);
+
+ /**
+ * 根据业务id列表查询解压任务列表
+ * @param bizIds
+ * @return
+ */
+ List getBusinessUnzipTasksByBizIds(List bizIds);
+}
diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/service/IFileHandleService.java b/blade-service/blade-file/src/main/java/org/springblade/file/service/IFileHandleService.java
new file mode 100644
index 0000000..bffbbee
--- /dev/null
+++ b/blade-service/blade-file/src/main/java/org/springblade/file/service/IFileHandleService.java
@@ -0,0 +1,20 @@
+package org.springblade.file.service;
+
+import org.springblade.file.pojo.vo.FileHandleVO;
+import org.springblade.file.pojo.vo.FileVO;
+
+/**
+ * 文件处理
+ * @author bfhuange
+ * @date 2024/10/22
+ */
+public interface IFileHandleService {
+
+ /**
+ * 下载文件并解压
+ *
+ * @param file obs对象存储的key合md5,md5可能为空
+ * @return
+ */
+ FileHandleVO downloadAndUnzip(FileVO file);
+}
diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/service/IFileService.java b/blade-service/blade-file/src/main/java/org/springblade/file/service/IFileService.java
new file mode 100644
index 0000000..f021fdf
--- /dev/null
+++ b/blade-service/blade-file/src/main/java/org/springblade/file/service/IFileService.java
@@ -0,0 +1,122 @@
+package org.springblade.file.service;
+
+import org.springblade.file.pojo.vo.PartEtagVO;
+
+import java.io.File;
+import java.io.InputStream;
+import java.util.List;
+
+/**
+ * 文件接口
+ * @author bfhuange
+ * @date 2024/10/21
+ */
+public interface IFileService {
+
+ /**
+ * 上传文件
+ * @param objectKey 文件key
+ * @param is 输入流
+ */
+ void uploadFile(String objectKey, InputStream is);
+
+ /**
+ * 上传临时文件
+ * @param objectKey 文件key
+ * @param is 输入流
+ * @param expires 过期时间,单位天,过期后自动删除
+ */
+ String uploadTempFile(String objectKey, InputStream is, Integer expires);
+
+ /**
+ * 上传文件
+ * @param objectKey 文件key
+ * @param file 文件
+ */
+ void uploadFile(String objectKey, File file);
+
+ /**
+ * 初始化分片上传
+ *
+ * @param objectKey 文件key
+ * @return 上传任务id
+ */
+ String initiateMultipartUpload(String objectKey);
+
+ /**
+ * 分段上传
+ * @param objectKey 文件key
+ * @param uploadId 上传任务id
+ * @param file 文件
+ * @param partNumber 分段编号
+ * @param offset 偏移位
+ * @param partSize 分段大小
+ * @return
+ */
+ PartEtagVO uploadPart(String objectKey, String uploadId, File file, int partNumber, Long offset, Long partSize);
+
+ /**
+ * 完成分段上传任务
+ * @param objectKey 文件key
+ * @param uploadId 上传任务id
+ * @param partEtagVOS 要合并的分段
+ */
+ void completeMultipartUpload(String objectKey, String uploadId, List partEtagVOS);
+
+ /**
+ * 取消分段上传任务
+ * @param objectKey
+ * @param uploadId
+ */
+ void cancelMultipartUpload(String objectKey, String uploadId);
+
+ /**
+ * 获取文件链接
+ * @param objectKey 文件key
+ * @param expires 过期时间,单位秒
+ * @return
+ */
+ String getFileUrl(String objectKey, Long expires);
+
+ /**
+ * 获取文件链接
+ * @param objectKey 文件key
+ * @param fileName 文件名
+ * @param expires 过期时间,单位秒
+ * @return
+ */
+ String getFileUrl(String objectKey, String fileName, Long expires);
+
+ /**
+ * 获取文件上传链接
+ *
+ * @param objectKey 文件key
+ * @param uploadId
+ * @param partNumber
+ * @param expires 过期时间,单位秒
+ * @return
+ */
+ String getUploadPartUrl(String objectKey, String uploadId, Integer partNumber, Long expires);
+
+ /**
+ * 获取文件链接批量
+ * @param objectKeys
+ * @param expires 过期时间
+ * @return
+ */
+ List getFileUrls(List objectKeys, Long expires);
+
+ /**
+ * 下载文件
+ * @param objectKey
+ * @param downloadFile 下载路径
+ */
+ void downloadFile(String objectKey, String downloadFile);
+
+ /**
+ * 获取文件md5
+ * @param file
+ * @return
+ */
+ String getFileMd5(File file);
+}
diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/service/IFileTaskPartService.java b/blade-service/blade-file/src/main/java/org/springblade/file/service/IFileTaskPartService.java
new file mode 100644
index 0000000..844aa44
--- /dev/null
+++ b/blade-service/blade-file/src/main/java/org/springblade/file/service/IFileTaskPartService.java
@@ -0,0 +1,38 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.file.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import org.springblade.file.pojo.entity.FileTaskPart;
+
+/**
+ * 服务类
+ *
+ * @author zhaowei
+ */
+public interface IFileTaskPartService extends IService {
+
+}
diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/service/IFileTaskService.java b/blade-service/blade-file/src/main/java/org/springblade/file/service/IFileTaskService.java
new file mode 100644
index 0000000..3a4a992
--- /dev/null
+++ b/blade-service/blade-file/src/main/java/org/springblade/file/service/IFileTaskService.java
@@ -0,0 +1,94 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.file.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import org.springblade.file.pojo.dto.FileTaskCreateDTO;
+import org.springblade.file.pojo.dto.FileTaskUpdateDTO;
+import org.springblade.file.pojo.entity.FileTask;
+import org.springblade.file.pojo.vo.FileTaskUpdateVO;
+
+import java.util.List;
+
+/**
+ * 服务类
+ *
+ * @author zhaowei
+ */
+public interface IFileTaskService extends IService {
+
+ /**
+ * 根据文件md5值查询最新一条上传任务记录
+ *
+ * @param id
+ * @param md5
+ * @return
+ */
+ FileTaskUpdateVO queryFileTask(Long id, String md5);
+
+ /**
+ * 创建文件任务
+ * @param param
+ * @return
+ */
+ Long createFileTask(FileTaskCreateDTO param);
+
+ /**
+ * 更新文件任务进度
+ * @param param
+ * @return
+ */
+ FileTaskUpdateVO updateFileTask(FileTaskUpdateDTO param);
+
+ /**
+ * 根据文件任务id获取上传链接
+ *
+ * @param id
+ * @param partNumber
+ * @return
+ */
+ String getUploadUrl(Long id, Integer partNumber);
+
+ /**
+ * 取消文件任务
+ * @param taskIds
+ */
+ void cancelBatchByIds(List taskIds);
+
+ /**
+ * 更新文件任务状态为失败
+ * @param id
+ * @return
+ */
+ FileTaskUpdateVO updateFailed(Long id);
+
+ /**
+ * 根据id列表更新文件任务状态为暂停
+ * @param param
+ * @return
+ */
+ boolean updatePaused(List param);
+}
diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/service/IOCRConvertService.java b/blade-service/blade-file/src/main/java/org/springblade/file/service/IOCRConvertService.java
new file mode 100644
index 0000000..6e6784c
--- /dev/null
+++ b/blade-service/blade-file/src/main/java/org/springblade/file/service/IOCRConvertService.java
@@ -0,0 +1,19 @@
+package org.springblade.file.service;
+
+import org.springblade.file.pojo.dto.FileCertificateBatchRecognitionDTO;
+import org.springblade.thirdparty.ocr.pojo.vo.TransportCertificateVO;
+
+/**
+ * ocr转换
+ * @author bfhuange
+ * @since 2025/3/13
+ */
+public interface IOCRConvertService {
+
+ /**
+ * 凭证批量识别,不分类
+ * @param param
+ * @return
+ */
+ TransportCertificateVO recognitionTransportCertificate(FileCertificateBatchRecognitionDTO param);
+}
diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/service/impl/AttachmentServiceImpl.java b/blade-service/blade-file/src/main/java/org/springblade/file/service/impl/AttachmentServiceImpl.java
new file mode 100644
index 0000000..67bd5fb
--- /dev/null
+++ b/blade-service/blade-file/src/main/java/org/springblade/file/service/impl/AttachmentServiceImpl.java
@@ -0,0 +1,255 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.file.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.IdWorker;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.io.FileUtils;
+import org.springblade.core.log.exception.ServiceException;
+import org.springblade.core.tool.api.R;
+import org.springblade.core.tool.utils.CollectionUtil;
+import org.springblade.core.tool.utils.SpringUtil;
+import org.springblade.core.tool.utils.StringPool;
+import org.springblade.core.tool.utils.StringUtil;
+import org.springblade.file.config.ObsProperties;
+import org.springblade.file.convert.BusinessAttachmentConvert;
+import org.springblade.file.exception.UnzipFailException;
+import org.springblade.file.mapper.AttachmentMapper;
+import org.springblade.file.pojo.entity.Attachment;
+import org.springblade.file.pojo.vo.AttachmentDetailVO;
+import org.springblade.file.pojo.vo.FileHandleVO;
+import org.springblade.file.pojo.vo.FileVO;
+import org.springblade.file.service.IAttachmentService;
+import org.springblade.file.service.IFileHandleService;
+import org.springblade.file.service.IFileService;
+import org.springblade.file.util.ObjectKeyUtils;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Propagation;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.File;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * 服务实现类
+ * @author zhaowei
+ */
+@Service
+@Slf4j
+@RequiredArgsConstructor
+public class AttachmentServiceImpl extends ServiceImpl implements IAttachmentService {
+
+ private final IFileService fileService;
+ private final BusinessAttachmentConvert convert;
+ private final ObsProperties obsProperties;
+ private final IFileHandleService fileHandleService;
+ private IAttachmentService self;
+
+ @Override
+ public R> batchOssUpload(MultipartFile[] files, String overWriteFileName){
+ try {
+ List attachmentList = new ArrayList<>();
+ // 文件数量为1个,且重写的文件名不为空,使用重写的文件名,给uniapp上传使用,uniapp上传的文件名不是原始文件名
+ boolean useOverWriteFileName = files.length == 1 && StringUtil.isNotBlank(overWriteFileName);
+ for(MultipartFile file : files){
+ String fileName = useOverWriteFileName ? overWriteFileName : file.getOriginalFilename();
+ String objectkey = ObjectKeyUtils.generateObjectKey(fileName);
+ fileService.uploadFile(objectkey, file.getInputStream());
+ // 插入附件数据
+ Attachment attachment = new Attachment();
+ Long id = IdWorker.getId();
+ attachment.setId(id);
+ attachment.setAttachmentName(fileName);
+ attachment.setSize(file.getSize());
+ attachment.setObjectKey(objectkey);
+ attachmentList.add(attachment);
+ }
+ this.saveBatch(attachmentList);
+ log.info("OBS上传 successfully");
+ return R.data(attachmentList,"OBS上传 successfully");
+ } catch (Exception e) {
+ log.error("OBS上传 failed,Exception", e);
+ }
+ return R.fail("OBS上传 failed");
+ }
+
+ @Override
+ public List getAttachment(String id) {
+ return this.list(new LambdaQueryWrapper()
+ .in(Attachment::getId, Arrays.stream(id.split(",")).toList())
+ );
+ }
+
+ @Override
+ public List batchOssUploadUnzipFilesAndDelete(List files, File workDir) {
+ if (CollectionUtil.isEmpty(files)) {
+ return Collections.emptyList();
+ }
+ List attachmentList = new ArrayList<>();
+ AtomicInteger index = new AtomicInteger(1);
+ try {
+ log.info("上传文件总数:{}", files.size());
+ for(File file : files){
+ String fileName = file.getName();
+ // 压缩文件所在压缩包父目录相对路径
+ String dir = this.getDir(workDir, file);
+ // 获取上传的目录前缀
+ String prefixDir = getUploadPrefix();
+ String objectkey = prefixDir + ObjectKeyUtils.generateObjectKey(fileName);
+ fileService.uploadFile(objectkey, file);
+ int completeFiles = index.getAndIncrement();
+ if (completeFiles % 10 == 0) {
+ log.info("上传文件总进度:{}/{}", completeFiles, files.size());
+ }
+ // 插入附件数据
+ Attachment attachment = new Attachment();
+ Long id = IdWorker.getId();
+ attachment.setId(id);
+ attachment.setDir(dir);
+ attachment.setAttachmentName(fileName);
+ attachment.setSize(file.length());
+ attachment.setObjectKey(objectkey);
+ attachmentList.add(attachment);
+ }
+ log.info("上传文件完成:{}", files.size());
+ // 新事务批量保存
+ this.getSelf().batchSaveWithNewTransaction(attachmentList);
+ log.info("OBS上传 successfully");
+ } catch (Exception e) {
+ log.error("OBS上传失败", e);
+ throw new ServiceException("OBS上传失败");
+ }
+ // 返回新增的附件列表
+ return attachmentList;
+ }
+
+ /**
+ * 获取上传文件目录前缀
+ * @return
+ */
+ private String getUploadPrefix() {
+ // 上传路径前缀 = 配置的解压上传路径 + /
+ return obsProperties.getUnzipUploadDir() + StringPool.SLASH;
+ }
+
+ /**
+ * 获取压缩文件所在压缩包父目录的相对路径,结尾不带/
+ * @param workDir 工作目录,解压的根目录
+ * @param file 解压后的文件
+ * @return
+ */
+ private String getDir(File workDir, File file) {
+ if (workDir == null) {
+ return StringPool.EMPTY;
+ }
+ Path workDirPath = workDir.toPath();
+ Path filePath = file.toPath();
+ return workDirPath.relativize(filePath).toString()
+ // 去掉文件名以及文件所在父目录的后缀/或\
+ .replace(File.separator + file.getName(), "")
+ // 将文件分隔符替换为/,windows下是\,需要处理
+ .replace(File.separator, StringPool.SLASH);
+ }
+
+ @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
+ @Override
+ public void batchSaveWithNewTransaction(List list) {
+ this.saveBatch(list);
+ }
+
+ @Override
+ public AttachmentDetailVO getDetail(Long id) {
+ if (id == null) {
+ return null;
+ }
+ Attachment attachment = this.getById(id);
+ if (attachment == null) {
+ return null;
+ }
+ AttachmentDetailVO detailVO = convert.entity2vo(attachment);
+ // 查询下载链接
+ String fileUrl = fileService.getFileUrl(attachment.getObjectKey(), null);
+ detailVO.setUrl(fileUrl);
+ return detailVO;
+ }
+
+ @Override
+ public List unzipAndUpload(FileVO file) {
+ // 1. 下载并解压文件
+ try {
+ FileHandleVO fileHandleVO = fileHandleService.downloadAndUnzip(file);
+ List unzippedFiles = fileHandleVO.getUnzippedFiles();
+ if (CollectionUtil.isEmpty(unzippedFiles)) {
+ log.warn("下载解压文件为空 objectKey:{}", file.getObjectKey());
+ throw new UnzipFailException("解压文件为空", fileHandleVO.getUnzippedDir());
+ }
+ File unzippedDir = fileHandleVO.getUnzippedDir();
+ // 2. 上传文件
+ long start = System.currentTimeMillis();
+ try {
+ List attachments = getSelf().batchOssUploadUnzipFilesAndDelete(unzippedFiles, unzippedDir);
+ if (CollectionUtil.isEmpty(attachments)) {
+ log.warn("上传解压文件为空 objectKey:{}", file.getObjectKey());
+ return Collections.emptyList();
+ }
+ // 删除下载的压缩文件
+ fileHandleVO.getDownloadFile().delete();
+ // 删除解压后的目录
+ FileUtils.deleteQuietly(unzippedDir);
+ return attachments;
+ } finally {
+ long end = System.currentTimeMillis();
+ log.info("文件:{} 解压后上传完成 耗时:{}", file.getObjectKey(), end - start);
+ }
+ } catch (UnzipFailException e) {
+ // 文件解压失败,删除解压后的文件
+ File unzipDir = e.getUnzipDir();
+ // 删除解压后的目录
+ FileUtils.deleteQuietly(unzipDir);
+ throw e;
+ }
+ }
+
+ /**
+ * 获取自己
+ * @return
+ */
+ private IAttachmentService getSelf() {
+ if (self == null) {
+ self = SpringUtil.getBean(IAttachmentService.class);
+ }
+ return self;
+ }
+}
diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/service/impl/BusinessAttachmentServiceImpl.java b/blade-service/blade-file/src/main/java/org/springblade/file/service/impl/BusinessAttachmentServiceImpl.java
new file mode 100644
index 0000000..db199e7
--- /dev/null
+++ b/blade-service/blade-file/src/main/java/org/springblade/file/service/impl/BusinessAttachmentServiceImpl.java
@@ -0,0 +1,136 @@
+package org.springblade.file.service.impl;
+
+import cn.hutool.core.collection.CollectionUtil;
+import com.baomidou.mybatisplus.core.toolkit.IdWorker;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springblade.core.secure.utils.AuthUtil;
+import org.springblade.core.tool.constant.BladeConstant;
+import org.springblade.core.tool.utils.StringUtil;
+import org.springblade.file.convert.BusinessAttachmentConvert;
+import org.springblade.file.mapper.BusinessAttachmentMapper;
+import org.springblade.file.pojo.dto.BusinessAttachmentAddDTO;
+import org.springblade.file.pojo.dto.BusinessAttachmentBatchAddDTO;
+import org.springblade.file.pojo.dto.BusinessAttachmentDeleteDTO;
+import org.springblade.file.pojo.dto.BusinessAttachmentQueryDTO;
+import org.springblade.file.pojo.entity.BusinessAttachment;
+import org.springblade.file.pojo.vo.BusinessAttachmentVO;
+import org.springblade.file.service.IBusinessAttachmentService;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * 业务附件信息表 服务实现类
+ *
+ * @author BladeX
+ * @since 2024-09-04
+ */
+@Slf4j
+@RequiredArgsConstructor
+@Service
+public class BusinessAttachmentServiceImpl extends ServiceImpl implements IBusinessAttachmentService {
+
+ private final BusinessAttachmentConvert convert;
+
+ @Transactional(rollbackFor = Exception.class)
+ @Override
+ public boolean saveAndRemove(BusinessAttachmentBatchAddDTO param) {
+ if (param == null) {
+ log.warn("批量保存业务附件,参数为空");
+ return false;
+ }
+ if (param.isRemove() && param.getBizId() != null) {
+ // 先删除
+ this.removeByBizIds(List.of(param.getBizId()));
+ }
+ List addList = convert.dto2entities(param);
+ if (CollectionUtil.isNotEmpty(addList)) {
+ return this.saveBatch(addList);
+ }
+ return true;
+ }
+
+ @Transactional(rollbackFor = Exception.class)
+ @Override
+ public boolean saveAndRemove(List params, BusinessAttachmentDeleteDTO deleteParam) {
+ boolean hasBizIds = deleteParam != null && deleteParam.isDelete() && CollectionUtil.isNotEmpty(deleteParam.getBizIds());
+ if (hasBizIds) {
+ // bizIds不为空,先删除
+ this.removeByBizIdsAndCodes(deleteParam.getBizIds(), deleteParam.getBizCodes());
+ }
+ if (CollectionUtil.isEmpty(params)) {
+ log.warn("保存业务附件,参数为空");
+ return true;
+ }
+ if (!hasBizIds && (deleteParam == null || deleteParam.isDelete())) {
+ // bizIds为空,从参数提取bizIds,再删除
+ // 删除参数为空,默认删除,删除参数中delete为true默认删除,false不删除
+ List bizIds = params.stream()
+ .map(BusinessAttachmentAddDTO::getBizId)
+ .distinct()
+ .toList();
+ this.removeByBizIds(bizIds);
+ }
+
+ List businessAttachments = convert.dtos2entities(params);
+
+ // 按照业务id分组排序
+ Map> bizIdAttachmentsMap = businessAttachments.stream()
+ .collect(Collectors.groupingBy(BusinessAttachment::getBizId));
+ String tenantId = AuthUtil.getTenantId();
+ for (Map.Entry> entry : bizIdAttachmentsMap.entrySet()) {
+ int i = 1;
+ for (BusinessAttachment businessAttachment : entry.getValue()) {
+ businessAttachment.setId(IdWorker.getId());
+ // 处理排序
+ businessAttachment.setSort(i++);
+ if (StringUtil.isBlank(tenantId)) {
+ businessAttachment.setTenantId(BladeConstant.ADMIN_TENANT_ID);
+ }
+ }
+ }
+ return this.saveBatch(businessAttachments);
+ }
+
+ @Transactional(rollbackFor = Exception.class)
+ @Override
+ public boolean removeByBizIds(List bizIds) {
+ return this.removeByBizIdsAndCodes(bizIds, null);
+ }
+
+ @Override
+ public List queryList(BusinessAttachmentQueryDTO param) {
+ return baseMapper.queryList(param);
+ }
+
+ /**
+ * 批量删除
+ * @param bizIds 业务id列表,为空,不删除
+ * @param bizCodes 业务编码列表,为空,删除所有
+ * @return
+ */
+ private boolean removeByBizIdsAndCodes(List bizIds, List bizCodes) {
+ if (CollectionUtil.isEmpty(bizIds)) {
+ log.warn("删除附件,参数为空");
+ return false;
+ }
+ List ids = this.list(Wrappers.lambdaQuery()
+ .in(BusinessAttachment::getBizId, bizIds)
+ // 业务编码列表不为空,添加业务编码查询条件
+ .in(CollectionUtil.isNotEmpty(bizCodes), BusinessAttachment::getBizCode, bizCodes)
+ ).stream()
+ .map(BusinessAttachment::getId)
+ .toList();
+ if (CollectionUtil.isNotEmpty(ids)) {
+ return this.removeBatchByIds(ids);
+ }
+ return true;
+ }
+}
diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/service/impl/BusinessUnzipDetailServiceImpl.java b/blade-service/blade-file/src/main/java/org/springblade/file/service/impl/BusinessUnzipDetailServiceImpl.java
new file mode 100644
index 0000000..ce66261
--- /dev/null
+++ b/blade-service/blade-file/src/main/java/org/springblade/file/service/impl/BusinessUnzipDetailServiceImpl.java
@@ -0,0 +1,71 @@
+package org.springblade.file.service.impl;
+
+import com.alibaba.fastjson2.JSON;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springblade.core.tool.utils.CollectionUtil;
+import org.springblade.file.exception.UnzipFailException;
+import org.springblade.file.listener.UnzipCompletedEvent;
+import org.springblade.file.listener.UnzipFailEvent;
+import org.springblade.file.mapper.BusinessUnzipDetailMapper;
+import org.springblade.file.pojo.entity.Attachment;
+import org.springblade.file.pojo.entity.BusinessUnzipDetail;
+import org.springblade.file.pojo.vo.FileVO;
+import org.springblade.file.service.IAttachmentService;
+import org.springblade.file.service.IBusinessUnzipDetailService;
+import org.springframework.context.ApplicationContext;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * @author bfhuange
+ * @date 2024/10/30
+ */
+@AllArgsConstructor
+@Slf4j
+@Service
+public class BusinessUnzipDetailServiceImpl extends ServiceImpl implements IBusinessUnzipDetailService {
+
+ private final IAttachmentService attachmentService;
+ private final ApplicationContext applicationContext;
+
+ @Async
+ @Override
+ public void unzipAndSaveAsync(Long unzipTaskId, FileVO file) {
+ log.info("解压文件并上传开始 unzipTaskId: {} file: {}", unzipTaskId, JSON.toJSONString(file));
+ long start = System.currentTimeMillis();
+ try {
+ // 1. 上传文件并解压
+ List attachments = attachmentService.unzipAndUpload(file);
+ if (CollectionUtil.isEmpty(attachments)) {
+ log.warn("上传解压文件为空 unzipTaskId:{} objectKey:{}", unzipTaskId, file.getObjectKey());
+ return;
+ }
+ // 2. 保存压缩文件和解压文件关系
+ List addParams = attachments.stream()
+ .map(attachment -> {
+ BusinessUnzipDetail addParam = new BusinessUnzipDetail();
+ addParam.setBusinessUnzipTaskId(unzipTaskId);
+ addParam.setAttachmentId(attachment.getId());
+ return addParam;
+ }).toList();
+ if (CollectionUtil.isNotEmpty(addParams)) {
+ this.saveBatch(addParams);
+ }
+ // 3. 发布解压完成事件
+ applicationContext.publishEvent(new UnzipCompletedEvent(unzipTaskId));
+ } catch (UnzipFailException e) {
+ log.error("解压文件异常", e);
+ // 发布解压失败事件
+ applicationContext.publishEvent(new UnzipFailEvent(unzipTaskId));
+ } catch (Exception e) {
+ log.error("解压文件并上传异常", e);
+ } finally {
+ long end = System.currentTimeMillis();
+ log.info("解压文件并上传 完成 unzipTaskId: {} objectKey: {} 耗时:{}", unzipTaskId, file.getObjectKey(), end - start);
+ }
+ }
+}
diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/service/impl/BusinessUnzipTaskServiceImpl.java b/blade-service/blade-file/src/main/java/org/springblade/file/service/impl/BusinessUnzipTaskServiceImpl.java
new file mode 100644
index 0000000..6bee0d2
--- /dev/null
+++ b/blade-service/blade-file/src/main/java/org/springblade/file/service/impl/BusinessUnzipTaskServiceImpl.java
@@ -0,0 +1,229 @@
+package org.springblade.file.service.impl;
+
+import com.alibaba.fastjson2.JSON;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.google.common.collect.Lists;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springblade.common.constant.DictTypeEnum;
+import org.springblade.core.log.exception.ServiceException;
+import org.springblade.core.log.utils.AssertUtils;
+import org.springblade.core.tool.utils.CollectionUtil;
+import org.springblade.core.tool.utils.StringUtil;
+import org.springblade.file.convert.FileTaskConvert;
+import org.springblade.file.listener.UnzipStartEvent;
+import org.springblade.file.pojo.dto.BusinessUnzipTaskAddDTO;
+import org.springblade.file.pojo.dto.BusinessUnzipTaskQueryDTO;
+import org.springblade.file.pojo.dto.BusinessUnzipTaskUpdateDTO;
+import org.springblade.file.pojo.entity.Attachment;
+import org.springblade.file.pojo.entity.BusinessUnzipTask;
+import org.springblade.file.mapper.BusinessUnzipTaskMapper;
+import org.springblade.file.pojo.entity.FileTask;
+import org.springblade.file.pojo.enums.FileTaskStatus;
+import org.springblade.file.pojo.enums.UnzipTask;
+import org.springblade.file.pojo.vo.BusinessUnzipDetailListVO;
+import org.springblade.file.pojo.vo.BusinessUnzipTaskListVO;
+import org.springblade.file.pojo.vo.FileVO;
+import org.springblade.file.service.IAttachmentService;
+import org.springblade.file.service.IBusinessUnzipDetailService;
+import org.springblade.file.service.IBusinessUnzipTaskService;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.springblade.file.service.IFileTaskService;
+import org.springblade.system.cache.DictCache;
+import org.springframework.context.ApplicationContext;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ *
+ * 业务解压任务表 服务实现类
+ *
+ *
+ * @author bfhuange
+ * @since 2024-10-28
+ */
+@Slf4j
+@AllArgsConstructor
+@Service
+public class BusinessUnzipTaskServiceImpl extends ServiceImpl implements IBusinessUnzipTaskService {
+
+ private final FileTaskConvert convert;
+ private final IFileTaskService fileTaskService;
+ private final IBusinessUnzipDetailService detailService;
+ private final IAttachmentService attachmentService;
+ private final ApplicationContext applicationContext;
+
+ @Transactional(rollbackFor = Exception.class)
+ @Override
+ public boolean save(BusinessUnzipTaskAddDTO param) {
+ BusinessUnzipTask addParam = convert.dto2entity(param);
+ String status = getStatus(param.getRelateType(), param.getRelateId());
+ addParam.setStatus(status);
+ boolean result = this.save(addParam);
+ if (result && UnzipTask.Status.UPLOADED.getCode().equals(status)) {
+ // 解压任务状态是上传完成,发布解压开始事件
+ applicationContext.publishEvent(new UnzipStartEvent(addParam));
+ }
+ return result;
+ }
+
+ @Override
+ public String getObjectKey(BusinessUnzipTask task) {
+ return this.getFile(task).getObjectKey();
+ }
+
+ @Override
+ public FileVO getFile(BusinessUnzipTask task) {
+ AssertUtils.notNull(task, "文件任务不存在");
+ String relateType = task.getRelateType();
+ AssertUtils.notBlank(relateType, "文件任务关联类型不能为空");
+ Long relateId = task.getRelateId();
+ AssertUtils.notNull(relateId, "文件任务关联id不能为空");
+ if (UnzipTask.RelateType.FILE_TASK.getCode().equals(relateType)) {
+ // 文件任务关联类型
+ FileTask fileTask = fileTaskService.getById(relateId);
+ AssertUtils.notNull(fileTask, "文件任务不存在");
+ return new FileVO(fileTask.getObjectKey(), fileTask.getMd5());
+ }
+ if (UnzipTask.RelateType.ATTACHMENT.getCode().equals(relateType)) {
+ // 附件关联类型
+ Attachment attachment = attachmentService.getById(relateId);
+ AssertUtils.notNull(attachment, "附件不存在");
+ // 附件暂时没有md5
+ return new FileVO(attachment.getObjectKey(), null);
+ }
+ throw new ServiceException("文件任务关联类型错误");
+ }
+
+ @Override
+ public IPage selectPage(IPage page, BusinessUnzipTaskQueryDTO param) {
+ List list = baseMapper.selectTaskList(page, param);
+ if (CollectionUtil.isNotEmpty(list)) {
+ // 处理状态枚举值名称
+ list.forEach(vo -> {
+ String status = vo.getStatus();
+ if (StringUtil.isNotBlank(status)) {
+ vo.setStatusStr(DictCache.getValue(DictTypeEnum.FILE_TASK_STATUS.getType(), vo.getStatus()));
+ }
+ });
+ }
+ IPage newPage = new Page<>(page.getCurrent(), page.getSize(), page.getTotal());
+ return newPage.setRecords(list);
+ }
+
+ @Transactional(rollbackFor = Exception.class)
+ @Override
+ public boolean updateReupload(BusinessUnzipTaskUpdateDTO param) {
+ log.info("修改业务解压任务重新上传参数:{}", JSON.toJSONString(param));
+ if (param == null || (param.getId() == null && param.getBizId() == null)) {
+ throw new ServiceException("参数不能为空");
+ }
+ // 校验业务解压任务
+ BusinessUnzipTask unzipTask = param.getId() != null ? this.getById(param.getId()) : this.getOne(Wrappers.lambdaQuery()
+ .eq(BusinessUnzipTask::getBizId, param.getBizId())
+ .last("limit 1")
+ );
+ AssertUtils.notNull(unzipTask, "业务任务不存在");
+ // 上传中或解压失败才可以重新上传
+ AssertUtils.isTrue(UnzipTask.Status.UPLOADING.getCode().equals(unzipTask.getStatus())
+ || UnzipTask.Status.UNZIP_FAIL.getCode().equals(unzipTask.getStatus()), "只有上传中或解压失败的任务才可以重新上传");
+
+ if (UnzipTask.Status.UPLOADING.getCode().equals(unzipTask.getStatus())) {
+ // 上传中的才需要取消上传任务,解压失败的无需取消
+ try {
+ // 取消文件任务
+ fileTaskService.cancelBatchByIds(Lists.newArrayList(unzipTask.getRelateId()));
+ } catch (Exception e) {
+ log.error("取消上传异常", e);
+ }
+ }
+
+ // 更新业务解压任务
+ BusinessUnzipTask updateParam = new BusinessUnzipTask();
+ updateParam.setId(unzipTask.getId());
+ updateParam.setRelateId(param.getRelateId());
+ String status = getStatus(param.getRelateType(), param.getRelateId());
+ updateParam.setStatus(status);
+ boolean result = this.updateById(updateParam);
+ if (result && UnzipTask.Status.UPLOADED.getCode().equals(status)) {
+ // 重新查询,防止解压开始事件中查询时事务未提交查询不到最新的数据
+ unzipTask = this.getById(unzipTask.getId());
+ // 解压任务状态是上传完成,发布解压开始事件
+ applicationContext.publishEvent(new UnzipStartEvent(unzipTask));
+ }
+ return result;
+ }
+
+ @Transactional(rollbackFor = Exception.class)
+ @Override
+ public boolean cancelBusinessUnzipTask(Long bizId, boolean cancelFile) {
+ List list = this.list(Wrappers.lambdaQuery()
+ .eq(BusinessUnzipTask::getBizId, bizId)
+ );
+ if (CollectionUtil.isEmpty(list)) {
+ return true;
+ }
+ if (cancelFile) {
+ boolean hasUpload = list.stream()
+ .anyMatch(task -> !UnzipTask.Status.UPLOADING.getCode().equals(task.getStatus()));
+ AssertUtils.isFalse(hasUpload, "当前任务已上传完成,不能取消");
+ // 取消文件任务
+ List taskIds = list.stream()
+ .filter(task -> UnzipTask.RelateType.FILE_TASK.getCode().equals(task.getRelateType()))
+ .map(BusinessUnzipTask::getRelateId)
+ .toList();
+ fileTaskService.cancelBatchByIds(taskIds);
+ }
+ // 删除业务文件解压任务
+ List unzipTaskIds = list.stream()
+ .map(BusinessUnzipTask::getId)
+ .toList();
+ this.removeBatchByIds(unzipTaskIds);
+ return true;
+ }
+
+ @Override
+ public List queryBusinessUnzipDetail(Long bizId) {
+ return baseMapper.queryBusinessUnzipDetail(bizId);
+ }
+
+ @Override
+ public List getBusinessUnzipTasksByBizIds(List bizIds) {
+ if (CollectionUtil.isEmpty(bizIds)) {
+ return Collections.emptyList();
+ }
+ BusinessUnzipTaskQueryDTO param = new BusinessUnzipTaskQueryDTO();
+ param.setBizIds(bizIds);
+ return baseMapper.selectTaskList(null, param);
+ }
+
+ /**
+ * 获取解压任务状态
+ * @param relateType 关联类型
+ * @param relateId 关联id
+ * @return
+ */
+ private String getStatus(String relateType, Long relateId) {
+ if (UnzipTask.RelateType.ATTACHMENT.getCode().equals(relateType)) {
+ // 关联类型是附件,状态直接就是上传完成
+ return UnzipTask.Status.UPLOADED.getCode();
+ }
+ if (UnzipTask.RelateType.FILE_TASK.getCode().equals(relateType)) {
+ // 关联类型是文件任务,查询文件任务状态
+ FileTask fileTask = fileTaskService.getById(relateId);
+ AssertUtils.notNull(fileTask, "文件任务不存在");
+ if (FileTaskStatus.COMPLETED.getCode().equals(fileTask.getStatus())) {
+ // 文件任务上传完成,解压任务设置为上传完成
+ return UnzipTask.Status.UPLOADED.getCode();
+ }
+ // 否则解压任务设置为上传中状态
+ return UnzipTask.Status.UPLOADING.getCode();
+ }
+ throw new ServiceException("关联类型错误");
+ }
+}
diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/service/impl/FileHandleServiceImpl.java b/blade-service/blade-file/src/main/java/org/springblade/file/service/impl/FileHandleServiceImpl.java
new file mode 100644
index 0000000..f3134ec
--- /dev/null
+++ b/blade-service/blade-file/src/main/java/org/springblade/file/service/impl/FileHandleServiceImpl.java
@@ -0,0 +1,92 @@
+package org.springblade.file.service.impl;
+
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springblade.core.tool.utils.StringUtil;
+import org.springblade.file.config.ObsProperties;
+import org.springblade.file.exception.UnzipFailException;
+import org.springblade.file.pojo.vo.FileHandleVO;
+import org.springblade.file.pojo.vo.FileVO;
+import org.springblade.file.service.IFileHandleService;
+import org.springblade.file.service.IFileService;
+import org.springblade.file.util.UnzipUtil;
+import org.springframework.stereotype.Service;
+
+import java.io.File;
+import java.util.List;
+
+/**
+ * @author bfhuange
+ * @date 2024/10/22
+ */
+@Slf4j
+@AllArgsConstructor
+@Service
+public class FileHandleServiceImpl implements IFileHandleService {
+
+ private final IFileService fileService;
+ private final ObsProperties obsProperties;
+
+ @Override
+ public FileHandleVO downloadAndUnzip(FileVO file) {
+ File downloadDir = new File(obsProperties.getTmpDownloadDir());
+ if (!downloadDir.exists()) {
+ downloadDir.mkdirs();
+ }
+ File downloadFile = new File(downloadDir, file.getObjectKey());
+ String downloadFilePath = downloadFile.getAbsolutePath();
+ File unzipDir = null;
+ long unzipStart = System.currentTimeMillis();
+ try {
+ // 下载文件
+ downloadFile(file, downloadFile, downloadFilePath);
+ // 2. 解压文件目录
+ unzipDir = new File(obsProperties.getTmpUnzipDir(), file.getObjectKey());
+ if (!unzipDir.exists()) {
+ unzipDir.mkdirs();
+ }
+ long start = System.currentTimeMillis();
+ List files = UnzipUtil.unzipOrUnrar(downloadFilePath, unzipDir);
+ long end = System.currentTimeMillis();
+ log.info("文件:{} 解压完成 共{}个 耗时:{}", file.getObjectKey(), files.size(), end - start);
+ FileHandleVO fileHandleVO = new FileHandleVO();
+ fileHandleVO.setDownloadFile(downloadFile);
+ fileHandleVO.setUnzippedDir(unzipDir);
+ fileHandleVO.setUnzippedFiles(files);
+ return fileHandleVO;
+ } catch (Exception e) {
+ log.error("文件解压失败异常", e);
+ throw new UnzipFailException(e, unzipDir);
+ } finally {
+ long unzipEnd = System.currentTimeMillis();
+ log.info("文件:{} 下载并解压完成 耗时:{}", file.getObjectKey(), unzipEnd - unzipStart);
+ }
+ }
+
+ /**
+ * 下载文件
+ *
+ * @param file
+ * @param downloadFile
+ * @param downloadFilePath
+ */
+ private void downloadFile(FileVO file, File downloadFile, String downloadFilePath) {
+ if (downloadFile.exists() && file.getMd5() != null) {
+ // 下载文件存在且md5不为空,可能是重新解压
+ String fileMd5 = fileService.getFileMd5(downloadFile);
+ if (StringUtil.equals(fileMd5, file.getMd5())) {
+ log.info("文件存在且md5一致,无需重新下载 文件路径:{}", downloadFile.getAbsolutePath());
+ return;
+ }
+ }
+ long downloadStart = System.currentTimeMillis();
+ try {
+ // 1. 下载文件
+ fileService.downloadFile(file.getObjectKey(), downloadFilePath);
+ } finally {
+ long downloadEnd = System.currentTimeMillis();
+ log.info("文件:{} 下载完成 耗时:{}", file.getObjectKey(), downloadEnd - downloadStart);
+ }
+ }
+
+}
diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/service/impl/FileTaskPartServiceImpl.java b/blade-service/blade-file/src/main/java/org/springblade/file/service/impl/FileTaskPartServiceImpl.java
new file mode 100644
index 0000000..540c170
--- /dev/null
+++ b/blade-service/blade-file/src/main/java/org/springblade/file/service/impl/FileTaskPartServiceImpl.java
@@ -0,0 +1,46 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.file.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springblade.file.mapper.FileTaskPartMapper;
+import org.springblade.file.pojo.entity.FileTaskPart;
+import org.springblade.file.service.IFileTaskPartService;
+import org.springframework.stereotype.Service;
+
+/**
+ * 服务实现类
+ *
+ * @author zhaowei
+ */
+@Service
+@Slf4j
+@AllArgsConstructor
+public class FileTaskPartServiceImpl extends ServiceImpl implements IFileTaskPartService {
+
+}
diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/service/impl/FileTaskServiceImpl.java b/blade-service/blade-file/src/main/java/org/springblade/file/service/impl/FileTaskServiceImpl.java
new file mode 100644
index 0000000..fdc8b4d
--- /dev/null
+++ b/blade-service/blade-file/src/main/java/org/springblade/file/service/impl/FileTaskServiceImpl.java
@@ -0,0 +1,360 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.file.service.impl;
+
+import com.baomidou.mybatisplus.core.toolkit.IdWorker;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springblade.core.log.exception.ServiceException;
+import org.springblade.core.log.utils.AssertUtils;
+import org.springblade.core.tool.utils.CollectionUtil;
+import org.springblade.core.tool.utils.StringUtil;
+import org.springblade.file.config.ObsProperties;
+import org.springblade.file.convert.FileTaskConvert;
+import org.springblade.file.listener.FileUploadedEvent;
+import org.springblade.file.mapper.BusinessUnzipTaskMapper;
+import org.springblade.file.mapper.FileTaskMapper;
+import org.springblade.file.pojo.dto.FileTaskCreateDTO;
+import org.springblade.file.pojo.dto.FileTaskUpdateDTO;
+import org.springblade.file.pojo.entity.BusinessUnzipTask;
+import org.springblade.file.pojo.entity.FileTask;
+import org.springblade.file.pojo.entity.FileTaskPart;
+import org.springblade.file.pojo.enums.FileTaskStatus;
+import org.springblade.file.pojo.vo.FileTaskUpdateVO;
+import org.springblade.file.pojo.vo.PartEtagVO;
+import org.springblade.file.service.IFileService;
+import org.springblade.file.service.IFileTaskPartService;
+import org.springblade.file.service.IFileTaskService;
+import org.springblade.file.util.ObjectKeyUtils;
+import org.springframework.context.ApplicationContext;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * 服务实现类
+ *
+ * @author zhaowei
+ */
+@Service
+@Slf4j
+@AllArgsConstructor
+public class FileTaskServiceImpl extends ServiceImpl implements IFileTaskService {
+
+ private final FileTaskConvert convert;
+ private final IFileTaskPartService partService;
+ private final IFileService fileService;
+ private final BusinessUnzipTaskMapper unzipTaskMapper;
+ private final ObsProperties obsProperties;
+ private final ApplicationContext applicationContext;
+
+ @Override
+ public FileTaskUpdateVO queryFileTask(Long id, String md5) {
+ if (id == null && StringUtil.isBlank(md5)) {
+ throw new ServiceException("id和md5不能都为空");
+ }
+ // 根据md5按创建时间倒序取一条
+ FileTask fileTask = this.getOne(Wrappers.lambdaQuery()
+ .eq(id != null, FileTask::getId, id)
+ .eq(StringUtil.isNotBlank(md5), FileTask::getMd5, md5)
+ .orderByDesc(FileTask::getCreateTime)
+ .last("limit 1")
+ );
+ if (fileTask == null) {
+ return null;
+ }
+ return getFileTaskUpdateVO(fileTask);
+ }
+
+ @Transactional(rollbackFor = Exception.class)
+ @Override
+ public Long createFileTask(FileTaskCreateDTO param) {
+ if (!param.isForce()) {
+ // 不是强制创建新任务,文件任务存在就报错
+ FileTask fileTask = this.getOne(Wrappers.lambdaQuery()
+ .eq(FileTask::getMd5, param.getMd5())
+ .last("limit 1")
+ );
+ AssertUtils.isNull(fileTask, "文件任务已存在");
+ }
+
+ // 1. 初始化上传任务
+ String fileName = param.getAttachmentName();
+ String fileSuffix = fileName.substring(fileName.lastIndexOf("."));
+ String objectKey = ObjectKeyUtils.generateObjectKey(obsProperties.getZipUploadDir(), fileName);
+ String uploadId = fileService.initiateMultipartUpload(objectKey);
+
+ // 2. 保存文件任务
+ FileTask addParam = convert.dto2entity(param);
+ addParam.setId(IdWorker.getId());
+ addParam.setObjectKey(objectKey);
+ addParam.setSuffix(fileSuffix);
+ // 当前位置
+ addParam.setCurrentIndex(0);
+ addParam.setUploadId(uploadId);
+ // 正在上传
+ addParam.setStatus(FileTaskStatus.UPLOADING.getCode());
+ this.save(addParam);
+
+ return addParam.getId();
+ }
+
+ @Transactional(rollbackFor = Exception.class)
+ @Override
+ public FileTaskUpdateVO updateFileTask(FileTaskUpdateDTO param) {
+ // 校验文件任务
+ FileTask fileTask = this.getById(param.getId());
+ AssertUtils.notNull(fileTask, "文件任务不存在");
+ AssertUtils.isTrue(!FileTaskStatus.COMPLETED.getCode().equals(fileTask.getStatus()), "文件任务已完成");
+
+ // 1. 新增分段
+ addPart(param, fileTask);
+
+ // 是否最后一段
+ boolean isLast = fileTask.getChunkTotal().equals(param.getPartNumber());
+ // 2. 更新文件任务
+ boolean update = updateTask(param, isLast);
+ // 3. 处理上传结束
+ handleFinished(isLast, update, fileTask);
+ return this.getUpdateVO(param.getId());
+ }
+
+ /**
+ * 处理结束
+ * @param isLast
+ * @param update
+ * @param fileTask
+ */
+ private void handleFinished(boolean isLast, boolean update, FileTask fileTask) {
+ if (isLast && update) {
+ // 最后一段并且更新成功,合并文件
+ List parts = partService.list(Wrappers.lambdaQuery()
+ .eq(FileTaskPart::getFileTaskId, fileTask.getId())
+ .orderByAsc(FileTaskPart::getPartNumber)
+ );
+
+ try {
+ // 按分段号去重,相同分段号取第一个,如果分段号相同,etag不同,报错
+ Map numberTag = parts.stream()
+ .collect(Collectors.toMap(FileTaskPart::getPartNumber, Function.identity(), (a, b) -> {
+ if (StringUtil.equals(a.getEtag(), b.getEtag())) {
+ return a;
+ }
+ log.error("etag不一致 id1:{} id2:{} etag1:{} etag2:{}", a.getId(), b.getId(), a.getEtag(), b.getEtag());
+ throw new ServiceException("etag不一致");
+ }));
+ // 获取要保留的分段id
+ List partId = numberTag.values().stream()
+ .map(FileTaskPart::getId)
+ .toList();
+ // 去重
+ parts = parts.stream()
+ .filter(e -> partId.contains(e.getId()))
+ .toList();
+ List partEtags = parts.stream()
+ .map(convert::entity2vo)
+ .toList();
+ // 3. 合并文件
+ fileService.completeMultipartUpload(fileTask.getObjectKey(), fileTask.getUploadId(), partEtags);
+ // 4. 删除文件任务分段
+ List partIds = parts.stream()
+ .map(FileTaskPart::getId)
+ .toList();
+ partService.removeByIds(partIds);
+ // 5. 发布上传完成事件
+ applicationContext.publishEvent(new FileUploadedEvent(fileTask.getId()));
+ } catch (Exception e) {
+ log.error("文件分段上传合并文件异常", e);
+ log.error("文件任务id:{} objectKey:{}", fileTask.getId(), fileTask.getObjectKey());
+ // 更新文件任务状态为失败
+ FileTask updateParam = new FileTask();
+ updateParam.setId(fileTask.getId());
+ updateParam.setStatus(FileTaskStatus.FAILED.getCode());
+ this.updateById(updateParam);
+ }
+ }
+ }
+
+ /**
+ * 新增分段
+ * @param param
+ * @param fileTask
+ */
+ private void addPart(FileTaskUpdateDTO param, FileTask fileTask) {
+ // 校验分段
+ FileTaskPart part = partService.getOne(Wrappers.lambdaQuery()
+ .eq(FileTaskPart::getFileTaskId, fileTask.getId())
+ .eq(FileTaskPart::getPartNumber, param.getPartNumber())
+ .last("limit 1")
+ );
+ if (part != null) {
+ // 分段存在
+ if (StringUtil.equals(part.getEtag(), param.getEtag())) {
+ // 且etag和当前的etag相同,不做任何处理
+ return;
+ }
+ // etag和当前的etag不同,报错
+ throw new ServiceException("当前分段已存在");
+ }
+
+ // 新增分段
+ FileTaskPart addParam = new FileTaskPart();
+ addParam.setId(IdWorker.getId());
+ addParam.setFileTaskId(fileTask.getId());
+ addParam.setPartNumber(param.getPartNumber());
+ addParam.setEtag(param.getEtag());
+ partService.save(addParam);
+ }
+
+ @Override
+ public String getUploadUrl(Long id, Integer partNumber) {
+ FileTask fileTask = this.getById(id);
+ AssertUtils.notNull(fileTask, "文件任务不存在");
+ AssertUtils.isTrue(!FileTaskStatus.COMPLETED.getCode().equals(fileTask.getStatus()), "文件任务已完成");
+ return fileService.getUploadPartUrl(fileTask.getObjectKey(), fileTask.getUploadId(), partNumber, null);
+ }
+
+ @Transactional(rollbackFor = Exception.class)
+ @Override
+ public void cancelBatchByIds(List taskIds) {
+ // 查询任务
+ List fileTasks = this.listByIds(taskIds);
+ if (CollectionUtil.isEmpty(fileTasks)) {
+ return;
+ }
+ // 校验状态
+ fileTasks.forEach(task -> AssertUtils.isFalse(FileTaskStatus.COMPLETED.getCode().equals(task.getStatus()), "文件任务已上传完成"));
+ // 调用obs取消上传任务
+ try {
+ for (FileTask fileTask : fileTasks) {
+ fileService.cancelMultipartUpload(fileTask.getObjectKey(), fileTask.getUploadId());
+ }
+ } catch (Exception e) {
+ log.error("调用obs取消文件上传任务异常", e);
+ throw new ServiceException("调用obs取消文件上传任务异常");
+ }
+ // 删除上传任务
+ this.removeBatchByIds(taskIds);
+ // 删除文件任务分片
+ List list = partService.list(Wrappers.lambdaQuery()
+ .in(FileTaskPart::getFileTaskId, taskIds)
+ );
+ if (CollectionUtil.isNotEmpty(list)) {
+ List partIds = list.stream()
+ .map(FileTaskPart::getId)
+ .toList();
+ partService.removeByIds(partIds);
+ }
+ }
+
+ @Override
+ public FileTaskUpdateVO updateFailed(Long id) {
+ // 校验文件任务
+ FileTask fileTask = this.getById(id);
+ AssertUtils.notNull(fileTask, "文件任务不存在");
+ AssertUtils.isTrue(!FileTaskStatus.COMPLETED.getCode().equals(fileTask.getStatus()), "文件任务已完成");
+ // 更新文件任务状态为失败
+ FileTask updateParam = new FileTask();
+ updateParam.setId(id);
+ updateParam.setStatus(FileTaskStatus.FAILED.getCode());
+ this.updateById(updateParam);
+ return this.getUpdateVO(id);
+ }
+
+ @Override
+ public boolean updatePaused(List ids) {
+ if (CollectionUtil.isEmpty(ids)) {
+ return false;
+ }
+ FileTask updateParam = new FileTask();
+ // 修改状态为已暂停
+ updateParam.setStatus(FileTaskStatus.PAUSED.getCode());
+ return this.update(updateParam, Wrappers.lambdaUpdate()
+ .in(FileTask::getId, ids)
+ // 只更新正在上传状态的
+ .eq(FileTask::getStatus, FileTaskStatus.UPLOADING.getCode())
+ );
+ }
+
+ /**
+ * 查询文件任务
+ * @param id
+ * @return
+ */
+ private FileTaskUpdateVO getUpdateVO(Long id) {
+ FileTask fileTask = this.getById(id);
+ return getFileTaskUpdateVO(fileTask);
+ }
+
+ /**
+ * 查询文件任务
+ * @param fileTask
+ * @return
+ */
+ private FileTaskUpdateVO getFileTaskUpdateVO(FileTask fileTask) {
+ if (fileTask == null) {
+ return null;
+ }
+ FileTaskUpdateVO updateVO = convert.entity2updateVo(fileTask);
+ BusinessUnzipTask businessUnzipTask = unzipTaskMapper.selectOne(Wrappers.lambdaQuery()
+ .eq(BusinessUnzipTask::getRelateId, fileTask.getId())
+ .last("limit 1")
+ );
+ if (businessUnzipTask != null && businessUnzipTask.getDocCode() != null) {
+ updateVO.setDocCode(businessUnzipTask.getDocCode());
+ }
+ return updateVO;
+ }
+
+ /**
+ * 更新任务
+ *
+ * @param param
+ * @param isLast 是否最后一段
+ * @return
+ */
+ private boolean updateTask(FileTaskUpdateDTO param, boolean isLast) {
+ FileTask updateParam = new FileTask();
+ updateParam.setCurrentIndex(param.getPartNumber());
+ if (isLast) {
+ // 最后一段,修改任务状态为完成
+ updateParam.setStatus(FileTaskStatus.COMPLETED.getCode());
+ } else {
+ // 非最后一段,修改任务状态为上传中
+ updateParam.setStatus(FileTaskStatus.UPLOADING.getCode());
+ }
+ return this.update(updateParam, Wrappers.lambdaUpdate()
+ .eq(FileTask::getId, param.getId())
+ .lt(FileTask::getCurrentIndex, param.getPartNumber())
+ );
+ }
+}
diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/service/impl/OCRConvertServiceImpl.java b/blade-service/blade-file/src/main/java/org/springblade/file/service/impl/OCRConvertServiceImpl.java
new file mode 100644
index 0000000..22290e5
--- /dev/null
+++ b/blade-service/blade-file/src/main/java/org/springblade/file/service/impl/OCRConvertServiceImpl.java
@@ -0,0 +1,40 @@
+package org.springblade.file.service.impl;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springblade.core.log.utils.AssertUtils;
+import org.springblade.file.convert.OCRConverter;
+import org.springblade.file.pojo.dto.FileCertificateBatchRecognitionDTO;
+import org.springblade.file.service.IFileService;
+import org.springblade.file.service.IOCRConvertService;
+import org.springblade.thirdparty.ocr.pojo.dto.CertificateBatchRecognitionDTO;
+import org.springblade.thirdparty.ocr.pojo.vo.TransportCertificateVO;
+import org.springblade.thirdparty.ocr.service.IOCRService;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * @author bfhuange
+ * @since 2025/3/13
+ */
+@Slf4j
+@RequiredArgsConstructor
+@Service
+public class OCRConvertServiceImpl implements IOCRConvertService {
+
+ private final IFileService fileService;
+ private final IOCRService ocrService;
+ private final OCRConverter converter;
+
+ @Override
+ public TransportCertificateVO recognitionTransportCertificate(FileCertificateBatchRecognitionDTO param) {
+ AssertUtils.notNull(param, "参数不能为空");
+ AssertUtils.notNull(param.getObjectKeys(), "图像obs key不能为空");
+ CertificateBatchRecognitionDTO ocrParam = converter.dto2ocr(param);
+ // 转换文件链接
+ List fileUrls = fileService.getFileUrls(param.getObjectKeys(), null);
+ ocrParam.setUrls(fileUrls);
+ return ocrService.recognitionTransportCertificate(ocrParam);
+ }
+}
diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/service/impl/ObsFileServiceImpl.java b/blade-service/blade-file/src/main/java/org/springblade/file/service/impl/ObsFileServiceImpl.java
new file mode 100644
index 0000000..82014e2
--- /dev/null
+++ b/blade-service/blade-file/src/main/java/org/springblade/file/service/impl/ObsFileServiceImpl.java
@@ -0,0 +1,293 @@
+package org.springblade.file.service.impl;
+
+import cn.hutool.core.net.URLEncodeUtil;
+import com.obs.services.ObsClient;
+import com.obs.services.exception.ObsException;
+import com.obs.services.internal.Constants;
+import com.obs.services.internal.utils.ServiceUtils;
+import com.obs.services.model.*;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springblade.core.log.exception.ServiceException;
+import org.springblade.core.tool.utils.CollectionUtil;
+import org.springblade.core.tool.utils.StringUtil;
+import org.springblade.file.config.ObsProperties;
+import org.springblade.file.pojo.vo.PartEtagVO;
+import org.springblade.file.service.IFileService;
+import org.springblade.file.service.support.DownloadProgressListener;
+import org.springblade.file.service.support.UploadProgressListener;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.NoSuchAlgorithmException;
+import java.time.Duration;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * 华为obs对象存储
+ * @author bfhuange
+ * @date 2024/10/21
+ */
+@Slf4j
+@RequiredArgsConstructor
+public class ObsFileServiceImpl implements IFileService {
+ private final ObsProperties properties;
+ private final ObsClient obsClient;
+
+ @Override
+ public void uploadFile(String objectKey, InputStream is) {
+ this.uploadFile(objectKey, null, is, null);
+ }
+
+ @Override
+ public String uploadTempFile(String objectKey, InputStream is, Integer expires) {
+ // 上传文件
+ this.uploadFile(objectKey, null, is, expires);
+ // 生成下载链接
+ return this.getFileUrl(objectKey, properties.getExpires());
+ }
+
+ @Override
+ public void uploadFile(String objectKey, File file) {
+ this.uploadFile(objectKey, file, null, null);
+ }
+
+ @Override
+ public String initiateMultipartUpload(String objectKey) {
+ InitiateMultipartUploadRequest initiateRequest = new InitiateMultipartUploadRequest(properties.getBucketName(), objectKey);
+ try {
+ InitiateMultipartUploadResult initiateResult = obsClient.initiateMultipartUpload(initiateRequest);
+ return initiateResult.getUploadId();
+ } catch (Exception e) {
+ log.error("初始化分段上传异常", e);
+ throw new ServiceException("初始化分段上传异常");
+ }
+ }
+
+ @Override
+ public PartEtagVO uploadPart(String objectKey, String uploadId, File file, int partNumber, Long offset, Long partSize) {
+ UploadPartRequest uploadPartRequest = new UploadPartRequest();
+ uploadPartRequest.setBucketName(properties.getBucketName());
+ uploadPartRequest.setObjectKey(objectKey);
+ uploadPartRequest.setUploadId(uploadId);
+ uploadPartRequest.setPartNumber(partNumber);
+ uploadPartRequest.setFile(file);
+ if (offset != null) {
+ uploadPartRequest.setOffset(offset);
+ }
+ if (partSize != null) {
+ uploadPartRequest.setPartSize(partSize);
+ }
+ try {
+ UploadPartResult uploadPartResult = obsClient.uploadPart(uploadPartRequest);
+ return new PartEtagVO(uploadPartResult.getEtag(), uploadPartResult.getPartNumber());
+ } catch (Exception e) {
+ log.error("分段上传异常", e);
+ throw new ServiceException("分段上传异常");
+ }
+ }
+
+ @Override
+ public void completeMultipartUpload(String objectKey, String uploadId, List partEtagVOS) {
+ if (CollectionUtil.isEmpty(partEtagVOS)) {
+ log.warn("完成分段上传,要合并的分段列表为空");
+ return;
+ }
+ // 参数转换
+ List list = partEtagVOS.stream()
+ .map(partEtagVO -> new PartEtag(partEtagVO.getEtag(), partEtagVO.getPartNumber()))
+ .collect(Collectors.toCollection(ArrayList::new));
+ CompleteMultipartUploadRequest request =
+ new CompleteMultipartUploadRequest(properties.getBucketName(), objectKey, uploadId, list);
+ try {
+ obsClient.completeMultipartUpload(request);
+ } catch (Exception e) {
+ log.error("完成分段上传异常", e);
+ throw new ServiceException("完成分段上传异常");
+ }
+ }
+
+ @Override
+ public void cancelMultipartUpload(String objectKey, String uploadId) {
+ AbortMultipartUploadRequest request = new AbortMultipartUploadRequest(properties.getBucketName(), objectKey, uploadId);
+ try {
+ obsClient.abortMultipartUpload(request);
+ } catch (Exception e) {
+ log.error("取消分段上传任务异常", e);
+ throw new ServiceException("取消分段上传任务异常");
+ }
+ }
+
+ @Override
+ public String getFileUrl(String objectKey, Long expires) {
+ return this.getFileUrl(objectKey, null, expires);
+ }
+
+ @Override
+ public String getFileUrl(String objectKey, String fileName, Long expires) {
+ if (StringUtil.isBlank(objectKey)) {
+ return null;
+ }
+ // 链接失效时间
+ expires = Optional.ofNullable(expires)
+ // 参数为空取配置
+ .orElse(Optional.ofNullable(properties.getExpires())
+ // 配置为空,默认1小时
+ .orElse(Duration.ofHours(1).getSeconds()));
+ TemporarySignatureRequest request = new TemporarySignatureRequest(HttpMethodEnum.GET, expires);
+ request.setBucketName(properties.getBucketName());
+ request.setObjectKey(objectKey);
+ request.setRequestDate(new Date());
+ if (StringUtil.isNotBlank(fileName)) {
+ // 文件名不为空,修改响应的文件名
+ Map queryParam = new HashMap<>();
+ String encodeFileName = encode(fileName);
+ queryParam.put(Constants.ObsRequestParams.RESPONSE_CONTENT_DISPOSITION, "attachment;filename*=UTF-8''" + encodeFileName);
+ request.setQueryParams(queryParam);
+ }
+ try {
+ TemporarySignatureResponse temporarySignature = obsClient.createTemporarySignature(request);
+ return temporarySignature.getSignedUrl();
+ } catch (Exception e) {
+ log.error("获取文件链接异常", e);
+ throw new ServiceException("获取文件链接异常");
+ }
+ }
+
+ /**
+ * 编码文件名
+ * @param fileName
+ * @return
+ */
+ private String encode(String fileName) {
+ int index = fileName.lastIndexOf(".");
+ if (index < 0) {
+ return URLEncodeUtil.encode(fileName);
+ }
+ // 文件名
+ String prefix = fileName.substring(0, index);
+ // 后缀
+ String ext = fileName.substring(index);
+ return URLEncodeUtil.encode(prefix) + ext;
+ }
+
+ @Override
+ public String getUploadPartUrl(String objectKey, String uploadId, Integer partNumber, Long expires) {
+ TemporarySignatureRequest request = new TemporarySignatureRequest();
+ request.setBucketName(properties.getBucketName());
+ request.setObjectKey(objectKey);
+ request.setRequestDate(new Date());
+ // 链接失效时间
+ expires = Optional.ofNullable(expires)
+ .orElse(properties.getExpires());
+ if (expires != null) {
+ request.setExpires(expires);
+ }
+ request.setMethod(HttpMethodEnum.PUT);
+ request.setHeaders(Map.of(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE));
+ Map params = new HashMap<>();
+ params.put(Constants.ObsRequestParams.PART_NUMBER, String.valueOf(partNumber));
+ params.put(Constants.ObsRequestParams.UPLOAD_ID, uploadId);
+ request.setQueryParams(params);
+ try {
+ TemporarySignatureResponse temporarySignature = obsClient.createTemporarySignature(request);
+ return temporarySignature.getSignedUrl();
+ } catch (Exception e) {
+ log.error("获取文件链接异常", e);
+ throw new ServiceException("获取文件链接异常");
+ }
+ }
+
+ @Override
+ public List getFileUrls(List objectKeys, Long expires) {
+ if (CollectionUtil.isEmpty(objectKeys)) {
+ return Collections.emptyList();
+ }
+ return objectKeys.stream()
+ .map(objectKey -> this.getFileUrl(objectKey, expires))
+ .toList();
+ }
+
+ @Override
+ public void downloadFile(String objectKey, String downloadFile) {
+ DownloadFileRequest request = new DownloadFileRequest(properties.getBucketName(), objectKey);
+ request.setDownloadFile(downloadFile);
+ // 开启断点续传模式
+ request.setEnableCheckpoint(true);
+ Long progressInterval = properties.getDownloadProgressInterval();
+ // 设置下载进度日志
+ request.setProgressListener(new DownloadProgressListener(objectKey, progressInterval));
+ request.setProgressInterval(progressInterval);
+ DownloadFileResult result = null;
+ try {
+ result = obsClient.downloadFile(request);
+ } catch (ObsException e) {
+ log.error("下载文件异常", e);
+ log.error("下载文件异常 objectKey:{} responseCode:{} errorCode:{} errorMessage:{} errorRequestId:{} errorHostId:{}", objectKey, e.getResponseCode(), e.getErrorCode(), e.getErrorMessage(), e.getErrorRequestId(), e.getErrorHostId());
+ throw new ServiceException("下载文件异常");
+ } catch (Exception e) {
+ log.error("下载文件异常", e);
+ throw new ServiceException("下载文件异常");
+ } finally {
+ String etag = Optional.ofNullable(result)
+ .map(DownloadFileResult::getObjectMetadata)
+ .map(ObjectMetadata::getEtag)
+ .orElse(null);
+ log.info("下载文件结束 objectKey:{} etag:{}", objectKey, etag);
+ }
+ }
+
+ /**
+ * 上传文件
+ * @param objectKey
+ * @param file
+ * @param is
+ * @param expires 过期时间,单位天,过期后自动删除
+ */
+ private void uploadFile(String objectKey, File file, InputStream is, Integer expires) {
+ if (file == null && is == null ) {
+ throw new ServiceException("文件不存在");
+ }
+ PutObjectRequest request = new PutObjectRequest();
+ request.setBucketName(properties.getBucketName());
+ request.setObjectKey(objectKey);
+ if (expires != null) {
+ request.setExpires(expires);
+ }
+ if (is != null) {
+ request.setInput(is);
+ } else {
+ request.setFile(file);
+ // 文件不为空,设置md5
+ String md5 = getFileMd5(file);
+ ObjectMetadata metadata = new ObjectMetadata();
+ metadata.setContentMd5(md5);
+ request.setMetadata(metadata);
+ }
+ Long progressInterval = properties.getUploadProgressInterval();
+ // 设置上传进度日志
+ request.setProgressListener(new UploadProgressListener(objectKey, progressInterval));
+ request.setProgressInterval(progressInterval);
+ try {
+ obsClient.putObject(request);
+ } catch (Exception e) {
+ log.error("上传文件异常", e);
+ throw new ServiceException("上传文件异常");
+ }
+ }
+
+ @Override
+ public String getFileMd5(File file) {
+ try {
+ return ServiceUtils.toBase64(ServiceUtils.computeMD5Hash(new FileInputStream(file)));
+ } catch (NoSuchAlgorithmException | IOException e) {
+ log.error("获取文件md5异常", e);
+ return null;
+ }
+ }
+}
diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/service/support/DownloadProgressListener.java b/blade-service/blade-file/src/main/java/org/springblade/file/service/support/DownloadProgressListener.java
new file mode 100644
index 0000000..7703cc5
--- /dev/null
+++ b/blade-service/blade-file/src/main/java/org/springblade/file/service/support/DownloadProgressListener.java
@@ -0,0 +1,31 @@
+package org.springblade.file.service.support;
+
+import com.obs.services.model.ProgressListener;
+import com.obs.services.model.ProgressStatus;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * 文件下载进度监听器
+ * @author bfhuange
+ * @date 2024/10/28
+ */
+@AllArgsConstructor
+@Slf4j
+public class DownloadProgressListener implements ProgressListener {
+
+ /**
+ * obs对象名称
+ */
+ private final String objectKey;
+
+ /**
+ * 进度间隔,如果新传输的大小不等于进度间隔,说明是结束了
+ */
+ private final long progressInterval;
+
+ @Override
+ public void progressChanged(ProgressStatus status) {
+ log.info("文件下载进度 文件名:{} 是否结束:{} 总大小:{} 已下载大小:{} 百分比:{} 下载速度:{}", objectKey, progressInterval != status.getNewlyTransferredBytes(), status.getTotalBytes(), status.getTransferredBytes(), status.getTransferPercentage(), String.format("%.2f", status.getAverageSpeed()));
+ }
+}
diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/service/support/UploadProgressListener.java b/blade-service/blade-file/src/main/java/org/springblade/file/service/support/UploadProgressListener.java
new file mode 100644
index 0000000..f1af602
--- /dev/null
+++ b/blade-service/blade-file/src/main/java/org/springblade/file/service/support/UploadProgressListener.java
@@ -0,0 +1,31 @@
+package org.springblade.file.service.support;
+
+import com.obs.services.model.ProgressListener;
+import com.obs.services.model.ProgressStatus;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * 文件上传进度监听器
+ * @author bfhuange
+ * @date 2024/10/28
+ */
+@AllArgsConstructor
+@Slf4j
+public class UploadProgressListener implements ProgressListener {
+
+ /**
+ * obs对象名称
+ */
+ private final String objectKey;
+
+ /**
+ * 进度间隔,如果新传输的大小不等于进度间隔,说明是结束了
+ */
+ private final long progressInterval;
+
+ @Override
+ public void progressChanged(ProgressStatus status) {
+ log.info("文件上传进度 文件名:{} 是否结束:{} 已上传大小:{} 上传速度:{}", objectKey, progressInterval != status.getNewlyTransferredBytes(), status.getTransferredBytes(), String.format("%.2f", status.getAverageSpeed()));
+ }
+}
diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/util/ObjectKeyUtils.java b/blade-service/blade-file/src/main/java/org/springblade/file/util/ObjectKeyUtils.java
new file mode 100644
index 0000000..e658836
--- /dev/null
+++ b/blade-service/blade-file/src/main/java/org/springblade/file/util/ObjectKeyUtils.java
@@ -0,0 +1,98 @@
+package org.springblade.file.util;
+
+import com.baomidou.mybatisplus.core.toolkit.IdWorker;
+import org.springblade.core.secure.utils.AuthUtil;
+import org.springblade.core.tool.utils.RandomType;
+import org.springblade.core.tool.utils.StringPool;
+import org.springblade.core.tool.utils.StringUtil;
+
+/**
+ * 对象存储key工具类
+ * @author bfhuange
+ * @date 2024/10/29
+ */
+public class ObjectKeyUtils {
+ /**
+ * object key 格式,前缀目录/ + id + 用户id后4位 + 后缀
+ */
+ private static final String FORMAT = "%s%s%s%s";
+ /**
+ * 用户id后缀位数
+ */
+ private static final int ID_SUFFIX_LENGTH = 4;
+
+ private ObjectKeyUtils() {}
+
+ /**
+ * 生成 object key
+ * @param dir 文件目录
+ * @param fileName 文件名
+ * @return
+ */
+ public static String generateObjectKey(String dir, String fileName) {
+ // 目录字符串
+ String dirString = ObjectKeyUtils.getDirString(dir);
+ // 文件名后缀
+ String fileNameSuffix = ObjectKeyUtils.getFileNameSuffix(fileName);
+ // 用户id后4位
+ String userIdSuffix = ObjectKeyUtils.getUserIdSuffix();
+ // 格式,目录 + id + 用户id后4位 + 后缀
+ return String.format(FORMAT, dirString, IdWorker.getId(), userIdSuffix, fileNameSuffix);
+ }
+
+ /**
+ * 生成 object key
+ * @param fileName 文件名
+ * @return
+ */
+ public static String generateObjectKey(String fileName) {
+ return ObjectKeyUtils.generateObjectKey(StringPool.EMPTY, fileName);
+ }
+
+ /**
+ * 获取用户id后缀字符串
+ * @return
+ */
+ private static String getUserIdSuffix() {
+ Long userId = AuthUtil.getUserId();
+ if (userId > 0) {
+ String idString = userId.toString();
+ if (idString.length() >= ID_SUFFIX_LENGTH) {
+ // 截取用户id后几位
+ return idString.substring(idString.length() - ID_SUFFIX_LENGTH);
+ }
+ }
+ // 未获取到用户id,取随机数字
+ return StringUtil.random(ID_SUFFIX_LENGTH, RandomType.INT);
+ }
+
+ /**
+ * 获取目录字符串
+ * @param dir
+ * @return
+ */
+ private static String getDirString(String dir) {
+ if (StringUtil.isBlank(dir)) {
+ return StringPool.EMPTY;
+ }
+ if (dir.endsWith(StringPool.SLASH)) {
+ return dir;
+ }
+ return dir + StringPool.SLASH;
+ }
+
+ /**
+ * 获取文件名后缀
+ * @param fileName 文件名
+ * @return
+ */
+ private static String getFileNameSuffix(String fileName) {
+ if (StringUtil.isBlank(fileName)) {
+ return StringPool.EMPTY;
+ }
+ if (!fileName.contains(StringPool.DOT)) {
+ return StringPool.EMPTY;
+ }
+ return fileName.substring(fileName.lastIndexOf(StringPool.DOT));
+ }
+}
diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/util/UnzipUtil.java b/blade-service/blade-file/src/main/java/org/springblade/file/util/UnzipUtil.java
new file mode 100644
index 0000000..af0aceb
--- /dev/null
+++ b/blade-service/blade-file/src/main/java/org/springblade/file/util/UnzipUtil.java
@@ -0,0 +1,245 @@
+package org.springblade.file.util;
+
+import cn.hutool.core.io.CharsetDetector;
+import com.github.junrar.Archive;
+import com.github.junrar.rarfile.FileHeader;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.compress.archivers.ArchiveEntry;
+import org.apache.commons.compress.archivers.ArchiveException;
+import org.apache.commons.compress.archivers.ArchiveInputStream;
+import org.apache.commons.compress.archivers.ArchiveStreamFactory;
+import org.apache.commons.compress.archivers.sevenz.SevenZArchiveEntry;
+import org.apache.commons.compress.archivers.sevenz.SevenZFile;
+import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
+import org.apache.commons.io.IOUtils;
+import org.springblade.core.log.exception.ServiceException;
+import org.springblade.core.tool.utils.StringPool;
+
+import java.io.*;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.zip.ZipInputStream;
+
+/**
+ * 解压工具类
+ * @author bfhuange
+ * @since 2025/2/28
+ */
+@Slf4j
+public class UnzipUtil {
+ /**
+ * rar格式
+ */
+ public static final String RAR = "rar";
+
+ /**
+ * macos下垃圾目录,不处理
+ */
+ private static final String MACOSX_DIR = "__MACOSX";
+
+ /**
+ * 解压文件
+ * @param zipFilePath 压缩文件路径
+ * @param unzipDir 解压目录
+ */
+ public static List unzipOrUnrar(String zipFilePath, File unzipDir) {
+ if (zipFilePath.toLowerCase().endsWith(RAR)) {
+ // 解压rar
+ return unrar(zipFilePath, unzipDir);
+ } else if (zipFilePath.toLowerCase().endsWith(ArchiveStreamFactory.SEVEN_Z)) {
+ // 解压7z
+ return un7z(zipFilePath, unzipDir);
+ }
+ // 解压其余格式
+ return unzip(zipFilePath, unzipDir);
+ }
+
+ /**
+ * 解压zip格式
+ * @param downloadFile
+ * @param destDir
+ */
+ public static List un7z(String downloadFile, File destDir) {
+ List files = new ArrayList<>();
+ try(
+ SevenZFile sevenZFile = new SevenZFile.Builder()
+ .setFile(new File(downloadFile))
+ .get()
+ ) {
+ SevenZArchiveEntry entry = sevenZFile.getNextEntry();
+ while (entry != null) {
+ log.debug("压缩文件条目:{}", entry.getName());
+ File file = new File(destDir, entry.getName());
+ if (!entry.isDirectory()) {
+ // 不是目录才处理
+ files.add(file);
+ File parentFile = file.getParentFile();
+ if (!parentFile.exists()) {
+ parentFile.mkdirs();
+ }
+ try(
+ OutputStream os = new FileOutputStream(file);
+ InputStream is = sevenZFile.getInputStream(entry);
+ ) {
+ IOUtils.copy(is, os);
+ }
+ }
+ entry = sevenZFile.getNextEntry();
+ }
+ } catch (IOException e) {
+ log.error("解压文件异常", e);
+ throw new RuntimeException(e);
+ }
+ return files;
+ }
+
+ /**
+ * 解压rar格式
+ * @param zipFilePath 压缩文件路径
+ * @param unzipDir 解压文件目录
+ */
+ public static List unrar(String zipFilePath, File unzipDir) {
+ List files = new ArrayList<>();
+ try (
+ // 加载rar文件
+ InputStream inputStream = Files.newInputStream(Paths.get(zipFilePath));
+ Archive archive = new Archive(inputStream);
+ ) {
+ List fileHeaders = archive.getFileHeaders();
+ if (fileHeaders == null) {
+ throw new ServiceException("压缩文件为空");
+ }
+ int total = fileHeaders.size();
+ for (int i = 0; i < fileHeaders.size(); i++) {
+ FileHeader fileHeader = fileHeaders.get(i);
+ log.debug("解压进度:{}/{} 文件:{}", i + 1, total, fileHeader.getFileName());
+ // 解压文件
+ File file = new File(unzipDir, fileHeader.getFileName());
+ if (!fileHeader.isDirectory()) {
+ // 不是目录才处理
+ files.add(file);
+ File parentFile = file.getParentFile();
+ if (!parentFile.exists()) {
+ parentFile.mkdirs();
+ }
+ InputStream is = archive.getInputStream(fileHeader);
+ try (OutputStream os = new FileOutputStream(file)) {
+ IOUtils.copy(is, os);
+ }
+ }
+ }
+ } catch (ServiceException e) {
+ throw e;
+ } catch (Exception e) {
+ log.error("解压文件异常", e);
+ throw new ServiceException("解压文件异常");
+ }
+ return files;
+ }
+
+ /**
+ * 解压zip格式
+ * @param zipFilePath 压缩文件路径
+ * @param unzipDir 解压目录
+ */
+ public static List unzip(String zipFilePath, File unzipDir) {
+ List files = new ArrayList<>();
+ ArchiveStreamFactory archiveStreamFactory = new ArchiveStreamFactory();
+ try(
+ FileInputStream is = new FileInputStream(zipFilePath);
+ BufferedInputStream bis = new BufferedInputStream(is);
+ ArchiveInputStream extends ArchiveEntry> ais = archiveStreamFactory.createArchiveInputStream(bis);
+ ) {
+ ArchiveEntry entry;
+ while ((entry = ais.getNextEntry()) != null) {
+ // 获取文件名称
+ String entryName = UnzipUtil.getEntryName(entry);
+ log.debug("压缩文件条目:{}", entryName);
+ if (entryName.contains(MACOSX_DIR)) {
+ // macos下垃圾目录,跳过
+ log.info("跳过macos下__MACOSX的文件:{}", entryName);
+ continue;
+ }
+ // 压缩文件内文件夹分隔符是\,linux下不会识别为文件夹,会识别成文件名,处理下
+ if (entryName.contains(StringPool.BACK_SLASH)) {
+ entryName = entryName.replace(StringPool.BACK_SLASH, StringPool.SLASH);
+ }
+ if (!isDirectory(entryName)) {
+ File file = new File(unzipDir, entryName);
+ if (file.getName().startsWith(StringPool.DOT)) {
+ // .开头的文件,是隐藏文件,跳过
+ log.info("跳过.开头的文件:{}", entryName);
+ continue;
+ }
+ // 不是目录才处理
+ files.add(file);
+ File parentFile = file.getParentFile();
+ if (!parentFile.exists()) {
+ parentFile.mkdirs();
+ }
+ try(OutputStream os = new FileOutputStream(file)) {
+ IOUtils.copy(ais, os);
+ }
+ }
+ }
+ } catch (IOException e) {
+ log.error("解压文件异常", e);
+ throw new RuntimeException(e);
+ } catch (ArchiveException e) {
+ throw new RuntimeException(e);
+ }
+ return files;
+ }
+
+ /**
+ * 获取文件名称
+ * @param entry
+ * @return
+ */
+ private static String getEntryName(ArchiveEntry entry) {
+ if (entry instanceof ZipArchiveEntry zipArchiveEntry) {
+ byte[] rawName = zipArchiveEntry.getRawName();
+ // 检测文件名编码
+ Charset charset = CharsetDetector.detect(new ByteArrayInputStream(rawName));
+ return new String(rawName, charset);
+ }
+ return entry.getName();
+ }
+
+ /**
+ * 判断压缩文件是否utf8编码
+ * @param zipFilePath 压缩文件路径
+ * @return
+ */
+ public static boolean isUtf8(String zipFilePath) {
+ try (
+ FileInputStream fileInputStream = new FileInputStream(zipFilePath);
+ ZipInputStream zipInputStream = new ZipInputStream(fileInputStream, StandardCharsets.UTF_8);
+ ) {
+ while (zipInputStream.getNextEntry() != null) {
+
+ }
+ return true;
+ } catch (Exception ignore) {
+ // gbk编码的用utf8编码读取会抛异常
+ return false;
+ }
+ }
+
+ /**
+ * 判断压缩文件中条目是否文件夹
+ * @param entryName
+ * @return
+ */
+ private static boolean isDirectory(String entryName) {
+ if (entryName == null) {
+ return false;
+ }
+ return entryName.endsWith(StringPool.SLASH) || entryName.endsWith(StringPool.BACK_SLASH);
+ }
+
+}
diff --git a/blade-service/blade-file/src/main/resources/bootstrap-dev.yml b/blade-service/blade-file/src/main/resources/bootstrap-dev.yml
new file mode 100644
index 0000000..303fc85
--- /dev/null
+++ b/blade-service/blade-file/src/main/resources/bootstrap-dev.yml
@@ -0,0 +1,6 @@
+#spring:
+# cloud:
+# nacos:
+# username: nacos
+# password: ${NACOS_PASSWORD:gr30wIs5%Hi7keQj}
+# server-addr: ${NACOS_ADDR:10.38.16.127:8848}
diff --git a/blade-service/blade-file/src/main/resources/bootstrap-prod.yml b/blade-service/blade-file/src/main/resources/bootstrap-prod.yml
new file mode 100644
index 0000000..673f396
--- /dev/null
+++ b/blade-service/blade-file/src/main/resources/bootstrap-prod.yml
@@ -0,0 +1,6 @@
+#spring:
+# cloud:
+# nacos:
+# username: nacos
+# password: rWrMrVTWyf%ekjuw
+# server-addr: ${NACOS_ADDR:192.168.0.242:8848}
diff --git a/blade-service/blade-file/src/main/resources/bootstrap-test.yml b/blade-service/blade-file/src/main/resources/bootstrap-test.yml
new file mode 100644
index 0000000..7586f5a
--- /dev/null
+++ b/blade-service/blade-file/src/main/resources/bootstrap-test.yml
@@ -0,0 +1,8 @@
+#server:
+# port: 38107
+#spring:
+# cloud:
+# nacos:
+# username: nacos
+# password: gr30wIs5%Hi7keQj
+# server-addr: ${NACOS_ADDR:10.38.16.127:8848}
diff --git a/blade-service/blade-file/src/main/resources/bootstrap.yml b/blade-service/blade-file/src/main/resources/bootstrap.yml
new file mode 100644
index 0000000..3dcc197
--- /dev/null
+++ b/blade-service/blade-file/src/main/resources/bootstrap.yml
@@ -0,0 +1,41 @@
+server:
+ port: 8107
+
+#数据源配置
+spring:
+ datasource:
+ url: ${blade.datasource.dev.url}
+ username: ${blade.datasource.dev.username}
+ password: ${blade.datasource.dev.password}
+
+#spring:
+# application:
+# name: blade-file
+# cloud:
+# nacos:
+# discovery:
+# namespace: ${spring.profiles.active}
+# # 不注册到nacos
+# #registerEnabled: false
+# config:
+# # 文件后缀名
+# file-extension: yaml
+# namespace: ${spring.profiles.active}
+# shared-configs:
+# - data-id: blade.yaml
+# refresh: true
+# - data-id: blade-${spring.profiles.active}.yaml
+# refresh: true
+# - data-id: third-party-api.yaml
+# refresh: true
+# sentinel:
+# datasource:
+# flow:
+# nacos:
+# serverAddr: ${spring.cloud.nacos.server-addr}
+# username: ${spring.cloud.nacos.username}
+# password: ${spring.cloud.nacos.password}
+# dataId: sentinel-${spring.application.name}.json
+# groupId: DEFAULT_GROUP
+# namespace: ${spring.profiles.active}
+# ruleType: flow
diff --git a/blade-service/blade-system/pom.xml b/blade-service/blade-system/pom.xml
index 87f4b33..4ab8af9 100644
--- a/blade-service/blade-system/pom.xml
+++ b/blade-service/blade-system/pom.xml
@@ -13,6 +13,13 @@
${project.artifactId}
jar
+
+
+ 1.18.30
+ 1.5.5.Final
+ 7.5.5
+
+
org.springblade
@@ -48,9 +55,11 @@
blade-mk-api
+
org.mapstruct
mapstruct
+ ${org.mapstruct.version}
@@ -80,6 +89,34 @@
org.apache.maven.plugins
maven-antrun-plugin
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ ${maven.plugin.version}
+
+ ${java.version}
+ ${java.version}
+ UTF-8
+
+ -parameters
+
+
+
+ org.projectlombok
+ lombok
+ ${lombok.version}
+
+
+ org.mapstruct
+ mapstruct-processor
+ ${org.mapstruct.version}
+
+
+
+
+
+
diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/controller/AppVersionController.java b/blade-service/blade-system/src/main/java/org/springblade/system/controller/AppVersionController.java
new file mode 100644
index 0000000..debea12
--- /dev/null
+++ b/blade-service/blade-system/src/main/java/org/springblade/system/controller/AppVersionController.java
@@ -0,0 +1,115 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.system.controller;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import lombok.AllArgsConstructor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springblade.core.boot.ctrl.BladeController;
+import org.springblade.core.mp.support.Query;
+import org.springblade.core.tool.api.R;
+import org.springblade.core.tool.utils.Func;
+import org.springblade.system.pojo.entity.AppVersion;
+import org.springblade.system.service.AppVersionService;
+import org.springframework.web.bind.annotation.*;
+
+
+@RestController
+@AllArgsConstructor
+@RequestMapping("appVersion")
+@Tag(name = "APP版本管理", description = "APP版本管理")
+public class AppVersionController extends BladeController {
+ private static final Logger logger = LoggerFactory.getLogger(AppVersionController.class);
+
+ private final AppVersionService appVersionService;
+
+ /**
+ * 分页
+ */
+ @GetMapping("/page")
+ @ApiOperationSupport(order = 1)
+ @Operation(summary = "分页")
+ public R> page(AppVersion modelVo, Query query) {
+ try {
+ return R.data(appVersionService.page(modelVo,query));
+ } catch (Exception e) {
+ logger.error("appVersion/page:" + e.getMessage());
+ return R.fail(e.getMessage());
+ }
+ }
+
+ /**
+ * 详情
+ */
+ @GetMapping("/detail/{id}")
+ @ApiOperationSupport(order = 2)
+ @Operation(summary = "详情")
+ public R detail(@PathVariable Long id) {
+ try {
+ return R.data(appVersionService.detail(id));
+ } catch (Exception e) {
+ logger.error("appVersion/detail:" + e.getMessage());
+ return R.fail(e.getMessage());
+ }
+ }
+
+ /**
+ * 发布版本/编辑
+ */
+ @PostMapping("/publishOrEdit")
+ @ApiOperationSupport(order = 3)
+ @Operation(summary = "发布版本/编辑")
+ public R publishOrEdit(@Valid @RequestBody AppVersion modelVo) {
+ try {
+ return R.data(appVersionService.publishOrEdit(modelVo));
+ } catch (Exception e) {
+ logger.error("appVersion/publishOrEdit:" + e.getMessage());
+ return R.fail(e.getMessage());
+ }
+ }
+
+ /**
+ * 删除
+ */
+ @PostMapping("/remove")
+ @ApiOperationSupport(order = 4)
+ @Operation(summary = "删除")
+ public R remove(@Parameter(description = "主键集合", required = true) @RequestParam String ids) {
+ try {
+ return R.status(appVersionService.removeByIds(Func.toLongList(ids)));
+ } catch (Exception e) {
+ logger.error("appVersion/remove:" + e.getMessage());
+ return R.fail(e.getMessage());
+ }
+ }
+
+}
diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/mapper/AppVersionMapper.java b/blade-service/blade-system/src/main/java/org/springblade/system/mapper/AppVersionMapper.java
new file mode 100644
index 0000000..571b8ce
--- /dev/null
+++ b/blade-service/blade-system/src/main/java/org/springblade/system/mapper/AppVersionMapper.java
@@ -0,0 +1,36 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.system.mapper;
+
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.springblade.system.pojo.entity.AppVersion;
+
+
+public interface AppVersionMapper extends BaseMapper {
+
+
+}
diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/AppVersionService.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/AppVersionService.java
new file mode 100644
index 0000000..536528b
--- /dev/null
+++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/AppVersionService.java
@@ -0,0 +1,54 @@
+package org.springblade.system.service;
+
+import cn.hutool.core.util.ObjectUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import lombok.AllArgsConstructor;
+import org.springblade.core.log.utils.AssertUtils;
+import org.springblade.core.mp.support.Condition;
+import org.springblade.core.mp.support.Query;
+import org.springblade.system.mapper.AppVersionMapper;
+import org.springblade.system.pojo.entity.AppVersion;
+import org.springframework.stereotype.Service;
+
+@Service
+@AllArgsConstructor
+public class AppVersionService extends ServiceImpl {
+
+ /**
+ * 分页
+ */
+ public IPage page(AppVersion modelVo, Query query){
+ query.setDescs("create_time");
+ IPage page = this.page(Condition.getPage(query), Condition.getQueryWrapper(modelVo));
+ return page;
+ }
+
+ /**
+ * 详情
+ */
+ public AppVersion detail(Long id){
+ AssertUtils.isTrue(ObjectUtil.isNotEmpty(id),"id不能为空");
+ AppVersion model = this.getById(id);
+ return model;
+ }
+
+ /**
+ * 发布版本/编辑
+ */
+ public AppVersion publishOrEdit(AppVersion modelVo) {
+ //版本号没有重复校验
+ AppVersion appVersion=this.getOne(new LambdaQueryWrapper().eq(AppVersion::getUpdateType, modelVo.getUpdateType())
+ .eq(AppVersion::getVersion, modelVo.getVersion())
+ .eq(AppVersion::getSubVersion, modelVo.getSubVersion())
+ );
+ if(ObjectUtil.isNotEmpty(appVersion)){
+ if(ObjectUtil.isEmpty(modelVo.getId())||(ObjectUtil.isNotEmpty(modelVo.getId())&&!modelVo.getId().equals(appVersion.getId()))){
+ throw new RuntimeException("该更新类型的版本号重复");
+ }
+ }
+ this.saveOrUpdate(modelVo);
+ return modelVo;
+ }
+}
diff --git a/blade-service/pom.xml b/blade-service/pom.xml
index 024bffa..409eb6f 100644
--- a/blade-service/pom.xml
+++ b/blade-service/pom.xml
@@ -19,6 +19,7 @@
blade-desk
blade-system
+ blade-file
diff --git a/pom.xml b/pom.xml
index 554de4e..492c624 100644
--- a/pom.xml
+++ b/pom.xml
@@ -25,11 +25,6 @@
blade
false
0.42.0
-
-
- 1.18.30
- 1.5.5.Final
-
@@ -142,11 +137,10 @@
${revision}
-
- org.mapstruct
- mapstruct
- ${org.mapstruct.version}
+ org.springblade
+ blade-file-api
+ ${revision}
@@ -292,19 +286,6 @@
-parameters
-
-
- org.projectlombok
- lombok
- ${lombok.version}
-
-
- org.mapstruct
- mapstruct-processor
- ${org.mapstruct.version}
-
-
-