feat:添加APP版本管理相关代码

This commit is contained in:
aguanleishen
2026-07-28 08:32:02 +08:00
parent 0970af239d
commit b5246d4e58
112 changed files with 7504 additions and 25 deletions

View File

@@ -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"]

View File

@@ -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}"]

View File

@@ -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"]

View File

@@ -0,0 +1,139 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>blade-service</artifactId>
<groupId>org.springblade</groupId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>blade-file</artifactId>
<name>${project.artifactId}</name>
<packaging>jar</packaging>
<properties>
<commons-compress.version>1.27.1</commons-compress.version>
<xz.version>1.10</xz.version>
<!-- 其他依赖版本 -->
<lombok.version>1.18.30</lombok.version>
<org.mapstruct.version>1.5.5.Final</org.mapstruct.version>
<junrar.version>7.5.5</junrar.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springblade</groupId>
<artifactId>blade-core-boot</artifactId>
</dependency>
<dependency>
<groupId>org.springblade</groupId>
<artifactId>blade-starter-swagger</artifactId>
</dependency>
<dependency>
<groupId>org.springblade</groupId>
<artifactId>blade-file-api</artifactId>
</dependency>
<dependency>
<groupId>org.springblade</groupId>
<artifactId>blade-wps-api</artifactId>
</dependency>
<dependency>
<groupId>org.springblade</groupId>
<artifactId>blade-ocr-api</artifactId>
</dependency>
<!--zip解压缩-->
<dependency>
<groupId>net.lingala.zip4j</groupId>
<artifactId>zip4j</artifactId>
<version>1.3.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- 解压 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
<version>${commons-compress.version}</version>
</dependency>
<dependency>
<groupId>org.tukaani</groupId>
<artifactId>xz</artifactId>
<version>${xz.version}</version>
</dependency>
<dependency>
<groupId>com.github.junrar</groupId>
<artifactId>junrar</artifactId>
<version>${junrar.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>io.fabric8</groupId>
<artifactId>docker-maven-plugin</artifactId>
<configuration>
<skip>${docker.fabric.skip}</skip>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven.plugin.version}</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>UTF-8</encoding>
<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
<!-- other annotation processors -->
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,47 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* 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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}

View File

@@ -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<BusinessAttachmentAddDTO> 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<List<BusinessAttachmentVO>> queryList(@Validated @RequestBody BusinessAttachmentQueryDTO param) {
return R.data(businessAttachmentService.queryList(param));
}
}

View File

@@ -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<IPage<BusinessUnzipTaskListVO>> 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<BusinessUnzipTaskListVO> 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));
}
}

View File

@@ -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<List<Attachment>> result = attachmentService.batchOssUpload(files, null);
List<Attachment> 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.<Attachment>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<String> 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<TransportCertificateVO> recognitionTransportCertificates(@RequestBody List<String> 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<TransportCertificateVO> 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<Boolean> publishEvent(String code, Long id) {
ApplicationEvent event = FileEvent.createEvent(code, id);
if (event == null) {
return R.fail("事件名称错误");
}
applicationContext.publishEvent(event);
return R.status(true);
}
}

View File

@@ -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<FileTaskUpdateVO> queryFileTask(@RequestParam(value = "md5") String md5) {
FileTaskUpdateVO fileTask = fileTaskService.queryFileTask(null, md5);
return R.data(fileTask);
}
@ApiLog("上传任务-创建文件任务")
@PostMapping("/createFileTask")
@Operation(summary = "创建文件任务", description = "创建文件任务")
public R<FileTaskUpdateVO> createFileTask(@RequestBody FileTaskCreateDTO param) {
Long id = fileTaskService.createFileTask(param);
return R.data(fileTaskService.queryFileTask(id, null));
}
@ApiLog("上传任务-更新文件任务进度")
@PostMapping("/updateFileTask")
@Operation(summary = "更新文件任务进度", description = "更新文件任务进度")
public R<FileTaskUpdateVO> updateFileTask(@RequestBody FileTaskUpdateDTO param) {
return R.data(fileTaskService.updateFileTask(param));
}
@ApiLog("上传任务-修改文件任务状态为失败")
@PostMapping("/updateFailed")
@Operation(summary = "更新文件任务为失败", description = "更新文件任务为失败")
public R<FileTaskUpdateVO> updateFailed(@NotNull(message = "文件任务id不能为空") Long id) {
return R.data(fileTaskService.updateFailed(id));
}
@ApiLog("上传任务-暂停文件任务")
@PostMapping("/updatePaused")
@Operation(summary = "更新文件任务为暂停", description = "更新文件任务为暂停")
public R<Boolean> 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<String> getUploadUrl(Long id, Integer partNumber) {
String uploadUrl = fileTaskService.getUploadUrl(id, partNumber);
return R.data(uploadUrl);
}
}

View File

@@ -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<BusinessAttachment> dtos2entities(List<BusinessAttachmentAddDTO> dtos);
AttachmentDetailVO entity2vo(Attachment entity);
default List<BusinessAttachment> 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();
}
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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<List<Attachment>> getAttachment(String id) {
return FR.data(attachmentService.getAttachment(id));
}
@Override
public FR<AttachmentDetailVO> 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<List<BusinessAttachmentVO>> 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<List<BusinessUnzipTaskListVO>> getBusinessUnzipTasksByBizIds(List<Long> bizIds) {
return FR.data(businessUnzipTaskService.getBusinessUnzipTasksByBizIds(bizIds));
}
@Override
public FR<List<BusinessUnzipDetailListVO>> queryBusinessUnzipDetail(@NotNull(message = "业务id不能为空") Long bizId) {
return FR.data(businessUnzipTaskService.queryBusinessUnzipDetail(bizId));
}
@SentinelResource("ocr:batchCards")
@Override
public FR<TransportCertificateVO> recognitionTransportCertificates(@Validated @RequestBody FileCertificateBatchRecognitionDTO param) {
return FR.data(ocrConvertService.recognitionTransportCertificate(param));
}
@Override
public FR<List<FileUrlVO>> getFileUrls(IdsDTO param) {
List<FileUrlVO> 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<List<Attachment>> ossUpload(MultipartFile[] files) {
R<List<Attachment>> r = attachmentService.batchOssUpload(files, null);
return R.isNotSuccess(r) ? FR.fail(r.getCode(), r.getMsg()) : FR.data(r.getData(), r.getMsg());
}
@Override
public FR<TempFileUrlVO> 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("上传临时文件异常");
}
}
}

View File

@@ -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<Long, ApplicationEvent> 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);
}
}

View File

@@ -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<String, UnzipCallback> callbackMap;
public FileListener(IBusinessUnzipTaskService taskService, IBusinessUnzipDetailService detailService, List<UnzipCallback> 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.<BusinessUnzipTask>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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,37 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* 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<Attachment> {
}

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.springblade.file.mapper.AttachmentMapper">
</mapper>

View File

@@ -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<BusinessAttachment> {
/**
* 查询业务附件列表
* @param param
* @return
*/
List<BusinessAttachmentVO> queryList(BusinessAttachmentQueryDTO param);
}

View File

@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.springblade.file.mapper.BusinessAttachmentMapper">
<!-- 通用查询映射结果 -->
<resultMap id="businessAttachmentResultMap" type="org.springblade.file.pojo.entity.BusinessAttachment">
<result column="id" property="id"/>
<result column="tenant_id" property="tenantId"/>
<result column="biz_id" property="bizId"/>
<result column="biz_code" property="bizCode"/>
<result column="attachment_id" property="attachmentId"/>
<result column="`desc`" property="desc"/>
<result column="sort" property="sort"/>
<result column="create_user" property="createUser"/>
<result column="create_time" property="createTime"/>
<result column="update_user" property="updateUser"/>
<result column="update_time" property="updateTime"/>
<result column="is_deleted" property="isDeleted"/>
</resultMap>
<select id="queryList" resultType="org.springblade.file.pojo.vo.BusinessAttachmentVO">
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 <foreach collection="bizIds" item="item" open="(" close=")" separator=",">#{item}</foreach>
<if test="bizCode != null and bizCode != ''">
and bba.biz_code = #{bizCode}
</if>
<if test="bizCodes != null and bizCodes.size() > 0">
and bba.biz_code in <foreach collection="bizCodes" item="item" open="(" close=")" separator=",">#{item}</foreach>
</if>
order by bba.biz_id, bba.sort, bba.id
</select>
</mapper>

View File

@@ -0,0 +1,16 @@
package org.springblade.file.mapper;
import org.springblade.file.pojo.entity.BusinessUnzipDetail;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* <p>
* 业务解压明细表 Mapper 接口
* </p>
*
* @author bfhuange
* @since 2024-10-30
*/
public interface BusinessUnzipDetailMapper extends BaseMapper<BusinessUnzipDetail> {
}

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.springblade.file.mapper.BusinessUnzipDetailMapper">
<!-- 通用查询映射结果 -->
<resultMap id="BaseResultMap" type="org.springblade.file.pojo.entity.BusinessUnzipDetail">
<id column="id" property="id" />
<result column="business_unzip_task_id" property="businessUnzipTaskId" />
<result column="attachment_id" property="attachmentId" />
<result column="create_user" property="createUser" />
<result column="create_time" property="createTime" />
<result column="update_user" property="updateUser" />
<result column="update_time" property="updateTime" />
<result column="is_deleted" property="isDeleted" />
</resultMap>
<!-- 通用查询结果列 -->
<sql id="Base_Column_List">
id, business_unzip_task_id, attachment_id, create_user, create_time, update_user, update_time, is_deleted
</sql>
</mapper>

View File

@@ -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;
/**
* <p>
* 业务解压任务表 Mapper 接口
* </p>
*
* @author bfhuange
* @since 2024-10-28
*/
public interface BusinessUnzipTaskMapper extends BaseMapper<BusinessUnzipTask> {
/**
* 查询列表
* @param page
* @param param
* @return
*/
List<BusinessUnzipTaskListVO> selectTaskList(IPage<BusinessUnzipTask> page, @Param("param") BusinessUnzipTaskQueryDTO param);
/**
* 根据业务id查询解压明细
* @param bizId
* @return
*/
List<BusinessUnzipDetailListVO> queryBusinessUnzipDetail(@Param("bizId") Long bizId);
}

View File

@@ -0,0 +1,95 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.springblade.file.mapper.BusinessUnzipTaskMapper">
<!-- 通用查询映射结果 -->
<resultMap id="BaseResultMap" type="org.springblade.file.pojo.entity.BusinessUnzipTask">
<id column="id" property="id" />
<result column="biz_id" property="bizId" />
<result column="department_id" property="departmentId" />
<result column="biz_code" property="bizCode" />
<result column="doc_code" property="docCode" />
<result column="relate_type" property="relateType" />
<result column="relate_id" property="relateId" />
<result column="status" property="status" />
<result column="tenant_id" property="tenantId" />
<result column="create_user" property="createUser" />
<result column="create_time" property="createTime" />
<result column="update_user" property="updateUser" />
<result column="update_time" property="updateTime" />
<result column="is_deleted" property="isDeleted" />
</resultMap>
<!-- 通用查询结果列 -->
<sql id="Base_Column_List">
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
</sql>
<select id="selectTaskList" resultType="org.springblade.file.pojo.vo.BusinessUnzipTaskListVO">
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
<if test="param.bizIds != null and param.bizIds.size() > 0">
and but.biz_id in <foreach collection="param.bizIds" item="item" open="(" close=")" separator=",">#{item}</foreach>
</if>
<if test="param.docCode != null and param.docCode != ''">
and but.doc_code like concat('%',#{param.docCode},'%')
</if>
<if test="param.relateType != null and param.relateType != ''">
and but.relate_type = #{param.relateType}
</if>
<if test="param.currentUserId != null">
and but.create_user = #{param.currentUserId}
</if>
<if test="param.attachmentName != null and param.attachmentName != ''">
and ft.attachment_name like concat('%',#{param.attachmentName},'%')
</if>
<if test="param.status != null and param.status != ''">
and ft.status = #{param.status}
</if>
order by Field(ft.status, 'uploading','failed', 'completed'), but.id desc
</select>
<select id="queryBusinessUnzipDetail" resultType="org.springblade.file.pojo.vo.BusinessUnzipDetailListVO">
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}
</select>
</mapper>

View File

@@ -0,0 +1,38 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* 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<FileTask> {
}

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.springblade.file.mapper.FileTaskMapper">
</mapper>

View File

@@ -0,0 +1,38 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* 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<FileTaskPart> {
}

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.springblade.file.mapper.FileTaskPartMapper">
</mapper>

View File

@@ -0,0 +1,89 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* 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<Attachment> {
/**
* 批量上传
*
* @param files
* @param fileName 文件名,如果 files只有1个且fileName不为空设置文件名为 fileName
* @return
*/
R<List<Attachment>> batchOssUpload(MultipartFile[] files, String fileName);
/**
* 获取附件多个附件id用逗号分割
* @param id
* @return List<Attachment>
*/
List<Attachment> getAttachment(String id);
/**
* 批量上传解压缩文件并删除文件
*
* @param files
* @param workDir 工作目录,取文件相对于工作目录的相对路径作为前缀
* @return
*/
List<Attachment> batchOssUploadUnzipFilesAndDelete(List<File> files, File workDir);
/**
* 新事务批量保存
* @param list
*/
void batchSaveWithNewTransaction(List<Attachment> list);
/**
* 获取附件详情
* @param id
* @return
*/
AttachmentDetailVO getDetail(Long id);
/**
* 解压文件并上传
* @param file
* @return
*/
List<Attachment> unzipAndUpload(FileVO file);
}

View File

@@ -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<BusinessAttachment> {
/**
* 保存
* @param param
* @return
*/
boolean saveAndRemove(BusinessAttachmentBatchAddDTO param);
/**
* 保存并删除
*
* @param params
* @param deleteParam 业务id不为空则先删除
* @return
*/
boolean saveAndRemove(List<BusinessAttachmentAddDTO> params, BusinessAttachmentDeleteDTO deleteParam);
/**
* 根据bizId列表删除
*
* @param bizIds
* @return
*/
boolean removeByBizIds(List<Long> bizIds);
/**
* 查询业务附件列表
*
* @param param
* @return
*/
List<BusinessAttachmentVO> queryList(BusinessAttachmentQueryDTO param);
}

View File

@@ -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;
/**
* <p>
* 业务解压明细表 服务类
* </p>
*
* @author bfhuange
* @since 2024-10-30
*/
public interface IBusinessUnzipDetailService extends IService<BusinessUnzipDetail> {
/**
* 异步解压并保存解压明细
* @param unzipTaskId 解压任务id
* @param file obs对象存储的key合md5值md5有可能为空
*/
void unzipAndSaveAsync(Long unzipTaskId, FileVO file);
}

View File

@@ -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;
/**
* <p>
* 业务解压任务表 服务类
* </p>
*
* @author bfhuange
* @since 2024-10-28
*/
public interface IBusinessUnzipTaskService extends IService<BusinessUnzipTask> {
/**
* 保存业务解压任务
* @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<BusinessUnzipTaskListVO> selectPage(IPage<BusinessUnzipTask> 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<BusinessUnzipDetailListVO> queryBusinessUnzipDetail(Long bizId);
/**
* 根据业务id列表查询解压任务列表
* @param bizIds
* @return
*/
List<BusinessUnzipTaskListVO> getBusinessUnzipTasksByBizIds(List<Long> bizIds);
}

View File

@@ -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合md5md5可能为空
* @return
*/
FileHandleVO downloadAndUnzip(FileVO file);
}

View File

@@ -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<PartEtagVO> 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<String> getFileUrls(List<String> objectKeys, Long expires);
/**
* 下载文件
* @param objectKey
* @param downloadFile 下载路径
*/
void downloadFile(String objectKey, String downloadFile);
/**
* 获取文件md5
* @param file
* @return
*/
String getFileMd5(File file);
}

View File

@@ -0,0 +1,38 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* 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<FileTaskPart> {
}

View File

@@ -0,0 +1,94 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* 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<FileTask> {
/**
* 根据文件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<Long> taskIds);
/**
* 更新文件任务状态为失败
* @param id
* @return
*/
FileTaskUpdateVO updateFailed(Long id);
/**
* 根据id列表更新文件任务状态为暂停
* @param param
* @return
*/
boolean updatePaused(List<Long> param);
}

View File

@@ -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);
}

View File

@@ -0,0 +1,255 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* 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<AttachmentMapper, Attachment> implements IAttachmentService {
private final IFileService fileService;
private final BusinessAttachmentConvert convert;
private final ObsProperties obsProperties;
private final IFileHandleService fileHandleService;
private IAttachmentService self;
@Override
public R<List<Attachment>> batchOssUpload(MultipartFile[] files, String overWriteFileName){
try {
List<Attachment> 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<Attachment> getAttachment(String id) {
return this.list(new LambdaQueryWrapper<Attachment>()
.in(Attachment::getId, Arrays.stream(id.split(",")).toList())
);
}
@Override
public List<Attachment> batchOssUploadUnzipFilesAndDelete(List<File> files, File workDir) {
if (CollectionUtil.isEmpty(files)) {
return Collections.emptyList();
}
List<Attachment> 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<Attachment> 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<Attachment> unzipAndUpload(FileVO file) {
// 1. 下载并解压文件
try {
FileHandleVO fileHandleVO = fileHandleService.downloadAndUnzip(file);
List<File> 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<Attachment> 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;
}
}

View File

@@ -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<BusinessAttachmentMapper, BusinessAttachment> 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<BusinessAttachment> addList = convert.dto2entities(param);
if (CollectionUtil.isNotEmpty(addList)) {
return this.saveBatch(addList);
}
return true;
}
@Transactional(rollbackFor = Exception.class)
@Override
public boolean saveAndRemove(List<BusinessAttachmentAddDTO> 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<Long> bizIds = params.stream()
.map(BusinessAttachmentAddDTO::getBizId)
.distinct()
.toList();
this.removeByBizIds(bizIds);
}
List<BusinessAttachment> businessAttachments = convert.dtos2entities(params);
// 按照业务id分组排序
Map<Long, List<BusinessAttachment>> bizIdAttachmentsMap = businessAttachments.stream()
.collect(Collectors.groupingBy(BusinessAttachment::getBizId));
String tenantId = AuthUtil.getTenantId();
for (Map.Entry<Long, List<BusinessAttachment>> 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<Long> bizIds) {
return this.removeByBizIdsAndCodes(bizIds, null);
}
@Override
public List<BusinessAttachmentVO> queryList(BusinessAttachmentQueryDTO param) {
return baseMapper.queryList(param);
}
/**
* 批量删除
* @param bizIds 业务id列表为空不删除
* @param bizCodes 业务编码列表,为空,删除所有
* @return
*/
private boolean removeByBizIdsAndCodes(List<Long> bizIds, List<String> bizCodes) {
if (CollectionUtil.isEmpty(bizIds)) {
log.warn("删除附件,参数为空");
return false;
}
List<Long> ids = this.list(Wrappers.<BusinessAttachment>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;
}
}

View File

@@ -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<BusinessUnzipDetailMapper, BusinessUnzipDetail> 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<Attachment> attachments = attachmentService.unzipAndUpload(file);
if (CollectionUtil.isEmpty(attachments)) {
log.warn("上传解压文件为空 unzipTaskId:{} objectKey:{}", unzipTaskId, file.getObjectKey());
return;
}
// 2. 保存压缩文件和解压文件关系
List<BusinessUnzipDetail> 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);
}
}
}

View File

@@ -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;
/**
* <p>
* 业务解压任务表 服务实现类
* </p>
*
* @author bfhuange
* @since 2024-10-28
*/
@Slf4j
@AllArgsConstructor
@Service
public class BusinessUnzipTaskServiceImpl extends ServiceImpl<BusinessUnzipTaskMapper, BusinessUnzipTask> 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<BusinessUnzipTaskListVO> selectPage(IPage<BusinessUnzipTask> page, BusinessUnzipTaskQueryDTO param) {
List<BusinessUnzipTaskListVO> 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<BusinessUnzipTaskListVO> 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.<BusinessUnzipTask>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<BusinessUnzipTask> list = this.list(Wrappers.<BusinessUnzipTask>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<Long> taskIds = list.stream()
.filter(task -> UnzipTask.RelateType.FILE_TASK.getCode().equals(task.getRelateType()))
.map(BusinessUnzipTask::getRelateId)
.toList();
fileTaskService.cancelBatchByIds(taskIds);
}
// 删除业务文件解压任务
List<Long> unzipTaskIds = list.stream()
.map(BusinessUnzipTask::getId)
.toList();
this.removeBatchByIds(unzipTaskIds);
return true;
}
@Override
public List<BusinessUnzipDetailListVO> queryBusinessUnzipDetail(Long bizId) {
return baseMapper.queryBusinessUnzipDetail(bizId);
}
@Override
public List<BusinessUnzipTaskListVO> getBusinessUnzipTasksByBizIds(List<Long> 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("关联类型错误");
}
}

View File

@@ -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<File> 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);
}
}
}

View File

@@ -0,0 +1,46 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* 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<FileTaskPartMapper, FileTaskPart> implements IFileTaskPartService {
}

View File

@@ -0,0 +1,360 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* 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<FileTaskMapper, FileTask> 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.<FileTask>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.<FileTask>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<FileTaskPart> parts = partService.list(Wrappers.<FileTaskPart>lambdaQuery()
.eq(FileTaskPart::getFileTaskId, fileTask.getId())
.orderByAsc(FileTaskPart::getPartNumber)
);
try {
// 按分段号去重相同分段号取第一个如果分段号相同etag不同报错
Map<Integer, FileTaskPart> 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<Long> partId = numberTag.values().stream()
.map(FileTaskPart::getId)
.toList();
// 去重
parts = parts.stream()
.filter(e -> partId.contains(e.getId()))
.toList();
List<PartEtagVO> partEtags = parts.stream()
.map(convert::entity2vo)
.toList();
// 3. 合并文件
fileService.completeMultipartUpload(fileTask.getObjectKey(), fileTask.getUploadId(), partEtags);
// 4. 删除文件任务分段
List<Long> 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.<FileTaskPart>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<Long> taskIds) {
// 查询任务
List<FileTask> 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<FileTaskPart> list = partService.list(Wrappers.<FileTaskPart>lambdaQuery()
.in(FileTaskPart::getFileTaskId, taskIds)
);
if (CollectionUtil.isNotEmpty(list)) {
List<Long> 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<Long> ids) {
if (CollectionUtil.isEmpty(ids)) {
return false;
}
FileTask updateParam = new FileTask();
// 修改状态为已暂停
updateParam.setStatus(FileTaskStatus.PAUSED.getCode());
return this.update(updateParam, Wrappers.<FileTask>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.<BusinessUnzipTask>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.<FileTask>lambdaUpdate()
.eq(FileTask::getId, param.getId())
.lt(FileTask::getCurrentIndex, param.getPartNumber())
);
}
}

View File

@@ -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<String> fileUrls = fileService.getFileUrls(param.getObjectKeys(), null);
ocrParam.setUrls(fileUrls);
return ocrService.recognitionTransportCertificate(ocrParam);
}
}

View File

@@ -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<PartEtagVO> partEtagVOS) {
if (CollectionUtil.isEmpty(partEtagVOS)) {
log.warn("完成分段上传,要合并的分段列表为空");
return;
}
// 参数转换
List<PartEtag> 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<String, Object> 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<String, Object> 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<String> getFileUrls(List<String> 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;
}
}
}

View File

@@ -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()));
}
}

View File

@@ -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()));
}
}

View File

@@ -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));
}
}

View File

@@ -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<File> 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<File> un7z(String downloadFile, File destDir) {
List<File> 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<File> unrar(String zipFilePath, File unzipDir) {
List<File> files = new ArrayList<>();
try (
// 加载rar文件
InputStream inputStream = Files.newInputStream(Paths.get(zipFilePath));
Archive archive = new Archive(inputStream);
) {
List<FileHeader> 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<File> unzip(String zipFilePath, File unzipDir) {
List<File> 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);
}
}

View File

@@ -0,0 +1,6 @@
#spring:
# cloud:
# nacos:
# username: nacos
# password: ${NACOS_PASSWORD:gr30wIs5%Hi7keQj}
# server-addr: ${NACOS_ADDR:10.38.16.127:8848}

View File

@@ -0,0 +1,6 @@
#spring:
# cloud:
# nacos:
# username: nacos
# password: rWrMrVTWyf%ekjuw
# server-addr: ${NACOS_ADDR:192.168.0.242:8848}

View File

@@ -0,0 +1,8 @@
#server:
# port: 38107
#spring:
# cloud:
# nacos:
# username: nacos
# password: gr30wIs5%Hi7keQj
# server-addr: ${NACOS_ADDR:10.38.16.127:8848}

View File

@@ -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