✨ 毛利分析报表导出接入 OSS 存储
This commit is contained in:
+54
-13
@@ -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<PrcProfitReportOrderRow> rows = profitReportService.selectOrderRows(query);
|
||||
AjaxResult file = new ExcelUtil<PrcProfitReportOrderRow>(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<PrcProfitReportProductRow> rows = profitReportService.selectProductRows(query);
|
||||
AjaxResult file = new ExcelUtil<PrcProfitReportProductRow>(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,22 +190,48 @@ public class PrcProfitReportController extends BaseController
|
||||
return export;
|
||||
}
|
||||
|
||||
private AjaxResult finishExport(PrcProfitReportExport export, int rowCount, AjaxResult file)
|
||||
private <T> AjaxResult finishExport(PrcProfitReportExport export, List<T> rows, Class<T> 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<T>(rowType).exportExcelToStream(rows, sheetName, output);
|
||||
}
|
||||
export.setStorageKey(String.valueOf(storageKey));
|
||||
export.setRowCount(rowCount);
|
||||
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);
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void failExport(PrcProfitReportExport export, RuntimeException e)
|
||||
{
|
||||
|
||||
@@ -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.<PrcProfitReportExport>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<Path> 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<Path> 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;
|
||||
}
|
||||
}
|
||||
@@ -768,11 +768,22 @@ public class ExcelUtil<T>
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 对list数据源将其里面的数据导入到excel表单
|
||||
*
|
||||
* @return 结果
|
||||
*/
|
||||
/** 写入调用方提供的流,不生成公共下载目录文件;异常交给调用方处理。 */
|
||||
public void exportExcelToStream(List<T> 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;
|
||||
|
||||
@@ -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/<uuid>.xlsx`,本地和测试为
|
||||
`bms/test/report/profit/yyyy/MM/dd/<uuid>.xlsx`。
|
||||
导出保持同步:临时 Excel 生成并上传成功后更新导出日志,再由前端发起下载。
|
||||
下载继续校验当前用户及原接口权限;历史本地文件名仍从原 download 目录读取。
|
||||
上传失败标记导出失败并清理临时文件,状态更新返回 0 时删除此次上传的 OSS 对象。
|
||||
|
||||
Reference in New Issue
Block a user