From 84a03fcf49e6911c2d84ec8d89fa34677d80682c Mon Sep 17 00:00:00 2001 From: jiang Date: Thu, 11 Dec 2025 09:09:40 +0800 Subject: [PATCH] =?UTF-8?q?=E9=97=AE=E9=A2=98=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../device/controller/DevMergeController.java | 25 + .../device/domain/EquipmentImportDTO.java | 2 +- .../device/mapper/DevMergeMapper.java | 9 +- .../device/service/DevMergeService.java | 4 + .../service/impl/DevMergeServiceImpl.java | 533 ++++++++++++++++-- .../bonus/material/utils/FolderZipUtil.java | 54 ++ .../mapper/material/device/DevMergeMapper.xml | 43 +- .../mapper/material/order/OrderInfoMapper.xml | 70 +-- 8 files changed, 663 insertions(+), 77 deletions(-) create mode 100644 bonus-modules/bonus-material-mall/src/main/java/com/bonus/material/utils/FolderZipUtil.java diff --git a/bonus-modules/bonus-material-mall/src/main/java/com/bonus/material/device/controller/DevMergeController.java b/bonus-modules/bonus-material-mall/src/main/java/com/bonus/material/device/controller/DevMergeController.java index e1a6ce5..d09c367 100644 --- a/bonus-modules/bonus-material-mall/src/main/java/com/bonus/material/device/controller/DevMergeController.java +++ b/bonus-modules/bonus-material-mall/src/main/java/com/bonus/material/device/controller/DevMergeController.java @@ -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); + } + /** * 下载导入模板 */ diff --git a/bonus-modules/bonus-material-mall/src/main/java/com/bonus/material/device/domain/EquipmentImportDTO.java b/bonus-modules/bonus-material-mall/src/main/java/com/bonus/material/device/domain/EquipmentImportDTO.java index deba796..c0d87e3 100644 --- a/bonus-modules/bonus-material-mall/src/main/java/com/bonus/material/device/domain/EquipmentImportDTO.java +++ b/bonus-modules/bonus-material-mall/src/main/java/com/bonus/material/device/domain/EquipmentImportDTO.java @@ -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; diff --git a/bonus-modules/bonus-material-mall/src/main/java/com/bonus/material/device/mapper/DevMergeMapper.java b/bonus-modules/bonus-material-mall/src/main/java/com/bonus/material/device/mapper/DevMergeMapper.java index 21f776d..aca594d 100644 --- a/bonus-modules/bonus-material-mall/src/main/java/com/bonus/material/device/mapper/DevMergeMapper.java +++ b/bonus-modules/bonus-material-mall/src/main/java/com/bonus/material/device/mapper/DevMergeMapper.java @@ -91,10 +91,17 @@ public interface DevMergeMapper { List listAllManufacturerNames(); - // 批量查询profession对应的typeId + // 批量查询profession对应的typeId Integer getTypeId(String professions); Integer getManufacturer(String manufacturers); + + List getFileName(String orderId); + + @MapKey("fileName") + Map getFileId(String orderId); + + Integer selNum(DevMergeVo o); } diff --git a/bonus-modules/bonus-material-mall/src/main/java/com/bonus/material/device/service/DevMergeService.java b/bonus-modules/bonus-material-mall/src/main/java/com/bonus/material/device/service/DevMergeService.java index e057897..495c48d 100644 --- a/bonus-modules/bonus-material-mall/src/main/java/com/bonus/material/device/service/DevMergeService.java +++ b/bonus-modules/bonus-material-mall/src/main/java/com/bonus/material/device/service/DevMergeService.java @@ -77,4 +77,8 @@ public interface DevMergeService { List listAllProfessionNames(); List listAllManufacturerNames(); + + void downloadEmptyFolderZip(HttpServletResponse response,String orderId); + + AjaxResult uploadAndUnzipMultiDeviceZip(MultipartFile zipFile,String orderId); } diff --git a/bonus-modules/bonus-material-mall/src/main/java/com/bonus/material/device/service/impl/DevMergeServiceImpl.java b/bonus-modules/bonus-material-mall/src/main/java/com/bonus/material/device/service/impl/DevMergeServiceImpl.java index aacbbe5..a763ce6 100644 --- a/bonus-modules/bonus-material-mall/src/main/java/com/bonus/material/device/service/impl/DevMergeServiceImpl.java +++ b/bonus-modules/bonus-material-mall/src/main/java/com/bonus/material/device/service/impl/DevMergeServiceImpl.java @@ -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 firstLevelFolders = devMergeMapper.getFileName(orderId); + if (firstLevelFolders == null || firstLevelFolders.isEmpty()) { + response.setStatus(HttpServletResponse.SC_BAD_REQUEST); + return; // 无文件夹时返回400 + } + // 2. 定义一级文件夹下的固定子文件夹名称 + List subFolders = Arrays.asList( + "装备外观/", + "合格证/", + "定期检验报告/", + "采购发票/" + ); + // 3. 构建所有需要生成的文件夹路径 + List 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 REQUIRED_SUB_FOLDERS = new HashSet<>(Arrays.asList( + "装备外观", "合格证", "定期检验报告", "采购发票" + )); + // 允许的文件类型 + private static final Set IMAGE_TYPES = new HashSet<>(Arrays.asList("jpg", "png")); + private static final Set 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 map = devMergeMapper.getFileId(orderId); + if (ObjectUtil.isEmpty(map)) { + if (zipFile.isEmpty()) { + return AjaxResult.error(400, "无设备可修改"); + } + } + Map result = new HashMap<>(); + int totalRootFolder = 0; + int successFileCount = 0; + List errorMsgList = new ArrayList<>(); + Map> 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 applyFolders = new HashSet<>(); + findAllApplyFolders(tempUnzipDir, applyFolders); + + // 5. 遍历所有Apply-目录并处理 + for (File applyFolder : applyFolders) { + String applyFolderName = applyFolder.getName(); + totalRootFolder++; + List 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 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 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 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 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 visitedPaths = new HashSet<>(); + private int depth = 0; + + + // 以下方法与之前一致,无需修改 + private void validateSubFolderStructure(File rootFolder, List errorList) { + Collection subDirs = Arrays.asList(FileUtil.ls(rootFolder.getPath())); + Set actualSubFolders = new HashSet<>(); + for (File subDir : subDirs) { + if (subDir.isDirectory()) { + actualSubFolders.add(subDir.getName()); + } + } + Set 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 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); + } + } } diff --git a/bonus-modules/bonus-material-mall/src/main/java/com/bonus/material/utils/FolderZipUtil.java b/bonus-modules/bonus-material-mall/src/main/java/com/bonus/material/utils/FolderZipUtil.java new file mode 100644 index 0000000..580a2e0 --- /dev/null +++ b/bonus-modules/bonus-material-mall/src/main/java/com/bonus/material/utils/FolderZipUtil.java @@ -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 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(); + } + } +} \ No newline at end of file diff --git a/bonus-modules/bonus-material-mall/src/main/resources/mapper/material/device/DevMergeMapper.xml b/bonus-modules/bonus-material-mall/src/main/resources/mapper/material/device/DevMergeMapper.xml index 5f7b512..0f2b93f 100644 --- a/bonus-modules/bonus-material-mall/src/main/resources/mapper/material/device/DevMergeMapper.xml +++ b/bonus-modules/bonus-material-mall/src/main/resources/mapper/material/device/DevMergeMapper.xml @@ -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 + + + + diff --git a/bonus-modules/bonus-material-mall/src/main/resources/mapper/material/order/OrderInfoMapper.xml b/bonus-modules/bonus-material-mall/src/main/resources/mapper/material/order/OrderInfoMapper.xml index ced76f4..8937e8c 100644 --- a/bonus-modules/bonus-material-mall/src/main/resources/mapper/material/order/OrderInfoMapper.xml +++ b/bonus-modules/bonus-material-mall/src/main/resources/mapper/material/order/OrderInfoMapper.xml @@ -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' - - and moi.code like concat('%',#{keyWord},'%') - - - AND moi.buyer_company = #{buyerCompany} - - - AND mdi.on_company = #{sellerCompany} - - - AND mdi.device_name like concat('%',#{deviceName},'%') - - - AND hh.order_status = #{orderStatus} - - - AND moi.code = #{code} - - - 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})) - - - AND up.dept_name like concat('%',#{czcompanyName},'%') - - - AND dept.dept_name like concat('%',#{companyName},'%') - + + + and moi.code like concat('%',#{keyWord},'%') + + + AND moi.buyer_company = #{buyerCompany} + + + AND mdi.on_company = #{sellerCompany} + + + AND mdi.device_name like concat('%',#{deviceName},'%') + + + AND hh.order_status = #{orderStatus} + + + AND moi.code = #{code} + + + 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})) + + + AND up.dept_name like concat('%',#{czcompanyName},'%') + + + AND dept.dept_name like concat('%',#{companyName},'%') + + + 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