diff --git a/urbanops-framework/urbanops-spring-boot-starter-excel/src/main/java/com/zteits/urbanops/framework/excel/core/convert/ImageListConverter.java b/urbanops-framework/urbanops-spring-boot-starter-excel/src/main/java/com/zteits/urbanops/framework/excel/core/convert/ImageListConverter.java index 5dedf72..41a6c96 100644 --- a/urbanops-framework/urbanops-spring-boot-starter-excel/src/main/java/com/zteits/urbanops/framework/excel/core/convert/ImageListConverter.java +++ b/urbanops-framework/urbanops-spring-boot-starter-excel/src/main/java/com/zteits/urbanops/framework/excel/core/convert/ImageListConverter.java @@ -7,27 +7,17 @@ import cn.idev.excel.metadata.data.ImageData; import cn.idev.excel.metadata.data.ReadCellData; import cn.idev.excel.metadata.data.WriteCellData; import cn.idev.excel.metadata.property.ExcelContentProperty; -import cn.idev.excel.util.FileUtils; import cn.idev.excel.util.ListUtils; import lombok.extern.slf4j.Slf4j; -import org.springframework.util.StringUtils; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.InputStream; -import java.net.HttpURLConnection; -import java.net.URL; import java.util.List; @Slf4j -public class ImageListConverter implements Converter> { +public class ImageListConverter implements Converter> { // 横向排列:每张图片宽度 + 间距 private static final int IMAGE_WIDTH = 120; private static final int IMAGE_GAP = 15; - private static final int MAX_IMAGE_SIZE = 10 * 1024 * 1024; - private static final int CONNECT_TIMEOUT = 5000; - private static final int READ_TIMEOUT = 10000; @Override public Class supportJavaTypeKey() { @@ -40,28 +30,27 @@ public class ImageListConverter implements Converter> { } @Override - public List convertToJavaData(ReadCellData readCellData, ExcelContentProperty contentProperty, + public List convertToJavaData(ReadCellData readCellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { return null; } + /** + * 这里改成接收 List + */ @Override - public WriteCellData convertToExcelData(List imageList, ExcelContentProperty contentProperty, + public WriteCellData convertToExcelData(List imageByteList, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { try { - if (imageList == null || imageList.isEmpty()) { + if (imageByteList == null || imageByteList.isEmpty()) { return new WriteCellData<>("无图片"); } List imageDataList = ListUtils.newArrayList(); int leftOffset = 0; // 横向偏移量 - for (String path : imageList) { - if (!StringUtils.hasText(path)) continue; - - byte[] imageBytes = path.startsWith("http") ? loadNetworkImage(path) : loadLocalImage(path); - if (imageBytes == null || imageBytes.length == 0 || imageBytes.length > MAX_IMAGE_SIZE) { - log.warn("跳过无效图片:{}", path); + for (byte[] imageBytes : imageByteList) { + if (imageBytes == null || imageBytes.length == 0) { continue; } @@ -69,11 +58,11 @@ public class ImageListConverter implements Converter> { imageData.setImage(imageBytes); imageData.setImageType(ImageData.ImageType.PICTURE_TYPE_PNG); - // ====================== 核心:横向偏移 ====================== + // 横向排列 imageData.setLeft(leftOffset); - // ============================================================= - imageDataList.add(imageData); + + // 下一张图片偏移 leftOffset += IMAGE_WIDTH + IMAGE_GAP; } @@ -88,35 +77,8 @@ public class ImageListConverter implements Converter> { return writeCellData; } catch (Exception e) { - log.error("图片导出失败", e); + log.error("图片导出异常", e); return new WriteCellData<>("图片加载失败"); } } - - private byte[] loadLocalImage(String imagePath) throws Exception { - File file = new File(imagePath); - if (!file.exists() || !file.isFile()) { - log.warn("本地图片不存在:{}", imagePath); - return null; - } - return FileUtils.readFileToByteArray(file); - } - - private byte[] loadNetworkImage(String imageUrl) throws Exception { - URL url = new URL(imageUrl); - HttpURLConnection conn = (HttpURLConnection) url.openConnection(); - conn.setConnectTimeout(CONNECT_TIMEOUT); - conn.setReadTimeout(READ_TIMEOUT); - try { - if (conn.getResponseCode() != 200) return null; - try (InputStream in = conn.getInputStream(); ByteArrayOutputStream out = new ByteArrayOutputStream()) { - byte[] buffer = new byte[4096]; - int len; - while ((len = in.read(buffer)) != -1) out.write(buffer, 0, len); - return out.toByteArray(); - } - } finally { - conn.disconnect(); - } - } } diff --git a/urbanops-framework/urbanops-spring-boot-starter-excel/src/main/java/com/zteits/urbanops/framework/excel/core/convert/ImageStringConverter.java b/urbanops-framework/urbanops-spring-boot-starter-excel/src/main/java/com/zteits/urbanops/framework/excel/core/convert/ImageStringConverter.java new file mode 100644 index 0000000..d99bc90 --- /dev/null +++ b/urbanops-framework/urbanops-spring-boot-starter-excel/src/main/java/com/zteits/urbanops/framework/excel/core/convert/ImageStringConverter.java @@ -0,0 +1,49 @@ +package com.zteits.urbanops.framework.excel.core.convert; + +import cn.idev.excel.converters.Converter; +import cn.idev.excel.enums.CellDataTypeEnum; +import cn.idev.excel.metadata.GlobalConfiguration; +import cn.idev.excel.metadata.data.ImageData; +import cn.idev.excel.metadata.data.ReadCellData; +import cn.idev.excel.metadata.data.WriteCellData; +import cn.idev.excel.metadata.property.ExcelContentProperty; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class ImageStringConverter implements Converter { + + @Override + public Class supportJavaTypeKey() { + return byte[].class; + } + + @Override + public CellDataTypeEnum supportExcelTypeKey() { + return CellDataTypeEnum.EMPTY; + } + + @Override + public byte[] convertToJavaData(ReadCellData cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { + return null; + } + + @Override + public WriteCellData convertToExcelData(byte[] imageBytes, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { + try { + if (imageBytes == null || imageBytes.length == 0) { + return new WriteCellData<>("无图片"); + } + + ImageData imageData = new ImageData(); + imageData.setImage(imageBytes); + imageData.setImageType(ImageData.ImageType.PICTURE_TYPE_PNG); + + WriteCellData cellData = new WriteCellData<>(); + cellData.setType(CellDataTypeEnum.EMPTY); + cellData.setImageDataList(java.util.Collections.singletonList(imageData)); + return cellData; + } catch (Exception e) { + return new WriteCellData<>("图片异常"); + } + } +} diff --git a/urbanops-framework/urbanops-spring-boot-starter-excel/src/main/java/com/zteits/urbanops/framework/excel/core/handler/HeadMergeHandler.java b/urbanops-framework/urbanops-spring-boot-starter-excel/src/main/java/com/zteits/urbanops/framework/excel/core/handler/HeadMergeHandler.java new file mode 100644 index 0000000..ce6593a --- /dev/null +++ b/urbanops-framework/urbanops-spring-boot-starter-excel/src/main/java/com/zteits/urbanops/framework/excel/core/handler/HeadMergeHandler.java @@ -0,0 +1,47 @@ +package com.zteits.urbanops.framework.excel.core.handler; + +import cn.idev.excel.write.handler.SheetWriteHandler; +import cn.idev.excel.write.handler.context.SheetWriteHandlerContext; +import cn.idev.excel.write.metadata.holder.WriteSheetHolder; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.util.CellRangeAddress; +import java.util.Arrays; +import java.util.List; + +/** + * 表头合并处理器(适配低版本 idev.excel,无 getSheet 方法) + * 纯表头场景、空数据也能正常执行,无需额外空行 + */ +public class HeadMergeHandler implements SheetWriteHandler { + + private final List regions; + + public HeadMergeHandler(CellRangeAddress... regions) { + this.regions = Arrays.asList(regions); + } + + @Override + public void afterSheetCreate(SheetWriteHandlerContext context) { + // 核心修正:从 WriteSheetHolder 拿 Sheet + WriteSheetHolder writeSheetHolder = context.getWriteSheetHolder(); + if (writeSheetHolder == null) { + return; + } + Sheet sheet = writeSheetHolder.getSheet(); + + for (CellRangeAddress area : regions) { + if (isValidMergedRegion(area)) { + sheet.addMergedRegionUnsafe(area); + } + } + } + + /** + * 过滤单单元格无效合并 + */ + private boolean isValidMergedRegion(CellRangeAddress area) { + int rowCnt = area.getLastRow() - area.getFirstRow() + 1; + int colCnt = area.getLastColumn() - area.getFirstColumn() + 1; + return rowCnt >= 2 || colCnt >= 2; + } +} diff --git a/urbanops-framework/urbanops-spring-boot-starter-excel/src/main/java/com/zteits/urbanops/framework/excel/core/util/ExcelUtils.java b/urbanops-framework/urbanops-spring-boot-starter-excel/src/main/java/com/zteits/urbanops/framework/excel/core/util/ExcelUtils.java index 7a02cdf..0ac7d43 100644 --- a/urbanops-framework/urbanops-spring-boot-starter-excel/src/main/java/com/zteits/urbanops/framework/excel/core/util/ExcelUtils.java +++ b/urbanops-framework/urbanops-spring-boot-starter-excel/src/main/java/com/zteits/urbanops/framework/excel/core/util/ExcelUtils.java @@ -1,13 +1,20 @@ package com.zteits.urbanops.framework.excel.core.util; +import cn.hutool.core.collection.CollectionUtil; +import cn.idev.excel.ExcelWriter; import cn.idev.excel.FastExcelFactory; import cn.idev.excel.converters.longconverter.LongStringConverter; +import cn.idev.excel.write.builder.ExcelWriterBuilder; import com.zteits.urbanops.framework.common.util.http.HttpUtils; import com.zteits.urbanops.framework.excel.core.handler.ColumnWidthMatchStyleStrategy; +import com.zteits.urbanops.framework.excel.core.handler.HeadMergeHandler; import com.zteits.urbanops.framework.excel.core.handler.SelectSheetWriteHandler; import jakarta.servlet.http.HttpServletResponse; +import org.apache.poi.ss.util.CellRangeAddress; import org.springframework.web.multipart.MultipartFile; +import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; import java.util.List; @@ -39,7 +46,8 @@ public class ExcelUtils { .registerConverter(new LongStringConverter()) // 避免 Long 类型丢失精度 .sheet(sheetName).doWrite(data); // 设置 header 和 contentType。写在最后的原因是,避免报错时,响应 contentType 已经被修改了 - response.addHeader("Content-Disposition", "attachment;filename=" + HttpUtils.encodeUtf8(filename)); + //response.addHeader("Content-Disposition", "attachment;filename=" + HttpUtils.encodeUtf8(filename)); + response.setHeader("Content-Disposition", "attachment;filename*=UTF-8''" + HttpUtils.encodeUtf8(filename)); response.setContentType("application/vnd.ms-excel;charset=UTF-8"); } @@ -74,4 +82,82 @@ public class ExcelUtils { .sheet(0) .doReadSync(); } + + + /** + * 导出多级表头,将列表以 Excel 响应给前端 + * + * @param response 响应 + * @param filename 文件名 + * @param sheetName Excel sheet 名 + * @param head Excel head 头 + * @param data 数据列表哦 + * @param 泛型,保证 head 和 data 类型的一致性 + * @throws IOException 写入失败的情况 + */ + public static void writeEmergencyTask(HttpServletResponse response, String filename, String sheetName, + Class head, List data, List> headList, List mergeList) throws IOException { + FastExcelFactory.write(response.getOutputStream()) + .head(headList) //动态自定义3行表头 + .autoCloseStream(false) // 不要自动关闭,交给 Servlet 自己处理 + .registerWriteHandler(new ColumnWidthMatchStyleStrategy()) // 基于 column 长度,自动适配。最大 255 宽度 + .registerWriteHandler(new SelectSheetWriteHandler(head)) // 基于固定 sheet 实现下拉框 + .registerConverter(new LongStringConverter()) // 避免 Long 类型丢失精度 + // 注册表头合并 + .registerWriteHandler(new HeadMergeHandler(mergeList.toArray(new CellRangeAddress[0]))) + .sheet(sheetName).doWrite(data); + // 设置 header 和 contentType。写在最后的原因是,避免报错时,响应 contentType 已经被修改了 + response.setHeader("Content-Disposition", "attachment;filename*=UTF-8''" + HttpUtils.encodeUtf8(filename)); + response.setContentType("application/vnd.ms-excel;charset=UTF-8"); + + } + + /** + * 初始化Excel、写入表头,返回全局复用的 ExcelWriter + * 不再返回 OutputStream,彻底解决多次创建写入器导致文件损坏 + */ + public static ExcelWriter initEmergencyTaskExcel(File targetFile, + String fileName, + String sheetName, + Class head, + List> headList, + List mergeList) throws IOException { + // 目录创建逻辑不变 + File parentFile = targetFile.getParentFile(); + if (!parentFile.exists()) { + boolean mkdirs = parentFile.mkdirs(); + if (!mkdirs) { + throw new IOException("目录创建失败:" + parentFile.getAbsolutePath()); + } + } + + // 输出流交给 ExcelWriter + FileOutputStream fos = new FileOutputStream(targetFile); + ExcelWriterBuilder writeBuilder = null; + if (CollectionUtil.isNotEmpty(headList)) { + writeBuilder = FastExcelFactory.write(fos) + .head(headList) //动态自定义3行表头 + .autoCloseStream(true) // 自动关闭 + .registerWriteHandler(new ColumnWidthMatchStyleStrategy()) // 基于 column 长度,自动适配。最大 255 宽度 + .registerWriteHandler(new SelectSheetWriteHandler(head)) // 基于固定 sheet 实现下拉框 + .registerConverter(new LongStringConverter()) // 避免 Long 类型丢失精度 + .registerWriteHandler(new HeadMergeHandler(mergeList.toArray(new CellRangeAddress[0]))); + } else { + writeBuilder = FastExcelFactory.write(fos, head) + .autoCloseStream(true) // 自动关闭 + .registerWriteHandler(new ColumnWidthMatchStyleStrategy()) // 基于 column 长度,自动适配。最大 255 宽度 + .registerWriteHandler(new SelectSheetWriteHandler(head)) // 基于固定 sheet 实现下拉框 + .registerConverter(new LongStringConverter()); // 避免 Long 类型丢失精度 + } + ExcelWriter excelWriter = writeBuilder.build(); + return excelWriter; + } + /** + * 最终收尾:刷写文件尾、释放资源(替代手动close流) + */ + public static void finishExcel(ExcelWriter excelWriter) { + if (excelWriter != null) { + excelWriter.finish(); + } + } } diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/emergencytask/EmergencyTaskController.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/emergencytask/EmergencyTaskController.java index a84b95f..993e6eb 100644 --- a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/emergencytask/EmergencyTaskController.java +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/emergencytask/EmergencyTaskController.java @@ -1,43 +1,52 @@ package com.zteits.urbanops.module.garden.controller.admin.emergencytask; -import com.zteits.urbanops.framework.common.biz.system.dict.dto.DictDataRespDTO; -import org.springframework.util.CollectionUtils; -import org.springframework.web.bind.annotation.*; +import cn.hutool.core.collection.CollectionUtil; +import com.zteits.urbanops.framework.apilog.core.annotation.ApiAccessLog; +import com.zteits.urbanops.framework.common.pojo.CommonResult; +import com.zteits.urbanops.framework.common.pojo.PageParam; +import com.zteits.urbanops.framework.common.pojo.PageResult; +import com.zteits.urbanops.module.garden.controller.admin.emergencytask.vo.EmergencyTaskApprovalReqVO; +import com.zteits.urbanops.module.garden.controller.admin.emergencytask.vo.EmergencyTaskPageReqVO; +import com.zteits.urbanops.module.garden.controller.admin.emergencytask.vo.EmergencyTaskRespVO; +import com.zteits.urbanops.module.garden.controller.admin.emergencytask.vo.EmergencyTaskSaveReqVO; +import com.zteits.urbanops.module.garden.service.emergencytask.*; +import com.zteits.urbanops.module.garden.service.filedownload.ExcelExportDispatchService; +import com.zteits.urbanops.module.infra.service.file.FileConfigService; +import com.zteits.urbanops.module.infra.service.file.FileService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; -import org.springframework.validation.annotation.Validated; +import jakarta.validation.Valid; +import lombok.extern.slf4j.Slf4j; import org.springframework.security.access.prepost.PreAuthorize; -import io.swagger.v3.oas.annotations.tags.Tag; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.Operation; - -import jakarta.validation.*; -import jakarta.servlet.http.*; -import java.util.*; -import java.io.IOException; -import java.util.stream.Collectors; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; -import com.zteits.urbanops.framework.common.pojo.PageParam; -import com.zteits.urbanops.framework.common.pojo.PageResult; -import com.zteits.urbanops.framework.common.pojo.CommonResult; +import java.util.List; +import static com.zteits.urbanops.framework.apilog.core.enums.OperateTypeEnum.EXPORT; import static com.zteits.urbanops.framework.common.pojo.CommonResult.success; -import com.zteits.urbanops.framework.excel.core.util.ExcelUtils; - -import com.zteits.urbanops.framework.apilog.core.annotation.ApiAccessLog; -import static com.zteits.urbanops.framework.apilog.core.enums.OperateTypeEnum.*; - -import com.zteits.urbanops.module.garden.controller.admin.emergencytask.vo.*; -import com.zteits.urbanops.module.garden.service.emergencytask.EmergencyTaskService; - @Tag(name = "管理后台 - 抢险任务主") @RestController @RequestMapping("/garden/emergency-task") @Validated +@Slf4j public class EmergencyTaskController { @Resource private EmergencyTaskService emergencyTaskService; + @Resource + private FileService fileService; + @Resource + private FileConfigService fileConfigService; + @Resource + private ExcelExportDispatchService dispatchService; + @Resource + private EmergencyExportHandler emergencyExportHandler; + @Resource + private EmergencyExportNewHandler emergencyExportNewHandler; @PostMapping("/create") @Operation(summary = "创建抢险任务主") @@ -100,81 +109,42 @@ public class EmergencyTaskController { @Operation(summary = "导出抢险任务主 Excel") @PreAuthorize("@ss.hasPermission('garden:emergency-task:export')") @ApiAccessLog(operateType = EXPORT) - public void exportEmergencyTaskExcel(@Valid EmergencyTaskPageReqVO pageReqVO, - HttpServletResponse response) throws IOException { + public CommonResult exportEmergencyTaskExcel(@Valid EmergencyTaskPageReqVO pageReqVO) { pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); List list = emergencyTaskService.getEmergencyTaskPage(pageReqVO).getList(); - List respList = new ArrayList<>(); - - //险情类型 1:树木倒伏 2:树木折枝 - List dictDataList = emergencyTaskService.getDictDataList("hazard_type"); - - //险情危害 1:砸车 2:砸房 3:砸人 4:无 - List hazardRiskList = emergencyTaskService.getDictDataList("hazard_risk"); - //树木权属 1:专业权属 2:街道权属 3:其他 - List treePermissionsList = emergencyTaskService.getDictDataList("tree_permissions"); - //机械类型 1:三轮车;2:货车;3:挖掘机 - List mechanicalTypeNameList = emergencyTaskService.getDictDataList("mechanical_type_name"); - - //案件状态 1-待提交 2-待审核 3-驳回 - List rescueStatusList = emergencyTaskService.getDictDataList("rescue_status"); - - list.forEach(emergencyTask -> { - EmergencyTaskExcleRespVO emergencyTaskExcleRespVO = new EmergencyTaskExcleRespVO(); - emergencyTaskExcleRespVO.setTaskNo(emergencyTask.getTaskNo()); - emergencyTaskExcleRespVO.setDiscoveryTime(emergencyTask.getDiscoveryTime()); - emergencyTaskExcleRespVO.setEmergencyTypeName(getLabel(dictDataList, emergencyTask.getEmergencyTypeId())); - emergencyTaskExcleRespVO.setEmergencyHarmName(getLabel(hazardRiskList, emergencyTask.getEmergencyHarmId())); - emergencyTaskExcleRespVO.setTreeOwnershipName(getLabel(treePermissionsList, emergencyTask.getTreeOwnershipId())); - emergencyTaskExcleRespVO.setEmergencyLocation(emergencyTask.getEmergencyLocation()); - emergencyTaskExcleRespVO.setReporter(emergencyTask.getReporter()); - emergencyTaskExcleRespVO.setContactPhone(emergencyTask.getContactPhone()); - emergencyTaskExcleRespVO.setEmergencyImg(emergencyTask.getEmergencyImg()); - emergencyTaskExcleRespVO.setTreeSpecies(emergencyTask.getTreeSpecies()); - emergencyTaskExcleRespVO.setTreeSpec(emergencyTask.getTreeSpec()); - emergencyTaskExcleRespVO.setRescueStartTime(emergencyTask.getRescueStartTime()); - emergencyTaskExcleRespVO.setRescueCompleteTime(emergencyTask.getRescueCompleteTime()); - emergencyTaskExcleRespVO.setPersonnelCount(emergencyTask.getPersonnelCount()); - emergencyTaskExcleRespVO.setEmergencyEndImg(emergencyTask.getEmergencyEndImg()); - emergencyTaskExcleRespVO.setRemarks(emergencyTask.getRemarks()); - emergencyTaskExcleRespVO.setBusiStatus(getLabel(rescueStatusList, emergencyTask.getBusiStatus())); - emergencyTaskExcleRespVO.setCompanyName(emergencyTask.getCompanyName()); - emergencyTaskExcleRespVO.setApprovalUserName(emergencyTask.getApprovalUserName()); - emergencyTaskExcleRespVO.setApprovalRemarks(emergencyTask.getApprovalRemarks()); - emergencyTaskExcleRespVO.setCreatorName(emergencyTask.getCreatorName()); - emergencyTaskExcleRespVO.setCreateTime(emergencyTask.getCreateTime()); - emergencyTaskExcleRespVO.setUpdaterName(emergencyTask.getUpdaterName()); - List machineryList = new ArrayList<>(); - if(CollectionUtils.isEmpty(emergencyTask.getEmergencyMachineryList())){ - emergencyTaskExcleRespVO.setMachineryInfo(Collections.emptyList()); - }else{ - emergencyTask.getEmergencyMachineryList().forEach(emergencyMachinery -> { - String machineryName = getLabel(mechanicalTypeNameList, emergencyMachinery.getMachineryNameId()); - Integer machineryCount = emergencyMachinery.getMachineryCount(); - machineryList.add(machineryName + ":" + machineryCount); - }); - emergencyTaskExcleRespVO.setMachineryInfo(machineryList); - } - respList.add(emergencyTaskExcleRespVO); - }); - - - // 导出 Excel - ExcelUtils.write(response, "抢险任务主.xls", "数据", EmergencyTaskExcleRespVO.class, respList); + // 无数据直接返回提示 + if (CollectionUtil.isEmpty(list)) { + return CommonResult.error(1 ,"暂无数据,无法导出"); + } + // 2. 调用调度服务创建任务 + Long taskId = dispatchService.createExportTask( + "EmergencyTaskExcle", + list, + emergencyExportHandler + ); + // 直接返回成功通知 + return CommonResult.success("导出任务已提交,数据正在后台生成,请稍后下载"); } - private String getLabel(List dictDataList, Integer id){ - if(CollectionUtils.isEmpty(dictDataList)){ - return ""; + @GetMapping("/export-excel-new") + @Operation(summary = "抢险案件表 Excel") + @PreAuthorize("@ss.hasPermission('garden:emergency-task:export')") + @ApiAccessLog(operateType = EXPORT) + public CommonResult exportEmergencyTaskExcelNew(@Valid EmergencyTaskPageReqVO pageReqVO){ + pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); + List list = emergencyTaskService.getEmergencyTaskPage(pageReqVO).getList(); + if (CollectionUtil.isEmpty(list)) { + return null; } - Map nameMap = dictDataList.stream() - .collect(Collectors.toMap( - DictDataRespDTO::getValue, // Key=id - DictDataRespDTO::getLabel // Value=name - )); - - return nameMap.get(String.valueOf( id)); + // 创建异步任务 + // 2. 调用调度服务创建任务 + Long taskId = dispatchService.createExportTask( + "EmergencyTaskExcleNew", + list, + emergencyExportNewHandler + ); + // 返回成功提示 + return success("导出任务已提交,数据正在后台生成,请稍后下载"); } - } diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/emergencytask/vo/EmergencyTaskExcleNewRespVO.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/emergencytask/vo/EmergencyTaskExcleNewRespVO.java new file mode 100644 index 0000000..3956373 --- /dev/null +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/emergencytask/vo/EmergencyTaskExcleNewRespVO.java @@ -0,0 +1,88 @@ +package com.zteits.urbanops.module.garden.controller.admin.emergencytask.vo; + +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; +import com.zteits.urbanops.framework.excel.core.convert.ImageStringConverter; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 抢险任务主 Response VO") +@Data +@ExcelIgnoreUnannotated +public class EmergencyTaskExcleNewRespVO { + + @Schema(description = "任务编号(唯一标识,如生成规则:TASK+时间戳)", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("案件编号") + private String taskNo; + + @Schema(description = "归属(一级部门id)", requiredMode = Schema.RequiredMode.REQUIRED, example = "20800") + @ExcelProperty("归属公司") + private String companyName; + + @Schema(description = "险情地点", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("险情地点") + private String emergencyLocation; + + @Schema(description = "发现险情时间", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("发现险情时间") + private LocalDateTime discoveryTime; + + @Schema(description = "险情类型(如:树木倒伏)", requiredMode = Schema.RequiredMode.REQUIRED, example = "5209") + @ExcelProperty("险情类型") + private String emergencyTypeName; + + @Schema(description = "树木权属(如:专业权属)", requiredMode = Schema.RequiredMode.REQUIRED, example = "19498") + @ExcelProperty("树木权属") + private String treeOwnershipName; + + @Schema(description = "险情危害(如:无)", requiredMode = Schema.RequiredMode.REQUIRED, example = "16325") + @ExcelProperty("险情危害") + private String emergencyHarmName; + + @Schema(description = "险情危害(如:无)", requiredMode = Schema.RequiredMode.REQUIRED, example = "16325") + @ExcelProperty("险情危害") + private Integer emergencyHarmId; + + @Schema(description = "数量/株", requiredMode = Schema.RequiredMode.REQUIRED, example = "3371") + @ExcelProperty("数量/株") + private Integer emergencyHarmNum; + + @Schema(description = "抢险开始时间", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("抢险开始时间") + private LocalDateTime rescueStartTime; + + @Schema(description = "树种(如:国槐)") + @ExcelProperty("树种") + private String treeSpecies; + + @Schema(description = "树木规格") + @ExcelProperty("树木规格") + private String treeSpec; + + @Schema(description = "抢险完成时间", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("抢险完成时间") + private LocalDateTime rescueCompleteTime; + + @Schema(description = "抢险中照片", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty(value = "抢险中照片", converter = ImageStringConverter.class) + private byte[] emergencyImg1; + + @Schema(description = "抢险中照片", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty(value = "抢险中照片", converter = ImageStringConverter.class) + private byte[] emergencyImg2; + + @Schema(description = "抢险中照片", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty(value = "抢险中照片", converter = ImageStringConverter.class) + private byte[] emergencyImg3; + + @Schema(description = "抢险结束照片", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty(value = "抢险结束照片", converter = ImageStringConverter.class) + private byte[] emergencyEndImg; + + @Schema(description = "预警信息") + @ExcelProperty("预警信息") + private String remarks; + +} diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/emergencytask/vo/EmergencyTaskExcleRespVO.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/emergencytask/vo/EmergencyTaskExcleRespVO.java index 41cf6d2..9671a01 100644 --- a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/emergencytask/vo/EmergencyTaskExcleRespVO.java +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/emergencytask/vo/EmergencyTaskExcleRespVO.java @@ -28,10 +28,18 @@ public class EmergencyTaskExcleRespVO { @ExcelProperty("险情类型") private String emergencyTypeName; + @Schema(description = "险情类型数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "5209") + @ExcelProperty("数量/株") + private Integer emergencyTypeNum; + @Schema(description = "险情危害(如:无)", requiredMode = Schema.RequiredMode.REQUIRED, example = "16325") @ExcelProperty("险情危害") private String emergencyHarmName; + @Schema(description = "险情危害数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "16325") + @ExcelProperty("数量") + private Integer emergencyHarmNum; + @Schema(description = "树木权属(如:专业权属)", requiredMode = Schema.RequiredMode.REQUIRED, example = "19498") @ExcelProperty("树木权属") private String treeOwnershipName; @@ -40,6 +48,10 @@ public class EmergencyTaskExcleRespVO { @ExcelProperty("险情地点") private String emergencyLocation; + @Schema(description = "所属街道", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("所属街道") + private String streetName; + @Schema(description = "上报人", requiredMode = Schema.RequiredMode.REQUIRED) @ExcelProperty("上报人") private String reporter; @@ -56,7 +68,7 @@ public class EmergencyTaskExcleRespVO { //@ExcelProperty(value="抢险中照片", converter = ListToStringConverter.class) @ExcelProperty(value = "抢险中照片", converter = ImageListConverter.class) @ColumnWidth(70) - private List emergencyImg; + private List emergencyImg; @Schema(description = "树种(如:国槐)") @ExcelProperty("树种") @@ -78,7 +90,7 @@ public class EmergencyTaskExcleRespVO { //@ExcelProperty(value="抢险结束照片", converter = ListToStringConverter.class) @ExcelProperty(value = "抢险结束照片", converter = ImageListConverter.class) @ColumnWidth(70) - private List emergencyEndImg; + private List emergencyEndImg; @Schema(description = "备注") @ExcelProperty("备注") @@ -96,7 +108,6 @@ public class EmergencyTaskExcleRespVO { @ExcelProperty("审批备注") private String approvalRemarks; - @Schema(description = "创建者", requiredMode = Schema.RequiredMode.REQUIRED, example = "赵六") @ExcelProperty("创建者") private String creatorName; diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/emergencytask/vo/EmergencyTaskPageReqVO.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/emergencytask/vo/EmergencyTaskPageReqVO.java index 30a4253..53c4367 100644 --- a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/emergencytask/vo/EmergencyTaskPageReqVO.java +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/emergencytask/vo/EmergencyTaskPageReqVO.java @@ -46,7 +46,9 @@ public class EmergencyTaskPageReqVO extends PageParam { @Schema(description = "归属(一级部门id)", example = "6353") private Long companyId; + @Schema(description = "所在街道ID", example = "1111111") + private String streetId; private List busiStatusList; -} \ No newline at end of file +} diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/emergencytask/vo/EmergencyTaskRespVO.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/emergencytask/vo/EmergencyTaskRespVO.java index e2a7f39..4a1e553 100644 --- a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/emergencytask/vo/EmergencyTaskRespVO.java +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/emergencytask/vo/EmergencyTaskRespVO.java @@ -1,8 +1,11 @@ package com.zteits.urbanops.module.garden.controller.admin.emergencytask.vo; +import com.fhs.core.trans.anno.Trans; +import com.fhs.core.trans.constant.TransType; import com.zteits.urbanops.framework.excel.core.convert.ListToStringConverter; import com.zteits.urbanops.module.garden.dal.dataobject.emergencytask.EmergencyMachineryDO; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; import lombok.*; import java.util.*; import java.time.LocalDateTime; @@ -136,4 +139,15 @@ public class EmergencyTaskRespVO { @ExcelProperty("部门") private String deptName; -} \ No newline at end of file + @Schema(description = "险情类型数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer emergencyTypeNum; + + @Schema(description = "险情危害数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer emergencyHarmNum; + + @Schema(description = "所在街道", requiredMode = Schema.RequiredMode.REQUIRED, example = "255") + private String streetId; + + @Schema(description = "所在街道", requiredMode = Schema.RequiredMode.REQUIRED, example = "255") + private String streetName; +} diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/emergencytask/vo/EmergencyTaskSaveReqVO.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/emergencytask/vo/EmergencyTaskSaveReqVO.java index 5583125..6068f76 100644 --- a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/emergencytask/vo/EmergencyTaskSaveReqVO.java +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/emergencytask/vo/EmergencyTaskSaveReqVO.java @@ -1,11 +1,13 @@ package com.zteits.urbanops.module.garden.controller.admin.emergencytask.vo; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.util.*; import jakarta.validation.constraints.*; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; +import java.util.List; @Schema(description = "管理后台 - 抢险任务主新增/修改 Request VO") @Data @@ -94,6 +96,16 @@ public class EmergencyTaskSaveReqVO { private String approvalUserName; @Schema(description = "使用机械", requiredMode = Schema.RequiredMode.REQUIRED, example = "赵六") - private List emergencyMachineryList; + private List emergencyMachineryList; -} \ No newline at end of file + @Schema(description = "险情类型数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer emergencyTypeNum; + + @Schema(description = "险情危害数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer emergencyHarmNum; + + @Schema(description = "所在街道", requiredMode = Schema.RequiredMode.REQUIRED, example = "255") + @NotEmpty(message = "所在街道") + private String streetId; + +} diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/filedownload/FileDownloadController.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/filedownload/FileDownloadController.java new file mode 100644 index 0000000..f1ae829 --- /dev/null +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/filedownload/FileDownloadController.java @@ -0,0 +1,124 @@ +package com.zteits.urbanops.module.garden.controller.admin.filedownload; + +import com.zteits.urbanops.framework.security.core.util.SecurityFrameworkUtils; +import jakarta.annotation.security.PermitAll; +import org.springframework.web.bind.annotation.*; +import jakarta.annotation.Resource; +import org.springframework.validation.annotation.Validated; +import org.springframework.security.access.prepost.PreAuthorize; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Operation; + +import jakarta.validation.constraints.*; +import jakarta.validation.*; +import jakarta.servlet.http.*; + +import java.io.FileInputStream; +import java.io.OutputStream; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.io.IOException; + +import com.zteits.urbanops.framework.common.pojo.PageParam; +import com.zteits.urbanops.framework.common.pojo.PageResult; +import com.zteits.urbanops.framework.common.pojo.CommonResult; +import com.zteits.urbanops.framework.common.util.object.BeanUtils; + +import static com.zteits.urbanops.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.zteits.urbanops.framework.common.pojo.CommonResult.success; + +import com.zteits.urbanops.framework.excel.core.util.ExcelUtils; + +import com.zteits.urbanops.framework.apilog.core.annotation.ApiAccessLog; +import static com.zteits.urbanops.framework.apilog.core.enums.OperateTypeEnum.*; +import static com.zteits.urbanops.module.garden.enums.ErrorCodeConstants.FILE_DOWNLOAD_NOT_EXISTS; +import static com.zteits.urbanops.module.infra.framework.file.core.utils.FileTypeUtils.writeAttachment; + +import com.zteits.urbanops.module.garden.controller.admin.filedownload.vo.*; +import com.zteits.urbanops.module.garden.dal.dataobject.filedownload.FileDownloadDO; +import com.zteits.urbanops.module.garden.service.filedownload.FileDownloadService; + +@Tag(name = "管理后台 - 文件异步下载任务") +@RestController +@RequestMapping("/garden/file-download") +@Validated +public class FileDownloadController { + + @Resource + private FileDownloadService fileDownloadService; + + @PostMapping("/create") + @Operation(summary = "创建文件异步下载任务") + @PreAuthorize("@ss.hasPermission('garden:file-download:create')") + public CommonResult createFileDownload(@Valid @RequestBody FileDownloadSaveReqVO createReqVO) { + return success(fileDownloadService.createFileDownload(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新文件异步下载任务") + @PreAuthorize("@ss.hasPermission('garden:file-download:update')") + public CommonResult updateFileDownload(@Valid @RequestBody FileDownloadSaveReqVO updateReqVO) { + fileDownloadService.updateFileDownload(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除文件异步下载任务") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('garden:file-download:delete')") + public CommonResult deleteFileDownload(@RequestParam("id") Long id) { + fileDownloadService.deleteFileDownload(id); + return success(true); + } + + @DeleteMapping("/delete-list") + @Parameter(name = "ids", description = "编号", required = true) + @Operation(summary = "批量删除文件异步下载任务") + @PreAuthorize("@ss.hasPermission('garden:file-download:delete')") + public CommonResult deleteFileDownloadList(@RequestParam("ids") List ids) { + fileDownloadService.deleteFileDownloadListByIds(ids); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得文件异步下载任务") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('garden:file-download:query')") + public CommonResult getFileDownload(@RequestParam("id") Long id) { + FileDownloadDO fileDownload = fileDownloadService.getFileDownload(id); + return success(BeanUtils.toBean(fileDownload, FileDownloadRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得文件异步下载任务分页") + //@PreAuthorize("@ss.hasPermission('garden:file-download:query')") + @PermitAll + public CommonResult> getFileDownloadPage(@Valid FileDownloadPageReqVO pageReqVO) { + PageResult pageResult = fileDownloadService.getFileDownloadPage(pageReqVO); + return success(BeanUtils.toBean(pageResult, FileDownloadRespVO.class)); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出文件异步下载任务 Excel") + @PreAuthorize("@ss.hasPermission('garden:file-download:export')") + @ApiAccessLog(operateType = EXPORT) + public void exportFileDownloadExcel(@Valid FileDownloadPageReqVO pageReqVO, + HttpServletResponse response) throws IOException { + pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); + List list = fileDownloadService.getFileDownloadPage(pageReqVO).getList(); + // 导出 Excel + ExcelUtils.write(response, "文件异步下载任务.xls", "数据", FileDownloadRespVO.class, + BeanUtils.toBean(list, FileDownloadRespVO.class)); + } + + @GetMapping("/download") + @Operation(summary = "下载文件异步下载任务") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('garden:file-download:query')") + public void downloadFileDownload(@RequestParam("id") Long id, HttpServletResponse response) throws IOException { + + fileDownloadService.writeAttachment(response, id); + } +} diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/filedownload/vo/FileDownloadPageReqVO.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/filedownload/vo/FileDownloadPageReqVO.java new file mode 100644 index 0000000..074a1c0 --- /dev/null +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/filedownload/vo/FileDownloadPageReqVO.java @@ -0,0 +1,58 @@ +package com.zteits.urbanops.module.garden.controller.admin.filedownload.vo; + +import lombok.*; +import java.util.*; +import io.swagger.v3.oas.annotations.media.Schema; +import com.zteits.urbanops.framework.common.pojo.PageParam; +import org.springframework.format.annotation.DateTimeFormat; +import java.time.LocalDateTime; + +import static com.zteits.urbanops.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 文件异步下载任务分页 Request VO") +@Data +public class FileDownloadPageReqVO extends PageParam { + + @Schema(description = "业务唯一编码::EmergencyTaskExcle") + private String busiCode; + + @Schema(description = "功能模块名称", example = "李四") + private String busiName; + + @Schema(description = "文件名称", example = "张三") + private String fileName; + + @Schema(description = "本地文件绝对路径") + private String filePath; + + @Schema(description = "状态:0待处理 1生成中 2成功 3失败", example = "2") + private Integer status; + + @Schema(description = "失败原因") + private String failMsg; + + @Schema(description = "文件过期时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] expireTime; + + @Schema(description = "文件生成完成时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] finishTime; + + @Schema(description = "文件大小(单位:字节)") + private Long fileSize; + + @Schema(description = "文件后缀:xlsx/zip/pdf") + private String fileSuffix; + + @Schema(description = "备注", example = "你猜") + private String remark; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + + @Schema(description = "创建人") + private String creator; + +} diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/filedownload/vo/FileDownloadRespVO.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/filedownload/vo/FileDownloadRespVO.java new file mode 100644 index 0000000..12d4cd2 --- /dev/null +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/filedownload/vo/FileDownloadRespVO.java @@ -0,0 +1,67 @@ +package com.zteits.urbanops.module.garden.controller.admin.filedownload.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import java.util.*; +import org.springframework.format.annotation.DateTimeFormat; +import java.time.LocalDateTime; +import cn.idev.excel.annotation.*; + +@Schema(description = "管理后台 - 文件异步下载任务 Response VO") +@Data +@ExcelIgnoreUnannotated +public class FileDownloadRespVO { + + @Schema(description = "主键ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "27429") + @ExcelProperty("主键ID") + private Long id; + + @Schema(description = "业务唯一编码::EmergencyTaskExcle", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("业务唯一编码::EmergencyTaskExcle") + private String busiCode; + + @Schema(description = "功能模块名称", example = "李四") + @ExcelProperty("功能模块名称") + private String busiName; + + @Schema(description = "文件名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三") + @ExcelProperty("文件名称") + private String fileName; + + @Schema(description = "本地文件绝对路径") + @ExcelProperty("本地文件绝对路径") + private String filePath; + + @Schema(description = "状态:0待处理 1生成中 2成功 3失败", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + @ExcelProperty("状态:0待处理 1生成中 2成功 3失败") + private Integer status; + + @Schema(description = "失败原因") + @ExcelProperty("失败原因") + private String failMsg; + + @Schema(description = "文件过期时间") + @ExcelProperty("文件过期时间") + private LocalDateTime expireTime; + + @Schema(description = "文件生成完成时间") + @ExcelProperty("文件生成完成时间") + private LocalDateTime finishTime; + + @Schema(description = "文件大小(单位:字节)") + @ExcelProperty("文件大小(单位:字节)") + private Long fileSize; + + @Schema(description = "文件后缀:xlsx/zip/pdf") + @ExcelProperty("文件后缀:xlsx/zip/pdf") + private String fileSuffix; + + @Schema(description = "备注", example = "你猜") + @ExcelProperty("备注") + private String remark; + + @Schema(description = "创建时间") + @ExcelProperty("创建时间") + private LocalDateTime createTime; + +} \ No newline at end of file diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/filedownload/vo/FileDownloadSaveReqVO.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/filedownload/vo/FileDownloadSaveReqVO.java new file mode 100644 index 0000000..a37b736 --- /dev/null +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/admin/filedownload/vo/FileDownloadSaveReqVO.java @@ -0,0 +1,53 @@ +package com.zteits.urbanops.module.garden.controller.admin.filedownload.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import java.util.*; +import jakarta.validation.constraints.*; +import org.springframework.format.annotation.DateTimeFormat; +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 文件异步下载任务新增/修改 Request VO") +@Data +public class FileDownloadSaveReqVO { + + @Schema(description = "主键ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "27429") + private Long id; + + @Schema(description = "业务唯一编码::EmergencyTaskExcle", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "业务唯一编码::EmergencyTaskExcle不能为空") + private String busiCode; + + @Schema(description = "功能模块名称", example = "李四") + private String busiName; + + @Schema(description = "文件名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三") + @NotEmpty(message = "文件名称不能为空") + private String fileName; + + @Schema(description = "本地文件绝对路径") + private String filePath; + + @Schema(description = "状态:0待处理 1生成中 2成功 3失败", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + @NotNull(message = "状态:0待处理 1生成中 2成功 3失败不能为空") + private Integer status; + + @Schema(description = "失败原因") + private String failMsg; + + @Schema(description = "文件过期时间") + private LocalDateTime expireTime; + + @Schema(description = "文件生成完成时间") + private LocalDateTime finishTime; + + @Schema(description = "文件大小(单位:字节)") + private Long fileSize; + + @Schema(description = "文件后缀:xlsx/zip/pdf") + private String fileSuffix; + + @Schema(description = "备注", example = "你猜") + private String remark; + +} \ No newline at end of file diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/dal/dataobject/emergencytask/EmergencyTaskDO.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/dal/dataobject/emergencytask/EmergencyTaskDO.java index d19711d..6ecc79e 100644 --- a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/dal/dataobject/emergencytask/EmergencyTaskDO.java +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/dal/dataobject/emergencytask/EmergencyTaskDO.java @@ -1,16 +1,15 @@ package com.zteits.urbanops.module.garden.dal.dataobject.emergencytask; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.zteits.urbanops.framework.mybatis.core.dataobject.BaseDO; import com.zteits.urbanops.framework.mybatis.core.type.StringArrayToListTypeHandler; -import com.zteits.urbanops.framework.mybatis.core.type.StringListTypeHandler; import lombok.*; -import java.util.*; -import java.time.LocalDateTime; -import java.time.LocalDateTime; -import java.time.LocalDateTime; -import java.time.LocalDateTime; + import java.time.LocalDateTime; -import com.baomidou.mybatisplus.annotation.*; -import com.zteits.urbanops.framework.mybatis.core.dataobject.BaseDO; +import java.util.List; /** * 抢险任务主 DO @@ -139,6 +138,16 @@ public class EmergencyTaskDO extends BaseDO { * 更新者 */ private String updaterName; - - -} \ No newline at end of file + /** + * 险情类型数量 + */ + private Integer emergencyTypeNum; + /** + * 险情危害数量 + */ + private Integer emergencyHarmNum; + /** + * 所在街道ID + */ + private String streetId; +} diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/dal/dataobject/filedownload/FileDownloadDO.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/dal/dataobject/filedownload/FileDownloadDO.java new file mode 100644 index 0000000..7ea7f2c --- /dev/null +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/dal/dataobject/filedownload/FileDownloadDO.java @@ -0,0 +1,82 @@ +package com.zteits.urbanops.module.garden.dal.dataobject.filedownload; + +import lombok.*; +import java.util.*; +import java.time.LocalDateTime; +import java.time.LocalDateTime; +import java.time.LocalDateTime; +import java.time.LocalDateTime; +import com.baomidou.mybatisplus.annotation.*; +import com.zteits.urbanops.framework.mybatis.core.dataobject.BaseDO; + +/** + * 文件异步下载任务 DO + * + * @author 超级管理员 + */ +@TableName("garden_file_download") +@KeySequence("garden_file_download_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class FileDownloadDO extends BaseDO { + + /** + * 主键ID + */ + @TableId + private Long id; + /** + * 业务唯一编码::EmergencyTaskExcle + */ + private String busiCode; + /** + * 功能模块名称 + */ + private String busiName; + /** + * 文件名称 + */ + private String fileName; + /** + * 本地文件绝对路径 + */ + private String filePath; + /** + * 存储类型:存储类型:LOCAL/OSS + */ + private String storageType; + /** + * 状态:0待处理 1生成中 2成功 3失败 + */ + private Integer status; + /** + * 失败原因 + */ + private String failMsg; + /** + * 文件过期时间 + */ + private LocalDateTime expireTime; + /** + * 文件生成完成时间 + */ + private LocalDateTime finishTime; + /** + * 文件大小(单位:字节) + */ + private Long fileSize; + /** + * 文件后缀:xlsx/zip/pdf + */ + private String fileSuffix; + /** + * 备注 + */ + private String remark; + + +} diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/dal/mysql/emergencytask/EmergencyTaskMapper.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/dal/mysql/emergencytask/EmergencyTaskMapper.java index 86d4f00..9ce79e0 100644 --- a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/dal/mysql/emergencytask/EmergencyTaskMapper.java +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/dal/mysql/emergencytask/EmergencyTaskMapper.java @@ -40,6 +40,7 @@ public interface EmergencyTaskMapper extends BaseMapperX { .likeIfPresent(EmergencyTaskDO::getEmergencyLocation, reqVO.getEmergencyLocation()) .eqIfPresent(EmergencyTaskDO::getBusiStatus, reqVO.getBusiStatus()) .eqIfPresent(EmergencyTaskDO::getCompanyId, reqVO.getCompanyId()) + .eqIfPresent(EmergencyTaskDO::getStreetId, reqVO.getStreetId()) .betweenIfPresent(EmergencyTaskDO::getDiscoveryTime, startDateTime, endDateTime); if(!CollectionUtils.isEmpty(reqVO.getBusiStatusList())){ sql.in(EmergencyTaskDO::getBusiStatus, reqVO.getBusiStatusList()); @@ -50,4 +51,4 @@ public interface EmergencyTaskMapper extends BaseMapperX { int deleteByTaskNo(@Param("taskNo") String taskNo); -} \ No newline at end of file +} diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/dal/mysql/filedownload/FileDownloadMapper.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/dal/mysql/filedownload/FileDownloadMapper.java new file mode 100644 index 0000000..7af509e --- /dev/null +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/dal/mysql/filedownload/FileDownloadMapper.java @@ -0,0 +1,38 @@ +package com.zteits.urbanops.module.garden.dal.mysql.filedownload; + +import java.util.*; + +import com.zteits.urbanops.framework.common.pojo.PageResult; +import com.zteits.urbanops.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.zteits.urbanops.framework.mybatis.core.mapper.BaseMapperX; +import com.zteits.urbanops.module.garden.dal.dataobject.filedownload.FileDownloadDO; +import org.apache.ibatis.annotations.Mapper; +import com.zteits.urbanops.module.garden.controller.admin.filedownload.vo.*; + +/** + * 文件异步下载任务 Mapper + * + * @author 超级管理员 + */ +@Mapper +public interface FileDownloadMapper extends BaseMapperX { + + default PageResult selectPage(FileDownloadPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(FileDownloadDO::getBusiCode, reqVO.getBusiCode()) + .likeIfPresent(FileDownloadDO::getBusiName, reqVO.getBusiName()) + .likeIfPresent(FileDownloadDO::getFileName, reqVO.getFileName()) + .eqIfPresent(FileDownloadDO::getFilePath, reqVO.getFilePath()) + .eqIfPresent(FileDownloadDO::getStatus, reqVO.getStatus()) + .eqIfPresent(FileDownloadDO::getFailMsg, reqVO.getFailMsg()) + .betweenIfPresent(FileDownloadDO::getExpireTime, reqVO.getExpireTime()) + .betweenIfPresent(FileDownloadDO::getFinishTime, reqVO.getFinishTime()) + .eqIfPresent(FileDownloadDO::getFileSize, reqVO.getFileSize()) + .eqIfPresent(FileDownloadDO::getFileSuffix, reqVO.getFileSuffix()) + .eqIfPresent(FileDownloadDO::getRemark, reqVO.getRemark()) + .betweenIfPresent(FileDownloadDO::getCreateTime, reqVO.getCreateTime()) + .eqIfPresent(FileDownloadDO::getCreator, reqVO.getCreator()) + .orderByDesc(FileDownloadDO::getId)); + } + +} diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/enums/ErrorCodeConstants.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/enums/ErrorCodeConstants.java index a7f3906..e6f1419 100644 --- a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/enums/ErrorCodeConstants.java +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/enums/ErrorCodeConstants.java @@ -155,4 +155,7 @@ public interface ErrorCodeConstants { // ========== 一树一档案巡检与风险评估 1-100-008-000 ========== ErrorCode TREE_INSPECTION_NOT_EXISTS = new ErrorCode(1100008001, "巡检评估记录不存在"); + //文件异步下载 + ErrorCode FILE_DOWNLOAD_NOT_EXISTS = new ErrorCode(1-100-000-001, "文件异步下载任务不存在"); + } diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/enums/ExportBusiCodeEnum.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/enums/ExportBusiCodeEnum.java new file mode 100644 index 0000000..ed80b0d --- /dev/null +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/enums/ExportBusiCodeEnum.java @@ -0,0 +1,74 @@ +package com.zteits.urbanops.module.garden.enums; + +import lombok.Getter; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +/** + * 导出业务编码枚举 + */ +@Getter +public enum ExportBusiCodeEnum { + + /** 旧版抢险案件导出 */ + EMERGENCY_TASK_OLD("EmergencyTaskExcle", "险情管理", "抢险案件表", "险情案件", ".xlsx"), + /** 新版抢险案件导出 */ + EMERGENCY_TASK_NEW("EmergencyTaskExcleNew", "险情管理", "抢险案件表", "险情案件", ".xlsx"); + + /** 业务编码 */ + private final String busiCode; + /** 业务名称 */ + private final String busiName; + /** 导出文件名(不含后缀) */ + private final String fileName; + /** Excel Sheet 名称 */ + private final String sheetName; + /** 文件后缀 */ + private final String fileSuffix; + + // 时间格式化:yyyyMMddHHmmss + private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmss"); + + /** + * 全参构造 + * @param busiCode 业务编码 + * @param busiName 业务名称 + * @param fileName 文件名 + * @param sheetName sheet名称 + * @param fileSuffix 文件后缀 + */ + ExportBusiCodeEnum(String busiCode, String busiName, String fileName, String sheetName, String fileSuffix) { + this.busiCode = busiCode; + this.busiName = busiName; + this.fileName = fileName; + this.sheetName = sheetName; + this.fileSuffix = fileSuffix; + } + + /** + * 根据业务编码获取枚举 + * @param busiCode 业务编码 + * @return 对应枚举,未匹配返回 null + */ + public static ExportBusiCodeEnum getByCode(String busiCode) { + if (busiCode == null) { + return null; + } + for (ExportBusiCodeEnum enumItem : values()) { + if (enumItem.getBusiCode().equals(busiCode)) { + return enumItem; + } + } + return null; + } + + /** + * 拼接完整文件名(文件名 + 后缀) + * @return 完整文件名 例:抢险案件表.xls + */ + public String getFullFileName() { + String nowTime = LocalDateTime.now().format(TIME_FORMATTER); + return String.format("%s_%s%s", this.fileName, nowTime, this.fileSuffix); + } +} diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/enums/FileTaskStatus.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/enums/FileTaskStatus.java new file mode 100644 index 0000000..413fe8a --- /dev/null +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/enums/FileTaskStatus.java @@ -0,0 +1,30 @@ +package com.zteits.urbanops.module.garden.enums; + +/** + * @Classname FileTaskStatus + * @Description * 文件异步任务状态 + * * 0待处理 1生成中 2成功 3失败 + * @Date 2026/6/12 15:49 + * @Created by wangqian + */ +public enum FileTaskStatus { + // FileTaskStatus 常见定义 + PENDING(0, "待处理"), // 刚创建,还没开始 + PROCESSING(1, "处理中"), // 正在分批导出 + SUCCESS(2, "成功"), // 全部分页写完 + FAIL(3, "失败"); // 中途出错 + + private final Integer code; + private final String desc; + + FileTaskStatus(Integer code, String desc) { + this.code = code; + this.desc = desc; + } + public Integer getCode() { + return code; + } + public String getDesc() { + return desc; + } +} diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/job/file/ClearExpiredFilesJob.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/job/file/ClearExpiredFilesJob.java new file mode 100644 index 0000000..36f142c --- /dev/null +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/job/file/ClearExpiredFilesJob.java @@ -0,0 +1,24 @@ +package com.zteits.urbanops.module.garden.job.file; + +import com.zteits.urbanops.framework.quartz.core.handler.JobHandler; +import com.zteits.urbanops.module.garden.service.filedownload.FileDownloadService; +import jakarta.annotation.Resource; + +/** + * @Classname ClearExpiredFilesJob + * @Description 清理过期文件 + * 1、mysql事件调度器将过期文件记录更新为 4-失效, + * 2、定时任务每天执行一次,查询到数据库中状态为4的记录,将文件删除 + * @Date 2026/6/15 12:25 + * @Created by wangqian + */ +public class ClearExpiredFilesJob implements JobHandler { + @Resource + private FileDownloadService fileDownloadService; + + @Override + public String execute(String param) throws Exception { + fileDownloadService.clearExpiredFiles(); + return "Successfully"; + } +} diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/emergencytask/EmergencyExportHandler.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/emergencytask/EmergencyExportHandler.java new file mode 100644 index 0000000..4684b6f --- /dev/null +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/emergencytask/EmergencyExportHandler.java @@ -0,0 +1,152 @@ +package com.zteits.urbanops.module.garden.service.emergencytask; + +import cn.idev.excel.ExcelWriter; +import cn.idev.excel.write.metadata.WriteSheet; +import com.zteits.urbanops.framework.common.biz.system.dict.dto.DictDataRespDTO; +import com.zteits.urbanops.module.garden.controller.admin.emergencytask.vo.EmergencyTaskExcleRespVO; +import com.zteits.urbanops.module.garden.controller.admin.emergencytask.vo.EmergencyTaskRespVO; +import com.zteits.urbanops.module.garden.service.filedownload.ExcelExportHandler; +import com.zteits.urbanops.module.garden.util.ImgDownUtils; +import com.zteits.urbanops.module.infra.framework.file.core.client.FileClient; +import com.zteits.urbanops.module.infra.service.file.FileConfigService; +import lombok.extern.slf4j.Slf4j; +import org.apache.poi.ss.util.CellRangeAddress; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; + +import jakarta.annotation.Resource; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +import static com.zteits.urbanops.module.garden.service.filedownload.ExcelExportAsyncService.BATCH_SIZE; + +/** + * 应急任务导出处理器 + * 实现 ExcelExportHandler 接口,替代原抽象类继承 + */ +@Slf4j +@Service +public class EmergencyExportHandler implements ExcelExportHandler { + + // 字典本地缓存 + private static final Map> DICT_CACHE = new ConcurrentHashMap<>(); + + // 字典类型常量(避免魔法值) + private static final String DICT_HAZARD_TYPE = "hazard_type"; + private static final String DICT_HAZARD_RISK = "hazard_risk"; + private static final String DICT_TREE_PERMISSIONS = "tree_permissions"; + private static final String DICT_MECHANICAL_TYPE = "mechanical_type"; + private static final String DICT_RESCUE_STATUS = "rescue_status"; + + @Resource + private EmergencyTaskService emergencyTaskService; + @Resource + private FileConfigService fileConfigService; + + // ===================== 接口方法实现 ===================== + @Override + public List convert(List batchList, int startIndex) { + + List voList = new ArrayList<>(batchList.size()); + // 从上下文获取字典,增加空兜底 + Map hazardTypeDict = getDictMap(DICT_HAZARD_TYPE); + Map hazardRiskDict = getDictMap(DICT_HAZARD_RISK); + Map treePermissionsDict = getDictMap(DICT_TREE_PERMISSIONS); + Map mechanicalTypeDict = getDictMap(DICT_MECHANICAL_TYPE); + Map rescueStatusDict = getDictMap(DICT_RESCUE_STATUS); + // 初始化文件客户端 + FileClient fileClient = fileConfigService.getMasterFileClient(); + + for (EmergencyTaskRespVO task : batchList) { + EmergencyTaskExcleRespVO vo = new EmergencyTaskExcleRespVO(); + vo.setTaskNo(task.getTaskNo()); + vo.setDiscoveryTime(task.getDiscoveryTime()); + vo.setReporter(task.getReporter()); + vo.setContactPhone(task.getContactPhone()); + vo.setEmergencyLocation(task.getEmergencyLocation()); + vo.setTreeSpecies(task.getTreeSpecies()); + vo.setTreeSpec(task.getTreeSpec()); + vo.setRescueStartTime(task.getRescueStartTime()); + vo.setRescueCompleteTime(task.getRescueCompleteTime()); + vo.setPersonnelCount(task.getPersonnelCount()); + vo.setRemarks(task.getRemarks()); + vo.setCompanyName(task.getCompanyName()); + vo.setApprovalUserName(task.getApprovalUserName()); + vo.setApprovalRemarks(task.getApprovalRemarks()); + vo.setCreatorName(task.getCreatorName()); + vo.setCreateTime(task.getCreateTime()); + vo.setUpdaterName(task.getUpdaterName()); + vo.setEmergencyHarmNum(task.getEmergencyHarmNum()); + vo.setEmergencyTypeNum(task.getEmergencyTypeNum()); + vo.setStreetName(task.getStreetName()); + + // 字典翻译 + vo.setEmergencyTypeName(getLabel(hazardTypeDict, task.getEmergencyTypeId())); + vo.setEmergencyHarmName(getLabel(hazardRiskDict, task.getEmergencyHarmId())); + vo.setTreeOwnershipName(getLabel(treePermissionsDict, task.getTreeOwnershipId())); + vo.setBusiStatus(getLabel(rescueStatusDict, task.getBusiStatus())); + + // 图片下载处理 + vo.setEmergencyImg(ImgDownUtils.batchDownloadImages(task.getEmergencyImg(), fileClient)); + vo.setEmergencyEndImg(ImgDownUtils.batchDownloadImages(task.getEmergencyEndImg(), fileClient)); + + // 机械设备信息拼接 + List machineryInfoList = new ArrayList<>(); + if (!CollectionUtils.isEmpty(task.getEmergencyMachineryList())) { + task.getEmergencyMachineryList().forEach(machinery -> + machineryInfoList.add(getLabel(mechanicalTypeDict, machinery.getMachineryNameId()) + + ":" + machinery.getMachineryCount())); + } + vo.setMachineryInfo(machineryInfoList); + voList.add(vo); + } + return voList; + } + + @Override + public Class getExcelVoClass() { + return EmergencyTaskExcleRespVO.class; + } + + @Override + public List> buildHead() { + return new ArrayList<>(); + } + + @Override + public List buildMergeRule() { + return new ArrayList<>(); + } + // ===================== 字典工具方法 ===================== + public Map getDictMap(String dictType) { + // 先从缓存取 + if (DICT_CACHE.containsKey(dictType)) { + return DICT_CACHE.get(dictType); + } + // 缓存不存在,查询数据库 + List dictList = emergencyTaskService.getDictDataList(dictType); + Map map = buildDictMap(dictList); + DICT_CACHE.put(dictType, map); + return map; + } + + private Map buildDictMap(List dictDataList) { + if (CollectionUtils.isEmpty(dictDataList)) { + return Collections.emptyMap(); + } + return dictDataList.stream() + .collect(Collectors.toMap( + DictDataRespDTO::getValue, + DictDataRespDTO::getLabel, + (v1, v2) -> v1 + )); + } + + private String getLabel(Map dictMap, Integer id) { + if (dictMap == null || dictMap.isEmpty() || id == null) { + return ""; + } + return dictMap.getOrDefault(String.valueOf(id), ""); + } +} diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/emergencytask/EmergencyExportNewHandler.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/emergencytask/EmergencyExportNewHandler.java new file mode 100644 index 0000000..f20a110 --- /dev/null +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/emergencytask/EmergencyExportNewHandler.java @@ -0,0 +1,184 @@ +package com.zteits.urbanops.module.garden.service.emergencytask; + +import com.zteits.urbanops.framework.common.biz.system.dict.dto.DictDataRespDTO; +import com.zteits.urbanops.module.garden.controller.admin.emergencytask.vo.EmergencyTaskExcleNewRespVO; +import com.zteits.urbanops.module.garden.controller.admin.emergencytask.vo.EmergencyTaskRespVO; +import com.zteits.urbanops.module.garden.service.filedownload.ExcelExportHandler; +import com.zteits.urbanops.module.garden.util.ImgDownUtils; +import com.zteits.urbanops.module.infra.framework.file.core.client.FileClient; +import com.zteits.urbanops.module.infra.service.file.FileConfigService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.apache.poi.ss.util.CellRangeAddress; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +import static com.zteits.urbanops.module.garden.service.filedownload.ExcelExportAsyncService.BATCH_SIZE; + +/** + * 新版应急任务导出处理器 + * 实现 ExcelExportHandler 接口 + */ +@Slf4j +@Service +public class EmergencyExportNewHandler implements ExcelExportHandler { + + // ===================== 常量定义(消除魔法值) ===================== + /** 字典类型编码 */ + private static final String DICT_HAZARD_TYPE = "hazard_type"; + private static final String DICT_HAZARD_RISK = "hazard_risk"; + private static final String DICT_TREE_PERMISSIONS = "tree_permissions"; + + /** 字典缓存:key=字典编码, value=字典映射 */ + private static final Map> DICT_CACHE = new ConcurrentHashMap<>(); + + @Resource + private EmergencyTaskService emergencyTaskService; + @Resource + private FileConfigService fileConfigService; + + // ===================== 接口方法实现 ===================== + @Override + public List convert(List sourceList, int startIndex) { + if (CollectionUtils.isEmpty(sourceList)) { + return Collections.emptyList(); + } + Map hazardTypeDict = getDictMap(DICT_HAZARD_TYPE); + Map hazardRiskDict = getDictMap(DICT_HAZARD_RISK); + Map treePermissionsDict = getDictMap(DICT_TREE_PERMISSIONS); + // 初始化文件客户端 + FileClient fileClient = fileConfigService.getMasterFileClient(); + + List respList = new ArrayList<>(sourceList.size()); + + // 合并图片收集+下载,减少一次全量遍历 + for (EmergencyTaskRespVO task : sourceList) { + EmergencyTaskExcleNewRespVO vo = new EmergencyTaskExcleNewRespVO(); + // 全局行号,跨分片连续不重复 + vo.setTaskNo(String.valueOf(startIndex++)); + + // 基础字段赋值 + vo.setDiscoveryTime(task.getDiscoveryTime()); + vo.setEmergencyHarmId(task.getEmergencyHarmId()); + vo.setEmergencyLocation(task.getEmergencyLocation()); + vo.setTreeSpecies(task.getTreeSpecies()); + vo.setTreeSpec(task.getTreeSpec()); + vo.setRescueStartTime(task.getRescueStartTime()); + vo.setRescueCompleteTime(task.getRescueCompleteTime()); + vo.setEmergencyHarmNum(task.getEmergencyTypeNum()); + vo.setRemarks(task.getRemarks()); + vo.setCompanyName(task.getCompanyName()); + + // 字典翻译 + vo.setEmergencyTypeName(getLabel(hazardTypeDict, task.getEmergencyTypeId())); + vo.setEmergencyHarmName(getLabel(hazardRiskDict, task.getEmergencyHarmId())); + vo.setTreeOwnershipName(getLabel(treePermissionsDict, task.getTreeOwnershipId())); + + // 图片处理(一次组装、一次下载,精简逻辑) + List emergencyImgs = Optional.ofNullable(task.getEmergencyImg()).orElse(Collections.emptyList()); + List endImgs = Optional.ofNullable(task.getEmergencyEndImg()).orElse(Collections.emptyList()); + List imgUrls = new ArrayList<>(4); + imgUrls.add(ImgDownUtils.getImgSafe(emergencyImgs, 0)); + imgUrls.add(ImgDownUtils.getImgSafe(emergencyImgs, 1)); + imgUrls.add(ImgDownUtils.getImgSafe(emergencyImgs, 2)); + imgUrls.add(ImgDownUtils.getImgSafe(endImgs, 0)); + + List imgBytes = ImgDownUtils.batchDownloadImages(imgUrls, fileClient); + vo.setEmergencyImg1(ImgDownUtils.getByteSafe(imgBytes, 0)); + vo.setEmergencyImg2(ImgDownUtils.getByteSafe(imgBytes, 1)); + vo.setEmergencyImg3(ImgDownUtils.getByteSafe(imgBytes, 2)); + vo.setEmergencyEndImg(ImgDownUtils.getByteSafe(imgBytes, 3)); + + respList.add(vo); + } + return respList; + } + + @Override + public Class getExcelVoClass() { + return EmergencyTaskExcleNewRespVO.class; + } + + @Override + public List> buildHead() { + List> head = new ArrayList<>(); + head.add(Arrays.asList("抢险案件表", "", "序号")); + head.add(Arrays.asList("", "抢险前", "作业队")); + head.add(Arrays.asList("", "抢险前", "险情地点")); + head.add(Arrays.asList("", "抢险前", "发现险情时间")); + head.add(Arrays.asList("", "抢险前", "险情类型")); + head.add(Arrays.asList("", "抢险前", "树木权属")); + head.add(Arrays.asList("", "抢险前", "险情危害")); + head.add(Arrays.asList("", "抢险前", "险情危害")); + head.add(Arrays.asList("", "抢险前", "数量/株")); + head.add(Arrays.asList("", "抢险中", "抢险开始时间")); + head.add(Arrays.asList("", "抢险中", "树种")); + head.add(Arrays.asList("", "抢险中", "树木规格")); + head.add(Arrays.asList("", "抢险后", "抢险完成后时间")); + head.add(Arrays.asList("", "照片", "抢险过程照片")); + head.add(Arrays.asList("", "照片", "抢险过程照片")); + head.add(Arrays.asList("", "照片", "抢险过程照片")); + head.add(Arrays.asList("", "照片", "抢险后照片")); + head.add(Arrays.asList("", "备注", "")); + return head; + } + + @Override + public List buildMergeRule() { + List mergeList = new ArrayList<>(); + mergeList.add(new CellRangeAddress(0, 0, 0, 17)); + mergeList.add(new CellRangeAddress(1, 1, 1, 8)); + mergeList.add(new CellRangeAddress(1, 1, 9, 11)); + mergeList.add(new CellRangeAddress(1, 1, 12, 12)); + mergeList.add(new CellRangeAddress(1, 1, 13, 16)); + mergeList.add(new CellRangeAddress(1, 1, 17, 17)); + mergeList.add(new CellRangeAddress(2, 2, 6, 7)); + return mergeList; + } + + // ===================== 字典缓存工具 ===================== + /** + * 获取字典映射(优先走本地缓存) + */ + private Map getDictMap(String dictType) { + if (DICT_CACHE.containsKey(dictType)) { + return DICT_CACHE.get(dictType); + } + List dictList = emergencyTaskService.getDictDataList(dictType); + Map map = buildDictMap(dictList); + DICT_CACHE.put(dictType, map); + log.info("加载字典[{}],数据量:{}", dictType, map.size()); + return map; + } + + /** + * 构建字典 key-value 映射 + */ + private Map buildDictMap(List dictDataList) { + if (CollectionUtils.isEmpty(dictDataList)) { + return Collections.emptyMap(); + } + return dictDataList.stream() + .collect(Collectors.toMap( + DictDataRespDTO::getValue, + DictDataRespDTO::getLabel, + (oldVal, newVal) -> oldVal + )); + } + + /** + * 字典翻译取值(防空、防NPE) + */ + private String getLabel(Map dictMap, Integer id) { + if (dictMap == null || dictMap.isEmpty() || id == null) { + return ""; + } + return dictMap.getOrDefault(String.valueOf(id), ""); + } + +} diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/emergencytask/EmergencyTaskServiceImpl.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/emergencytask/EmergencyTaskServiceImpl.java index 4728a33..6a438c5 100644 --- a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/emergencytask/EmergencyTaskServiceImpl.java +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/emergencytask/EmergencyTaskServiceImpl.java @@ -2,17 +2,21 @@ package com.zteits.urbanops.module.garden.service.emergencytask; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.RandomUtil; import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; import com.zteits.urbanops.framework.common.biz.system.dict.dto.DictDataRespDTO; import com.zteits.urbanops.framework.datapermission.core.annotation.DataPermission; import com.zteits.urbanops.framework.security.core.util.SecurityFrameworkUtils; import com.zteits.urbanops.module.garden.dal.dataobject.emergencytask.EmergencyMachineryDO; +import com.zteits.urbanops.module.garden.dal.dataobject.road.RoadStreetDO; import com.zteits.urbanops.module.garden.dal.mysql.emergencytask.EmergencyMachineryMapper; +import com.zteits.urbanops.module.garden.dal.mysql.road.RoadStreetMapper; import com.zteits.urbanops.module.system.api.dept.DeptApi; import com.zteits.urbanops.module.system.api.dept.dto.DeptRespDTO; import com.zteits.urbanops.module.system.api.dict.DictDataApi; import org.apache.commons.lang3.StringUtils; +import org.apache.poi.ss.formula.functions.T; import org.springframework.stereotype.Service; import jakarta.annotation.Resource; import org.springframework.util.CollectionUtils; @@ -20,6 +24,7 @@ import org.springframework.validation.annotation.Validated; import org.springframework.transaction.annotation.Transactional; import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; import com.zteits.urbanops.module.garden.controller.admin.emergencytask.vo.*; @@ -56,6 +61,9 @@ public class EmergencyTaskServiceImpl implements EmergencyTaskService { @Resource private DictDataApi dictDataApi; + @Resource + private RoadStreetMapper roadStreetMapper; + @Transactional(rollbackFor = Exception.class) @Override public Long createEmergencyTask(EmergencyTaskSaveReqVO createReqVO, boolean isSave) { @@ -92,6 +100,9 @@ public class EmergencyTaskServiceImpl implements EmergencyTaskService { emergencyTask.setUpdater(userId); emergencyTask.setUpdaterName(userName); emergencyTask.setApprovalRemarks(""); + emergencyTask.setStreetId(createReqVO.getStreetId()); + emergencyTask.setEmergencyHarmNum(createReqVO.getEmergencyHarmNum()); + emergencyTask.setEmergencyTypeNum(createReqVO.getEmergencyTypeNum()); if(isSave){ emergencyTask.setCreator(userId); emergencyTask.setCreatorName(userName); @@ -202,22 +213,87 @@ public class EmergencyTaskServiceImpl implements EmergencyTaskService { if(companyId != null && companyId.equals(100L)){ pageReqVO.setBusiStatusList(Arrays.asList(2,3,4,5)); } + // 2. 分页查询主表数据 PageResult page = emergencyTaskMapper.selectPage(pageReqVO); + List doList = page.getList(); + if (CollectionUtil.isEmpty(doList)) { + // 无数据直接返回,跳过后续逻辑 + return new PageResult<>(Collections.emptyList(), page.getTotal()); + } + // 预定义集合容量,避免 ArrayList 扩容 + int dataSize = doList.size(); + List taskNoList = new ArrayList<>(dataSize); + Set orgIdSet = new HashSet<>(dataSize); + + for (EmergencyTaskDO taskDO : doList) { + String taskNo = taskDO.getTaskNo(); + Long companyIdVal = taskDO.getCompanyId(); + Long deptIdVal = taskDO.getDeptId(); + + if (ObjectUtil.isNotEmpty(taskNo)) { + taskNoList.add(taskNo); + } - List list = page.getList(); - List listTwo = BeanUtils.toBean(list, EmergencyTaskRespVO.class); + if (ObjectUtil.isNotNull(companyIdVal)) { + orgIdSet.add(companyIdVal); + } + if (ObjectUtil.isNotNull(deptIdVal)) { + orgIdSet.add(deptIdVal); + } + } - listTwo.forEach(emergencyTaskRespVO -> { - List detailList = emergencyMachineryMapper.selectList(EmergencyMachineryDO::getTaskNo, emergencyTaskRespVO.getTaskNo()); - if(!CollectionUtil.isEmpty(detailList)){ - emergencyTaskRespVO.setEmergencyMachineryList(detailList); + // ===================== 批量查询关联数据(彻底解决 N+1) ===================== + // 机械明细 + List allMachineryList = emergencyMachineryMapper.selectList( + EmergencyMachineryDO::getTaskNo, taskNoList + ); + // 街道信息 + List streetList = getDictDataList("street_belong"); + + // 部门/公司信息 + Map deptMap = deptApi.getDeptMap(orgIdSet); + + // ===================== 内存分组 & 映射(指定Map初始容量,规避key重复) ===================== + // taskNo -> 机械明细列表 + Map> machineryGroupMap = allMachineryList.stream() + .collect(Collectors.groupingBy( + EmergencyMachineryDO::getTaskNo, + () -> new HashMap<>(allMachineryList.size()), + Collectors.toList() + )); + + // streetId -> 街道对象,防key重复 + Map roadStreetMap = streetList.stream() + .collect(Collectors.toMap( + DictDataRespDTO::getValue, + item -> item, + (oldVal, newVal) -> newVal, // 重复key保留后者 + () -> new HashMap<>(streetList.size()) + )); + + // 5. DO 转 VO + 批量赋值关联数据 + List voList = BeanUtils.toBean(doList, EmergencyTaskRespVO.class); + for (EmergencyTaskRespVO vo : voList) { + String taskNo = vo.getTaskNo(); + List detailList = machineryGroupMap.get(taskNo); + if (CollectionUtil.isNotEmpty(detailList)) { + vo.setEmergencyMachineryList(detailList); } - }); - addDataInspectionPlan(listTwo); + DictDataRespDTO roadStreet = roadStreetMap.get(vo.getStreetId()); + if (roadStreet != null) { + vo.setStreetName(roadStreet.getLabel()); + } + DeptRespDTO company = deptMap.get(vo.getCompanyId()); + if(company != null){ + vo.setCompanyName(company.getName()); + } + } - // 转换返回 - return new PageResult<>(listTwo, page.getTotal()); + // 6. 原有业务方法 + //addDataInspectionPlan(voList); + // 转换返回 + return new PageResult<>(voList, page.getTotal()); } @@ -257,4 +333,4 @@ public class EmergencyTaskServiceImpl implements EmergencyTaskService { } -} \ No newline at end of file +} diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/filedownload/ExcelExportAsyncService.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/filedownload/ExcelExportAsyncService.java new file mode 100644 index 0000000..155b188 --- /dev/null +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/filedownload/ExcelExportAsyncService.java @@ -0,0 +1,148 @@ +package com.zteits.urbanops.module.garden.service.filedownload; + +import cn.idev.excel.EasyExcel; +import cn.idev.excel.ExcelWriter; +import cn.idev.excel.write.metadata.WriteSheet; +import com.zteits.urbanops.framework.excel.core.util.ExcelUtils; +import com.zteits.urbanops.module.garden.dal.dataobject.filedownload.FileDownloadDO; +import com.zteits.urbanops.module.garden.dal.mysql.filedownload.FileDownloadMapper; +import com.zteits.urbanops.module.garden.enums.FileTaskStatus; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; + +import java.io.File; +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Slf4j +@Service +public class ExcelExportAsyncService { + + @Value("${file.upload-local.path}") + private String localExcelPath; + + @Value("${file.upload-local.expire-days}") + private Integer expireDay; + + public static final int BATCH_SIZE = 500; + + private final FileDownloadMapper fileDownloadMapper; + + public ExcelExportAsyncService(FileDownloadMapper fileDownloadMapper) { + this.fileDownloadMapper = fileDownloadMapper; + } + + /** + * 异步导出入口 + */ + @Async + public void doAsyncExport( + FileDownloadDO downloadDO, + List sourceList, + String sheetName, + ExcelExportHandler exportHandler + ) { + String busiCode = downloadDO.getBusiCode(); + ExcelWriter excelWriter = null; + File excelFile = null; + + try { + // 更新为处理中 + downloadDO.setStatus(FileTaskStatus.PROCESSING.getCode()); + fileDownloadMapper.updateById(downloadDO); + + String fullPath = buildFilePath(downloadDO); + excelFile = new File(fullPath); + File parentDir = excelFile.getParentFile(); + + if (!parentDir.exists() && !parentDir.mkdirs()) { + throw new IOException("目录创建失败:" + parentDir.getAbsolutePath()); + } + if (!parentDir.canRead() || !parentDir.canWrite()) { + throw new IOException("目录无读写权限:" + parentDir.getAbsolutePath()); + } + + // 初始化Excel + excelWriter = ExcelUtils.initEmergencyTaskExcel( + excelFile, + downloadDO.getFileName(), + sheetName, + exportHandler.getExcelVoClass(), + exportHandler.buildHead(), + exportHandler.buildMergeRule() + ); + downloadDO.setFilePath(fullPath); + fileDownloadMapper.updateById(downloadDO); + log.info("Excel初始化完成,业务编码:{},文件路径:{}", busiCode, fullPath); + + autoPageWrite(excelWriter, downloadDO, sheetName, sourceList, exportHandler); + + ExcelUtils.finishExcel(excelWriter); + excelWriter = null; + + if (excelFile.exists()) { + downloadDO.setFileSize(excelFile.length()); + } + updateSuccess(downloadDO); + log.info("导出任务执行成功,业务编码:{}", busiCode); + + } catch (IOException ioEx) { + log.error("导出IO异常,业务编码:{}", busiCode, ioEx); + updateFail(downloadDO, "文件读写异常:" + ioEx.getMessage()); + } catch (Exception e) { + log.error("导出任务异常,业务编码:{}", busiCode, e); + updateFail(downloadDO, e.getMessage()); + } finally { + ExcelUtils.finishExcel(excelWriter); + } + } + + /** 自动分片写入 */ + private void autoPageWrite(ExcelWriter excelWriter, FileDownloadDO downloadDO, + String sheetName, List sourceList, ExcelExportHandler handler) { + if (CollectionUtils.isEmpty(sourceList)) { + log.info("导出数据为空,业务编码:{}", downloadDO.getBusiCode()); + return; + } + WriteSheet writeSheet + = EasyExcel.writerSheet(sheetName).build(); + int total = sourceList.size(); + int page = 0; + while (page * BATCH_SIZE < total) { + int start = page * BATCH_SIZE; + int end = Math.min(start + BATCH_SIZE, total); + List batchData = new ArrayList<>(sourceList.subList(start, end)); + List voList = handler.convert(batchData, start + 1); + excelWriter.write(voList, writeSheet); + log.info("自动分片写入完成,分片:{}, 条数:{}", page + 1, batchData.size()); + voList.clear(); + page++; + } + } + + private String buildFilePath(FileDownloadDO downloadDO) { + return new File(localExcelPath, downloadDO.getFileName()).getAbsolutePath(); + } + + private void updateSuccess(FileDownloadDO downloadDO) { + downloadDO.setStatus(FileTaskStatus.SUCCESS.getCode()); + LocalDateTime now = LocalDateTime.now(); + downloadDO.setFinishTime(now); + int days = expireDay != null ? expireDay : 30; + downloadDO.setExpireTime(now.plusDays(days)); + fileDownloadMapper.updateById(downloadDO); + } + + private void updateFail(FileDownloadDO downloadDO, String errorMsg) { + downloadDO.setStatus(FileTaskStatus.FAIL.getCode()); + String msg = errorMsg == null ? "未知异常" : errorMsg; + downloadDO.setFailMsg("导出失败:" + msg); + downloadDO.setFinishTime(LocalDateTime.now()); + fileDownloadMapper.updateById(downloadDO); + } +} diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/filedownload/ExcelExportDispatchService.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/filedownload/ExcelExportDispatchService.java new file mode 100644 index 0000000..273b9ab --- /dev/null +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/filedownload/ExcelExportDispatchService.java @@ -0,0 +1,67 @@ +package com.zteits.urbanops.module.garden.service.filedownload; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.zteits.urbanops.module.garden.dal.dataobject.filedownload.FileDownloadDO; +import com.zteits.urbanops.module.garden.dal.mysql.filedownload.FileDownloadMapper; +import com.zteits.urbanops.module.garden.enums.ExportBusiCodeEnum; +import com.zteits.urbanops.module.garden.enums.FileTaskStatus; +import jakarta.annotation.Resource; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; + +import java.util.List; + +/** + * Excel导出统一调度服务 + * 统一创建导出任务、并发控制、触发异步 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class ExcelExportDispatchService { + + private final FileDownloadMapper fileDownloadMapper; + private final ExcelExportAsyncService excelExportAsyncService; + + /** + * 创建导出任务(可指定是否自动分片) + */ + public Long createExportTask(String busiCode, List sourceList, ExcelExportHandler exportHandler) { + ExportBusiCodeEnum busiEnum = ExportBusiCodeEnum.getByCode(busiCode); + if (busiEnum == null) { + throw new IllegalArgumentException("不支持的导出业务编码:" + busiCode); + } + + // 防并发:同业务正在处理中的任务互斥 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper() + .eq(FileDownloadDO::getBusiCode, busiCode) + .eq(FileDownloadDO::getStatus, FileTaskStatus.PROCESSING.getCode()); + List existTasks = fileDownloadMapper.selectList(queryWrapper); + if (!CollectionUtils.isEmpty(existTasks)) { + throw new IllegalArgumentException("当前业务导出任务正在处理中,请稍后再试"); + } + + // 新增任务记录 + FileDownloadDO downloadDO = new FileDownloadDO(); + downloadDO.setBusiCode(busiCode); + downloadDO.setBusiName(busiEnum.getBusiName()); + downloadDO.setFileName(busiEnum.getFullFileName()); + downloadDO.setFileSuffix(busiEnum.getFileSuffix()); + downloadDO.setStatus(FileTaskStatus.PENDING.getCode()); + downloadDO.setStorageType("LOCAL"); + fileDownloadMapper.insert(downloadDO); + + Long taskId = downloadDO.getId(); + + // 调用异步导出 + excelExportAsyncService.doAsyncExport( + downloadDO, + sourceList, + busiEnum.getSheetName(), + exportHandler + ); + return taskId; + } +} diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/filedownload/ExcelExportHandler.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/filedownload/ExcelExportHandler.java new file mode 100644 index 0000000..30b011b --- /dev/null +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/filedownload/ExcelExportHandler.java @@ -0,0 +1,35 @@ +package com.zteits.urbanops.module.garden.service.filedownload; + +import org.apache.poi.ss.util.CellRangeAddress; + +import java.util.List; + +/** + * Excel导出处理器接口 + * 每个导出业务实现此接口,替代原抽象类 + * @param 源数据类型 + * @param Excel VO类型 + */ +public interface ExcelExportHandler { + + /** + * 源数据转换为Excel VO + */ + List convert(List batchList, int startIndex); + + /** + * 获取Excel VO实体Class + */ + Class getExcelVoClass(); + + /** + * 构建表头 + */ + List> buildHead(); + + /** + * 单元格合并规则 + */ + List buildMergeRule(); + +} diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/filedownload/FileDownloadService.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/filedownload/FileDownloadService.java new file mode 100644 index 0000000..df1ba5a --- /dev/null +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/filedownload/FileDownloadService.java @@ -0,0 +1,77 @@ +package com.zteits.urbanops.module.garden.service.filedownload; + +import com.zteits.urbanops.framework.common.pojo.PageResult; +import com.zteits.urbanops.module.garden.controller.admin.filedownload.vo.FileDownloadPageReqVO; +import com.zteits.urbanops.module.garden.controller.admin.filedownload.vo.FileDownloadSaveReqVO; +import com.zteits.urbanops.module.garden.dal.dataobject.filedownload.FileDownloadDO; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; + +import java.util.List; + +/** + * 文件异步下载任务 Service 接口 + * + * @author 超级管理员 + */ +public interface FileDownloadService { + + /** + * 创建文件异步下载任务 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createFileDownload(@Valid FileDownloadSaveReqVO createReqVO); + + /** + * 更新文件异步下载任务 + * + * @param updateReqVO 更新信息 + */ + void updateFileDownload(@Valid FileDownloadSaveReqVO updateReqVO); + + /** + * 删除文件异步下载任务 + * + * @param id 编号 + */ + void deleteFileDownload(Long id); + + /** + * 批量删除文件异步下载任务 + * + * @param ids 编号 + */ + void deleteFileDownloadListByIds(List ids); + + /** + * 获得文件异步下载任务 + * + * @param id 编号 + * @return 文件异步下载任务 + */ + FileDownloadDO getFileDownload(Long id); + + /** + * 获得文件异步下载任务分页 + * + * @param pageReqVO 分页查询 + * @return 文件异步下载任务分页 + */ + PageResult getFileDownloadPage(FileDownloadPageReqVO pageReqVO); + /** + * @Author wangqian + * @Description 文件下载 + * @Date 2026/6/15 10:19 + * @Return void + */ + void writeAttachment(HttpServletResponse response, Long id); + /** + * @Author wangqian + * @Description 清理过期文件 + * @Date 2026/6/15 12:27 + * @Return void + */ + void clearExpiredFiles(); +} diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/filedownload/FileDownloadServiceImpl.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/filedownload/FileDownloadServiceImpl.java new file mode 100644 index 0000000..818adcc --- /dev/null +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/filedownload/FileDownloadServiceImpl.java @@ -0,0 +1,166 @@ +package com.zteits.urbanops.module.garden.service.filedownload; + +import com.zteits.urbanops.framework.common.pojo.PageResult; +import com.zteits.urbanops.framework.common.util.object.BeanUtils; +import com.zteits.urbanops.module.garden.controller.admin.filedownload.vo.FileDownloadPageReqVO; +import com.zteits.urbanops.module.garden.controller.admin.filedownload.vo.FileDownloadSaveReqVO; +import com.zteits.urbanops.module.garden.dal.dataobject.filedownload.FileDownloadDO; +import com.zteits.urbanops.module.garden.dal.mysql.filedownload.FileDownloadMapper; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Objects; + +import static com.zteits.urbanops.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.zteits.urbanops.framework.common.exception.util.ServiceExceptionUtil.exception0; +import static com.zteits.urbanops.module.garden.enums.ErrorCodeConstants.FILE_DOWNLOAD_NOT_EXISTS; + +/** + * 文件异步下载任务 Service 实现类 + * + * @author 超级管理员 + */ +@Service +@Validated +@Slf4j +public class FileDownloadServiceImpl implements FileDownloadService { + + @Resource + private FileDownloadMapper fileDownloadMapper; + + @Override + public Long createFileDownload(FileDownloadSaveReqVO createReqVO) { + // 插入 + FileDownloadDO fileDownload = BeanUtils.toBean(createReqVO, FileDownloadDO.class); + fileDownloadMapper.insert(fileDownload); + + // 返回 + return fileDownload.getId(); + } + + @Override + public void updateFileDownload(FileDownloadSaveReqVO updateReqVO) { + // 校验存在 + validateFileDownloadExists(updateReqVO.getId()); + // 更新 + FileDownloadDO updateObj = BeanUtils.toBean(updateReqVO, FileDownloadDO.class); + fileDownloadMapper.updateById(updateObj); + } + + @Override + public void deleteFileDownload(Long id) { + // 校验存在 + validateFileDownloadExists(id); + // 删除 + fileDownloadMapper.deleteById(id); + } + + @Override + public void deleteFileDownloadListByIds(List ids) { + // 删除 + fileDownloadMapper.deleteByIds(ids); + } + + + private void validateFileDownloadExists(Long id) { + if (fileDownloadMapper.selectById(id) == null) { + throw exception(FILE_DOWNLOAD_NOT_EXISTS); + } + } + + @Override + public FileDownloadDO getFileDownload(Long id) { + return fileDownloadMapper.selectById(id); + } + + @Override + public PageResult getFileDownloadPage(FileDownloadPageReqVO pageReqVO) { + // 当前登录用户不为空,创建人为空,则当前登录用户为创建人 + if (Objects.isNull(pageReqVO.getCreator())) { + throw exception0(400 ,"创建人不能为空"); + } + return fileDownloadMapper.selectPage(pageReqVO); + } + + @Override + public void writeAttachment(HttpServletResponse response, Long id) { + // 1. 查询下载任务记录 + FileDownloadDO fileDownload = fileDownloadMapper.selectById(id); + if (fileDownload == null) { + try { + response.setContentType("text/plain;charset=UTF-8"); + response.getWriter().write("下载任务不存在"); + } catch (IOException e) { + throw exception0( 400, e.getMessage()); + } + return; + } + + // 2. 组装文件对象(替换为你DO中实际的文件路径/文件名字段) + String filePath = fileDownload.getFilePath(); + String fileName = fileDownload.getFileName(); + File file = new File(filePath); + // 3. 校验文件合法性 + if (!file.exists() || !file.isFile()) { + try { + response.setContentType("text/plain;charset=UTF-8"); + response.getWriter().write("文件不存在或已被删除"); + } catch (IOException e) { + throw exception0( 400, e.getMessage()); + } + return; + } + + try { + // 4. 设置下载响应头,解决中文文件名乱码 + response.setContentType("application/octet-stream"); + String encodeName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.name()); + response.setHeader("Content-Disposition", "attachment;filename=" + encodeName); + response.setContentLength((int) file.length()); + + // 5. 流拷贝下载,try-with-resources 自动关闭流,防止资源泄漏 + byte[] buffer = new byte[8 * 1024]; + try (FileInputStream fis = new FileInputStream(file); OutputStream os = response.getOutputStream()) { + + int len; + while ((len = fis.read(buffer)) != -1) { + os.write(buffer, 0, len); + } + os.flush(); + } + } catch (IOException e) { + throw exception0( 400, e.getMessage()); + } + } + + @Override + public void clearExpiredFiles() { + log.info("[clearExpiredFiles][开始清理过期的文件]"); + List fileDownload = fileDownloadMapper.selectList(FileDownloadDO::getStatus, 4); + if (fileDownload.isEmpty()) { + log.info("[clearExpiredFiles][没有过期的文件]"); + return; + } + for (FileDownloadDO fileDownloadDO : fileDownload) { + log.info("[clearExpiredFiles][清理过期文件 id={}] ", fileDownloadDO.getId()); + //删除本地文件 + File file = new File(fileDownloadDO.getFilePath()); + if (file.exists()) { + file.delete(); + //log.info("[clearExpiredFiles][删除本地文件 id={}] ", fileDownloadDO.getId()); + fileDownloadMapper.deleteById(fileDownloadDO.getId()); + } + } + } + +} diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/util/ImgDownUtils.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/util/ImgDownUtils.java new file mode 100644 index 0000000..15bac77 --- /dev/null +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/util/ImgDownUtils.java @@ -0,0 +1,76 @@ +package com.zteits.urbanops.module.garden.util; + +import com.zteits.urbanops.module.infra.framework.file.core.client.FileClient; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.StringUtils; + +import java.util.ArrayList; +import java.util.List; + +/** + * @Classname FileUtils + * @Description 文件处理 + * @Date 2026/6/8 22:43 + * @Created by wangqian + */ +@Slf4j +public class ImgDownUtils { + + /** + * 获取图片 + * @param imgList + * @param index + * @return + */ + public static String getImgSafe(List imgList, int index) { + if (imgList == null || imgList.size() <= index || imgList.get(index) == null) { + return ""; // 空图片返回空字符串,不报错 + } + return imgList.get(index); + } + /** + * 获取图片字节 + * @param list + * @param idx + * @return + */ + public static byte[] getByteSafe(List list, int idx) { + return (list == null || idx >= list.size()) ? null : list.get(idx); + } + /** + * 获取图片真实路径 + * @param fullUrl + * @return + */ + public static String getRealPath(String fullUrl) { + if (fullUrl == null) return null; + int index = fullUrl.indexOf("/quanyu/"); + return index >= 0 ? fullUrl.substring(index + 8) : fullUrl; + } + + /** + * 批量下载图片 + * @param imageUrls + * @param client + * @return + */ + public static List batchDownloadImages(List imageUrls, FileClient client) { + List result = new ArrayList<>(); + if (imageUrls == null) return result; + for (String url : imageUrls) { + if (StringUtils.isEmpty(url)) { + result.add(null); + continue; + } + try { + String realPath = ImgDownUtils.getRealPath(url); + byte[] bytes = client.getContent(realPath); + result.add(bytes); + } catch (Exception e) { + log.error("[图片下载] 异常:{},路径:{}", e.getMessage(), url); + result.add(null); + } + } + return result; + } +} diff --git a/urbanops-server/src/main/java/com/zteits/urbanops/server/UrbanopsServerApplication.java b/urbanops-server/src/main/java/com/zteits/urbanops/server/UrbanopsServerApplication.java index 5b77eab..26a80ea 100644 --- a/urbanops-server/src/main/java/com/zteits/urbanops/server/UrbanopsServerApplication.java +++ b/urbanops-server/src/main/java/com/zteits/urbanops/server/UrbanopsServerApplication.java @@ -2,6 +2,7 @@ package com.zteits.urbanops.server; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableAsync; /** * 项目的启动类 @@ -14,6 +15,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; */ @SuppressWarnings("SpringComponentScan") // 忽略 IDEA 无法识别 ${urbanops.info.base-package} @SpringBootApplication(scanBasePackages = {"${urbanops.info.base-package}.server", "${urbanops.info.base-package}.module"}) +@EnableAsync // 开启异步线程池 public class UrbanopsServerApplication { public static void main(String[] args) { diff --git a/urbanops-server/src/main/resources/application-dev.yaml b/urbanops-server/src/main/resources/application-dev.yaml index cee80be..4e088e8 100644 --- a/urbanops-server/src/main/resources/application-dev.yaml +++ b/urbanops-server/src/main/resources/application-dev.yaml @@ -222,6 +222,9 @@ pf4j: #文件上传默认路径配置 file: path: uploadPath + upload-local: + path: /home/user/upload/ + expire-days: 30 # 文件有效天数 flow: apps: - appid: yl