贴标申请,仓储转运

This commit is contained in:
2026-09-20 16:55:55 +08:00
parent 2ce2fff8d7
commit 82ff6589cb
8 changed files with 53 additions and 3 deletions
@@ -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<ReturnOrderResponses.Detail> 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<byte[]> 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<java.util.List<ReturnOrderResponses.RelabelCandidate>> relabelCandidates(
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable String returnNo) {
@@ -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: 查询退货单可转换标候选
@@ -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(
@@ -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");
}
@@ -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);