fix(lastleg): complete relabel document contract and regression gates
This commit is contained in:
+3
-1
@@ -67,6 +67,8 @@ public class RelabelApplicationController {
|
||||
@PathVariable String relabelNo, @RequestParam String scope, @RequestParam(required = false) Long itemId,
|
||||
@RequestPart("file") MultipartFile file) {
|
||||
requirePlatform(user);
|
||||
if (!"TOTAL".equalsIgnoreCase(scope) && !"ITEM".equalsIgnoreCase(scope)) throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "资料范围必须为 TOTAL 或 ITEM");
|
||||
if ("TOTAL".equalsIgnoreCase(scope) && itemId != null) throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "总资料不能指定明细");
|
||||
String customerId = service.customerId(relabelNo);
|
||||
return ApiResponse.success("ITEM".equalsIgnoreCase(scope)
|
||||
? service.uploadItemFile(customerId, relabelNo, itemId == null ? 0L : itemId, operatorLong(user), file)
|
||||
@@ -80,7 +82,7 @@ public class RelabelApplicationController {
|
||||
@RequestParam(required = false) Long itemId) {
|
||||
requirePlatform(user);
|
||||
var file = service.downloadFile(service.customerId(relabelNo), relabelNo, scope, itemId, fileId);
|
||||
return ResponseEntity.ok().header("Content-Disposition", ContentDisposition.inline().filename(file.fileName(), StandardCharsets.UTF_8).build().toString())
|
||||
return ResponseEntity.ok().cacheControl(org.springframework.http.CacheControl.noStore()).header("Content-Disposition", ContentDisposition.inline().filename(file.fileName(), StandardCharsets.UTF_8).build().toString())
|
||||
.contentType(org.springframework.http.MediaType.parseMediaType(file.contentType())).contentLength(file.content().length).body(file.content());
|
||||
}
|
||||
private void requirePlatform(AuthenticatedUser user) {
|
||||
|
||||
+2
-2
@@ -37,7 +37,7 @@ public class HttpRelabelDocumentFileGateway implements RelabelDocumentFileGatewa
|
||||
.onStatus(HttpStatusCode::isError, (request, result) -> { throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "换标PDF上传失败"); })
|
||||
.body(MetadataEnvelope.class);
|
||||
requireSuccess(response, "换标PDF上传失败"); Metadata data = response == null ? null : response.data();
|
||||
if (data == null || data.fileId() == null) throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "文件服务未返回换标PDF信息");
|
||||
if (data == null || data.fileId() == null || data.fileId() <= 0) throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "文件服务未返回换标PDF信息");
|
||||
return new UploadedFile(data.fileId(), data.fileName(), data.contentType(), data.sizeBytes() == null ? 0 : data.sizeBytes());
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ public class HttpRelabelDocumentFileGateway implements RelabelDocumentFileGatewa
|
||||
throw new ResponseStatusException(result.getStatusCode().is4xxClientError() ? HttpStatus.NOT_FOUND : HttpStatus.SERVICE_UNAVAILABLE, "换标PDF读取失败");
|
||||
}).toEntity(byte[].class);
|
||||
String name = response.getHeaders().getContentDisposition().getFilename();
|
||||
return new DownloadedFile(response.getBody() == null ? new byte[0] : response.getBody(), name == null ? fileId + ".pdf" : name,
|
||||
return new DownloadedFile(response.getBody() == null ? new byte[0] : response.getBody(), name == null ? "换标资料.pdf" : name,
|
||||
response.getHeaders().getContentType() == null ? MediaType.APPLICATION_PDF_VALUE : response.getHeaders().getContentType().toString());
|
||||
}
|
||||
|
||||
|
||||
+75
-6
@@ -16,6 +16,8 @@ import com.baiyue.lastleg.inventory.mapper.InventoryBalanceMapper;
|
||||
import com.baiyue.lastleg.inventory.mapper.InventoryPositionMapper;
|
||||
import com.baiyue.lastleg.relabel.dto.RelabelRequests;
|
||||
import com.baiyue.lastleg.relabel.dto.RelabelResponses;
|
||||
import com.baiyue.lastleg.relabel.gateway.RelabelDocumentFileGateway;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import com.baiyue.lastleg.relabel.entity.RelabelApplication;
|
||||
import com.baiyue.lastleg.relabel.entity.RelabelItem;
|
||||
import com.baiyue.lastleg.relabel.entity.RelabelStatusHistory;
|
||||
@@ -63,6 +65,75 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
@Resource private InventoryCommandService inventoryCommands;
|
||||
@Resource private InventoryBalanceMapper inventoryBalances;
|
||||
@Resource private InventoryPositionMapper inventoryPositions;
|
||||
@Resource private RelabelDocumentFileGateway documentFiles;
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public String customerId(String relabelNo) { return require(null, relabelNo).getCustomerId(); }
|
||||
|
||||
@Transactional
|
||||
public RelabelResponses.Detail uploadTotalFile(String customerId, String relabelNo, long operatorId, MultipartFile file) {
|
||||
RelabelApplication application = lockForDocument(customerId, relabelNo);
|
||||
requirePdf(file);
|
||||
var uploaded = documentFiles.upload(application.getCustomerId(), "OVERSEAS_RELABEL_TOTAL_DOCUMENT", String.valueOf(application.getId()), operatorId, file);
|
||||
application.setTotalFileId(String.valueOf(uploaded.fileId())); application.setTotalFileName(uploaded.fileName());
|
||||
application.setTotalFileUrl(documentUrl(relabelNo, uploaded.fileId(), null)); application.setUpdatedAt(LocalDateTime.now());
|
||||
applications.updateById(application);
|
||||
return detail(application.getCustomerId(), relabelNo);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public RelabelResponses.Detail uploadItemFile(String customerId, String relabelNo, long itemId, long operatorId, MultipartFile file) {
|
||||
RelabelApplication application = lockForDocument(customerId, relabelNo);
|
||||
RelabelItem item = requireDocumentItem(application, itemId);
|
||||
requirePdf(file);
|
||||
var uploaded = documentFiles.upload(application.getCustomerId(), "OVERSEAS_RELABEL_ITEM_DOCUMENT", String.valueOf(item.getId()), operatorId, file);
|
||||
item.setRelabelFileId(String.valueOf(uploaded.fileId())); item.setRelabelFileName(uploaded.fileName());
|
||||
item.setRelabelFileUrl(documentUrl(relabelNo, uploaded.fileId(), itemId));
|
||||
items.updateById(item);
|
||||
application.setUpdatedAt(LocalDateTime.now()); applications.updateById(application);
|
||||
return detail(application.getCustomerId(), relabelNo);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public RelabelDocumentFileGateway.DownloadedFile downloadFile(String customerId, String relabelNo, String scope, Long itemId, long fileId) {
|
||||
RelabelApplication application = require(customerId, relabelNo);
|
||||
if(fileId <= 0) throw notFound("换标资料不存在");
|
||||
if ("ITEM".equalsIgnoreCase(scope)) {
|
||||
if (itemId == null) throw badRequest("明细资料必须指定明细");
|
||||
RelabelItem item = requireDocumentItem(application, itemId);
|
||||
if (!String.valueOf(fileId).equals(item.getRelabelFileId())) throw notFound("换标资料未绑定当前明细");
|
||||
return documentFiles.download(application.getCustomerId(), "OVERSEAS_RELABEL_ITEM_DOCUMENT", String.valueOf(item.getId()), fileId);
|
||||
}
|
||||
if (!"TOTAL".equalsIgnoreCase(scope) || itemId != null) throw badRequest("资料范围必须为 TOTAL 或 ITEM,总资料不指定明细");
|
||||
if (!String.valueOf(fileId).equals(application.getTotalFileId())) throw notFound("换标资料未绑定当前申请单");
|
||||
return documentFiles.download(application.getCustomerId(), "OVERSEAS_RELABEL_TOTAL_DOCUMENT", String.valueOf(application.getId()), fileId);
|
||||
}
|
||||
|
||||
private RelabelApplication lockForDocument(String customerId, String relabelNo) {
|
||||
if (!StringUtils.hasText(customerId)) throw badRequest("客户不能为空");
|
||||
RelabelApplication application = applications.lockNo(customerId, relabelNo);
|
||||
if (application == null) throw notFound("Relabel application not found");
|
||||
if (!Set.of(PENDING_PROCESSING, "PROCESSING").contains(application.getStatus())) throw conflict("当前换标状态不允许修改资料");
|
||||
return application;
|
||||
}
|
||||
|
||||
private RelabelItem requireDocumentItem(RelabelApplication application, long itemId) {
|
||||
RelabelItem item = itemId > 0 ? items.selectById(itemId) : null;
|
||||
if (item == null || !Objects.equals(item.getApplicationId(), application.getId()) || !Objects.equals(item.getCustomerId(), application.getCustomerId())) throw notFound("换标明细不属于当前申请单");
|
||||
return item;
|
||||
}
|
||||
|
||||
private static void requirePdf(MultipartFile file) {
|
||||
if (file == null || file.isEmpty() || file.getSize() > 10L * 1024 * 1024) throw badRequest("请上传不超过10MB的PDF文件");
|
||||
if (file.getOriginalFilename() == null || !file.getOriginalFilename().toLowerCase(java.util.Locale.ROOT).endsWith(".pdf") || !"application/pdf".equalsIgnoreCase(file.getContentType())) throw badRequest("换标资料仅支持PDF文件");
|
||||
try (var stream = file.getInputStream()) {
|
||||
if (!java.util.Arrays.equals(stream.readNBytes(5), "%PDF-".getBytes(StandardCharsets.US_ASCII))) throw badRequest("文件内容不是PDF");
|
||||
} catch (java.io.IOException exception) { throw badRequest("无法读取PDF文件"); }
|
||||
}
|
||||
|
||||
private static String documentUrl(String relabelNo, long fileId, Long itemId) {
|
||||
return "/api/platform/v1/last-leg/relabel-applications/" + java.net.URLEncoder.encode(relabelNo, StandardCharsets.UTF_8) + "/files/" + fileId + "/download?scope=" + (itemId == null ? "TOTAL" : "ITEM&itemId=" + itemId);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<RelabelResponses.InventoryCandidate> candidates(String customerId, String operationWarehouseId,
|
||||
@@ -101,7 +172,6 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
int total = 0;
|
||||
int notified = 0;
|
||||
List<SourceInventory> sources = new java.util.ArrayList<>();
|
||||
Set<Long> selectedPositions = new HashSet<>();
|
||||
Set<String> selectedCartonMarks = new HashSet<>();
|
||||
for (RelabelRequests.Item item : request.items()) {
|
||||
validateOperation(item);
|
||||
@@ -110,9 +180,6 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
}
|
||||
total = Math.addExact(total, item.totalPackageCount());
|
||||
notified = Math.addExact(notified, item.notifiedPackageCount());
|
||||
if (!selectedPositions.add(item.sourceInventoryPositionId())) {
|
||||
throw badRequest("The same inventory position cannot be selected more than once");
|
||||
}
|
||||
if (!selectedCartonMarks.add(item.cartonMark().trim().toUpperCase())) {
|
||||
throw badRequest("The same carton mark cannot create more than one relabel task");
|
||||
}
|
||||
@@ -135,11 +202,12 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
long applicationId = IdWorker.getId();
|
||||
String operationWarehouseId = request.operationWarehouseId().trim();
|
||||
String relabelNo = orderNoGenerator.next("RELABEL", operationWarehouseId, applicationId);
|
||||
int holdIndex = 0;
|
||||
for (SourceInventory source : sources) {
|
||||
inventoryCommands.hold(customerId, operatorId, new InventoryCommandRequests.Move(
|
||||
source.request().sourceInventoryPositionId(), source.request().notifiedPackageCount(), relabelNo,
|
||||
null, "RELABEL:" + relabelNo, request.idempotencyKey().trim() + ":HOLD:"
|
||||
+ source.request().sourceInventoryPositionId(), "贴标申请冻结原入仓单库存"));
|
||||
null, "RELABEL:" + relabelNo, "RELABEL:" + applicationId + ":HOLD:" + holdIndex++,
|
||||
"贴标申请冻结原入仓单库存"));
|
||||
}
|
||||
LastlegOrder tailOrder = insertTailOrder(customerId, request, requestHash, relabelNo,
|
||||
operationWarehouseId, total, now);
|
||||
@@ -321,6 +389,7 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
Objects.requireNonNullElse(value.getItemCount(), detailItems.size()),
|
||||
Objects.requireNonNullElse(value.getTotalPackageCount(), 0),
|
||||
Objects.requireNonNullElse(value.getNotifiedPackageCount(), 0), skuCodes,
|
||||
value.getTotalFileId(), value.getTotalFileName(), value.getTotalFileUrl(),
|
||||
value.getChargeAmount(), value.getChargeCurrency(), value.getWalletStatus(), value.getWalletErrorMessage(),
|
||||
value.getCreatedAt());
|
||||
}
|
||||
|
||||
@@ -346,7 +346,7 @@ paths:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/ChangeReceivingWarehouseRequest' }
|
||||
responses:
|
||||
'200': { description: 更新后的柜级摘要, content: { application/json: { schema: { type: object, properties: { code: { type: string, enum: ['200'] }, data: { $ref: '#/components/schemas/ContainerPlanProgressSummary' } } } } }
|
||||
'200': { description: 更新后的柜级摘要, content: { application/json: { schema: { type: object, properties: { code: { type: string, enum: ['200'] }, data: { $ref: '#/components/schemas/ContainerPlanProgressSummary' } } } } } }
|
||||
'400': { description: 请求无效或目标不是有效 OVERSEAS 仓 }
|
||||
'409': { description: 版本冲突、幂等键冲突或PDA接柜会话已建立 }
|
||||
/api/platform/v1/last-leg/orders/{id}/destination:
|
||||
@@ -624,7 +624,7 @@ paths:
|
||||
description: 权限 platform:overseas-warehouse:outbound:list;只按全局唯一 outboundNo 定位并从记录解析客户范围,不接收 customerId。
|
||||
security: [{ bearerAuth: [] }]
|
||||
parameters: [{ name: outboundNo, in: path, required: true, schema: { type: string } }]
|
||||
responses: { '200': { description: 出库单、出库托盘及关联派送单详情;pallets[].palletNo 为 Warehouse 权威实物托盘码 }, '404': { description: 出库单不存在 } }
|
||||
responses: { '200': { description: "出库单、出库托盘及关联派送单详情;pallets[].palletNo 为 Warehouse 权威实物托盘码" }, '404': { description: 出库单不存在 } }
|
||||
/api/platform/v1/last-leg/outbound-orders/{outboundNo}/cancel:
|
||||
post:
|
||||
summary: 取消未执行出库单
|
||||
@@ -1172,6 +1172,38 @@ paths:
|
||||
- { name: relabelNo, in: path, required: true, schema: { type: string, maxLength: 64 } }
|
||||
requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/RelabelStatusChange' } } } }
|
||||
responses: { '200': { description: 统一 ApiResponse 包装的换标单详情 }, '409': { description: 非法状态迁移或关联尾程单缺失 } }
|
||||
/api/platform/v1/last-leg/relabel-applications/{relabelNo}/files:
|
||||
post:
|
||||
summary: 上传总换标PDF或明细换标PDF
|
||||
description: 权限 platform:overseas-warehouse:relabel-order:create。仅PENDING_PROCESSING/PROCESSING可替换,PDF内容且不超过10MiB。客户从申请单解析,ITEM必须属于该申请单同客户。File类别分别为OVERSEAS_RELABEL_TOTAL_DOCUMENT(application.id)及OVERSEAS_RELABEL_ITEM_DOCUMENT(item.id)。成功保存File返回的资料元数据,旧资料不再允许通过当前绑定下载。
|
||||
security: [{ bearerAuth: [] }]
|
||||
parameters:
|
||||
- { name: relabelNo, in: path, required: true, schema: { type: string } }
|
||||
- { name: scope, in: query, required: true, schema: { type: string, enum: [TOTAL, ITEM] } }
|
||||
- { name: itemId, in: query, description: ITEM必填,TOTAL不得传入, schema: { type: integer, format: int64, minimum: 1 } }
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
multipart/form-data:
|
||||
schema: { type: object, required: [file], properties: { file: { type: string, format: binary } } }
|
||||
responses:
|
||||
'200': { description: 换标详情;application包含totalFileId/totalFileName/totalFileUrl,items包含relabelFileId/relabelFileName/relabelFileUrl。URL为受权下载路径,文件ID按字符串返回 }
|
||||
'400': { description: 非PDF、超限或资料范围无效 }
|
||||
'404': { description: 申请或明细不属于当前客户 }
|
||||
'409': { description: 换标状态不允许修改资料 }
|
||||
/api/platform/v1/last-leg/relabel-applications/{relabelNo}/files/{fileId}/download:
|
||||
get:
|
||||
summary: 读取当前绑定的换标PDF
|
||||
description: 权限 platform:overseas-warehouse:relabel-order:list;按申请单客户、资料类别、application.id或item.id及fileId精确授权。仅当前已保存资料可读,未绑定或替换的文件拒绝。响应Cache-Control为no-store。
|
||||
security: [{ bearerAuth: [] }]
|
||||
parameters:
|
||||
- { name: relabelNo, in: path, required: true, schema: { type: string } }
|
||||
- { name: fileId, in: path, required: true, schema: { type: integer, format: int64, minimum: 1 } }
|
||||
- { name: scope, in: query, required: true, schema: { type: string, enum: [TOTAL, ITEM] } }
|
||||
- { name: itemId, in: query, schema: { type: integer, format: int64, minimum: 1 } }
|
||||
responses:
|
||||
'200': { description: PDF字节, content: { application/pdf: { schema: { type: string, format: binary } } } }
|
||||
'404': { description: 资料未绑定或归属不匹配 }
|
||||
/api/merchant/v1/last-leg/return-orders:
|
||||
get:
|
||||
summary: 客户分页查询退货签收订单
|
||||
@@ -1336,10 +1368,10 @@ components:
|
||||
properties:
|
||||
skuCode: { type: string, maxLength: 128 }
|
||||
quantity: { type: integer, minimum: 1 }
|
||||
length: { type: number, exclusiveMinimum: 0, description: '箱长,CM' }
|
||||
width: { type: number, exclusiveMinimum: 0, description: '箱宽,CM' }
|
||||
height: { type: number, exclusiveMinimum: 0, description: '箱高,CM' }
|
||||
weight: { type: number, exclusiveMinimum: 0, description: '箱重,KG' }
|
||||
length: { type: number, minimum: 0, exclusiveMinimum: true, description: '箱长,CM' }
|
||||
width: { type: number, minimum: 0, exclusiveMinimum: true, description: '箱宽,CM' }
|
||||
height: { type: number, minimum: 0, exclusiveMinimum: true, description: '箱高,CM' }
|
||||
weight: { type: number, minimum: 0, exclusiveMinimum: true, description: '箱重,KG' }
|
||||
idempotencyKey: { type: string, minLength: 1, maxLength: 255 }
|
||||
InventoryBalance:
|
||||
type: object
|
||||
|
||||
+27
@@ -34,6 +34,33 @@ class RelabelApplicationControllerTest {
|
||||
assertThrows(ResponseStatusException.class, () -> controller.create(platform(), "0", request()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void uploadRejectsMerchantAndUnknownScopeBeforeResolvingCustomer() {
|
||||
var merchant = new AuthenticatedUser(7L, 101L, 0L, 8L, "customer", "MERCHANT", "WEB", Set.of());
|
||||
assertThrows(ResponseStatusException.class, () -> controller.uploadFile(merchant, "RL-1", "TOTAL", null, null));
|
||||
assertThrows(ResponseStatusException.class, () -> controller.uploadFile(platform(), "RL-1", "PHOTO", null, null));
|
||||
assertThrows(ResponseStatusException.class, () -> controller.uploadFile(platform(), "RL-1", "TOTAL", 22L, null));
|
||||
org.mockito.Mockito.verifyNoInteractions(service);
|
||||
}
|
||||
|
||||
@Test
|
||||
void uploadUsesCustomerResolvedFromApplication() {
|
||||
org.mockito.Mockito.when(service.customerId("RL-1")).thenReturn("101");
|
||||
var file = new org.springframework.mock.web.MockMultipartFile("file", "labels.pdf", "application/pdf", "%PDF-1.7".getBytes());
|
||||
controller.uploadFile(platform(), "RL-1", "ITEM", 22L, file);
|
||||
verify(service).uploadItemFile("101", "RL-1", 22L, 7L, file);
|
||||
}
|
||||
|
||||
@Test
|
||||
void downloadUsesOwnedBindingAndForbidsCaching() {
|
||||
org.mockito.Mockito.when(service.customerId("RL-1")).thenReturn("101");
|
||||
org.mockito.Mockito.when(service.downloadFile("101", "RL-1", "TOTAL", null, 99L))
|
||||
.thenReturn(new com.baiyue.lastleg.relabel.gateway.RelabelDocumentFileGateway.DownloadedFile("%PDF-1.7".getBytes(), "labels.pdf", "application/pdf"));
|
||||
var response = controller.downloadFile(platform(), "RL-1", 99L, "TOTAL", null);
|
||||
org.junit.jupiter.api.Assertions.assertEquals("no-store", response.getHeaders().getCacheControl());
|
||||
org.junit.jupiter.api.Assertions.assertEquals("application/pdf", response.getHeaders().getContentType().toString());
|
||||
}
|
||||
|
||||
private RelabelRequests.Create request() {
|
||||
return new RelabelRequests.Create("CUSTOMER-1", "WH-1",
|
||||
LocalDateTime.of(2026, 8, 20, 10, 0), "Li", "13800000000", null, null, "key-1",
|
||||
|
||||
+3
-1
@@ -230,7 +230,9 @@ class RelabelApplicationServiceTest {
|
||||
|
||||
service.create("10001", "operator-1", request);
|
||||
|
||||
verify(inventoryCommands, times(2)).hold(eq("10001"), eq("operator-1"), any());
|
||||
ArgumentCaptor<InventoryCommandRequests.Move> holds = ArgumentCaptor.forClass(InventoryCommandRequests.Move.class);
|
||||
verify(inventoryCommands, times(2)).hold(eq("10001"), eq("operator-1"), holds.capture());
|
||||
assertEquals(2, holds.getAllValues().stream().map(InventoryCommandRequests.Move::idempotencyKey).distinct().count());
|
||||
verify(items, times(2)).insert(any(RelabelItem.class));
|
||||
}
|
||||
|
||||
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
package com.baiyue.lastleg.relabel.service;
|
||||
|
||||
import com.baiyue.lastleg.relabel.entity.*;
|
||||
import com.baiyue.lastleg.relabel.mapper.*;
|
||||
import com.baiyue.lastleg.relabel.gateway.RelabelDocumentFileGateway;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
|
||||
class RelabelDocumentServiceTest {
|
||||
final RelabelApplicationMapper applications=mock(RelabelApplicationMapper.class);
|
||||
final RelabelItemMapper items=mock(RelabelItemMapper.class);
|
||||
final RelabelDocumentFileGateway files=mock(RelabelDocumentFileGateway.class);
|
||||
final RelabelApplicationService service=new RelabelApplicationService();
|
||||
final RelabelApplication application=new RelabelApplication();
|
||||
final MockMultipartFile pdf=new MockMultipartFile("file","labels.pdf","application/pdf","%PDF-1.7\nfixture".getBytes());
|
||||
RelabelDocumentServiceTest(){
|
||||
application.setId(11L);application.setCustomerId("101");application.setRelabelNo("RL-1");application.setStatus("PENDING_PROCESSING");
|
||||
ReflectionTestUtils.setField(service,"applications",applications);ReflectionTestUtils.setField(service,"items",items);
|
||||
ReflectionTestUtils.setField(service,"documentFiles",files);ReflectionTestUtils.setField(service,"orders",mock(LastlegOrderMapper.class));
|
||||
ReflectionTestUtils.setField(service,"histories",mock(RelabelStatusHistoryMapper.class));ReflectionTestUtils.setField(service,"objectMapper",new ObjectMapper());
|
||||
when(applications.lockNo("101","RL-1")).thenReturn(application);when(applications.findNo("101","RL-1")).thenReturn(application);
|
||||
}
|
||||
@Test void totalUploadPersistsReturnedMetadataAndExposesSummary(){
|
||||
when(files.upload("101","OVERSEAS_RELABEL_TOTAL_DOCUMENT","11",7L,pdf)).thenReturn(new RelabelDocumentFileGateway.UploadedFile(99L,"labels.pdf","application/pdf",15));
|
||||
var result=service.uploadTotalFile("101","RL-1",7L,pdf);
|
||||
assertThat(result.application().totalFileId()).isEqualTo("99");
|
||||
assertThat(result.application().totalFileName()).isEqualTo("labels.pdf");
|
||||
assertThat(result.application().totalFileUrl()).endsWith("/files/99/download?scope=TOTAL");
|
||||
verify(applications).updateById(application);
|
||||
}
|
||||
@Test void itemUploadBindsOwnCustomerApplicationAndDistinctCategory(){
|
||||
RelabelItem item=item();when(items.selectById(22L)).thenReturn(item);
|
||||
when(files.upload("101","OVERSEAS_RELABEL_ITEM_DOCUMENT","22",7L,pdf)).thenReturn(new RelabelDocumentFileGateway.UploadedFile(98L,"item.pdf","application/pdf",15));
|
||||
service.uploadItemFile("101","RL-1",22L,7L,pdf);
|
||||
assertThat(item.getRelabelFileId()).isEqualTo("98");assertThat(item.getRelabelFileUrl()).endsWith("scope=ITEM&itemId=22");
|
||||
verify(items).updateById(item);
|
||||
}
|
||||
@Test void rejectsCrossCustomerAndCrossApplicationItemsBeforeFileUpload(){
|
||||
assertThatThrownBy(()->service.uploadTotalFile("102","RL-1",7L,pdf)).isInstanceOf(ResponseStatusException.class);
|
||||
RelabelItem item=item();item.setApplicationId(12L);when(items.selectById(22L)).thenReturn(item);
|
||||
assertThatThrownBy(()->service.uploadItemFile("101","RL-1",22L,7L,pdf)).hasMessageContaining("不属于");
|
||||
item.setApplicationId(11L);item.setCustomerId("102");
|
||||
assertThatThrownBy(()->service.uploadItemFile("101","RL-1",22L,7L,pdf)).hasMessageContaining("不属于");
|
||||
verifyNoInteractions(files);
|
||||
}
|
||||
@Test void rejectsFakePdfAndTerminalChangesBeforeUpload(){
|
||||
var fake=new MockMultipartFile("file","labels.pdf","application/pdf","not pdf".getBytes());
|
||||
assertThatThrownBy(()->service.uploadTotalFile("101","RL-1",7L,fake)).hasMessageContaining("不是PDF");
|
||||
application.setStatus("COMPLETED");
|
||||
assertThatThrownBy(()->service.uploadTotalFile("101","RL-1",7L,pdf)).hasMessageContaining("不允许修改资料");
|
||||
verifyNoInteractions(files);
|
||||
}
|
||||
@Test void downloadRequiresCurrentBindingAndExactScope(){
|
||||
application.setTotalFileId("99");
|
||||
service.downloadFile("101","RL-1","TOTAL",null,99L);
|
||||
verify(files).download("101","OVERSEAS_RELABEL_TOTAL_DOCUMENT","11",99L);
|
||||
assertThatThrownBy(()->service.downloadFile("101","RL-1","TOTAL",null,98L)).hasMessageContaining("未绑定");
|
||||
assertThatThrownBy(()->service.downloadFile("101","RL-1","other",null,99L)).hasMessageContaining("资料范围");
|
||||
assertThatThrownBy(()->service.downloadFile("101","RL-1","ITEM",null,99L)).hasMessageContaining("指定明细");
|
||||
verifyNoMoreInteractions(files);
|
||||
}
|
||||
@Test void fileFailureDoesNotPersistBindings(){
|
||||
when(files.upload(anyString(),anyString(),anyString(),anyLong(),any())).thenThrow(new IllegalStateException("offline"));
|
||||
assertThatThrownBy(()->service.uploadTotalFile("101","RL-1",7L,pdf)).hasMessage("offline");
|
||||
verify(applications,never()).updateById(any(RelabelApplication.class));assertThat(application.getTotalFileId()).isNull();
|
||||
}
|
||||
private RelabelItem item(){RelabelItem item=new RelabelItem();item.setId(22L);item.setApplicationId(11L);item.setCustomerId("101");return item;}
|
||||
}
|
||||
Reference in New Issue
Block a user