From 9c25c374c47ed3b808ddcf6c7f230f8d8cc8b2c8 Mon Sep 17 00:00:00 2001 From: caiyuchao Date: Tue, 2 Sep 2025 18:17:29 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E4=B8=8B=E8=BD=BDtar?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../license/enums/ErrorCodeConstants.java | 1 + .../agt-module-license-server/pom.xml | 5 + .../admin/license/LicenseController.java | 10 +- .../license/framework/util/FileTypeUtils.java | 97 +++++++++++++++++++ .../service/license/LicenseService.java | 3 + .../service/license/LicenseServiceImpl.java | 42 ++++++++ 6 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 agt-module-license/agt-module-license-server/src/main/java/org/agt/module/license/framework/util/FileTypeUtils.java diff --git a/agt-module-license/agt-module-license-api/src/main/java/org/agt/module/license/enums/ErrorCodeConstants.java b/agt-module-license/agt-module-license-api/src/main/java/org/agt/module/license/enums/ErrorCodeConstants.java index 54fc6a3..fae139c 100644 --- a/agt-module-license/agt-module-license-api/src/main/java/org/agt/module/license/enums/ErrorCodeConstants.java +++ b/agt-module-license/agt-module-license-api/src/main/java/org/agt/module/license/enums/ErrorCodeConstants.java @@ -19,6 +19,7 @@ public interface ErrorCodeConstants { ErrorCode LICENSE_NOT_EXISTS = new ErrorCode(1_100_003_001, "License不存在"); ErrorCode LICENSE_SN_DUPLICATE = new ErrorCode(1_100_003_002, "License SN`{}`已存在"); ErrorCode LICENSE_IMPORT_LIST_IS_EMPTY = new ErrorCode(1_100_003_003, "导入License数据不能为空!"); + ErrorCode LICENSE_DOWNLOAD_FAILED = new ErrorCode(1_100_003_004, "下载失败!"); ErrorCode COMMENT_NOT_EXISTS = new ErrorCode(1_100_004_001, "评论不存在"); ErrorCode COMMENT_EXITS_CHILDREN = new ErrorCode(1_100_004_002, "存在子评论,无法删除"); diff --git a/agt-module-license/agt-module-license-server/pom.xml b/agt-module-license/agt-module-license-server/pom.xml index c728b7c..85a965f 100644 --- a/agt-module-license/agt-module-license-server/pom.xml +++ b/agt-module-license/agt-module-license-server/pom.xml @@ -121,6 +121,11 @@ agt-spring-boot-starter-monitor + + org.apache.tika + tika-core + + diff --git a/agt-module-license/agt-module-license-server/src/main/java/org/agt/module/license/controller/admin/license/LicenseController.java b/agt-module-license/agt-module-license-server/src/main/java/org/agt/module/license/controller/admin/license/LicenseController.java index 3825907..e436e1e 100644 --- a/agt-module-license/agt-module-license-server/src/main/java/org/agt/module/license/controller/admin/license/LicenseController.java +++ b/agt-module-license/agt-module-license-server/src/main/java/org/agt/module/license/controller/admin/license/LicenseController.java @@ -253,7 +253,7 @@ public class LicenseController { }) @PreAuthorize("@ss.hasPermission('license:license:import')") public CommonResult importCodeExcel(@RequestParam("file") MultipartFile file, - @RequestParam(value = "updateSupport", required = false, defaultValue = "false") Boolean updateSupport) throws Exception { + @RequestParam(value = "updateSupport", required = false, defaultValue = "false") Boolean updateSupport) throws Exception { List list = ExcelUtils.read(file, LicenseCodeImportExcelVO.class); return success(licenseService.importCodeList(list, updateSupport)); } @@ -265,4 +265,12 @@ public class LicenseController { licenseService.updateDetailById(updateReqVO); return success(true); } + + @Operation(summary = "下载") + @GetMapping("/download") + @Parameter(name = "tableId", description = "id", required = true, example = "1024") + public void download(@RequestParam("id") Long id, + HttpServletResponse response) { + licenseService.download(response, id); + } } \ No newline at end of file diff --git a/agt-module-license/agt-module-license-server/src/main/java/org/agt/module/license/framework/util/FileTypeUtils.java b/agt-module-license/agt-module-license-server/src/main/java/org/agt/module/license/framework/util/FileTypeUtils.java new file mode 100644 index 0000000..2055673 --- /dev/null +++ b/agt-module-license/agt-module-license-server/src/main/java/org/agt/module/license/framework/util/FileTypeUtils.java @@ -0,0 +1,97 @@ +package org.agt.module.license.framework.util; + +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.ttl.TransmittableThreadLocal; +import jakarta.servlet.http.HttpServletResponse; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.agt.framework.common.util.http.HttpUtils; +import org.apache.tika.Tika; +import org.apache.tika.mime.MimeTypeException; +import org.apache.tika.mime.MimeTypes; + +import java.io.IOException; + +/** + * 文件类型 Utils + * + * @author 千通源码 + */ +@Slf4j +public class FileTypeUtils { + + private static final ThreadLocal TIKA = TransmittableThreadLocal.withInitial(Tika::new); + + /** + * 获得文件的 mineType,对于 doc,jar 等文件会有误差 + * + * @param data 文件内容 + * @return mineType 无法识别时会返回“application/octet-stream” + */ + @SneakyThrows + public static String getMineType(byte[] data) { + return TIKA.get().detect(data); + } + + /** + * 已知文件名,获取文件类型,在某些情况下比通过字节数组准确,例如使用 jar 文件时,通过名字更为准确 + * + * @param name 文件名 + * @return mineType 无法识别时会返回“application/octet-stream” + */ + public static String getMineType(String name) { + return TIKA.get().detect(name); + } + + /** + * 在拥有文件和数据的情况下,最好使用此方法,最为准确 + * + * @param data 文件内容 + * @param name 文件名 + * @return mineType 无法识别时会返回“application/octet-stream” + */ + public static String getMineType(byte[] data, String name) { + return TIKA.get().detect(data, name); + } + + /** + * 根据 mineType 获得文件后缀 + * + * 注意:如果获取不到,或者发生异常,都返回 null + * + * @param mineType 类型 + * @return 后缀,例如说 .pdf + */ + public static String getExtension(String mineType) { + try { + return MimeTypes.getDefaultMimeTypes().forName(mineType).getExtension(); + } catch (MimeTypeException e) { + log.warn("[getExtension][获取文件后缀({}) 失败]", mineType, e); + return null; + } + } + + /** + * 返回附件 + * + * @param response 响应 + * @param filename 文件名 + * @param content 附件内容 + */ + public static void writeAttachment(HttpServletResponse response, String filename, byte[] content) throws IOException { + // 设置 header 和 contentType + response.setHeader("Content-Disposition", "attachment;filename=" + HttpUtils.encodeUtf8(filename)); + String contentType = getMineType(content, filename); + response.setContentType(contentType); + // 针对 video 的特殊处理,解决视频地址在移动端播放的兼容性问题 + if (StrUtil.containsIgnoreCase(contentType, "video")) { + response.setHeader("Content-Length", String.valueOf(content.length - 1)); + response.setHeader("Content-Range", String.valueOf(content.length - 1)); + response.setHeader("Accept-Ranges", "bytes"); + } + // 输出附件 + IoUtil.write(response.getOutputStream(), false, content); + } + +} diff --git a/agt-module-license/agt-module-license-server/src/main/java/org/agt/module/license/service/license/LicenseService.java b/agt-module-license/agt-module-license-server/src/main/java/org/agt/module/license/service/license/LicenseService.java index 43af9fb..df8814c 100644 --- a/agt-module-license/agt-module-license-server/src/main/java/org/agt/module/license/service/license/LicenseService.java +++ b/agt-module-license/agt-module-license-server/src/main/java/org/agt/module/license/service/license/LicenseService.java @@ -1,5 +1,6 @@ package org.agt.module.license.service.license; +import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import org.agt.framework.common.pojo.PageResult; import org.agt.module.license.controller.admin.license.vo.ImportRespVO; @@ -99,4 +100,6 @@ public interface LicenseService { ImportRespVO importCodeList(List list, Boolean updateSupport); void updateDetailById(LicenseDetailVO licenseDetail); + + void download(HttpServletResponse response, Long id); } \ No newline at end of file diff --git a/agt-module-license/agt-module-license-server/src/main/java/org/agt/module/license/service/license/LicenseServiceImpl.java b/agt-module-license/agt-module-license-server/src/main/java/org/agt/module/license/service/license/LicenseServiceImpl.java index 30feb8e..fd75e6e 100644 --- a/agt-module-license/agt-module-license-server/src/main/java/org/agt/module/license/service/license/LicenseServiceImpl.java +++ b/agt-module-license/agt-module-license-server/src/main/java/org/agt/module/license/service/license/LicenseServiceImpl.java @@ -3,13 +3,18 @@ package org.agt.module.license.service.license; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.io.FileUtil; import cn.hutool.core.io.resource.ResourceUtil; +import cn.hutool.core.util.CharsetUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.ZipUtil; +import cn.hutool.extra.compress.CompressUtil; +import cn.hutool.extra.compress.archiver.Archiver; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; +import org.agt.framework.common.pojo.CommonResult; import org.agt.framework.common.pojo.PageResult; import org.agt.framework.common.util.object.BeanUtils; import org.agt.framework.dict.core.DictFrameworkUtils; @@ -37,17 +42,21 @@ import org.agt.module.license.dal.mysql.license.LicenseMapper; import org.agt.module.license.dal.mysql.license.LicenseProviderMapper; import org.agt.module.license.dal.mysql.project.ProjectMapper; import org.agt.module.license.enums.LicenseStatusEnum; +import org.agt.module.license.framework.util.FileTypeUtils; import org.agt.module.system.api.mail.MailSendApi; import org.agt.module.system.api.mail.dto.MailSendSingleToUserReqDTO; import org.agt.module.system.api.notify.NotifyMessageSendApi; import org.agt.module.system.api.notify.dto.NotifySendSingleToUserReqDTO; +import org.apache.commons.compress.archivers.ArchiveStreamFactory; import org.jetbrains.annotations.NotNull; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.IOException; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; @@ -61,6 +70,7 @@ import java.util.Optional; import java.util.stream.Collectors; import static org.agt.framework.common.exception.util.ServiceExceptionUtil.exception; +import static org.agt.module.license.enums.ErrorCodeConstants.LICENSE_DOWNLOAD_FAILED; import static org.agt.module.license.enums.ErrorCodeConstants.LICENSE_IMPORT_LIST_IS_EMPTY; import static org.agt.module.license.enums.ErrorCodeConstants.LICENSE_NOT_EXISTS; import static org.agt.module.license.enums.ErrorCodeConstants.LICENSE_SN_DUPLICATE; @@ -408,6 +418,7 @@ public class LicenseServiceImpl implements LicenseService { // File tempFile = new File("D:/temp/temp.zip"); File tempFile = new File("/usr/local/licGen/temp.zip"); + ZipUtil.zip(tempFile, pathList.toArray(new String[0]), inputStreamList.toArray(new ByteArrayInputStream[inputStreamList.size()])); String fileURL = fileApi.createFile(FileUtil.readBytes(tempFile), licenseDO.getSerialNo() + "_" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")) + ".zip"); @@ -988,4 +999,35 @@ public class LicenseServiceImpl implements LicenseService { } licenseDetailMapper.updateById(licenseDetailDO); } + + public void download(HttpServletResponse response, Long id) { + try { + List detailList = licenseDetailMapper.selectList(Wrappers.lambdaQuery() + .eq(LicenseDetailDO::getLicenseId, id)); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + Archiver archiver = CompressUtil.createArchiver(CharsetUtil.CHARSET_UTF_8, ArchiveStreamFactory.TAR, outputStream); + + for (LicenseDetailDO detail : detailList) { + addArchiverFile(archiver, detail.getFileUrl()); + addArchiverFile(archiver, detail.getFileUrlLegacy()); + } + archiver.finish().close(); + FileTypeUtils.writeAttachment(response, "temp.tar", outputStream.toByteArray()); + } catch (IOException e) { + throw exception(LICENSE_DOWNLOAD_FAILED); + } + } + + private void addArchiverFile(Archiver archiver, String fileUrl) { + String tempFilePath = "/usr/local/licGen/"; + if (StrUtil.isNotBlank(fileUrl)) { + String fileName = fileUrl.substring(fileUrl.lastIndexOf("/") + 1, fileUrl.lastIndexOf("_")) + ".ini"; + CommonResult result = fileApi.getFileContent(fileUrl); + byte[] fileContent = result.getData(); + archiver.add(FileUtil.writeBytes(fileContent, tempFilePath + fileName)); + FileUtil.del(tempFilePath + fileName); + } + } + } \ No newline at end of file