样本导入功能

This commit is contained in:
LHD_HY 2026-01-09 14:37:01 +08:00
parent f00ef3c082
commit e6c769c141
9 changed files with 271 additions and 10 deletions

View File

@ -5,15 +5,35 @@ import com.bonus.common.annotation.SysLog;
import com.bonus.common.core.controller.BaseController; import com.bonus.common.core.controller.BaseController;
import com.bonus.common.core.domain.AjaxResult; import com.bonus.common.core.domain.AjaxResult;
import com.bonus.common.core.page.TableDataInfo; 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.dto.SampleLibraryDto;
import com.bonus.common.domain.data.vo.SampleLibraryVo; import com.bonus.common.domain.data.vo.SampleLibraryVo;
import com.bonus.common.enums.OperaType; 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 com.bonus.web.service.data.SampleService;
import io.swagger.annotations.ApiOperation; 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.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource; 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.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 * @className:SampleController
@ -29,6 +49,12 @@ public class SampleController extends BaseController {
@Resource(name = "SampleService") @Resource(name = "SampleService")
private SampleService sampleService; private SampleService sampleService;
@Resource
private MinioUtil minioUtil;
@Resource
private MinioConfig minioConfig;
@ApiOperation(notes = "查询样本库列表数据",value = "查询样本库列表数据") @ApiOperation(notes = "查询样本库列表数据",value = "查询样本库列表数据")
@RequiresPermissions("data:sample:list") @RequiresPermissions("data:sample:list")
@GetMapping("/getSampleList") @GetMapping("/getSampleList")
@ -70,4 +96,119 @@ public class SampleController extends BaseController {
return sampleService.delLabelData(dto); 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<SampleDto> 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<SampleDto> sampleList) {
// 支持的图片格式
List<String> 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);
}
}
} }

View File

@ -2,6 +2,7 @@ package com.bonus.web.service.data;
import com.bonus.common.core.domain.AjaxResult; import com.bonus.common.core.domain.AjaxResult;
import com.bonus.common.domain.data.dto.LabelGroupDto; 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.dto.SampleLibraryDto;
import com.bonus.common.domain.data.vo.SampleLibraryVo; import com.bonus.common.domain.data.vo.SampleLibraryVo;
import com.bonus.common.utils.ValidatorsUtils; import com.bonus.common.utils.ValidatorsUtils;
@ -13,6 +14,7 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.interceptor.TransactionAspectSupport; import org.springframework.transaction.interceptor.TransactionAspectSupport;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@ -134,4 +136,46 @@ public class SampleService {
} }
return AjaxResult.success(vo); 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<SampleDto> 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());
}
}
} }

View File

@ -33,13 +33,18 @@ public class SampleDto {
/** /**
* 文件格式 * 文件格式
*/ */
private String fileSuffiex; private String fileSuffix;
/** /**
* 文件大小 * 文件大小
*/ */
private BigDecimal fileSize; private BigDecimal fileSize;
/**
* 文件路径
*/
private String filePath;
/** /**
* 创建时间 * 创建时间
*/ */

View File

@ -33,7 +33,7 @@ public class SampleVo {
/** /**
* 文件格式 * 文件格式
*/ */
private String fileSuffiex; private String fileSuffix;
/** /**
* 文件大小 * 文件大小

View File

@ -44,6 +44,21 @@ public class FileUtil {
return Paths.get(baseDir, datePath, uniqueFileName).toString(); 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();
}
/** /**
* 获取文件扩展名 * 获取文件扩展名
*/ */

View File

@ -1,5 +1,6 @@
package com.bonus.data.mapper; 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.dto.SampleLibraryDto;
import com.bonus.common.domain.data.vo.SampleLibraryVo; import com.bonus.common.domain.data.vo.SampleLibraryVo;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
@ -62,4 +63,12 @@ public interface DISampleMapper {
* @date 2025/12/22 13:18 * @date 2025/12/22 13:18
*/ */
SampleLibraryVo getSampleDetail(SampleLibraryDto dto); SampleLibraryVo getSampleDetail(SampleLibraryDto dto);
/**
* 批量插入样本数据
* @param sampleList 样本列表
* @author lhdhy
* @date 2026/01/05
*/
void batchInsertSample(@Param("sampleList") List<SampleDto> sampleList);
} }

View File

@ -1,5 +1,6 @@
package com.bonus.data.service; 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.dto.SampleLibraryDto;
import com.bonus.common.domain.data.vo.SampleLibraryVo; import com.bonus.common.domain.data.vo.SampleLibraryVo;
@ -60,4 +61,12 @@ public interface DISampleService {
* @date 2025/12/22 13:17 * @date 2025/12/22 13:17
*/ */
SampleLibraryVo getSampleDetail(SampleLibraryDto dto); SampleLibraryVo getSampleDetail(SampleLibraryDto dto);
/**
* 批量插入样本数据
* @param sampleList 样本列表
* @author lhdhy
* @date 2026/01/05
*/
void batchInsertSample(List<SampleDto> sampleList);
} }

View File

@ -1,5 +1,6 @@
package com.bonus.data.service.impl; 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.dto.SampleLibraryDto;
import com.bonus.common.domain.data.vo.SampleLibraryVo; import com.bonus.common.domain.data.vo.SampleLibraryVo;
import com.bonus.data.mapper.DISampleMapper; import com.bonus.data.mapper.DISampleMapper;
@ -47,4 +48,11 @@ public class DSampleServiceImpl implements DISampleService {
public SampleLibraryVo getSampleDetail(SampleLibraryDto dto) { public SampleLibraryVo getSampleDetail(SampleLibraryDto dto) {
return diSampleMapper.getSampleDetail(dto); return diSampleMapper.getSampleDetail(dto);
} }
@Override
public void batchInsertSample(List<SampleDto> sampleList) {
if (sampleList != null && !sampleList.isEmpty()) {
diSampleMapper.batchInsertSample(sampleList);
}
}
} }

View File

@ -73,7 +73,7 @@
LEFT JOIN ( LEFT JOIN (
SELECT sample_library_id, COUNT(*) AS num SELECT sample_library_id, COUNT(*) AS num
FROM tb_sample FROM tb_sample
WHERE del_flag = '1' WHERE del_flag = '0'
GROUP BY sample_library_id GROUP BY sample_library_id
) A ON tsl.sample_library_id = A.sample_library_id ) A ON tsl.sample_library_id = A.sample_library_id
LEFT JOIN sys_dict_data sdd LEFT JOIN sys_dict_data sdd
@ -109,4 +109,34 @@
sample_library_desc sample_library_desc
FROM tb_sample_library WHERE sample_library_id = #{sampleLibraryId} FROM tb_sample_library WHERE sample_library_id = #{sampleLibraryId}
</select> </select>
<!-- 新增:批量插入样本数据 -->
<insert id="batchInsertSample">
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
<foreach collection="sampleList" item="item" separator=",">
(
#{item.sampleLibraryId},
#{item.fileName},
#{item.fileSuffix},
#{item.fileSize},
#{item.filePath},
#{item.createUserId},
#{item.updateUserId},
'0',
NOW(),
NOW()
)
</foreach>
</insert>
</mapper> </mapper>