From eb67ccc3cb7cc4410c3d0e36dabffc7e72016cbd Mon Sep 17 00:00:00 2001 From: caojia <1301869380@qq.com> Date: Fri, 16 Oct 2026 17:09:34 +0800 Subject: [PATCH] =?UTF-8?q?:sparkles:=20=E6=AF=9B=E5=88=A9=E5=88=86?= =?UTF-8?q?=E6=9E=90=E6=8A=A5=E8=A1=A8=E5=AF=BC=E5=87=BA=E6=8E=A5=E5=85=A5?= =?UTF-8?q?=20OSS=20=E5=AD=98=E5=82=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../report/PrcProfitReportController.java | 75 +++++++--- .../com/bms/report/ProfitReportOssTest.java | 135 ++++++++++++++++++ .../com/bms/common/utils/poi/ExcelUtil.java | 21 ++- bms-back/docs/oss-bill-storage.md | 9 ++ 4 files changed, 218 insertions(+), 22 deletions(-) create mode 100644 bms-back/bms-admin/src/test/java/com/bms/report/ProfitReportOssTest.java diff --git a/bms-back/bms-admin/src/main/java/com/bms/web/controller/report/PrcProfitReportController.java b/bms-back/bms-admin/src/main/java/com/bms/web/controller/report/PrcProfitReportController.java index 73e2512e..ef20114f 100644 --- a/bms-back/bms-admin/src/main/java/com/bms/web/controller/report/PrcProfitReportController.java +++ b/bms-back/bms-admin/src/main/java/com/bms/web/controller/report/PrcProfitReportController.java @@ -1,5 +1,7 @@ package com.bms.web.controller.report; +import java.io.IOException; +import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -23,6 +25,9 @@ import com.bms.common.core.page.TableDataInfo; import com.bms.common.enums.BusinessType; import com.bms.common.exception.ServiceException; import com.bms.common.utils.file.FileUtils; +import com.bms.common.utils.file.OssStorage; +import com.bms.common.utils.DateUtils; +import com.bms.common.utils.uuid.IdUtils; import com.bms.common.utils.poi.ExcelUtil; // 提成功能暂停使用:import com.bms.price.domain.PrcProfitCommissionRate; import com.bms.price.domain.PrcProfitReportExport; @@ -40,6 +45,9 @@ public class PrcProfitReportController extends BaseController @Autowired private IPrcProfitReportService profitReportService; + @Autowired + private OssStorage ossStorage; + @PreAuthorize("@ss.hasPermi('report:profit:list')") @GetMapping("/summary") public AjaxResult summary(PrcProfitReportQuery query) @@ -100,9 +108,7 @@ public class PrcProfitReportController extends BaseController try { List rows = profitReportService.selectOrderRows(query); - AjaxResult file = new ExcelUtil(PrcProfitReportOrderRow.class) - .exportExcel(rows, "订单毛利报表"); - return finishExport(export, rows.size(), file); + return finishExport(export, rows, PrcProfitReportOrderRow.class, "订单毛利报表"); } catch (RuntimeException e) { @@ -120,9 +126,7 @@ public class PrcProfitReportController extends BaseController try { List rows = profitReportService.selectProductRows(query); - AjaxResult file = new ExcelUtil(PrcProfitReportProductRow.class) - .exportExcel(rows, "产品毛利报表"); - return finishExport(export, rows.size(), file); + return finishExport(export, rows, PrcProfitReportProductRow.class, "产品毛利报表"); } catch (RuntimeException e) { @@ -145,11 +149,22 @@ public class PrcProfitReportController extends BaseController { PrcProfitReportExport export = profitReportService.selectExport(exportId, getUsername()); if (export == null || !"SUCCESS".equals(export.getExportStatus()) - || export.getStorageKey() == null || !FileUtils.checkAllowDownload(export.getStorageKey())) + || export.getStorageKey() == null || export.getStorageKey().isBlank()) { throw new ServiceException("导出文件不存在或尚未生成成功"); } + if (export.getStorageKey().startsWith("oss://")) + { + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + FileUtils.setAttachmentResponseHeader(response, export.getFileName()); + ossStorage.download(export.getStorageKey(), response.getOutputStream()); + return; + } + if (!FileUtils.checkAllowDownload(export.getStorageKey())) + { + throw new ServiceException("导出文件路径非法"); + } Path downloadRoot = Paths.get(BmsConfig.getDownloadPath()).toAbsolutePath().normalize(); Path filePath = downloadRoot.resolve(export.getStorageKey()).normalize(); if (!filePath.startsWith(downloadRoot) || !Files.isRegularFile(filePath)) @@ -175,21 +190,47 @@ public class PrcProfitReportController extends BaseController return export; } - private AjaxResult finishExport(PrcProfitReportExport export, int rowCount, AjaxResult file) + private AjaxResult finishExport(PrcProfitReportExport export, List rows, Class rowType, String sheetName) { - Object storageKey = file.get("msg"); - if (storageKey == null || String.valueOf(storageKey).isBlank()) + Path temp = null; + try { - throw new ServiceException("导出文件生成失败"); + temp = Files.createTempFile("profit-report-", ".xlsx"); + try (OutputStream output = Files.newOutputStream(temp)) + { + new ExcelUtil(rowType).exportExcelToStream(rows, sheetName, output); + } + String objectKey = "report/profit/" + DateUtils.datePath() + "/" + IdUtils.fastSimpleUUID() + ".xlsx"; + export.setStorageKey(ossStorage.upload(temp, objectKey, + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")); + export.setRowCount(rows.size()); + export.setExportStatus("SUCCESS"); + if (profitReportService.completeExport(export) != 1) + { + ossStorage.delete(export.getStorageKey()); + throw new ServiceException("保存导出日志失败"); + } + return success(export); } - export.setStorageKey(String.valueOf(storageKey)); - export.setRowCount(rowCount); - export.setExportStatus("SUCCESS"); - if (profitReportService.completeExport(export) != 1) + catch (IOException e) { - throw new ServiceException("保存导出日志失败"); + logger.error("毛利报表生成或上传失败,exportId={}", export.getExportId(), e); + throw new ServiceException("毛利报表生成或上传失败,请重新导出"); + } + finally + { + if (temp != null) + { + try + { + Files.deleteIfExists(temp); + } + catch (IOException e) + { + logger.warn("毛利报表临时文件清理失败,exportId={}", export.getExportId(), e); + } + } } - return success(export); } private void failExport(PrcProfitReportExport export, RuntimeException e) diff --git a/bms-back/bms-admin/src/test/java/com/bms/report/ProfitReportOssTest.java b/bms-back/bms-admin/src/test/java/com/bms/report/ProfitReportOssTest.java new file mode 100644 index 00000000..742377a0 --- /dev/null +++ b/bms-back/bms-admin/src/test/java/com/bms/report/ProfitReportOssTest.java @@ -0,0 +1,135 @@ +package com.bms.report; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.concurrent.atomic.AtomicReference; + +import com.bms.common.config.BmsConfig; +import com.bms.common.core.domain.AjaxResult; +import com.bms.common.exception.ServiceException; +import com.bms.common.utils.file.OssStorage; +import com.bms.price.domain.PrcProfitReportExport; +import com.bms.price.domain.vo.PrcProfitReportQuery; +import com.bms.price.service.IPrcProfitReportService; +import com.bms.web.controller.report.PrcProfitReportController; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +class ProfitReportOssTest { + @TempDir + Path tempDir; + + private final IPrcProfitReportService service = mock(IPrcProfitReportService.class); + private final OssStorage storage = mock(OssStorage.class); + + private PrcProfitReportController controller() { + PrcProfitReportController controller = new PrcProfitReportController() { + @Override + public String getUsername() { return "tester"; } + }; + ReflectionTestUtils.setField(controller, "profitReportService", service); + ReflectionTestUtils.setField(controller, "ossStorage", storage); + when(service.createExport(any())).thenAnswer(invocation -> { + invocation.getArgument(0).setExportId(1L); + return 1; + }); + when(service.selectOrderRows(any())).thenReturn(Collections.emptyList()); + when(service.selectProductRows(any())).thenReturn(Collections.emptyList()); + return controller; + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void bothDimensionsUploadValidExcelAndCleanTempFile(boolean product) throws Exception { + PrcProfitReportController controller = controller(); + AtomicReference generated = new AtomicReference<>(); + when(storage.upload(any(), startsWith("report/profit/"), anyString())).thenAnswer(invocation -> { + Path file = invocation.getArgument(0); + generated.set(file); + try (XSSFWorkbook workbook = new XSSFWorkbook(file.toFile())) { + assertTrue(workbook.getSheetAt(0).getRow(0).getPhysicalNumberOfCells() > 0); + } + verify(service, never()).completeExport(any()); + return "oss://lionqueen/bms/test/report/profit/test.xlsx"; + }); + when(service.completeExport(any())).thenReturn(1); + AjaxResult result = product ? controller.exportProducts(new PrcProfitReportQuery()) + : controller.exportOrders(new PrcProfitReportQuery()); + PrcProfitReportExport export = (PrcProfitReportExport) result.get("data"); + assertEquals("SUCCESS", export.getExportStatus()); + assertTrue(export.getStorageKey().startsWith("oss://")); + assertFalse(Files.exists(generated.get())); + verify(service).completeExport(export); + verify(service, never()).failExport(anyLong(), anyString()); + } + + @Test + void uploadFailureMarksFailedAndCleansTemp() throws Exception { + PrcProfitReportController controller = controller(); + AtomicReference generated = new AtomicReference<>(); + when(storage.upload(any(), anyString(), anyString())).thenAnswer(invocation -> { + generated.set(invocation.getArgument(0)); + throw new IOException("upload unavailable"); + }); + assertThrows(ServiceException.class, () -> controller.exportProducts(new PrcProfitReportQuery())); + assertFalse(Files.exists(generated.get())); + verify(service, never()).completeExport(any()); + verify(service).failExport(eq(1L), anyString()); + } + + @Test + void unsuccessfulStatusUpdateDeletesUploadedObject() throws Exception { + PrcProfitReportController controller = controller(); + when(storage.upload(any(), anyString(), anyString())).thenReturn("oss://lionqueen/bms/test/report/profit/test.xlsx"); + assertThrows(ServiceException.class, () -> controller.exportOrders(new PrcProfitReportQuery())); + verify(storage).delete("oss://lionqueen/bms/test/report/profit/test.xlsx"); + verify(service).failExport(eq(1L), anyString()); + } + + @Test + void ossDownloadUsesCurrentUserAndStorage() throws Exception { + PrcProfitReportController controller = controller(); + when(service.selectExport(1L, "tester")).thenReturn(export("oss://lionqueen/bms/test/report/profit/test.xlsx")); + MockHttpServletResponse response = new MockHttpServletResponse(); + controller.downloadExport(1L, response); + verify(storage).download(eq("oss://lionqueen/bms/test/report/profit/test.xlsx"), same(response.getOutputStream())); + } + + @Test + void historicalLocalFileStillDownloads() throws Exception { + String previousProfile = BmsConfig.getProfile(); + try { + new BmsConfig().setProfile(tempDir.toString()); + Path file = tempDir.resolve("download/old.xlsx"); + Files.createDirectories(file.getParent()); + Files.write(file, new byte[] {1, 2, 3}); + PrcProfitReportController controller = controller(); + when(service.selectExport(1L, "tester")).thenReturn(export("old.xlsx")); + MockHttpServletResponse response = new MockHttpServletResponse(); + controller.downloadExport(1L, response); + assertArrayEquals(new byte[] {1, 2, 3}, response.getContentAsByteArray()); + verifyNoInteractions(storage); + } finally { + new BmsConfig().setProfile(previousProfile); + } + } + + private PrcProfitReportExport export(String key) { + PrcProfitReportExport export = new PrcProfitReportExport(); + export.setStorageKey(key); + export.setExportStatus("SUCCESS"); + export.setFileName("profit.xlsx"); + return export; + } +} diff --git a/bms-back/bms-common/src/main/java/com/bms/common/utils/poi/ExcelUtil.java b/bms-back/bms-common/src/main/java/com/bms/common/utils/poi/ExcelUtil.java index eb903630..5f711ff2 100644 --- a/bms-back/bms-common/src/main/java/com/bms/common/utils/poi/ExcelUtil.java +++ b/bms-back/bms-common/src/main/java/com/bms/common/utils/poi/ExcelUtil.java @@ -768,11 +768,22 @@ public class ExcelUtil } } - /** - * 对list数据源将其里面的数据导入到excel表单 - * - * @return 结果 - */ + /** 写入调用方提供的流,不生成公共下载目录文件;异常交给调用方处理。 */ + public void exportExcelToStream(List list, String sheetName, OutputStream output) throws IOException + { + try + { + this.init(list, sheetName, StringUtils.EMPTY, Type.EXPORT); + writeSheet(); + wb.write(output); + } + finally + { + IOUtils.closeQuietly(wb); + } + } + + /** 对 list 数据源生成本地 Excel 文件,返回文件名。 */ public AjaxResult exportExcel() { OutputStream out = null; diff --git a/bms-back/docs/oss-bill-storage.md b/bms-back/docs/oss-bill-storage.md index 07192284..cf9f7732 100644 --- a/bms-back/docs/oss-bill-storage.md +++ b/bms-back/docs/oss-bill-storage.md @@ -46,3 +46,12 @@ oss: - 普通附件、头像、导入文件及导出模板维持现有存储方式。 SDK 自定义域名与客户端配置参考:[阿里云 OSS Java SDK 文档](https://www.alibabacloud.com/help/en/oss/developer-reference/oss-java-sdk/)。 + +## 毛利分析报表 + +产品、订单两个维度的导出也使用上述 OSS 配置。生产文件路径为 +`bms/prod/report/profit/yyyy/MM/dd/.xlsx`,本地和测试为 +`bms/test/report/profit/yyyy/MM/dd/.xlsx`。 +导出保持同步:临时 Excel 生成并上传成功后更新导出日志,再由前端发起下载。 +下载继续校验当前用户及原接口权限;历史本地文件名仍从原 download 目录读取。 +上传失败标记导出失败并清理临时文件,状态更新返回 0 时删除此次上传的 OSS 对象。