diff --git a/deploy/sql/头程数据库统一增量-MySQL8.sql b/deploy/sql/头程数据库统一增量-MySQL8.sql index 9d1ddbd2f..d4f4fcce5 100644 --- a/deploy/sql/头程数据库统一增量-MySQL8.sql +++ b/deploy/sql/头程数据库统一增量-MySQL8.sql @@ -6364,7 +6364,7 @@ INSERT INTO iam_permission (id, tenant_id, app_code, permission_type, parent_id, INSERT INTO iam_permission (id, tenant_id, app_code, permission_type, parent_id, permission_code, permission_name, route_path, http_method, sort_no, status, built_in, created_at, updated_at, deleted) VALUES (5306180,0,'MERCHANT','MENU',0,'merchant:overseas-warehouse:discard:list','货物废弃','/overseas-warehouse/cargo-disposal','GET',130,'ACTIVE',1,CURRENT_TIMESTAMP(3),CURRENT_TIMESTAMP(3),0); INSERT INTO iam_permission (id, tenant_id, app_code, permission_type, parent_id, permission_code, permission_name, route_path, http_method, sort_no, status, built_in, created_at, updated_at, deleted) VALUES (5306181,0,'MERCHANT','BUTTON',5306180,'merchant:overseas-warehouse:discard:create','创建货物废弃申请',NULL,'POST',10,'ACTIVE',1,CURRENT_TIMESTAMP(3),CURRENT_TIMESTAMP(3),0); INSERT INTO iam_permission (id, tenant_id, app_code, permission_type, parent_id, permission_code, permission_name, route_path, http_method, sort_no, status, built_in, created_at, updated_at, deleted) VALUES (5306182,0,'MERCHANT','BUTTON',5306180,'merchant:overseas-warehouse:discard:cancel','取消货物废弃申请',NULL,'POST',20,'ACTIVE',1,CURRENT_TIMESTAMP(3),CURRENT_TIMESTAMP(3),0); -INSERT INTO iam_permission (id, tenant_id, app_code, permission_type, parent_id, permission_code, permission_name, route_path, http_method, sort_no, status, built_in, created_at, updated_at, deleted) VALUES (5306190,0,'MERCHANT','MENU',0,'merchant:overseas-warehouse:return-order:list','退货预报','/overseas-warehouse/return-relabel','GET',140,'ACTIVE',1,CURRENT_TIMESTAMP(3),CURRENT_TIMESTAMP(3),0); +INSERT INTO iam_permission (id, tenant_id, app_code, permission_type, parent_id, permission_code, permission_name, route_path, http_method, sort_no, status, built_in, created_at, updated_at, deleted) VALUES (5306190,0,'MERCHANT','MENU',0,'merchant:overseas-warehouse:return-order:list','退货换标','/overseas-warehouse/return-relabel','GET',140,'ACTIVE',1,CURRENT_TIMESTAMP(3),CURRENT_TIMESTAMP(3),0); INSERT INTO iam_permission (id, tenant_id, app_code, permission_type, parent_id, permission_code, permission_name, route_path, http_method, sort_no, status, built_in, created_at, updated_at, deleted) VALUES (5306191,0,'MERCHANT','BUTTON',5306190,'merchant:overseas-warehouse:return-order:create','创建退货预报',NULL,'POST',10,'ACTIVE',1,CURRENT_TIMESTAMP(3),CURRENT_TIMESTAMP(3),0); INSERT INTO iam_permission (id, tenant_id, app_code, permission_type, parent_id, permission_code, permission_name, route_path, http_method, sort_no, status, built_in, created_at, updated_at, deleted) VALUES (5306192,0,'MERCHANT','BUTTON',5306190,'merchant:overseas-warehouse:return-order:relabel','退货转换标申请',NULL,'POST',20,'ACTIVE',1,CURRENT_TIMESTAMP(3),CURRENT_TIMESTAMP(3),0); diff --git a/iam/src/main/resources/db/lastleg-permissions.sql b/iam/src/main/resources/db/lastleg-permissions.sql index 69ba4426e..cc2641888 100644 --- a/iam/src/main/resources/db/lastleg-permissions.sql +++ b/iam/src/main/resources/db/lastleg-permissions.sql @@ -99,7 +99,7 @@ VALUES (5306180,0,'MERCHANT','MENU',0,'merchant:overseas-warehouse:discard:list','货物废弃','/overseas-warehouse/cargo-disposal','GET',130,'ACTIVE',1,0), (5306181,0,'MERCHANT','BUTTON',5306180,'merchant:overseas-warehouse:discard:create','创建货物废弃申请',NULL,'POST',10,'ACTIVE',1,0), (5306182,0,'MERCHANT','BUTTON',5306180,'merchant:overseas-warehouse:discard:cancel','取消货物废弃申请',NULL,'POST',20,'ACTIVE',1,0), - (5306190,0,'MERCHANT','MENU',0,'merchant:overseas-warehouse:return-order:list','退货预报','/overseas-warehouse/return-relabel','GET',140,'ACTIVE',1,0), + (5306190,0,'MERCHANT','MENU',0,'merchant:overseas-warehouse:return-order:list','退货换标','/overseas-warehouse/return-relabel','GET',140,'ACTIVE',1,0), (5306191,0,'MERCHANT','BUTTON',5306190,'merchant:overseas-warehouse:return-order:create','创建退货预报',NULL,'POST',10,'ACTIVE',1,0), (5306192,0,'MERCHANT','BUTTON',5306190,'merchant:overseas-warehouse:return-order:relabel','退货转换标申请',NULL,'POST',20,'ACTIVE',1,0) ON DUPLICATE KEY UPDATE permission_type=VALUES(permission_type), parent_id=VALUES(parent_id), diff --git a/iam/src/test/java/com/baiyue/iam/repository/OverseasCustomerPermissionSqlContractTest.java b/iam/src/test/java/com/baiyue/iam/repository/OverseasCustomerPermissionSqlContractTest.java index 9874d5f52..ba27c34e8 100644 --- a/iam/src/test/java/com/baiyue/iam/repository/OverseasCustomerPermissionSqlContractTest.java +++ b/iam/src/test/java/com/baiyue/iam/repository/OverseasCustomerPermissionSqlContractTest.java @@ -51,6 +51,7 @@ class OverseasCustomerPermissionSqlContractTest { "'merchant:cross-border:pallet-shipping:list','美本土散板','/cross-border/pallet-shipping'", "'merchant:warehouse:inbound:list','海外仓入库订单','/orders/inbound'", "'merchant:overseas-warehouse:return-inventory:list','退货库存','/overseas-warehouse/return-inventory'", + "'merchant:overseas-warehouse:return-order:list','退货换标','/overseas-warehouse/return-relabel'", "'merchant:overseas-warehouse:relabel:files','管理贴标文件'", "'merchant:overseas-warehouse:discard:cancel','取消货物废弃申请'"); } diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/controller/MerchantReturnOrderController.java b/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/controller/MerchantReturnOrderController.java index d5628de57..09e870bb6 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/controller/MerchantReturnOrderController.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/controller/MerchantReturnOrderController.java @@ -10,6 +10,8 @@ import com.baiyue.lastleg.security.OverseasPermissions; import jakarta.annotation.Resource; import jakarta.validation.Valid; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; @@ -28,6 +30,14 @@ public class MerchantReturnOrderController { @GetMapping("/{returnNo}") @PreAuthorize("hasAuthority('" + OverseasPermissions.MERCHANT_RETURN_ORDER_LIST + "')") public ApiResponse detail(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable String returnNo) { return ApiResponse.success(service.detail(customer(user), returnNo)); } + @GetMapping("/{returnNo}/photos/files/{fileId}/download") + @PreAuthorize("hasAuthority('" + OverseasPermissions.MERCHANT_RETURN_ORDER_LIST + "')") + public ResponseEntity downloadPhoto(@AuthenticationPrincipal AuthenticatedUser user, + @PathVariable String returnNo, @PathVariable long fileId) { + var file = service.downloadPhoto(customer(user), returnNo, fileId); + return ResponseEntity.ok().contentType(MediaType.parseMediaType(file.contentType())) + .header("Cache-Control", "no-store").body(file.content()); + } @GetMapping("/{returnNo}/relabel-candidates") @PreAuthorize("hasAuthority('" + OverseasPermissions.MERCHANT_RETURN_ORDER_RELABEL + "')") public ApiResponse> relabelCandidates( @AuthenticationPrincipal AuthenticatedUser user, @PathVariable String returnNo) { diff --git a/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml b/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml index c0d00b457..dbdbcfe8b 100644 --- a/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml +++ b/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml @@ -1516,6 +1516,17 @@ paths: security: [{ bearerAuth: [] }] parameters: [{ name: returnNo, in: path, required: true, schema: { type: string } }] responses: { '200': { description: 当前客户退货订单详情 }, '404': { description: 当前客户下退货订单不存在 } } + /api/merchant/v1/last-leg/return-orders/{returnNo}/photos/files/{fileId}/download: + get: + summary: 客户下载退货签收打托照片 + description: 权限 merchant:overseas-warehouse:return-order:list;仅允许下载当前客户、当前退货单关联的签收照片,响应为原始文件且禁止缓存。 + security: [{ bearerAuth: [] }] + parameters: + - { name: returnNo, in: path, required: true, schema: { type: string } } + - { name: fileId, in: path, required: true, schema: { type: integer, format: int64 } } + responses: + '200': { description: 照片文件, content: { image/*: { schema: { type: string, format: binary } } } } + '404': { description: 当前客户下退货单或关联照片不存在 } /api/merchant/v1/last-leg/return-orders/{returnNo}/relabel-candidates: get: summary: 查询退货单可转换标候选 diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/merchant/MerchantOverseasCustomerApiContractTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/merchant/MerchantOverseasCustomerApiContractTest.java index c9092655e..c547cb562 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/merchant/MerchantOverseasCustomerApiContractTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/merchant/MerchantOverseasCustomerApiContractTest.java @@ -87,6 +87,7 @@ class MerchantOverseasCustomerApiContractTest { "create", OverseasPermissions.MERCHANT_RETURN_ORDER_CREATE, "page", OverseasPermissions.MERCHANT_RETURN_ORDER_LIST, "detail", OverseasPermissions.MERCHANT_RETURN_ORDER_LIST, + "downloadPhoto", OverseasPermissions.MERCHANT_RETURN_ORDER_LIST, "relabelCandidates", OverseasPermissions.MERCHANT_RETURN_ORDER_RELABEL, "createRelabel", OverseasPermissions.MERCHANT_RETURN_ORDER_RELABEL)); assertPermissions(MerchantRelabelApplicationController.class, Map.of( diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/merchant/MerchantOverseasOpenApiContractTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/merchant/MerchantOverseasOpenApiContractTest.java index 28d124284..0d3e23529 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/merchant/MerchantOverseasOpenApiContractTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/merchant/MerchantOverseasOpenApiContractTest.java @@ -32,7 +32,8 @@ class MerchantOverseasOpenApiContractTest { "/api/merchant/v1/last-leg/local-looseboard-orders:", "/api/merchant/v1/last-leg/local-looseboard-orders/{orderNo}:", "/api/merchant/v1/last-leg/inbound-plans:", - "/api/merchant/v1/last-leg/inbound-plans/{planNo}:"}) { + "/api/merchant/v1/last-leg/inbound-plans/{planNo}:", + "/api/merchant/v1/last-leg/return-orders/{returnNo}/photos/files/{fileId}/download:"}) { assertThat(yaml).contains(path); assertThat(pathBlock(yaml, path)).doesNotContain("name: customerId"); } diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/returnreceipt/service/ReturnReceiptServiceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/returnreceipt/service/ReturnReceiptServiceTest.java index fab443c9d..2bc877ac7 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/returnreceipt/service/ReturnReceiptServiceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/returnreceipt/service/ReturnReceiptServiceTest.java @@ -22,6 +22,7 @@ import com.baiyue.lastleg.relabel.mapper.RelabelApplicationMapper; import com.baiyue.lastleg.relabel.mapper.RelabelItemMapper; import com.baiyue.lastleg.relabel.entity.RelabelApplication; import com.baiyue.lastleg.relabel.entity.RelabelItem; +import com.baiyue.lastleg.relabel.pda.gateway.RelabelPhotoFileGateway; import com.baiyue.lastleg.service.FbaWarehouseValidator; import com.baiyue.lastleg.service.LastlegOrderNoGenerator; import com.fasterxml.jackson.databind.ObjectMapper; @@ -67,6 +68,7 @@ class ReturnReceiptServiceTest { @Mock RelabelApplicationMapper relabelApplications; @Mock RelabelItemMapper relabelItems; @Mock FbaWarehouseValidator fbaWarehouseValidator; + @Mock RelabelPhotoFileGateway photoFiles; ReturnReceiptService service; PdaWarehouseIdentity identity; @@ -91,6 +93,30 @@ class ReturnReceiptServiceTest { identity = new PdaWarehouseIdentity("operator-1", "仓管员", 10L); } + @Test + void merchantPhotoDownloadUsesAuthenticatedCustomerAndResolvedReturnOrder() { + ReflectionTestUtils.setField(service, "photoFiles", photoFiles); + ReturnOrder order = order("RT-1", "10", ReturnReceiptService.COMPLETED, 2, 2); + var downloaded = new RelabelPhotoFileGateway.DownloadedFile(new byte[] { 1, 2 }, "proof.jpg", "image/jpeg"); + when(orders.findNo("customer-1", "RT-1")).thenReturn(order); + when(photoFiles.download("customer-1", 1L, 99L)).thenReturn(downloaded); + + assertEquals(downloaded, service.downloadPhoto("customer-1", "RT-1", 99L)); + verify(photoFiles).download("customer-1", 1L, 99L); + } + + @Test + void merchantPhotoDownloadRejectsUnknownCustomerOrderBeforeFileAccess() { + ReflectionTestUtils.setField(service, "photoFiles", photoFiles); + when(orders.findNo("other-customer", "RT-1")).thenReturn(null); + + ResponseStatusException error = assertThrows(ResponseStatusException.class, + () -> service.downloadPhoto("other-customer", "RT-1", 99L)); + + assertEquals(404, error.getStatusCode().value()); + verifyNoInteractions(photoFiles); + } + @Test void multiSkuPartialRelabelLocksOnlyRequestedQuantities() { verifyMultiSkuRelabel(30, 15);