From 47723c7c0fa4685ebed27c5e00d3df732b50abb7 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Tue, 1 Sep 2026 21:03:17 +0800 Subject: [PATCH] feat(file): register domestic order exports --- config/nacos/cn/file-service.yml | 10 ++ config/nacos/local/file-service.yml | 10 ++ .../file/service/ExportSchemaContracts.java | 45 +++++++- .../file/service/ExportTaskService.java | 16 ++- file/src/main/resources/application.yml | 9 ++ .../openapi/domestic-order-export-v1.yaml | 109 ++++++++++++++++++ ...esticWarehouseOrderExportContractTest.java | 60 ++++++++++ .../file/service/ExportTaskServiceTest.java | 59 ++++++++++ .../service/GenericXlsxExportRunnerTest.java | 52 +++++++++ 9 files changed, 361 insertions(+), 9 deletions(-) create mode 100644 file/src/main/resources/openapi/domestic-order-export-v1.yaml create mode 100644 file/src/test/java/com/baiyue/file/config/DomesticWarehouseOrderExportContractTest.java diff --git a/config/nacos/cn/file-service.yml b/config/nacos/cn/file-service.yml index 40c954665..1f230df04 100644 --- a/config/nacos/cn/file-service.yml +++ b/config/nacos/cn/file-service.yml @@ -22,6 +22,16 @@ baiyue: max-attempts: 3 batch-size: 500 + DOMESTIC_WAREHOUSE_ORDER_LIST: + provider-service: order-service + provider-path: /internal/exports/v1/DOMESTIC_WAREHOUSE_ORDER_LIST/batches + required-permission: platform:order:export + app-scope: PLATFORM + enabled: true + max-rows: 200000 + max-attempts: 3 + batch-size: 500 + LOADING_LIST_BUY_DECLARATION_ITEMS: provider-service: firstleg-service provider-path: /internal/exports/v1/LOADING_LIST_BUY_DECLARATION_ITEMS/batches diff --git a/config/nacos/local/file-service.yml b/config/nacos/local/file-service.yml index c913b54f2..1ecfc0ed3 100644 --- a/config/nacos/local/file-service.yml +++ b/config/nacos/local/file-service.yml @@ -18,6 +18,16 @@ baiyue: max-attempts: 3 batch-size: 500 + DOMESTIC_WAREHOUSE_ORDER_LIST: + provider-service: order-service + provider-path: /internal/exports/v1/DOMESTIC_WAREHOUSE_ORDER_LIST/batches + required-permission: platform:order:export + app-scope: PLATFORM + enabled: true + max-rows: 200000 + max-attempts: 3 + batch-size: 500 + LOADING_LIST_BUY_DECLARATION_ITEMS: provider-service: firstleg-service provider-path: /internal/exports/v1/LOADING_LIST_BUY_DECLARATION_ITEMS/batches diff --git a/file/src/main/java/com/baiyue/file/service/ExportSchemaContracts.java b/file/src/main/java/com/baiyue/file/service/ExportSchemaContracts.java index 9b0dded1a..d3e76f9ea 100644 --- a/file/src/main/java/com/baiyue/file/service/ExportSchemaContracts.java +++ b/file/src/main/java/com/baiyue/file/service/ExportSchemaContracts.java @@ -7,6 +7,7 @@ import java.util.List; final class ExportSchemaContracts { static final String BUY_DECLARATION_ITEMS = "LOADING_LIST_BUY_DECLARATION_ITEMS"; + static final String DOMESTIC_WAREHOUSE_ORDER_LIST = "DOMESTIC_WAREHOUSE_ORDER_LIST"; static final List BUY_DECLARATION_COLUMNS = List.of( column("customerName", "客户名称", ExportColumnType.STRING), column("orderNo", "订单号", ExportColumnType.STRING), @@ -21,20 +22,54 @@ final class ExportSchemaContracts { column("volume", "体积", ExportColumnType.DECIMAL), column("mark", "唛头", ExportColumnType.STRING), column("customerRemark", "客户备注", ExportColumnType.STRING)); + static final List DOMESTIC_WAREHOUSE_ORDER_COLUMNS = List.of( + column("inboundCode", "入仓码", ExportColumnType.STRING), + column("orderNo", "订单号", ExportColumnType.STRING), + column("mainStatus", "订单主状态", ExportColumnType.STRING), + column("customerName", "客户", ExportColumnType.STRING), + column("channelName", "渠道", ExportColumnType.STRING), + column("channelChanged", "曾变更渠道", ExportColumnType.BOOLEAN), + column("receivingWarehouseName", "定位仓库", ExportColumnType.STRING), + column("destinationWarehouseName", "交货仓", ExportColumnType.STRING), + column("expectedShipDate", "预计出运日期", ExportColumnType.DATE), + column("expectedArrivalAt", "预计到货时间", ExportColumnType.DATETIME), + column("expectedVolumeCbm", "预计体积(m³)", ExportColumnType.DECIMAL), + column("totalPackageCount", "箱数", ExportColumnType.INTEGER), + column("totalQuantity", "产品数量", ExportColumnType.INTEGER), + column("actualGrossWeightKg", "实测毛重(kg)", ExportColumnType.DECIMAL), + column("actualVolumeCbm", "实测体积(m³)", ExportColumnType.DECIMAL), + column("chargeableWeightKg", "计费重量(kg)", ExportColumnType.DECIMAL), + column("chargeableVolumeCbm", "计费体积(m³)", ExportColumnType.DECIMAL), + column("feeReviewStatus", "费用审核状态", ExportColumnType.STRING), + column("skuNos", "SKU", ExportColumnType.STRING), + column("taxMode", "报关方式", ExportColumnType.STRING), + column("containerMode", "装柜方式", ExportColumnType.STRING), + column("customsInspection", "是否商检", ExportColumnType.BOOLEAN), + column("containerNo", "集装箱号", ExportColumnType.STRING), + column("cabinetNo", "柜次号", ExportColumnType.STRING), + column("vesselName", "船名", ExportColumnType.STRING), + column("voyageNo", "航次", ExportColumnType.STRING), + column("createdAt", "创建时间", ExportColumnType.DATETIME)); private ExportSchemaContracts() { } static void validate(String exportType, List columns) { - if (!BUY_DECLARATION_ITEMS.equals(exportType)) return; - if (columns.size() != BUY_DECLARATION_COLUMNS.size()) { - throw ExportFailure.permanent("买单报关导出列定义不匹配"); + if (BUY_DECLARATION_ITEMS.equals(exportType)) { + validate(columns, BUY_DECLARATION_COLUMNS, "买单报关导出列定义不匹配"); + } else if (DOMESTIC_WAREHOUSE_ORDER_LIST.equals(exportType)) { + validate(columns, DOMESTIC_WAREHOUSE_ORDER_COLUMNS, "国内仓订单导出列定义不匹配"); } + } + + private static void validate(List columns, List expectedColumns, + String failureMessage) { + if (columns.size() != expectedColumns.size()) throw ExportFailure.permanent(failureMessage); for (int index = 0; index < columns.size(); index++) { ExportColumn actual = columns.get(index); - ExpectedColumn expected = BUY_DECLARATION_COLUMNS.get(index); + ExpectedColumn expected = expectedColumns.get(index); if (!expected.key.equals(actual.key()) || !expected.title.equals(actual.title()) || expected.type != actual.type()) { - throw ExportFailure.permanent("买单报关导出列定义不匹配"); + throw ExportFailure.permanent(failureMessage); } } } diff --git a/file/src/main/java/com/baiyue/file/service/ExportTaskService.java b/file/src/main/java/com/baiyue/file/service/ExportTaskService.java index 59fc84765..22071a29a 100644 --- a/file/src/main/java/com/baiyue/file/service/ExportTaskService.java +++ b/file/src/main/java/com/baiyue/file/service/ExportTaskService.java @@ -44,7 +44,7 @@ public class ExportTaskService { if (!user.permissions().contains(permission)) { throw new BizException(ResultCode.FORBIDDEN, "缺少该导出类型的操作权限"); } - validateParameters(request.parameters()); + validateParameters(type, request.parameters()); validateTypeParameters(type, request.parameters()); String parametersJson = parametersJson(request.parameters()); long id = IdWorker.getId(); @@ -140,15 +140,23 @@ public class ExportTaskService { catch (JsonProcessingException exception) { throw new BizException(ResultCode.BAD_REQUEST, "导出筛选条件无法序列化"); } } - private void validateParameters(Map parameters) { + private void validateParameters(String type, Map parameters) { Map values = parameters == null ? Map.of() : parameters; - if (values.size() > 20) throw new BizException(ResultCode.VALIDATION_FAILED, "导出筛选条件最多 20 项"); + int maxItems = ExportSchemaContracts.DOMESTIC_WAREHOUSE_ORDER_LIST.equals(type) ? 32 : 20; + if (values.size() > maxItems) { + throw new BizException(ResultCode.VALIDATION_FAILED, "导出筛选条件最多 " + maxItems + " 项"); + } values.forEach((key, value) -> { if (key == null || key.isBlank() || key.length() > 64) throw new BizException(ResultCode.VALIDATION_FAILED, "导出筛选键无效"); validateParameterValue(value, 1); }); try { - if (objectMapper.writeValueAsBytes(values).length > 4096) throw new BizException(ResultCode.VALIDATION_FAILED, "导出筛选条件不能超过 4KB"); + int maxBytes = ExportSchemaContracts.DOMESTIC_WAREHOUSE_ORDER_LIST.equals(type) + ? 512 * 1024 : 4096; + if (objectMapper.writeValueAsBytes(values).length > maxBytes) { + throw new BizException(ResultCode.VALIDATION_FAILED, + "导出筛选条件不能超过 " + (maxBytes / 1024) + "KB"); + } } catch (JsonProcessingException exception) { throw new BizException(ResultCode.BAD_REQUEST, "导出筛选条件无法序列化"); } diff --git a/file/src/main/resources/application.yml b/file/src/main/resources/application.yml index c609212f6..d142be68e 100644 --- a/file/src/main/resources/application.yml +++ b/file/src/main/resources/application.yml @@ -101,6 +101,15 @@ baiyue: max-rows: 200000 max-attempts: 3 batch-size: 500 + DOMESTIC_WAREHOUSE_ORDER_LIST: + provider-service: order-service + provider-path: /internal/exports/v1/DOMESTIC_WAREHOUSE_ORDER_LIST/batches + required-permission: platform:order:export + app-scope: PLATFORM + enabled: true + max-rows: 200000 + max-attempts: 3 + batch-size: 500 LOADING_LIST_BUY_DECLARATION_ITEMS: provider-service: firstleg-service provider-path: /internal/exports/v1/LOADING_LIST_BUY_DECLARATION_ITEMS/batches diff --git a/file/src/main/resources/openapi/domestic-order-export-v1.yaml b/file/src/main/resources/openapi/domestic-order-export-v1.yaml new file mode 100644 index 000000000..b29e1b41b --- /dev/null +++ b/file/src/main/resources/openapi/domestic-order-export-v1.yaml @@ -0,0 +1,109 @@ +openapi: 3.0.3 +info: + title: File Domestic Warehouse Order Export Contract + version: 1.0.0 + description: >- + File 只持有异步任务、XLSX 文件、任务归属和下载权限。订单筛选、数据权限、分页游标及行数据由 + Order 的 DOMESTIC_WAREHOUSE_ORDER_LIST Provider 持有,File 不复制订单业务查询。 +paths: + /api/platform/v1/files/export-tasks: + post: + summary: 创建国内仓订单 XLSX 导出任务 + description: >- + exportType 固定 DOMESTIC_WAREHOUSE_ORDER_LIST,format 固定 XLSX,调用人必须具有 + platform:order:export。selectedOrderNos 非空时导出勾选业务订单号;为空或省略时由 Order + 按 parameters 中当前列表筛选导出全部匹配记录,parameters 不接受 page、size 或内部数据库 ID。 + requestBody: + required: true + content: + application/json: + schema: {$ref: '#/components/schemas/CreateDomesticOrderExportTask'} + responses: + '200': {description: 返回异步任务,初始状态 PENDING} + '403': {description: 缺少 platform:order:export} + /api/platform/v1/files/export-tasks/{taskId}/download: + get: + summary: 下载本人已完成的订单导出文件 + parameters: + - {name: taskId, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}} + responses: + '200': + description: XLSX 文件;他人任务仅 platform:download:all 可下载 + headers: + Cache-Control: {schema: {type: string, example: no-store}} + Content-Disposition: {schema: {type: string, example: attachment}} + content: + application/vnd.openxmlformats-officedocument.spreadsheetml.sheet: + schema: {type: string, format: binary} +components: + schemas: + CreateDomesticOrderExportTask: + type: object + required: [exportType, format, taskName, parameters] + properties: + exportType: {type: string, enum: [DOMESTIC_WAREHOUSE_ORDER_LIST]} + format: {type: string, enum: [XLSX]} + taskName: {type: string, minLength: 1, maxLength: 128} + parameters: {$ref: '#/components/schemas/DomesticOrderExportParameters'} + DomesticOrderExportParameters: + type: object + description: >- + 透传给 Order Provider;selectedOrderNos 非空时优先,其余字段与平台订单列表当前筛选一致。 + properties: + selectedOrderNos: + type: array + maxItems: 5000 + uniqueItems: true + items: {type: string, minLength: 1, maxLength: 64, description: 订单业务编号} + tenantId: {type: string, pattern: '^[1-9][0-9]*$'} + keyword: {type: string, maxLength: 64} + orderNo: {type: string, maxLength: 64} + mainStatus: {type: string} + customerId: {type: string, pattern: '^[1-9][0-9]*$'} + receivingWarehouseId: {type: string, pattern: '^[1-9][0-9]*$'} + destinationWarehouseId: {type: string, pattern: '^[1-9][0-9]*$'} + inboundCode: {type: string, maxLength: 64} + internalOrderStatus: {type: string, maxLength: 32} + expectedArrivalDate: {type: string, format: date} + feeReviewStatus: {type: string} + channelName: {type: string, maxLength: 128} + containerNo: {type: string, maxLength: 64} + cabinetNo: {type: string, maxLength: 64} + sailingDate: {type: string, format: date} + loaded: {type: boolean} + fullContainer: {type: boolean} + measurementReviewStatus: {type: string} + fbaCode: {type: string, maxLength: 64} + createdFrom: {type: string, format: date-time} + createdTo: {type: string, format: date-time} + DomesticOrderExportColumns: + type: object + description: Provider schemaVersion=1 固定列顺序;不包含任何内部数据库 ID。 + properties: + inboundCode: {type: string, title: 入仓码} + orderNo: {type: string, title: 订单号} + mainStatus: {type: string, title: 订单主状态} + customerName: {type: string, title: 客户} + channelName: {type: string, title: 渠道} + channelChanged: {type: boolean, title: 曾变更渠道} + receivingWarehouseName: {type: string, title: 定位仓库} + destinationWarehouseName: {type: string, title: 交货仓} + expectedShipDate: {type: string, format: date, title: 预计出运日期} + expectedArrivalAt: {type: string, format: date-time, title: 预计到货时间} + expectedVolumeCbm: {type: number, title: 预计体积(m³)} + totalPackageCount: {type: integer, title: 箱数} + totalQuantity: {type: integer, title: 产品数量} + actualGrossWeightKg: {type: number, title: 实测毛重(kg)} + actualVolumeCbm: {type: number, title: 实测体积(m³)} + chargeableWeightKg: {type: number, title: 计费重量(kg)} + chargeableVolumeCbm: {type: number, title: 计费体积(m³)} + feeReviewStatus: {type: string, title: 费用审核状态} + skuNos: {type: string, title: SKU} + taxMode: {type: string, title: 报关方式} + containerMode: {type: string, title: 装柜方式} + customsInspection: {type: boolean, title: 是否商检} + containerNo: {type: string, title: 集装箱号} + cabinetNo: {type: string, title: 柜次号} + vesselName: {type: string, title: 船名} + voyageNo: {type: string, title: 航次} + createdAt: {type: string, format: date-time, title: 创建时间} diff --git a/file/src/test/java/com/baiyue/file/config/DomesticWarehouseOrderExportContractTest.java b/file/src/test/java/com/baiyue/file/config/DomesticWarehouseOrderExportContractTest.java new file mode 100644 index 000000000..38fb236eb --- /dev/null +++ b/file/src/test/java/com/baiyue/file/config/DomesticWarehouseOrderExportContractTest.java @@ -0,0 +1,60 @@ +package com.baiyue.file.config; + +import org.junit.jupiter.api.Test; +import org.yaml.snakeyaml.Yaml; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +class DomesticWarehouseOrderExportContractTest { + private static final String TYPE = "DOMESTIC_WAREHOUSE_ORDER_LIST"; + + @Test void applicationAndNacosRegisterTheSameOrderProvider() throws Exception { + for (String file : List.of("file/src/main/resources/application.yml", + "config/nacos/local/file-service.yml", "config/nacos/cn/file-service.yml")) { + String source = Files.readString(repositoryRoot().resolve(file)); + assertThat(source).contains(TYPE + ":") + .contains("provider-service: order-service") + .contains("provider-path: /internal/exports/v1/" + TYPE + "/batches") + .contains("required-permission: platform:order:export") + .contains("app-scope: PLATFORM"); + } + } + + @Test void fileOpenApiFreezesSelectionPrecedenceFiltersAndBusinessColumns() throws Exception { + String source = Files.readString(repositoryRoot().resolve( + "file/src/main/resources/openapi/domestic-order-export-v1.yaml")); + Map document = map(new Yaml().load(source)); + Map schemas = map(map(document.get("components")).get("schemas")); + Map parameters = map(map(schemas.get("DomesticOrderExportParameters")).get("properties")); + Map columns = map(map(schemas.get("DomesticOrderExportColumns")).get("properties")); + + assertThat(parameters).containsKeys("selectedOrderNos", "tenantId", "keyword", "orderNo", "mainStatus", + "customerId", "receivingWarehouseId", "destinationWarehouseId", "inboundCode", + "internalOrderStatus", "expectedArrivalDate", "feeReviewStatus", "channelName", "containerNo", + "cabinetNo", "sailingDate", "loaded", "fullContainer", "measurementReviewStatus", "fbaCode", + "createdFrom", "createdTo").doesNotContainKeys("page", "size", "orderId"); + assertThat(columns.keySet()).containsExactly("inboundCode", "orderNo", "mainStatus", "customerName", + "channelName", "channelChanged", "receivingWarehouseName", "destinationWarehouseName", + "expectedShipDate", "expectedArrivalAt", "expectedVolumeCbm", "totalPackageCount", "totalQuantity", + "actualGrossWeightKg", "actualVolumeCbm", "chargeableWeightKg", "chargeableVolumeCbm", + "feeReviewStatus", "skuNos", "taxMode", "containerMode", "customsInspection", "containerNo", + "cabinetNo", "vesselName", "voyageNo", "createdAt"); + assertThat(columns).containsKeys("expectedVolumeCbm", "expectedArrivalAt") + .doesNotContainKeys("orderId", "tenantId", "customerId", "inboundId", "receivingWarehouseId", + "destinationWarehouseId"); + assertThat(source).contains("platform:order:export", "selectedOrderNos 非空时优先", "no-store"); + } + + private static Path repositoryRoot() { + Path current = Path.of("").toAbsolutePath(); + return Files.isDirectory(current.resolve("file")) ? current : current.getParent(); + } + + @SuppressWarnings("unchecked") + private static Map map(Object value) { return (Map) value; } +} diff --git a/file/src/test/java/com/baiyue/file/service/ExportTaskServiceTest.java b/file/src/test/java/com/baiyue/file/service/ExportTaskServiceTest.java index 3e81e1be6..b9347a7ba 100644 --- a/file/src/test/java/com/baiyue/file/service/ExportTaskServiceTest.java +++ b/file/src/test/java/com/baiyue/file/service/ExportTaskServiceTest.java @@ -18,10 +18,13 @@ import jakarta.annotation.Resource; import com.baiyue.file.config.ExportDefinitionRegistry; import java.util.Map; import java.util.List; +import java.util.LinkedHashMap; import java.util.Set; +import java.util.stream.IntStream; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.verify; @@ -112,6 +115,62 @@ class ExportTaskServiceTest { .getResultCode()); } + @Test + void domesticOrderExportAcceptsSelectedBusinessNumbersAndTheCompleteFilterSnapshot() throws Exception { + var definition = definition(); + definition.setProviderService("order-service"); + definition.setProviderPath("/internal/exports/v1/DOMESTIC_WAREHOUSE_ORDER_LIST/batches"); + definition.setRequiredPermission("platform:order:export"); + when(registry.require(ExportSchemaContracts.DOMESTIC_WAREHOUSE_ORDER_LIST)).thenReturn(definition); + when(states.createConstrained(any(FileExportTask.class))).thenAnswer(invocation -> + new ExportStateService.CreationResult(invocation.getArgument(0), true)); + var user = user(1L, Set.of("platform:order:export")); + List selected = IntStream.range(0, 5000).mapToObj(index -> "ORD-" + index).toList(); + + var selectedResponse = service.create(user, new CreateExportTaskRequest( + ExportSchemaContracts.DOMESTIC_WAREHOUSE_ORDER_LIST, "XLSX", "国内仓订单", Map.of( + "selectedOrderNos", selected))); + + assertEquals(ExportSchemaContracts.DOMESTIC_WAREHOUSE_ORDER_LIST, selectedResponse.exportType()); + var filters = new LinkedHashMap(); + filters.put("tenantId", "10001"); filters.put("keyword", "订单"); filters.put("orderNo", "ORD-1"); + filters.put("mainStatus", "WAIT_MEASURE"); filters.put("customerId", "10001"); + filters.put("receivingWarehouseId", "11"); filters.put("destinationWarehouseId", "12"); + filters.put("inboundCode", "IN-1"); filters.put("internalOrderStatus", "APPROVED"); + filters.put("expectedArrivalDate", "2026-09-01"); filters.put("feeReviewStatus", "PENDING"); + filters.put("channelName", "海运"); filters.put("containerNo", "CONT-1"); + filters.put("cabinetNo", "CAB-1"); filters.put("sailingDate", "2026-09-02"); + filters.put("loaded", false); filters.put("fullContainer", true); + filters.put("measurementReviewStatus", "APPROVED"); filters.put("fbaCode", "FBA-1"); + filters.put("createdFrom", "2026-08-01T00:00:00"); filters.put("createdTo", "2026-09-01T23:59:59"); + + service.create(user, new CreateExportTaskRequest( + ExportSchemaContracts.DOMESTIC_WAREHOUSE_ORDER_LIST, "XLSX", "国内仓订单", filters)); + + var tasks = org.mockito.ArgumentCaptor.forClass(FileExportTask.class); + verify(states, org.mockito.Mockito.times(2)).createConstrained(tasks.capture()); + Map selectedSnapshot = new ObjectMapper().readValue(tasks.getAllValues().get(0).getParametersJson(), Map.class); + Map filterSnapshot = new ObjectMapper().readValue(tasks.getAllValues().get(1).getParametersJson(), Map.class); + assertEquals(5000, ((List) selectedSnapshot.get("selectedOrderNos")).size()); + assertEquals(filters, filterSnapshot); + } + + @Test + void domesticOrderExportRequiresItsDedicatedPermission() { + var definition = definition(); + definition.setProviderService("order-service"); + definition.setProviderPath("/internal/exports/v1/DOMESTIC_WAREHOUSE_ORDER_LIST/batches"); + definition.setRequiredPermission("platform:order:export"); + when(registry.require(ExportSchemaContracts.DOMESTIC_WAREHOUSE_ORDER_LIST)).thenReturn(definition); + + BizException denied = assertThrows(BizException.class, () -> service.create(user(1L, Set.of("platform:order:list")), + new CreateExportTaskRequest(ExportSchemaContracts.DOMESTIC_WAREHOUSE_ORDER_LIST, + "XLSX", "国内仓订单", Map.of()))); + + assertEquals(ResultCode.FORBIDDEN, denied.getResultCode()); + assertTrue(denied.getMessage().contains("权限")); + } + private AuthenticatedUser user(long id, Set permissions) { return new AuthenticatedUser(id, 0L, 0L, id, "user" + id, "PLATFORM", "WEB", permissions); } diff --git a/file/src/test/java/com/baiyue/file/service/GenericXlsxExportRunnerTest.java b/file/src/test/java/com/baiyue/file/service/GenericXlsxExportRunnerTest.java index 0e551932f..d983ad4ba 100644 --- a/file/src/test/java/com/baiyue/file/service/GenericXlsxExportRunnerTest.java +++ b/file/src/test/java/com/baiyue/file/service/GenericXlsxExportRunnerTest.java @@ -136,6 +136,58 @@ class GenericXlsxExportRunnerTest { } } + @Test void domesticOrderProviderMustReturnTheFrozenBusinessColumnsWithoutInternalIds() throws Exception { + List invalid = ExportSchemaContracts.DOMESTIC_WAREHOUSE_ORDER_COLUMNS.stream() + .filter(column -> !"expectedVolumeCbm".equals(column.key())) + .map(column -> new ExportColumn(column.key(), column.title(), column.type(), 18)).toList(); + var batch = new ExportBatchResponse(1, "sha256:domestic-orders", "snapshot-orders", + invalid, List.of(), null, false, 0L); + when(provider.batch(anyString(), eq(ExportSchemaContracts.DOMESTIC_WAREHOUSE_ORDER_LIST), any(), any())) + .thenReturn(ApiResponse.success(batch)); + FileExportTask task = domesticOrderTask(61L, Map.of()); + var definition = new ExportDefinitionRegistry.Definition(); definition.setMaxRows(200000); definition.setBatchSize(500); + + assertThrows(ExportFailure.class, + () -> runner.run(task, definition, new AtomicBoolean(), ignored -> { })); + } + + @Test void domesticOrderColumnsGenerateWorkbookAndPassTheSelectionSnapshotToOrder() throws Exception { + List columns = ExportSchemaContracts.DOMESTIC_WAREHOUSE_ORDER_COLUMNS.stream() + .map(column -> new ExportColumn(column.key(), column.title(), column.type(), 18)).toList(); + Map row = new java.util.LinkedHashMap<>(); + row.put("inboundCode", "IN-1"); row.put("orderNo", "ORD-1"); + row.put("expectedVolumeCbm", "1.25"); row.put("expectedArrivalAt", "2026-09-03T12:30:00"); + var batch = new ExportBatchResponse(1, "sha256:domestic-orders", "snapshot-orders", + columns, List.of(row), null, false, 1L); + when(provider.batch(anyString(), eq(ExportSchemaContracts.DOMESTIC_WAREHOUSE_ORDER_LIST), any(), any())) + .thenReturn(ApiResponse.success(batch)); + Map parameters = Map.of("selectedOrderNos", List.of("ORD-1", "ORD-2")); + FileExportTask task = domesticOrderTask(62L, parameters); + var definition = new ExportDefinitionRegistry.Definition(); definition.setMaxRows(200000); definition.setBatchSize(500); + + var result = runner.run(task, definition, new AtomicBoolean(), ignored -> { }); + + try (var workbook = new XSSFWorkbook(storage.load(result.objectKey()).getInputStream())) { + var sheet = workbook.getSheetAt(0); + assertEquals("订单号", sheet.getRow(0).getCell(1).getStringCellValue()); + assertEquals("预计到货时间", sheet.getRow(0).getCell(9).getStringCellValue()); + assertEquals("预计体积(m³)", sheet.getRow(0).getCell(10).getStringCellValue()); + assertEquals(1.25d, sheet.getRow(1).getCell(10).getNumericCellValue()); + } + ArgumentCaptor request = ArgumentCaptor.forClass(ExportBatchRequest.class); + verify(provider).batch(anyString(), eq(ExportSchemaContracts.DOMESTIC_WAREHOUSE_ORDER_LIST), + eq(definition), request.capture()); + assertEquals(parameters, request.getValue().parameters()); + } + + private FileExportTask domesticOrderTask(long id, Map parameters) throws Exception { + FileExportTask task = new FileExportTask(); task.setId(id); task.setTenantId(0L); + task.setAppCode("PLATFORM"); task.setRequestedBy(7L); task.setDispatchVersion(1); + task.setExportType(ExportSchemaContracts.DOMESTIC_WAREHOUSE_ORDER_LIST); task.setTaskName("国内仓订单"); + task.setParametersJson(new ObjectMapper().writeValueAsString(parameters)); + return task; + } + @Configuration static class TestConfig { @Bean ObjectMapper objectMapper() { return new ObjectMapper(); }