问题修改
This commit is contained in:
parent
ad27e10673
commit
84a03fcf49
|
|
@ -20,6 +20,7 @@ import com.bonus.material.device.domain.vo.DevInfoVo;
|
|||
import com.bonus.material.device.domain.vo.DevMergeVo;
|
||||
import com.bonus.material.device.service.DevInfoService;
|
||||
import com.bonus.material.device.service.DevMergeService;
|
||||
import com.bonus.material.utils.FolderZipUtil;
|
||||
import com.bonus.material.utils.ReflectUtils;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
|
|
@ -290,6 +291,30 @@ public class DevMergeController extends BaseController {
|
|||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 下载指定空文件夹结构的 ZIP 包
|
||||
* 目标结构:
|
||||
* ├── business/
|
||||
* │ ├── report/
|
||||
* │ └── data/
|
||||
* └── temp/
|
||||
* 访问路径:http://localhost:8080/download/zip
|
||||
*/
|
||||
@PostMapping("/zip")
|
||||
public void downloadEmptyFolderZip(HttpServletResponse response, String orderId) {
|
||||
service.downloadEmptyFolderZip(response, orderId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传包含多个一级目录的ZIP包
|
||||
* 无需传设备ID,自动识别ZIP内的Apply-xxx目录
|
||||
*/
|
||||
@PostMapping("/upload-multi")
|
||||
public AjaxResult uploadMultiDeviceZip(@RequestParam MultipartFile file, String orderId) {
|
||||
return service.uploadAndUnzipMultiDeviceZip(file, orderId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载导入模板
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ public class EquipmentImportDTO {
|
|||
@Excel(name = "规格型号", sort = 3, align = HorizontalAlignment.CENTER)
|
||||
private String specification;
|
||||
|
||||
@Excel(name = "资产原值", sort = 4, align = HorizontalAlignment.CENTER)
|
||||
@Excel(name = "资产原值(万元)", sort = 4, align = HorizontalAlignment.CENTER)
|
||||
@DecimalMin(value = "0.00", message = "资产原值不能小于0")
|
||||
private BigDecimal originalValue;
|
||||
|
||||
|
|
|
|||
|
|
@ -91,10 +91,17 @@ public interface DevMergeMapper {
|
|||
|
||||
List<String> listAllManufacturerNames();
|
||||
|
||||
// 批量查询profession对应的typeId
|
||||
// 批量查询profession对应的typeId
|
||||
Integer getTypeId(String professions);
|
||||
|
||||
|
||||
Integer getManufacturer(String manufacturers);
|
||||
|
||||
List<String> getFileName(String orderId);
|
||||
|
||||
@MapKey("fileName")
|
||||
Map<String, Object> getFileId(String orderId);
|
||||
|
||||
Integer selNum(DevMergeVo o);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -77,4 +77,8 @@ public interface DevMergeService {
|
|||
List<String> listAllProfessionNames();
|
||||
|
||||
List<String> listAllManufacturerNames();
|
||||
|
||||
void downloadEmptyFolderZip(HttpServletResponse response,String orderId);
|
||||
|
||||
AjaxResult uploadAndUnzipMultiDeviceZip(MultipartFile zipFile,String orderId);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,51 +1,31 @@
|
|||
package com.bonus.material.device.service.impl;
|
||||
|
||||
import cn.hutool.core.collection.CollectionUtil;
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.PhoneUtil;
|
||||
import com.bonus.common.biz.constant.MaterialConstants;
|
||||
import com.bonus.common.biz.domain.*;
|
||||
import com.bonus.common.biz.enums.HttpCodeEnum;
|
||||
import com.bonus.common.biz.enums.MaStatusEnum;
|
||||
import com.bonus.common.core.exception.ServiceException;
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.bonus.common.core.utils.DateUtils;
|
||||
import com.bonus.common.core.utils.StringUtils;
|
||||
import com.bonus.common.core.utils.bean.BeanUtils;
|
||||
import com.bonus.common.core.utils.bean.BeanValidators;
|
||||
import com.bonus.common.core.utils.poi.ExcelUtil;
|
||||
import com.bonus.common.core.web.domain.AjaxResult;
|
||||
import com.bonus.common.security.utils.SecurityUtils;
|
||||
import com.bonus.material.basic.domain.BmSlideShow;
|
||||
import com.bonus.material.book.domain.BookCarInfoDto;
|
||||
import com.bonus.material.devchange.domain.MaDevFile;
|
||||
import com.bonus.material.devchange.domain.MaDevInfo;
|
||||
import com.bonus.material.devchange.domain.MaDevInfoXlsx;
|
||||
import com.bonus.material.devchange.domain.MapBean;
|
||||
import com.bonus.material.devchange.mapper.MaDevInfoMapper;
|
||||
import com.bonus.material.device.domain.DevInfo;
|
||||
import com.bonus.material.device.domain.EquipmentImportDTO;
|
||||
import com.bonus.material.device.domain.MaDevQc;
|
||||
import com.bonus.material.device.domain.Table;
|
||||
import com.bonus.material.device.domain.dto.DevInfoImpDto;
|
||||
import com.bonus.material.device.domain.dto.InfoMotionDto;
|
||||
import com.bonus.material.device.domain.vo.*;
|
||||
import com.bonus.material.device.mapper.BmFileInfoMapper;
|
||||
import com.bonus.material.device.domain.vo.DevInfoPropertyVo;
|
||||
import com.bonus.material.device.domain.vo.DevInfoVo;
|
||||
import com.bonus.material.device.domain.vo.DevMergeVo;
|
||||
import com.bonus.material.device.mapper.DevInfoMapper;
|
||||
import com.bonus.material.device.mapper.DevMergeMapper;
|
||||
import com.bonus.material.device.mapper.MaDevQcMapper;
|
||||
import com.bonus.material.device.service.DevInfoService;
|
||||
import com.bonus.material.device.service.DevMergeService;
|
||||
import com.bonus.material.device.service.MaDevQcService;
|
||||
import com.bonus.system.api.model.LoginUser;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.bonus.material.utils.FolderZipUtil;
|
||||
import com.bonus.system.api.RemoteFileService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.poi.ss.usermodel.*;
|
||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
|
@ -55,23 +35,19 @@ import javax.annotation.Resource;
|
|||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.validation.ConstraintViolation;
|
||||
import javax.validation.Validator;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URLEncoder;
|
||||
import java.io.*;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeFormatterBuilder;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.time.format.ResolverStyle;
|
||||
import java.time.temporal.ChronoField;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipException;
|
||||
import java.util.zip.ZipInputStream;
|
||||
|
||||
import static com.bonus.common.biz.constant.MaterialConstants.ADMIN_ID;
|
||||
import static com.bonus.common.biz.constant.MaterialConstants.PROVINCE_COMPANY_DEPT_ID;
|
||||
import static com.bonus.common.biz.enums.MaStatusEnum.*;
|
||||
|
||||
/**
|
||||
* 设备信息Service业务层处理
|
||||
|
|
@ -82,6 +58,8 @@ import static com.bonus.common.biz.enums.MaStatusEnum.*;
|
|||
@Slf4j
|
||||
public class DevMergeServiceImpl implements DevMergeService {
|
||||
|
||||
@Resource
|
||||
private RemoteFileService remoteFileService;
|
||||
@Resource
|
||||
private DevMergeMapper devMergeMapper;
|
||||
@Resource
|
||||
|
|
@ -127,6 +105,13 @@ public class DevMergeServiceImpl implements DevMergeService {
|
|||
|
||||
@Override
|
||||
public AjaxResult submitOrder(DevMergeVo o) {
|
||||
|
||||
Integer num = devMergeMapper.selNum(o);
|
||||
if (num == 0) {
|
||||
return AjaxResult.warn("请先添加装备");
|
||||
}
|
||||
|
||||
|
||||
Integer i = devMergeMapper.submitOrder(o);
|
||||
if (i > 0) {
|
||||
devMergeMapper.updateDeviceStatus(o);
|
||||
|
|
@ -548,6 +533,37 @@ public class DevMergeServiceImpl implements DevMergeService {
|
|||
return devMergeMapper.listAllManufacturerNames();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param response
|
||||
*/
|
||||
@Override
|
||||
public void downloadEmptyFolderZip(HttpServletResponse response, String orderId) {
|
||||
// 1. 从数据库查询一级文件夹名称列表
|
||||
List<String> firstLevelFolders = devMergeMapper.getFileName(orderId);
|
||||
if (firstLevelFolders == null || firstLevelFolders.isEmpty()) {
|
||||
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
|
||||
return; // 无文件夹时返回400
|
||||
}
|
||||
// 2. 定义一级文件夹下的固定子文件夹名称
|
||||
List<String> subFolders = Arrays.asList(
|
||||
"装备外观/",
|
||||
"合格证/",
|
||||
"定期检验报告/",
|
||||
"采购发票/"
|
||||
);
|
||||
// 3. 构建所有需要生成的文件夹路径
|
||||
List<String> allFolderPaths = new ArrayList<>();
|
||||
for (String firstFolder : firstLevelFolders) {
|
||||
// 添加一级文件夹
|
||||
allFolderPaths.add(firstFolder + "/");
|
||||
// 添加一级文件夹下的所有子文件夹
|
||||
for (String subFolder : subFolders) {
|
||||
allFolderPaths.add(firstFolder + "/" + subFolder);
|
||||
}
|
||||
}
|
||||
// 4. 生成 ZIP 包并下载(文件名为「订单+文件夹模板.zip」)
|
||||
FolderZipUtil.buildEmptyFolderZip(response, orderId + "-文件夹模板.zip", allFolderPaths);
|
||||
}
|
||||
|
||||
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyMMdd");
|
||||
private static final String SEPARATOR = "-";
|
||||
|
|
@ -586,4 +602,443 @@ public class DevMergeServiceImpl implements DevMergeService {
|
|||
}
|
||||
return String.format("%0" + SEQUENCE_LENGTH + "d", sequence);
|
||||
}
|
||||
|
||||
|
||||
// 固定子目录
|
||||
private static final Set<String> REQUIRED_SUB_FOLDERS = new HashSet<>(Arrays.asList(
|
||||
"装备外观", "合格证", "定期检验报告", "采购发票"
|
||||
));
|
||||
// 允许的文件类型
|
||||
private static final Set<String> IMAGE_TYPES = new HashSet<>(Arrays.asList("jpg", "png"));
|
||||
private static final Set<String> PDF_TYPE = new HashSet<>(Collections.singletonList("pdf"));
|
||||
// 一级目录前缀
|
||||
private static final String ROOT_FOLDER_PREFIX = "Apply-";
|
||||
// 系统临时目录
|
||||
private static final String SYSTEM_TEMP_DIR = System.getProperty("java.io.tmpdir");
|
||||
|
||||
/**
|
||||
* 上传并解压多一级目录的ZIP包(适配任意层级的Apply-目录)
|
||||
*/
|
||||
@Override
|
||||
public AjaxResult uploadAndUnzipMultiDeviceZip(MultipartFile zipFile, String orderId) {
|
||||
Map<String, Object> map = devMergeMapper.getFileId(orderId);
|
||||
if (ObjectUtil.isEmpty(map)) {
|
||||
if (zipFile.isEmpty()) {
|
||||
return AjaxResult.error(400, "无设备可修改");
|
||||
}
|
||||
}
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
int totalRootFolder = 0;
|
||||
int successFileCount = 0;
|
||||
List<String> errorMsgList = new ArrayList<>();
|
||||
Map<String, List<String>> rootFolderErrorMap = new HashMap<>();
|
||||
// 1. 基础校验
|
||||
if (zipFile.isEmpty()) {
|
||||
return AjaxResult.error(400, "上传的ZIP包不能为空");
|
||||
}
|
||||
|
||||
String zipFileName = zipFile.getOriginalFilename();
|
||||
if (zipFileName == null || !zipFileName.toLowerCase().endsWith(".zip")) {
|
||||
return AjaxResult.error(400, "仅支持上传ZIP格式文件");
|
||||
}
|
||||
|
||||
// 使用更安全的临时目录名
|
||||
String timestamp = String.valueOf(System.nanoTime());
|
||||
String random = UUID.randomUUID().toString().substring(0, 8);
|
||||
String tempSubDirName = "device_zip_" + timestamp + "_" + random;
|
||||
|
||||
File tempUnzipDir = new File(SYSTEM_TEMP_DIR, tempSubDirName);
|
||||
|
||||
try {
|
||||
// 2. 创建临时目录
|
||||
FileUtil.mkdir(tempUnzipDir);
|
||||
|
||||
// 3. 验证ZIP文件完整性并解压
|
||||
boolean unzipSuccess = safeUnzip(zipFile, tempUnzipDir, errorMsgList);
|
||||
if (!unzipSuccess) {
|
||||
// 解压失败,但可能部分文件解压成功,可以选择继续处理或直接返回
|
||||
if (errorMsgList.stream().anyMatch(msg -> msg.contains("损坏") || msg.contains("无效") || msg.contains("MALFORMED"))) {
|
||||
return AjaxResult.error(400, "ZIP文件损坏或格式错误:" + String.join("; ", errorMsgList));
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 核心:递归遍历所有层级,找出所有Apply-开头的目录
|
||||
Set<File> applyFolders = new HashSet<>();
|
||||
findAllApplyFolders(tempUnzipDir, applyFolders);
|
||||
|
||||
// 5. 遍历所有Apply-目录并处理
|
||||
for (File applyFolder : applyFolders) {
|
||||
String applyFolderName = applyFolder.getName();
|
||||
totalRootFolder++;
|
||||
List<String> rootErrorList = new ArrayList<>();
|
||||
|
||||
try {
|
||||
// 6. 校验子目录结构(装备外观/合格证等)
|
||||
validateSubFolderStructure(applyFolder, rootErrorList);
|
||||
|
||||
// 7. 遍历子目录文件并处理
|
||||
for (String subFolder : REQUIRED_SUB_FOLDERS) {
|
||||
File subDir = new File(applyFolder, subFolder);
|
||||
if (FileUtil.exist(subDir)) {
|
||||
Collection<File> files = FileUtil.loopFiles(subDir);
|
||||
for (File file : files) {
|
||||
try {
|
||||
validateFileType(subFolder, file, rootErrorList);
|
||||
// 移动文件到业务存储目录
|
||||
AjaxResult fileResult = remoteFileService.upload(convert(file, file.getName(), null));
|
||||
JSONObject json = (JSONObject) JSON.toJSON(fileResult);
|
||||
if (json.getInteger("code") == 200) {
|
||||
JSONObject jsonObject = (JSONObject) JSON.toJSON(map);
|
||||
if (jsonObject != null) {
|
||||
|
||||
MaDevFile item = new MaDevFile();
|
||||
if ("装备外观".equals(subFolder)) {
|
||||
item.setFileType(1);
|
||||
} else if ("合格证".equals(subFolder)) {
|
||||
item.setFileType(2);
|
||||
} else if ("定期检验报告".equals(subFolder)) {
|
||||
item.setFileType(3);
|
||||
} else if ("采购发票".equals(subFolder)) {
|
||||
item.setFileType(4);
|
||||
}
|
||||
item.setMaId(jsonObject.getJSONObject(applyFolderName).getInteger("id"));
|
||||
JSONObject data = json.getJSONObject("data");
|
||||
String url = data.getString("url");
|
||||
item.setFileName(file.getName());
|
||||
// 这里编写对每个 image 的处理逻辑,比如打印、处理等
|
||||
item.setFileUrl(url);
|
||||
item.setCreator(Math.toIntExact(SecurityUtils.getLoginUser().getUserid()));
|
||||
devMergeMapper.interFile(item);
|
||||
}
|
||||
|
||||
}
|
||||
successFileCount++;
|
||||
} catch (IllegalArgumentException e) {
|
||||
rootErrorList.add(e.getMessage());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
rootErrorList.add("缺失子目录:" + applyFolderName + "/" + subFolder);
|
||||
}
|
||||
}
|
||||
|
||||
if (!rootErrorList.isEmpty()) {
|
||||
rootFolderErrorMap.put(applyFolderName, rootErrorList);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
rootErrorList.add("Apply目录处理失败:" + e.getMessage());
|
||||
rootFolderErrorMap.put(applyFolderName, rootErrorList);
|
||||
}
|
||||
}
|
||||
// 8. 组装结果
|
||||
if (totalRootFolder == 0) {
|
||||
return AjaxResult.error(400, "未找到有效的Apply-开头的目录");
|
||||
}
|
||||
result.put("code", 200);
|
||||
result.put("msg", "ZIP包解压完成(识别到" + totalRootFolder + "个Apply-目录)");
|
||||
result.put("totalRootFolder", totalRootFolder);
|
||||
result.put("successFileCount", successFileCount);
|
||||
result.put("rootFolderErrorMap", rootFolderErrorMap);
|
||||
result.put("otherError", errorMsgList);
|
||||
|
||||
return AjaxResult.success(result);
|
||||
|
||||
} catch (IllegalArgumentException e) {
|
||||
return AjaxResult.error(400, e.getMessage());
|
||||
} catch (Exception e) {
|
||||
log.error("ZIP解压/上传失败", e);
|
||||
return AjaxResult.error(500, "ZIP解压/上传失败:" + e.getMessage());
|
||||
} finally {
|
||||
// 9. 清理临时目录
|
||||
try {
|
||||
if (tempUnzipDir.exists()) {
|
||||
FileUtil.del(tempUnzipDir);
|
||||
log.debug("清理临时目录: {}", tempUnzipDir.getAbsolutePath());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("清理临时目录失败: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从File对象中获取Apply-目录的纯名称(核心方法)
|
||||
*
|
||||
* @param applyFolder Apply-开头的目录File对象
|
||||
* @return 文件夹名称(如Apply-22099-13-123)
|
||||
*/
|
||||
private String getApplyFolderName(File applyFolder) {
|
||||
// 方式1:直接获取文件名(推荐,最简洁)
|
||||
String folderName = applyFolder.getName();
|
||||
|
||||
// 可选:额外校验(确保是Apply-开头,避免异常)
|
||||
if (!folderName.startsWith(ROOT_FOLDER_PREFIX)) {
|
||||
throw new IllegalArgumentException("非合法的Apply-目录:" + folderName);
|
||||
}
|
||||
|
||||
return folderName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 File 转换为 MultipartFile
|
||||
*
|
||||
* @param file 要转换的文件
|
||||
* @param originalFilename 原始文件名(可空,默认为file.getName())
|
||||
* @param contentType 内容类型(可空,默认为null)
|
||||
* @return MultipartFile 对象
|
||||
* @throws IOException 文件读取异常
|
||||
*/
|
||||
public static MultipartFile convert(File file, String originalFilename, String contentType) throws IOException {
|
||||
if (originalFilename == null) {
|
||||
originalFilename = file.getName();
|
||||
}
|
||||
|
||||
try (FileInputStream input = new FileInputStream(file)) {
|
||||
return new MockMultipartFile(
|
||||
"file", // form field name
|
||||
originalFilename, // original file name
|
||||
contentType, // content type
|
||||
input // file content
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 简化的转换方法
|
||||
*/
|
||||
public static MultipartFile convert(File file) throws IOException {
|
||||
return convert(file, file.getName(), null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全的ZIP解压方法
|
||||
*/
|
||||
private boolean safeUnzip(MultipartFile zipFile, File destDir, List<String> errorMsgList) {
|
||||
long startTime = System.currentTimeMillis();
|
||||
int extractedCount = 0;
|
||||
|
||||
// 先验证ZIP文件头
|
||||
try (InputStream inputStream = zipFile.getInputStream()) {
|
||||
byte[] header = new byte[4];
|
||||
if (inputStream.read(header) != 4 ||
|
||||
!(header[0] == 0x50 && header[1] == 0x4B &&
|
||||
header[2] == 0x03 && header[3] == 0x04)) {
|
||||
errorMsgList.add("无效的ZIP文件格式");
|
||||
return false;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
errorMsgList.add("读取ZIP文件失败: " + e.getMessage());
|
||||
return false;
|
||||
}
|
||||
|
||||
// 尝试多种编码方式解压
|
||||
Charset[] charsets = {
|
||||
StandardCharsets.UTF_8,
|
||||
Charset.forName("GBK"),
|
||||
Charset.forName("GB2312"),
|
||||
Charset.forName("ISO-8859-1"),
|
||||
StandardCharsets.US_ASCII
|
||||
};
|
||||
|
||||
for (Charset charset : charsets) {
|
||||
try {
|
||||
log.debug("尝试使用编码 {} 解压ZIP文件", charset.name());
|
||||
if (unzipWithCharset(zipFile, destDir, charset)) {
|
||||
extractedCount = countFiles(destDir);
|
||||
log.info("使用编码 {} 成功解压 {} 个文件", charset.name(), extractedCount);
|
||||
return extractedCount > 0;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.debug("编码 {} 解压失败: {}", charset.name(), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
errorMsgList.add("无法解压ZIP文件,尝试所有编码都失败");
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用指定编码解压ZIP文件
|
||||
*/
|
||||
private boolean unzipWithCharset(MultipartFile zipFile, File destDir, Charset charset) throws IOException {
|
||||
try (InputStream inputStream = zipFile.getInputStream();
|
||||
ZipInputStream zipIn = new ZipInputStream(inputStream, charset)) {
|
||||
|
||||
ZipEntry entry;
|
||||
byte[] buffer = new byte[1024 * 1024]; // 1MB buffer
|
||||
int fileCount = 0;
|
||||
|
||||
while ((entry = zipIn.getNextEntry()) != null) {
|
||||
fileCount++;
|
||||
String entryName = entry.getName();
|
||||
|
||||
// 安全检查:防止目录遍历攻击
|
||||
File entryFile = new File(destDir, entryName);
|
||||
String canonicalDestPath = destDir.getCanonicalPath();
|
||||
String canonicalEntryPath = entryFile.getCanonicalPath();
|
||||
|
||||
if (!canonicalEntryPath.startsWith(canonicalDestPath + File.separator)) {
|
||||
throw new SecurityException("尝试访问外部目录: " + entryName);
|
||||
}
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
if (!entryFile.exists() && !entryFile.mkdirs()) {
|
||||
throw new IOException("创建目录失败: " + entryFile.getAbsolutePath());
|
||||
}
|
||||
} else {
|
||||
File parentDir = entryFile.getParentFile();
|
||||
if (!parentDir.exists() && !parentDir.mkdirs()) {
|
||||
throw new IOException("创建父目录失败: " + parentDir.getAbsolutePath());
|
||||
}
|
||||
|
||||
try (OutputStream out = new FileOutputStream(entryFile)) {
|
||||
int len;
|
||||
long totalBytes = 0;
|
||||
while ((len = zipIn.read(buffer)) > 0) {
|
||||
out.write(buffer, 0, len);
|
||||
totalBytes += len;
|
||||
|
||||
// 可选:限制单个文件大小
|
||||
if (totalBytes > 100 * 1024 * 1024) { // 100MB
|
||||
throw new IOException("文件过大: " + entryName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
zipIn.closeEntry();
|
||||
|
||||
// 限制解压的文件数量
|
||||
if (fileCount > 10000) {
|
||||
throw new IOException("ZIP包内文件数量过多,可能为恶意压缩包");
|
||||
}
|
||||
}
|
||||
|
||||
return fileCount > 0;
|
||||
} catch (ZipException e) {
|
||||
throw new IOException("ZIP文件格式错误: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算目录下的文件数量
|
||||
*/
|
||||
private int countFiles(File dir) {
|
||||
if (!dir.exists() || !dir.isDirectory()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int count = 0;
|
||||
Queue<File> queue = new LinkedList<>();
|
||||
queue.add(dir);
|
||||
|
||||
while (!queue.isEmpty()) {
|
||||
File current = queue.poll();
|
||||
File[] files = current.listFiles();
|
||||
if (files != null) {
|
||||
for (File file : files) {
|
||||
if (file.isDirectory()) {
|
||||
queue.add(file);
|
||||
} else {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* 改进的findAllApplyFolders方法,避免无限递归
|
||||
*/
|
||||
private void findAllApplyFolders(File rootDir, Set<File> applyFolders) {
|
||||
if (rootDir == null || !rootDir.exists() || !rootDir.isDirectory()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 防止无限递归(例如符号链接)
|
||||
try {
|
||||
String canonicalPath = rootDir.getCanonicalPath();
|
||||
if (visitedPaths.contains(canonicalPath)) {
|
||||
return;
|
||||
}
|
||||
visitedPaths.add(canonicalPath);
|
||||
} catch (IOException e) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 深度限制
|
||||
if (depth > 50) {
|
||||
return;
|
||||
}
|
||||
depth++;
|
||||
|
||||
try {
|
||||
File[] files = rootDir.listFiles();
|
||||
if (files != null) {
|
||||
for (File file : files) {
|
||||
if (file.isDirectory()) {
|
||||
String name = file.getName();
|
||||
if (name.startsWith("Apply-") || name.startsWith("apply-")) {
|
||||
applyFolders.add(file);
|
||||
} else {
|
||||
// 递归查找子目录
|
||||
findAllApplyFolders(file, applyFolders);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
depth--;
|
||||
}
|
||||
}
|
||||
|
||||
// 添加成员变量用于防止无限递归
|
||||
private Set<String> visitedPaths = new HashSet<>();
|
||||
private int depth = 0;
|
||||
|
||||
|
||||
// 以下方法与之前一致,无需修改
|
||||
private void validateSubFolderStructure(File rootFolder, List<String> errorList) {
|
||||
Collection<File> subDirs = Arrays.asList(FileUtil.ls(rootFolder.getPath()));
|
||||
Set<String> actualSubFolders = new HashSet<>();
|
||||
for (File subDir : subDirs) {
|
||||
if (subDir.isDirectory()) {
|
||||
actualSubFolders.add(subDir.getName());
|
||||
}
|
||||
}
|
||||
Set<String> missingFolders = new HashSet<>(REQUIRED_SUB_FOLDERS);
|
||||
missingFolders.removeAll(actualSubFolders);
|
||||
if (!missingFolders.isEmpty()) {
|
||||
errorList.add("缺失必填子目录:" + String.join(",", missingFolders));
|
||||
}
|
||||
}
|
||||
|
||||
private void validateFileType(String subFolder, File file, List<String> errorList) {
|
||||
if (file.isDirectory()) {
|
||||
return;
|
||||
}
|
||||
String fileExt = FileUtil.extName(file).toLowerCase();
|
||||
if (fileExt.isEmpty()) {
|
||||
throw new IllegalArgumentException(file.getParentFile().getName() + "/" + file.getName() + ":无后缀文件不允许上传");
|
||||
}
|
||||
switch (subFolder) {
|
||||
case "装备外观":
|
||||
if (!IMAGE_TYPES.contains(fileExt)) {
|
||||
throw new IllegalArgumentException(
|
||||
subFolder + "/" + file.getName() + ":仅支持图片(" + String.join(",", IMAGE_TYPES) + ")");
|
||||
}
|
||||
break;
|
||||
case "合格证":
|
||||
case "定期检验报告":
|
||||
case "采购发票":
|
||||
if (!IMAGE_TYPES.contains(fileExt) && !PDF_TYPE.contains(fileExt)) {
|
||||
throw new IllegalArgumentException(
|
||||
subFolder + "/" + file.getName() + ":仅支持图片+PDF(" + String.join(",", IMAGE_TYPES) + ",pdf)");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("不支持的子目录:" + subFolder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,54 @@
|
|||
package com.bonus.material.utils;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
/**
|
||||
* 构建指定文件夹结构的 ZIP 工具类(适配 HttpServletResponse 输出)
|
||||
*/
|
||||
public class FolderZipUtil {
|
||||
|
||||
/**
|
||||
* 生成仅包含指定文件夹结构的空 ZIP 包(直接输出到响应流)
|
||||
*
|
||||
* @param response HTTP 响应对象
|
||||
* @param zipFileName 下载的 ZIP 文件名
|
||||
* @param folderPaths 目标文件夹路径列表(如:["docs/", "docs/img/"])
|
||||
*/
|
||||
public static void buildEmptyFolderZip(HttpServletResponse response, String zipFileName, List<String> folderPaths) {
|
||||
try {
|
||||
// 1. 设置响应头(解决中文文件名乱码 + 触发下载)
|
||||
response.setContentType("application/octet-stream");
|
||||
response.setCharacterEncoding("UTF-8");
|
||||
// 编码文件名,适配不同浏览器
|
||||
String encodedFileName = URLEncoder.encode(zipFileName, StandardCharsets.UTF_8.name())
|
||||
.replaceAll("\\+", "%20");
|
||||
response.setHeader("Content-Disposition", "attachment; filename=\"" + encodedFileName + "\"");
|
||||
// 禁止缓存
|
||||
response.setHeader("Pragma", "no-cache");
|
||||
response.setHeader("Cache-Control", "no-cache");
|
||||
response.setDateHeader("Expires", 0);
|
||||
|
||||
// 2. 直接通过响应流构建 ZIP(无内存缓存)
|
||||
try (ZipOutputStream zipOut = new ZipOutputStream(response.getOutputStream(), StandardCharsets.UTF_8)) {
|
||||
// 遍历文件夹路径,创建空文件夹条目
|
||||
for (String folderPath : folderPaths) {
|
||||
// 确保文件夹路径以 / 结尾(ZIP 中 / 结尾表示文件夹)
|
||||
String normalizedPath = folderPath.endsWith("/") ? folderPath : folderPath + "/";
|
||||
ZipEntry folderEntry = new ZipEntry(normalizedPath);
|
||||
zipOut.putNextEntry(folderEntry);
|
||||
zipOut.closeEntry(); // 空文件夹无需写入内容,直接关闭条目
|
||||
}
|
||||
zipOut.flush(); // 刷新流,确保内容输出
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 异常时返回 500 状态码
|
||||
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -10,7 +10,7 @@
|
|||
aaa.create_user AS createUser,
|
||||
aaa.create_time AS createTime,
|
||||
aaa.status AS status,
|
||||
COUNT(bbb.dev_id) AS devCount,
|
||||
SUM( CASE WHEN mdi.is_active = '1' THEN 1 ELSE 0 END ) AS devCount,
|
||||
aaa.order_number AS orderNumber,
|
||||
SUM(CASE WHEN mdi.entry_status = 1 THEN 1 ELSE 0 END) AS agree,
|
||||
SUM(CASE WHEN mdi.entry_status = 2 THEN 1 ELSE 0 END) AS reject
|
||||
|
|
@ -611,4 +611,45 @@
|
|||
WHERE CONCAT_WS('/', proType, mainGx, childGx, devCategory, devSubcategory) = #{profession}
|
||||
LIMIT 1
|
||||
</select>
|
||||
<select id="getFileName" resultType="java.lang.String">
|
||||
SELECT CONCAT_WS('-', 'Apply', mdi.ma_id, mdi.device_name, mdi.item_type_model)
|
||||
from ma_apply cds
|
||||
LEFT JOIN ma_apply_details cdrd ON cdrd.cs_id = cds.id
|
||||
LEFT JOIN ma_dev_info mdi ON cdrd.dev_id = mdi.ma_id
|
||||
INNER JOIN ma_type_view mtv ON mtv.typeId = mdi.type_id
|
||||
LEFT JOIN jj_sing_project jsp ON mdi.on_project = jsp.pro_code
|
||||
LEFT JOIN sys_dept sd ON sd.dept_id = mdi.on_company
|
||||
LEFT JOIN (SELECT max(next_check_time) next_check_time, ma_id from ma_dev_qc GROUP BY ma_id) mdq on
|
||||
mdi.ma_id = mdq.ma_id
|
||||
LEFT JOIN ma_supplier ms ON ms.supplier_id = mdi.supplier_id
|
||||
LEFT JOIN sys_cnarea sc ON sc.area_code = mdi.province_id
|
||||
WHERE mdi.is_active = '1'
|
||||
and cds.id = #{orderId}
|
||||
</select>
|
||||
<select id="getFileId" resultType="java.util.Map">
|
||||
SELECT mdi.ma_id AS id,
|
||||
CONCAT_WS('-', 'Apply', mdi.ma_id, mdi.device_name, mdi.item_type_model) AS fileName
|
||||
from ma_apply cds
|
||||
LEFT JOIN ma_apply_details cdrd ON cdrd.cs_id = cds.id
|
||||
LEFT JOIN ma_dev_info mdi ON cdrd.dev_id = mdi.ma_id
|
||||
INNER JOIN ma_type_view mtv ON mtv.typeId = mdi.type_id
|
||||
LEFT JOIN jj_sing_project jsp ON mdi.on_project = jsp.pro_code
|
||||
LEFT JOIN sys_dept sd ON sd.dept_id = mdi.on_company
|
||||
LEFT JOIN (SELECT max(next_check_time) next_check_time, ma_id from ma_dev_qc GROUP BY ma_id) mdq on
|
||||
mdi.ma_id = mdq.ma_id
|
||||
LEFT JOIN ma_supplier ms ON ms.supplier_id = mdi.supplier_id
|
||||
LEFT JOIN sys_cnarea sc ON sc.area_code = mdi.province_id
|
||||
WHERE mdi.is_active = '1'
|
||||
and cds.id = #{orderId}
|
||||
</select>
|
||||
<select id="selNum" resultType="java.lang.Integer">
|
||||
SELECT COUNT(1)
|
||||
FROM ma_dev_info
|
||||
where entry_status = 3
|
||||
and ma_id IN (select dev_id
|
||||
from ma_apply_details
|
||||
where cs_id = #{id})
|
||||
AND is_active = '1'
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
|
|
|||
|
|
@ -186,37 +186,38 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
LEFT JOIN sys_user su2 ON su2.user_id = mdi.creator
|
||||
left join sys_dept dept on dept.dept_id = moi.buyer_company
|
||||
left join sys_dept up on up.dept_id = mdi.on_company
|
||||
WHERE
|
||||
mt.del_flag = '0'
|
||||
<if test="keyWord!=null and keyWord!=''">
|
||||
and moi.code like concat('%',#{keyWord},'%')
|
||||
</if>
|
||||
<if test="buyerCompany != null">
|
||||
AND moi.buyer_company = #{buyerCompany}
|
||||
</if>
|
||||
<if test="sellerCompany != null">
|
||||
AND mdi.on_company = #{sellerCompany}
|
||||
</if>
|
||||
<if test="deviceName != null and deviceName != ''">
|
||||
AND mdi.device_name like concat('%',#{deviceName},'%')
|
||||
</if>
|
||||
<if test="orderStatus != null and orderStatus != ''">
|
||||
AND hh.order_status = #{orderStatus}
|
||||
</if>
|
||||
<if test="code != null and code != ''">
|
||||
AND moi.code = #{code}
|
||||
</if>
|
||||
<if test="startTime != null and endTime != null ">
|
||||
AND ((hh.rent_begin_time BETWEEN #{startTime} AND #{endTime})
|
||||
OR (hh.rent_end_time BETWEEN #{startTime} AND #{endTime})
|
||||
OR (hh.rent_begin_time < #{startTime} AND hh.rent_end_time > #{endTime}))
|
||||
</if>
|
||||
<if test="czcompanyName != null and czcompanyName != ''">
|
||||
AND up.dept_name like concat('%',#{czcompanyName},'%')
|
||||
</if>
|
||||
<if test="companyName != null and companyName != ''">
|
||||
AND dept.dept_name like concat('%',#{companyName},'%')
|
||||
</if>
|
||||
<where>
|
||||
<if test="keyWord!=null and keyWord!=''">
|
||||
and moi.code like concat('%',#{keyWord},'%')
|
||||
</if>
|
||||
<if test="buyerCompany != null">
|
||||
AND moi.buyer_company = #{buyerCompany}
|
||||
</if>
|
||||
<if test="sellerCompany != null">
|
||||
AND mdi.on_company = #{sellerCompany}
|
||||
</if>
|
||||
<if test="deviceName != null and deviceName != ''">
|
||||
AND mdi.device_name like concat('%',#{deviceName},'%')
|
||||
</if>
|
||||
<if test="orderStatus != null and orderStatus != ''">
|
||||
AND hh.order_status = #{orderStatus}
|
||||
</if>
|
||||
<if test="code != null and code != ''">
|
||||
AND moi.code = #{code}
|
||||
</if>
|
||||
<if test="startTime != null and endTime != null ">
|
||||
AND ((hh.rent_begin_time BETWEEN #{startTime} AND #{endTime})
|
||||
OR (hh.rent_end_time BETWEEN #{startTime} AND #{endTime})
|
||||
OR (hh.rent_begin_time < #{startTime} AND hh.rent_end_time > #{endTime}))
|
||||
</if>
|
||||
<if test="czcompanyName != null and czcompanyName != ''">
|
||||
AND up.dept_name like concat('%',#{czcompanyName},'%')
|
||||
</if>
|
||||
<if test="companyName != null and companyName != ''">
|
||||
AND dept.dept_name like concat('%',#{companyName},'%')
|
||||
</if>
|
||||
</where>
|
||||
|
||||
GROUP BY
|
||||
moi.buyer_company,
|
||||
moi.`code`,
|
||||
|
|
@ -343,7 +344,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
LEFT JOIN sys_dept sd1 ON sd1.dept_id = subquery.first_ancestor
|
||||
) dept ON dept.deptId = su.dept_id
|
||||
WHERE
|
||||
mt.del_flag = '0' and moi.order_id = #{orderId} limit 1
|
||||
moi.order_id = #{orderId} limit 1
|
||||
</select>
|
||||
|
||||
<select id="selectOrderDetailsByOrderId" resultType="com.bonus.material.order.domain.OrderDetailDto">
|
||||
|
|
@ -418,7 +419,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
LEFT JOIN sys_dept sdept on sdept.dept_id = mdi.on_company
|
||||
LEFT JOIN sys_user su1 ON moi.buyer_id = su1.user_id AND su1.del_flag != 2
|
||||
WHERE
|
||||
mt.del_flag = 0 and hh.order_id = #{orderId}
|
||||
hh.order_id = #{orderId}
|
||||
</select>
|
||||
|
||||
<select id="getOrderStatusCount" resultType="com.bonus.material.order.domain.OrderInfoDto">
|
||||
|
|
@ -468,8 +469,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
LEFT JOIN sys_dept dept ON dept.dept_id = moi.buyer_company
|
||||
LEFT JOIN sys_dept up ON up.dept_id = mdi.on_company
|
||||
WHERE
|
||||
mt.del_flag = '0'
|
||||
AND hh.order_id = #{orderId}
|
||||
hh.order_id = #{orderId}
|
||||
</select>
|
||||
|
||||
<select id="getRentDetails" resultType="com.bonus.material.comprehensive.entity.RentDetailDto">
|
||||
|
|
|
|||
Loading…
Reference in New Issue