diff --git a/bonus-modules/bonus-material/src/main/java/com/bonus/material/settlement/controller/SltAgreementInfoController.java b/bonus-modules/bonus-material/src/main/java/com/bonus/material/settlement/controller/SltAgreementInfoController.java index 5bd7595f..fcf6ef36 100644 --- a/bonus-modules/bonus-material/src/main/java/com/bonus/material/settlement/controller/SltAgreementInfoController.java +++ b/bonus-modules/bonus-material/src/main/java/com/bonus/material/settlement/controller/SltAgreementInfoController.java @@ -1,14 +1,12 @@ package com.bonus.material.settlement.controller; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.OutputStream; +import java.io.*; import java.math.BigDecimal; import java.math.RoundingMode; import java.net.URLEncoder; import java.text.SimpleDateFormat; import java.util.*; +import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; import javax.annotation.Resource; import javax.servlet.http.HttpServletResponse; @@ -31,6 +29,7 @@ import com.bonus.material.common.annotation.PreventRepeatSubmit; import com.bonus.material.common.domain.dto.SelectDto; import com.bonus.material.part.domain.PartLeaseInfo; import com.bonus.material.settlement.domain.*; +import com.bonus.material.settlement.domain.dto.ExportProgressManager; import com.bonus.material.settlement.domain.vo.*; import com.bonus.material.settlement.mapper.SltAgreementInfoMapper; import com.bonus.material.settlement.mapper.SltAgreementReduceMapper; @@ -47,6 +46,8 @@ import org.apache.commons.lang3.StringUtils; import org.apache.poi.hssf.usermodel.*; import org.apache.poi.ss.formula.functions.T; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import com.bonus.common.log.annotation.SysLog; import com.bonus.common.security.annotation.RequiresPermissions; @@ -88,6 +89,9 @@ public class SltAgreementInfoController extends BaseController { @Resource private SltAgreementReduceMapper sltAgreementRecudceMapper; + @Autowired + private ExportProgressManager exportProgressManager; + /** * 查询结算信息列表 */ @@ -2023,6 +2027,236 @@ public class SltAgreementInfoController extends BaseController { } +// /** +// * 一键批量导出未结算报表(zip) +// */ +// @ApiOperation(value = "一键批量导出未结算报表") +// @PreventRepeatSubmit +// @SysLog(title = "结算信息", businessType = OperaType.EXPORT, logType = 1,module = "结算管理->一键批量导出未结算报表") +// @PostMapping("/exportUnsettled") +// public void exportUnsettled(HttpServletResponse response, @RequestBody List list) throws Exception { +// // 创建临时文件夹 +// String tempDir = System.getProperty("java.io.tmpdir") + File.separator + UUID.randomUUID(); +// new File(tempDir).mkdirs(); +// +// try { +// for(SltAgreementInfo info : list){ +// if(info.getAgreementId() == null){ +// continue; +// } +// //根据协议id获取结算单位和结算工程 +// SltAgreementInfo agreementInfo = sltAgreementInfoMapper.getUnitAndProjectByAgreementId(info.getAgreementId()); +// // 生成文件名 +// String fileName = ""; +// String unitName = ""; +// String projectName = ""; +// // 清理非法字符(Windows文件名不允许: \ / : * ? " < > |) +// unitName = cleanFileName(agreementInfo.getUnitName()); +// projectName = cleanFileName(agreementInfo.getProjectName()); +// String agreementCode = cleanFileName(agreementInfo.getAgreementCode()); +// +// // 构建安全的文件名(限制总长度不超过150字符) +// String rawFileName = agreementCode + "-" + unitName + "-" + projectName + "_结算单.xls"; +// fileName = rawFileName.length() > 150 ? +// rawFileName.substring(0, 150) + ".xls" : rawFileName; +// +// //租赁费用明细 +// BigDecimal totalCostLease = BigDecimal.valueOf(0.00); +// List leaseList = new ArrayList<>(); +// leaseList = sltAgreementInfoMapper.getLeaseList(info); +// +// for (SltAgreementInfo bean : leaseList) { +// if (null == bean.getLeasePrice()) { +// bean.setLeasePrice(BigDecimal.valueOf(0.00)); +// } +// if (null == bean.getNum()) { +// bean.setNum(BigDecimal.valueOf(0L)); +// } +// if (null == bean.getLeaseDays()) { +// bean.setLeaseDay(0L); +// } +// BigDecimal leasePrice = bean.getLeasePrice(); +// BigDecimal num = bean.getNum(); +// BigDecimal leaseDays = new BigDecimal(bean.getLeaseDays()); +// BigDecimal costs = leasePrice.multiply(num).multiply(leaseDays); +// if(costs!=null){ +// totalCostLease = totalCostLease.add(costs); +// } +// bean.setCosts(costs); +// } +// List lease = Convert.toList(SltLeaseInfo.class, leaseList); +// +// //丢失费用明细 +// BigDecimal totalCostLose = BigDecimal.valueOf(0.00); +// List loseList = new ArrayList<>(); +// +// loseList = sltAgreementInfoMapper.getLoseList(info); +// +// for (SltAgreementInfo bean : loseList) { +// if (null == bean.getBuyPrice()) { +// bean.setBuyPrice(BigDecimal.valueOf(0.00)); +// } +// if (null == bean.getNum()) { +// bean.setNum(BigDecimal.valueOf(0L)); +// } +// BigDecimal buyPrice = bean.getBuyPrice(); +// BigDecimal num = bean.getNum(); +// // 原价 x 数量 +// BigDecimal costs = buyPrice.multiply(num); +// if(costs!=null){ +// totalCostLose = totalCostLose.add(costs); +// } +// //计算租赁费用 +// bean.setCosts(costs); +// } +// List lose = Convert.toList(SltLeaseInfo.class, loseList); +// +// //维修费用明细 +// BigDecimal totalCostRepair = BigDecimal.valueOf(0.00); +// List repairList = new ArrayList<>(); +// +// +// List taskRepairList = taskMapper.getTaskIdList(info); +// List taskRepairList2 = new ArrayList<>(); +// taskRepairList2 = checkTeamAgreementInfo(info); +// if (null != taskRepairList && !taskRepairList.isEmpty()) { +// if (null != taskRepairList2 && !taskRepairList2.isEmpty()) { +// taskRepairList.addAll(taskRepairList2); +// } +// repairList = sltAgreementInfoMapper.getRepairDetailsList(info, taskRepairList); +// } +// +// for (SltAgreementInfo bean : repairList) { +// if (bean.getCosts()!=null && (bean.getPartType().equals("收费"))) { +// totalCostRepair = totalCostRepair.add(bean.getCosts()); +// } +// } +// List repair = Convert.toList(SltLeaseInfo.class, repairList); +// +// //报废费用明细 +// BigDecimal totalCostScrap = BigDecimal.valueOf(0.00); +// List scrapList = new ArrayList<>(); +// +// List taskScrapList = taskMapper.getTaskIdList(info); +// +// List taskScrapList2 = new ArrayList<>(); +// taskScrapList2 = checkTeamAgreementInfo(info); +// +// if (null != taskScrapList && !taskScrapList.isEmpty()) { +// +// if (null != taskScrapList2 && !taskScrapList2.isEmpty()) { +// taskScrapList.addAll(taskScrapList2); +// } +// scrapList = sltAgreementInfoMapper.getScrapDetailsList(info, taskScrapList); +// } +// +// +// for (SltAgreementInfo bean : scrapList) { +// if (bean.getCosts()!=null && (bean.getPartType().equals("收费"))) { +// totalCostScrap = totalCostScrap.add(bean.getCosts()); +// } +// } +// List scrap = Convert.toList(SltLeaseInfo.class, scrapList); +// +// //减免费用明细 +// BigDecimal totalCostReduction = BigDecimal.valueOf(0.00); +// List reductionList = new ArrayList<>(); +// +// if (info.getAgreementId() != null){ +// SltAgreementReduce bean =new SltAgreementReduce(); +// bean.setAgreementId(info.getAgreementId()); +// reductionList = sltAgreementRecudceMapper.getReductionList(bean); +// } +// +// for (SltAgreementReduce reduction : reductionList) { +// if(reduction.getLeaseMoney()!=null){ +// totalCostReduction = totalCostReduction.add(reduction.getLeaseMoney()); +// } +// } +// List reduction = Convert.toList(SltLeaseInfo.class, reductionList); +// +// +// List> resultsLease = new ArrayList<>(); +// List> resultsLose = new ArrayList<>(); +// List> resultsRepair = new ArrayList<>(); +// List> resultsScrap = new ArrayList<>(); +// List> resultsReduction = new ArrayList<>(); +// if (lease!= null) { +// for (SltLeaseInfo bean : lease) { +// Map maps = outReceiveDetailsBeanToMap(bean, 1, 1); +// resultsLease.add(maps); +// } +// } +// if (lose!= null) { +// for (SltLeaseInfo bean : lose) { +// Map maps = outReceiveDetailsBeanToMap(bean, 2, 1); +// resultsLose.add(maps); +// } +// } +// if (repair!= null) { +// for (SltLeaseInfo bean : repair) { +// Map maps = outReceiveDetailsBeanToMap(bean, 3, 1); +// resultsRepair.add(maps); +// } +// } +// if (scrap!= null) { +// for (SltLeaseInfo bean : scrap) { +// Map maps = outReceiveDetailsBeanToMap(bean, 4, 1); +// resultsScrap.add(maps); +// } +// } +// if (reduction!= null) { +// for (SltLeaseInfo bean : reduction) { +// Map maps = outReceiveDetailsBeanToMap(bean, 5, 1); +// resultsReduction.add(maps); +// } +// } +// +// List headersLease = receiveDetailsHeader(1,1); +// List headersLose = receiveDetailsHeader(2,1); +// List headersRepair = receiveDetailsHeader(3,1); +// List headersScrap = receiveDetailsHeader(4,1); +// List headersReduction = receiveDetailsHeader(5,1); +// +//// fileName = agreementInfo.getAgreementCode() + "-" + unitName + "-" + projectName+ "_结算单.xls" ; +// // 导出单个Excel文件 +// String filePath = tempDir + File.separator + fileName; +// try (FileOutputStream fos = new FileOutputStream(filePath)) { +// HSSFWorkbook workbook = PoiOutPage.excelForcheckAll(resultsLease,resultsLose,resultsRepair,resultsScrap,resultsReduction, headersLease,headersLose,headersRepair,headersScrap,headersReduction, "结算明细",projectName,unitName,totalCostLease,totalCostLose,totalCostRepair,totalCostScrap,totalCostReduction); +// workbook.write(fos); +// } +// +// } +// // 创建压缩包 +// String zipFileName = "结算单_" + new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()) + ".zip"; +// response.setContentType("application/zip"); +// response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(zipFileName, "UTF-8")); +// +// try (ZipOutputStream zipOut = new ZipOutputStream(response.getOutputStream())) { +// File[] files = new File(tempDir).listFiles(); +// if (files != null) { +// for (File file : files) { +// zipOut.putNextEntry(new ZipEntry(file.getName())); +// try (FileInputStream fis = new FileInputStream(file)) { +// byte[] buffer = new byte[1024]; +// int len; +// while ((len = fis.read(buffer)) > 0) { +// zipOut.write(buffer, 0, len); +// } +// } +// zipOut.closeEntry(); +// } +// } +// } +// } catch (Exception e) { +// log.error("一键批量导出未结算报表失败", e); +// } finally { +// // 删除临时文件 +// FileUtils.deleteDirectory(new File(tempDir)); +// } +// } + + /************************************************************************/ /** * 一键批量导出未结算报表(zip) */ @@ -2030,12 +2264,119 @@ public class SltAgreementInfoController extends BaseController { @PreventRepeatSubmit @SysLog(title = "结算信息", businessType = OperaType.EXPORT, logType = 1,module = "结算管理->一键批量导出未结算报表") @PostMapping("/exportUnsettled") - public void exportUnsettled(HttpServletResponse response, @RequestBody List list) throws Exception { - // 创建临时文件夹 - String tempDir = System.getProperty("java.io.tmpdir") + File.separator + UUID.randomUUID(); + public ResponseEntity> exportUnsettled(HttpServletResponse response, @RequestBody List list) throws Exception { + +// 生成任务ID + String taskId = UUID.randomUUID().toString(); + + // 初始化进度 + exportProgressManager.initProgress(taskId, list.size()); + + // 异步执行导出任务 + CompletableFuture.runAsync(() -> { + try { + executeExport(taskId, list); + } catch (Exception e) { + log.error("导出任务执行失败, taskId: {}", taskId, e); + exportProgressManager.errorProgress(taskId, "导出失败: " + e.getMessage()); + } + }); + + // 立即返回任务ID + Map result = new HashMap<>(); + result.put("taskId", taskId); + result.put("message", "导出任务已开始,请通过taskId查询进度"); + result.put("total", String.valueOf(list.size())); + return ResponseEntity.ok(result); + } + + + @ApiOperation(value = "查询导出进度") + @GetMapping("/exportProgress/{taskId}") + public ResponseEntity getExportProgress(@PathVariable String taskId) { + ExportProgressManager.ExportProgress progress = exportProgressManager.getProgress(taskId); + if (progress == null) { + return ResponseEntity.notFound().build(); + } + return ResponseEntity.ok(progress); + } + + @ApiOperation(value = "下载导出文件") + @PostMapping("/downloadExport") + public void downloadExportFile(@RequestBody SltAgreementInfo info, HttpServletResponse response) { + try { + ExportProgressManager.ExportProgress progress = exportProgressManager.getProgress(info.getTaskId()); + if (progress == null || !"completed".equals(progress.getStatus())) { + response.setStatus(HttpStatus.NOT_FOUND.value()); + response.getWriter().write("文件不存在或导出未完成"); + return; + } + + File file = new File(progress.getFileUrl()); + if (!file.exists()) { + response.setStatus(HttpStatus.NOT_FOUND.value()); + response.getWriter().write("文件不存在"); + return; + } + + String fileName = "结算单_" + new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()) + ".zip"; + response.setContentType("application/zip"); + response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(fileName, "UTF-8")); + + try (FileInputStream fis = new FileInputStream(file); + OutputStream os = response.getOutputStream()) { + byte[] buffer = new byte[1024]; + int len; + while ((len = fis.read(buffer)) > 0) { + os.write(buffer, 0, len); + } + } + + } catch (Exception e) { + log.error("下载文件失败, taskId: {}", info.getTaskId(), e); + try { + response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); + response.getWriter().write("下载失败: " + e.getMessage()); + } catch (IOException ex) { + log.error("写入响应失败", ex); + } + } + } + + @ApiOperation(value = "清理导出任务") + @DeleteMapping("/cleanExport/{taskId}") + public ResponseEntity> cleanExportTask(@PathVariable String taskId) { + try { + ExportProgressManager.ExportProgress progress = exportProgressManager.getProgress(taskId); + if (progress != null && progress.getFileUrl() != null) { + File file = new File(progress.getFileUrl()); + if (file.exists()) { + file.delete(); + } + } + exportProgressManager.removeProgress(taskId); + + Map result = new HashMap<>(); + result.put("message", "任务清理成功"); + return ResponseEntity.ok(result); + } catch (Exception e) { + log.error("清理任务失败, taskId: {}", taskId, e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } + + + /** + * 执行导出任务的核心方法 + */ + private void executeExport(String taskId, List list) throws Exception { + String tempDir = System.getProperty("java.io.tmpdir") + File.separator + "export_" + taskId; + String zipFilePath = System.getProperty("java.io.tmpdir") + File.separator + "export_" + taskId + ".zip"; + new File(tempDir).mkdirs(); try { + int processed = 0; for(SltAgreementInfo info : list){ if(info.getAgreementId() == null){ continue; @@ -2222,36 +2563,67 @@ public class SltAgreementInfoController extends BaseController { workbook.write(fos); } - } - // 创建压缩包 - String zipFileName = "结算单_" + new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()) + ".zip"; - response.setContentType("application/zip"); - response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(zipFileName, "UTF-8")); + processed++; + exportProgressManager.updateProgress(taskId, 1); - try (ZipOutputStream zipOut = new ZipOutputStream(response.getOutputStream())) { - File[] files = new File(tempDir).listFiles(); - if (files != null) { - for (File file : files) { - zipOut.putNextEntry(new ZipEntry(file.getName())); - try (FileInputStream fis = new FileInputStream(file)) { - byte[] buffer = new byte[1024]; - int len; - while ((len = fis.read(buffer)) > 0) { - zipOut.write(buffer, 0, len); - } - } - zipOut.closeEntry(); - } - } + // 每处理完一个文件,短暂休息以避免资源过度占用 + Thread.sleep(100); } + + // 创建压缩包 + createZipFile(tempDir, zipFilePath); + + // 标记任务完成 + exportProgressManager.completeProgress(taskId, zipFilePath); + } catch (Exception e) { - log.error("一键批量导出未结算报表失败", e); + exportProgressManager.errorProgress(taskId, "导出过程发生错误: " + e.getMessage()); + throw e; } finally { - // 删除临时文件 - FileUtils.deleteDirectory(new File(tempDir)); + // 清理临时文件夹,但保留zip文件 + try { + FileUtils.deleteDirectory(new File(tempDir)); + } catch (IOException e) { + log.warn("清理临时文件夹失败: {}", e.getMessage()); + } } } + + /** + * 创建压缩包 + */ + private void createZipFile(String sourceDir, String zipFilePath) throws IOException { + try (ZipOutputStream zipOut = new ZipOutputStream(new FileOutputStream(zipFilePath))) { + File[] files = new File(sourceDir).listFiles(); + if (files != null) { + for (File file : files) { + zipOut.putNextEntry(new ZipEntry(file.getName())); + try (FileInputStream fis = new FileInputStream(file)) { + byte[] buffer = new byte[1024]; + int len; + while ((len = fis.read(buffer)) > 0) { + zipOut.write(buffer, 0, len); + } + } + zipOut.closeEntry(); + } + } + } + } + + + + + + + + + + + + + /************************************************************************/ // 添加文件名清理工具方法 private String cleanFileName(String name) { if (name == null) { diff --git a/bonus-modules/bonus-material/src/main/java/com/bonus/material/settlement/domain/SltAgreementInfo.java b/bonus-modules/bonus-material/src/main/java/com/bonus/material/settlement/domain/SltAgreementInfo.java index 31be7f5d..dcc097b4 100644 --- a/bonus-modules/bonus-material/src/main/java/com/bonus/material/settlement/domain/SltAgreementInfo.java +++ b/bonus-modules/bonus-material/src/main/java/com/bonus/material/settlement/domain/SltAgreementInfo.java @@ -289,4 +289,6 @@ public class SltAgreementInfo extends BaseEntity { /** 工程是否竣工 */ @ApiModelProperty(value = "工程是否竣工") private Integer isFinish; + + private String taskId; } diff --git a/bonus-modules/bonus-material/src/main/java/com/bonus/material/settlement/domain/dto/AsyncConfig.java b/bonus-modules/bonus-material/src/main/java/com/bonus/material/settlement/domain/dto/AsyncConfig.java new file mode 100644 index 00000000..4f33d048 --- /dev/null +++ b/bonus-modules/bonus-material/src/main/java/com/bonus/material/settlement/domain/dto/AsyncConfig.java @@ -0,0 +1,28 @@ +package com.bonus.material.settlement.domain.dto; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +import java.util.concurrent.Executor; +import java.util.concurrent.ThreadPoolExecutor; + +@Configuration +@EnableAsync +public class AsyncConfig { + + @Bean("exportTaskExecutor") + public Executor exportTaskExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(2); + executor.setMaxPoolSize(5); + executor.setQueueCapacity(10); + executor.setThreadNamePrefix("export-"); + executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); + executor.setWaitForTasksToCompleteOnShutdown(true); + executor.setAwaitTerminationSeconds(60); + executor.initialize(); + return executor; + } +} \ No newline at end of file diff --git a/bonus-modules/bonus-material/src/main/java/com/bonus/material/settlement/domain/dto/ExportProgressManager.java b/bonus-modules/bonus-material/src/main/java/com/bonus/material/settlement/domain/dto/ExportProgressManager.java new file mode 100644 index 00000000..b85bc249 --- /dev/null +++ b/bonus-modules/bonus-material/src/main/java/com/bonus/material/settlement/domain/dto/ExportProgressManager.java @@ -0,0 +1,88 @@ +package com.bonus.material.settlement.domain.dto; + +import org.springframework.stereotype.Component; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@Component +public class ExportProgressManager { + + private final Map progressMap = new ConcurrentHashMap<>(); + + public void initProgress(String taskId, int total) { + ExportProgress progress = new ExportProgress(); + progress.setTotal(total); + progress.setCurrent(0); + progress.setStatus("processing"); + progress.setPercentage(0); + progressMap.put(taskId, progress); + } + + public void updateProgress(String taskId, int increment) { + ExportProgress progress = progressMap.get(taskId); + if (progress != null) { + progress.setCurrent(progress.getCurrent() + increment); + progress.setPercentage(calculatePercentage(progress.getCurrent(), progress.getTotal())); + } + } + + public void completeProgress(String taskId, String fileUrl) { + ExportProgress progress = progressMap.get(taskId); + if (progress != null) { + progress.setStatus("completed"); + progress.setFileUrl(fileUrl); + progress.setPercentage(100); + } + } + + public void errorProgress(String taskId, String errorMsg) { + ExportProgress progress = progressMap.get(taskId); + if (progress != null) { + progress.setStatus("error"); + progress.setErrorMsg(errorMsg); + } + } + + public ExportProgress getProgress(String taskId) { + return progressMap.get(taskId); + } + + public void removeProgress(String taskId) { + progressMap.remove(taskId); + } + + private int calculatePercentage(int current, int total) { + if (total == 0) return 100; + int percentage = (int) ((current * 100.0) / total); + return Math.min(percentage, 100); + } + + public static class ExportProgress { + private int total; + private int current; + private int percentage; + private String status; // processing, completed, error + private String fileUrl; + private String errorMsg; + + // getters and setters + public int getTotal() { return total; } + public void setTotal(int total) { this.total = total; } + + public int getCurrent() { return current; } + public void setCurrent(int current) { this.current = current; } + + public int getPercentage() { return percentage; } + public void setPercentage(int percentage) { this.percentage = percentage; } + + public String getStatus() { return status; } + public void setStatus(String status) { this.status = status; } + + public String getFileUrl() { return fileUrl; } + public void setFileUrl(String fileUrl) { this.fileUrl = fileUrl; } + + public String getErrorMsg() { return errorMsg; } + public void setErrorMsg(String errorMsg) { this.errorMsg = errorMsg; } + } +} \ No newline at end of file