From e6c769c14111d46636817378ba4d235130b804b6 Mon Sep 17 00:00:00 2001 From: LHD_HY <2872546851@qq.com> Date: Fri, 9 Jan 2026 14:37:01 +0800 Subject: [PATCH] =?UTF-8?q?=E6=A0=B7=E6=9C=AC=E5=AF=BC=E5=85=A5=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../web/controller/data/SampleController.java | 141 ++++++++++++++++++ .../bonus/web/service/data/SampleService.java | 44 ++++++ .../common/domain/data/dto/SampleDto.java | 7 +- .../bonus/common/domain/data/vo/SampleVo.java | 2 +- .../java/com/bonus/common/utils/FileUtil.java | 15 ++ .../com/bonus/data/mapper/DISampleMapper.java | 9 ++ .../bonus/data/service/DISampleService.java | 9 ++ .../data/service/impl/DSampleServiceImpl.java | 8 + .../main/resources/mapper/DSampleMapper.xml | 46 +++++- 9 files changed, 271 insertions(+), 10 deletions(-) diff --git a/bonus-admin/src/main/java/com/bonus/web/controller/data/SampleController.java b/bonus-admin/src/main/java/com/bonus/web/controller/data/SampleController.java index ba6514c..83986d9 100644 --- a/bonus-admin/src/main/java/com/bonus/web/controller/data/SampleController.java +++ b/bonus-admin/src/main/java/com/bonus/web/controller/data/SampleController.java @@ -5,15 +5,35 @@ import com.bonus.common.annotation.SysLog; import com.bonus.common.core.controller.BaseController; import com.bonus.common.core.domain.AjaxResult; import com.bonus.common.core.page.TableDataInfo; +import com.bonus.common.domain.data.dto.SampleDto; import com.bonus.common.domain.data.dto.SampleLibraryDto; import com.bonus.common.domain.data.vo.SampleLibraryVo; import com.bonus.common.enums.OperaType; +import com.bonus.common.utils.FileUtil; +import com.bonus.file.config.MinioConfig; +import com.bonus.file.util.MinioUtil; import com.bonus.web.service.data.SampleService; import io.swagger.annotations.ApiOperation; +import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; +import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream; +import org.apache.commons.compress.utils.IOUtils; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; import javax.annotation.Resource; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.Map; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import static jdk.nashorn.internal.runtime.regexp.joni.Config.log; /** * @className:SampleController @@ -29,6 +49,12 @@ public class SampleController extends BaseController { @Resource(name = "SampleService") private SampleService sampleService; + @Resource + private MinioUtil minioUtil; + + @Resource + private MinioConfig minioConfig; + @ApiOperation(notes = "查询样本库列表数据",value = "查询样本库列表数据") @RequiresPermissions("data:sample:list") @GetMapping("/getSampleList") @@ -70,4 +96,119 @@ public class SampleController extends BaseController { return sampleService.delLabelData(dto); } + @ApiOperation(value = "导入样本ZIP包", notes = "批量导入ZIP内的图片文件到指定样本库(支持多层ZIP嵌套+中文文件名)") + @PostMapping("/importSampleFile") + @SysLog(title = "数据管理", module = "数据管理->样本库管理->导入样本", businessType = OperaType.IMPORT, details = "导入样本ZIP包", logType = 1) + @RequiresPermissions("data:sample:import") + public AjaxResult importSampleFile( + @RequestParam("sampleLibraryId") Long sampleLibraryId, + @RequestParam("file") MultipartFile file) { + // 1. 基础参数校验 + if (sampleLibraryId == null || file.isEmpty()) { + return AjaxResult.error("样本库ID和ZIP文件不能为空"); + } + String fileName = file.getOriginalFilename(); + if (fileName == null || !fileName.toLowerCase().endsWith(".zip")) { + return AjaxResult.error("仅支持上传ZIP格式文件"); + } + + // 2. 解析ZIP包(支持多层嵌套+UTF-8编码),并上传图片到MinIO生成filePath + List sampleList = new ArrayList<>(); + try { + // 先将MultipartFile缓存为字节数组,避免流被多次消费 + byte[] zipBytes = file.getBytes(); + try (InputStream inputStream = new ByteArrayInputStream(zipBytes)) { + parseNestedZipWithUtf8(inputStream, "", sampleList); + } + } catch (Exception e) { + return AjaxResult.error("解析ZIP文件失败:" + e.getMessage()); + } + + // 3. 校验有效文件数量 + if (sampleList.isEmpty()) { + return AjaxResult.error("ZIP包内未检测到有效图片文件(仅支持jpg/png/jpeg)"); + } + + // 4. 调用业务层导入(仅入库filePath) + return sampleService.importSampleFile(sampleLibraryId, getUserId(), sampleList); + } + + /** + * 递归解析多层嵌套的ZIP文件,上传图片到MinIO并生成filePath + * @param inputStream 当前ZIP的输入流 + * @param parentPath 父级路径(标识嵌套层级) + * @param sampleList 存储有效图片的列表 + */ + private void parseNestedZipWithUtf8(InputStream inputStream, String parentPath, List sampleList) { + // 支持的图片格式 + List validImageSuffix = Arrays.asList("jpg", "png", "jpeg"); + + try (ZipArchiveInputStream zais = new ZipArchiveInputStream( + inputStream, + StandardCharsets.UTF_8.name(), // 指定UTF-8编码 + true, // 允许非标准ZIP格式 + true // 解析注释 + )) { + ZipArchiveEntry entry; + while ((entry = zais.getNextZipEntry()) != null) { + // 跳过文件夹 + if (entry.isDirectory()) { + continue; + } + + // 拼接完整路径(含嵌套层级) + String entryFullName = parentPath + entry.getName(); + int suffixIndex = entryFullName.lastIndexOf("."); + if (suffixIndex == -1 || suffixIndex == entryFullName.length() - 1) { + continue; // 无后缀/后缀为空,跳过 + } + String suffix = entryFullName.substring(suffixIndex + 1).toLowerCase(); + + // 情况1:当前是ZIP文件 → 递归解析 + if ("zip".equals(suffix)) { + // 完整读取子ZIP的字节 + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + IOUtils.copy(zais, baos); + byte[] zipBytes = baos.toByteArray(); + // 递归解析子ZIP + parseNestedZipWithUtf8(new ByteArrayInputStream(zipBytes), entryFullName + "/", sampleList); + } + // 情况2:当前是图片文件 → 上传到MinIO并生成filePath + else if (validImageSuffix.contains(suffix)) { + try { + // 读取图片文件字节 + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + IOUtils.copy(zais, baos); + byte[] imageBytes = baos.toByteArray(); + ByteArrayInputStream imageInputStream = new ByteArrayInputStream(imageBytes); + + // 生成MinIO存储路径(filePath) + String uploadPath = FileUtil.generateZipDatePath(entryFullName, "sampleImage"); + // 上传图片到MinIO + minioUtil.uploadFile(minioConfig.getBucketName(), uploadPath, imageInputStream); + + // 拼接完整的filePath(存储到数据库) + String filePath = uploadPath; + + // 封装SampleDto(仅设置filePath) + SampleDto sampleDto = new SampleDto(); + sampleDto.setFileName(entryFullName); // 含嵌套路径的完整文件名 + sampleDto.setFileSuffix(suffix); + // 处理文件大小 + long fileSize = entry.getSize() == -1 ? imageBytes.length : entry.getSize(); + sampleDto.setFileSize(new BigDecimal(fileSize)); + // 仅存储filePath到数据库 + sampleDto.setFilePath(filePath); + + sampleList.add(sampleDto); + } catch (Exception e) { + throw new RuntimeException("上传图片文件失败:" + entryFullName + ",错误:" + e.getMessage(), e); + } + } + } + } catch (Exception e) { + throw new RuntimeException("解析嵌套ZIP失败(路径:" + parentPath + "):" + e.getMessage(), e); + } + } + } diff --git a/bonus-admin/src/main/java/com/bonus/web/service/data/SampleService.java b/bonus-admin/src/main/java/com/bonus/web/service/data/SampleService.java index 3418d85..f32cbfe 100644 --- a/bonus-admin/src/main/java/com/bonus/web/service/data/SampleService.java +++ b/bonus-admin/src/main/java/com/bonus/web/service/data/SampleService.java @@ -2,6 +2,7 @@ package com.bonus.web.service.data; import com.bonus.common.core.domain.AjaxResult; import com.bonus.common.domain.data.dto.LabelGroupDto; +import com.bonus.common.domain.data.dto.SampleDto; import com.bonus.common.domain.data.dto.SampleLibraryDto; import com.bonus.common.domain.data.vo.SampleLibraryVo; import com.bonus.common.utils.ValidatorsUtils; @@ -13,6 +14,7 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.interceptor.TransactionAspectSupport; import javax.annotation.Resource; +import java.math.BigDecimal; import java.util.Collections; import java.util.List; @@ -134,4 +136,46 @@ public class SampleService { } return AjaxResult.success(vo); } + + /** + * 批量导入样本文件(新增核心方法) + * @param sampleLibraryId 样本库ID + * @param createUserId 创建人ID + * @param sampleList 样本列表 + * @return AjaxResult + * @author lhdhy + * @date 2026/01/05 + */ + @Transactional(rollbackFor = Exception.class) + public AjaxResult importSampleFile(Long sampleLibraryId, Long createUserId, List sampleList) { + try { + // 1. 校验样本库是否存在 + SampleLibraryDto libraryDto = new SampleLibraryDto(); + libraryDto.setSampleLibraryId(sampleLibraryId); + SampleLibraryVo libraryVo = diSampleService.getSampleDetail(libraryDto); + if (libraryVo == null) { + return AjaxResult.error("样本库不存在"); + } + + // 2. 封装样本公共字段 + for (SampleDto sample : sampleList) { + sample.setSampleLibraryId(sampleLibraryId); + sample.setCreateUserId(createUserId); + sample.setUpdateUserId(createUserId); + sample.setDelFlag("0"); + // 兜底文件大小(避免null) + if (sample.getFileSize() == null) { + sample.setFileSize(BigDecimal.ZERO); + } + } + + // 3. 批量插入样本数据 + diSampleService.batchInsertSample(sampleList); + return AjaxResult.success("导入成功,共导入 " + sampleList.size() + " 个图片文件"); + } catch (Exception e) { + log.error("样本文件导入失败", e); + TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); + return AjaxResult.error("导入失败:" + e.getMessage()); + } + } } diff --git a/bonus-common/src/main/java/com/bonus/common/domain/data/dto/SampleDto.java b/bonus-common/src/main/java/com/bonus/common/domain/data/dto/SampleDto.java index cae117b..628ece8 100644 --- a/bonus-common/src/main/java/com/bonus/common/domain/data/dto/SampleDto.java +++ b/bonus-common/src/main/java/com/bonus/common/domain/data/dto/SampleDto.java @@ -33,13 +33,18 @@ public class SampleDto { /** * 文件格式 */ - private String fileSuffiex; + private String fileSuffix; /** * 文件大小 */ private BigDecimal fileSize; + /** + * 文件路径 + */ + private String filePath; + /** * 创建时间 */ diff --git a/bonus-common/src/main/java/com/bonus/common/domain/data/vo/SampleVo.java b/bonus-common/src/main/java/com/bonus/common/domain/data/vo/SampleVo.java index e5525f8..70ee27d 100644 --- a/bonus-common/src/main/java/com/bonus/common/domain/data/vo/SampleVo.java +++ b/bonus-common/src/main/java/com/bonus/common/domain/data/vo/SampleVo.java @@ -33,7 +33,7 @@ public class SampleVo { /** * 文件格式 */ - private String fileSuffiex; + private String fileSuffix; /** * 文件大小 diff --git a/bonus-common/src/main/java/com/bonus/common/utils/FileUtil.java b/bonus-common/src/main/java/com/bonus/common/utils/FileUtil.java index 8ab176d..9da15f2 100644 --- a/bonus-common/src/main/java/com/bonus/common/utils/FileUtil.java +++ b/bonus-common/src/main/java/com/bonus/common/utils/FileUtil.java @@ -44,6 +44,21 @@ public class FileUtil { return Paths.get(baseDir, datePath, uniqueFileName).toString(); } + /** + * 生成日期目录格式的存储路径 + */ + public static String generateZipDatePath(String fileName, String baseDir) { + // 生成日期目录:年/月/日(与原有方法逻辑一致) + SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd"); + String datePath = sdf.format(new Date()); + // 生成唯一文件名(复用原有工具方法) + String fileExtension = getFileExtension(fileName); + String uniqueFileName = UUID.randomUUID().toString().replaceAll("-","") + fileExtension; + + // 构建完整路径:baseDir/年/月/日/UUID.扩展名(与原有方法逻辑一致) + return Paths.get(baseDir, datePath, uniqueFileName).toString(); + } + /** * 获取文件扩展名 */ diff --git a/bonus-data/src/main/java/com/bonus/data/mapper/DISampleMapper.java b/bonus-data/src/main/java/com/bonus/data/mapper/DISampleMapper.java index 1e9f03d..7524841 100644 --- a/bonus-data/src/main/java/com/bonus/data/mapper/DISampleMapper.java +++ b/bonus-data/src/main/java/com/bonus/data/mapper/DISampleMapper.java @@ -1,5 +1,6 @@ package com.bonus.data.mapper; +import com.bonus.common.domain.data.dto.SampleDto; import com.bonus.common.domain.data.dto.SampleLibraryDto; import com.bonus.common.domain.data.vo.SampleLibraryVo; import org.apache.ibatis.annotations.Param; @@ -62,4 +63,12 @@ public interface DISampleMapper { * @date 2025/12/22 13:18 */ SampleLibraryVo getSampleDetail(SampleLibraryDto dto); + + /** + * 批量插入样本数据 + * @param sampleList 样本列表 + * @author lhdhy + * @date 2026/01/05 + */ + void batchInsertSample(@Param("sampleList") List sampleList); } diff --git a/bonus-data/src/main/java/com/bonus/data/service/DISampleService.java b/bonus-data/src/main/java/com/bonus/data/service/DISampleService.java index f541cc2..ff64cfe 100644 --- a/bonus-data/src/main/java/com/bonus/data/service/DISampleService.java +++ b/bonus-data/src/main/java/com/bonus/data/service/DISampleService.java @@ -1,5 +1,6 @@ package com.bonus.data.service; +import com.bonus.common.domain.data.dto.SampleDto; import com.bonus.common.domain.data.dto.SampleLibraryDto; import com.bonus.common.domain.data.vo.SampleLibraryVo; @@ -60,4 +61,12 @@ public interface DISampleService { * @date 2025/12/22 13:17 */ SampleLibraryVo getSampleDetail(SampleLibraryDto dto); + + /** + * 批量插入样本数据 + * @param sampleList 样本列表 + * @author lhdhy + * @date 2026/01/05 + */ + void batchInsertSample(List sampleList); } diff --git a/bonus-data/src/main/java/com/bonus/data/service/impl/DSampleServiceImpl.java b/bonus-data/src/main/java/com/bonus/data/service/impl/DSampleServiceImpl.java index 795a2fe..f2048f2 100644 --- a/bonus-data/src/main/java/com/bonus/data/service/impl/DSampleServiceImpl.java +++ b/bonus-data/src/main/java/com/bonus/data/service/impl/DSampleServiceImpl.java @@ -1,5 +1,6 @@ package com.bonus.data.service.impl; +import com.bonus.common.domain.data.dto.SampleDto; import com.bonus.common.domain.data.dto.SampleLibraryDto; import com.bonus.common.domain.data.vo.SampleLibraryVo; import com.bonus.data.mapper.DISampleMapper; @@ -47,4 +48,11 @@ public class DSampleServiceImpl implements DISampleService { public SampleLibraryVo getSampleDetail(SampleLibraryDto dto) { return diSampleMapper.getSampleDetail(dto); } + + @Override + public void batchInsertSample(List sampleList) { + if (sampleList != null && !sampleList.isEmpty()) { + diSampleMapper.batchInsertSample(sampleList); + } + } } diff --git a/bonus-data/src/main/resources/mapper/DSampleMapper.xml b/bonus-data/src/main/resources/mapper/DSampleMapper.xml index af71f61..75a29a4 100644 --- a/bonus-data/src/main/resources/mapper/DSampleMapper.xml +++ b/bonus-data/src/main/resources/mapper/DSampleMapper.xml @@ -43,9 +43,9 @@ UPDATE tb_sample_library SET sample_library_name = #{params.sampleLibraryName}, - sample_library_label = #{params.sampleLibraryLabel}, - sample_library_type = #{params.sampleLibraryType}, - sample_library_desc = #{params.sampleLibraryDesc} + sample_library_label = #{params.sampleLibraryLabel}, + sample_library_type = #{params.sampleLibraryType}, + sample_library_desc = #{params.sampleLibraryDesc} WHERE sample_library_id = #{params.sampleLibraryId}; @@ -73,7 +73,7 @@ LEFT JOIN ( SELECT sample_library_id, COUNT(*) AS num FROM tb_sample - WHERE del_flag = '1' + WHERE del_flag = '0' GROUP BY sample_library_id ) A ON tsl.sample_library_id = A.sample_library_id LEFT JOIN sys_dict_data sdd @@ -103,10 +103,40 @@ + + + + INSERT INTO tb_sample ( + sample_library_id, + file_name, + file_suffix, + file_size, + file_path, + create_user_id, + update_user_id, + del_flag, + create_time, + update_time + ) VALUES + + ( + #{item.sampleLibraryId}, + #{item.fileName}, + #{item.fileSuffix}, + #{item.fileSize}, + #{item.filePath}, + #{item.createUserId}, + #{item.updateUserId}, + '0', + NOW(), + NOW() + ) + +