货柜pda 添加操作人
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
package com.baiyue.firstleg.client;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
@FeignClient(name = "iam-service", contextId = "firstlegDomesticPdaOperatorClient",
|
||||
url = "${baiyue.clients.iam.url:}", path = "/internal/iam")
|
||||
public interface DomesticPdaOperatorClient {
|
||||
@GetMapping("/domestic-pda-operators/{authUserId}")
|
||||
ApiResponse<Operator> required(@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@PathVariable long authUserId,
|
||||
@RequestParam long warehouseId);
|
||||
|
||||
record Operator(Long userId, String displayName) { }
|
||||
}
|
||||
+2
-2
@@ -92,9 +92,9 @@ public class PlatformLoadingListController {
|
||||
planId, warehouseId, user.userId(), user.username(), idempotencyKey, request));
|
||||
}
|
||||
|
||||
@Operation(summary = "确认整份预装计划", description = "按 loadingPlanId 原子确认全部已分配入库预装票据;要求关联订舱已放舱,真实集装箱号可在PDA确认装柜完成前补录。成功后唯一创建待领取 PDA 任务,claim 后才进入 LOADING。")
|
||||
@Operation(summary = "确认预装并推送指定 PDA 作业员", description = "按 loadingPlanId 原子确认预装、创建任务并分配给启用的国内头程 PDA 作业员;仅被指定人员可在 PDA 查看和领取。")
|
||||
@PostMapping("/loading-plans/{planId}/confirm")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:preload:edit')")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:preload:edit') and hasAuthority('platform:firstleg:pda:assign')")
|
||||
public ApiResponse<PreloadConfirmationResponse> confirmPreload(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable @Positive long planId,
|
||||
|
||||
+11
-1
@@ -64,7 +64,9 @@ public class PlatformLoadingPdaController {
|
||||
@RequestParam(required = false) Long loadingPlanId,
|
||||
@RequestParam @Positive long warehouseId) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
return ApiResponse.success(service.task(cabinetNo, loadingPlanId, warehouseId));
|
||||
Task result = service.task(cabinetNo, loadingPlanId, warehouseId);
|
||||
service.requireVisibleTo(result.id(), user.userId(), canManage(user));
|
||||
return ApiResponse.success(result);
|
||||
}
|
||||
|
||||
@GetMapping("/tasks/pending")
|
||||
@@ -90,6 +92,7 @@ public class PlatformLoadingPdaController {
|
||||
@PathVariable @Positive long taskId,
|
||||
@RequestParam @Positive long warehouseId) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
service.requireVisibleTo(taskId, user.userId(), canManage(user));
|
||||
return ApiResponse.success(service.task(taskId, warehouseId));
|
||||
}
|
||||
|
||||
@@ -143,6 +146,7 @@ public class PlatformLoadingPdaController {
|
||||
@PathVariable @Positive long taskId,
|
||||
@RequestParam @Positive long warehouseId) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
service.requireVisibleTo(taskId, user.userId(), canManage(user));
|
||||
return ApiResponse.success(service.inventorySnapshot(taskId, warehouseId));
|
||||
}
|
||||
|
||||
@@ -238,7 +242,13 @@ public class PlatformLoadingPdaController {
|
||||
public ApiResponse<List<Operation>> operations(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable @Positive long taskId) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
service.requireVisibleTo(taskId, user.userId(), canManage(user));
|
||||
return ApiResponse.success(service.operations(taskId));
|
||||
}
|
||||
|
||||
private static boolean canManage(AuthenticatedUser user) {
|
||||
return user.permissions().contains("platform:firstleg:pda:assign")
|
||||
|| user.permissions().contains("platform:firstleg:pda:review");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -102,7 +102,8 @@ public final class LoadingListDtos {
|
||||
}
|
||||
}
|
||||
|
||||
public record ConfirmPreloadRequest(@NotNull @Min(0) Integer expectedVersion) {
|
||||
public record ConfirmPreloadRequest(@NotNull @Min(0) Integer expectedVersion,
|
||||
@NotNull @Min(1) Long assigneeId) {
|
||||
}
|
||||
|
||||
public record PreloadConfirmationResponse(
|
||||
|
||||
@@ -108,7 +108,7 @@ public interface LoadingPickTaskMapper extends BaseMapper<LoadingPickTaskEntity>
|
||||
LEFT JOIN fl_loading_pick_assignment assignment ON assignment.pick_task_id = task.id
|
||||
WHERE task.task_status IN ('LOADING','COMPLETING','COMPLETION_FAILED') AND booking.record_status = 'ACTIVE'
|
||||
AND booking.warehouse_id = #{warehouseId}
|
||||
AND (assignment.pick_task_id IS NULL OR assignment.assignee_id = #{operatorId})
|
||||
AND assignment.assignee_id = #{operatorId}
|
||||
<if test="keyword != null and keyword != ''">
|
||||
AND (booking.container_no LIKE CONCAT('%', #{keyword}, '%')
|
||||
OR booking.warehouse_name LIKE CONCAT('%', #{keyword}, '%'))
|
||||
|
||||
@@ -18,6 +18,7 @@ import com.baiyue.firstleg.dto.LoadingListDtos.DocumentAttachRequest;
|
||||
import com.baiyue.firstleg.dto.LoadingListDtos.DocumentCompleteRequest;
|
||||
import com.baiyue.firstleg.dto.LoadingListDtos.LifecycleResolution;
|
||||
import com.baiyue.firstleg.client.LoadingDocumentFileClient;
|
||||
import com.baiyue.firstleg.client.DomesticPdaOperatorClient;
|
||||
import com.baiyue.firstleg.entity.ContainerDocumentEntity;
|
||||
import com.baiyue.firstleg.mapper.LoadingListQueryMapper.AggregateRow;
|
||||
import com.baiyue.firstleg.mapper.LoadingListQueryMapper.HistoryRow;
|
||||
@@ -78,6 +79,8 @@ public class LoadingListService {
|
||||
private ObjectMapper objectMapper;
|
||||
@Resource
|
||||
private LoadingDocumentFileClient documentFiles;
|
||||
@Resource
|
||||
private DomesticPdaOperatorClient domesticPdaOperators;
|
||||
@Value("${baiyue.security.internal-service-token:}")
|
||||
private String internalToken;
|
||||
|
||||
@@ -509,12 +512,21 @@ public class LoadingListService {
|
||||
String idempotencyKey,
|
||||
ConfirmPreloadRequest request) {
|
||||
inboundHoldGuard.requirePlanNotHeld(planId);
|
||||
String requestHash = sha256("CONFIRM_PRELOAD|" + planId + '|' + request.expectedVersion());
|
||||
String requestHash = sha256("CONFIRM_PRELOAD|" + planId + '|' + request.expectedVersion()
|
||||
+ '|' + request.assigneeId());
|
||||
String operationId = operationId("CONFIRM_PRELOAD", planId, idempotencyKey);
|
||||
HistoryRow replay = repository.history(operationId);
|
||||
if (replay != null) return confirmationReplay(replay, requestHash);
|
||||
|
||||
AggregateRow before = requirePlan(planId);
|
||||
var operatorResponse = domesticPdaOperators.required(internalToken, request.assigneeId(), before.getWarehouseId());
|
||||
var assignee = operatorResponse == null ? null : operatorResponse.data();
|
||||
if (operatorResponse == null || !operatorResponse.successful() || assignee == null
|
||||
|| !request.assigneeId().equals(assignee.userId())
|
||||
|| !StringUtils.hasText(assignee.displayName())) {
|
||||
throw badRequest("所选国内头程 PDA 作业员无效");
|
||||
}
|
||||
|
||||
List<Link> links = loadingPickService.requirePreloadConfirmationReady(planId);
|
||||
requireContainerFeesReadyForPreloadConfirmation(links, operationId);
|
||||
if (repository.touchForPreloadConfirmation(planId, request.expectedVersion(),
|
||||
@@ -533,6 +545,12 @@ public class LoadingListService {
|
||||
.forEach(containerBookingId -> loadingPickService.beginIfReady(
|
||||
containerBookingId, operatorId, operatorName));
|
||||
|
||||
var pickTask = pickRepository.taskByPlan(planId);
|
||||
if (pickTask == null || pickRepository.createAssignment(pickTask.getId(), assignee.userId(),
|
||||
assignee.displayName().trim(), operatorId, trim(operatorName), "ASSIGNED") != 1) {
|
||||
throw conflict("PDA任务创建或分配失败,请刷新后重试");
|
||||
}
|
||||
|
||||
AggregateRow after = requirePlan(planId);
|
||||
PreloadDetail detail = repository.preloadDetail(after);
|
||||
PreloadConfirmationResponse response = new PreloadConfirmationResponse(planId,
|
||||
|
||||
@@ -24,6 +24,7 @@ import com.baiyue.firstleg.dto.LoadingPickDtos.InventorySnapshot;
|
||||
import com.baiyue.firstleg.dto.LoadingPickDtos.InventoryUnit;
|
||||
import com.baiyue.firstleg.dto.LoadingPickDtos.PendingTask;
|
||||
import com.baiyue.firstleg.domain.Iso6346ContainerNumber;
|
||||
import com.baiyue.firstleg.client.DomesticPdaOperatorClient;
|
||||
import com.baiyue.firstleg.entity.LoadingPickAssignmentEntity;
|
||||
import com.baiyue.firstleg.entity.LoadingPickHistoryEntity;
|
||||
import com.baiyue.firstleg.entity.LoadingPickItemEntity;
|
||||
@@ -54,6 +55,7 @@ import com.baiyue.firstleg.oceanbooking.repository.OceanBookingRepository;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
@@ -110,6 +112,10 @@ public class LoadingPickService {
|
||||
private InboundHoldGuard inboundHoldGuard;
|
||||
@Resource
|
||||
private com.baiyue.firstleg.mapper.DropshipPalletizationMapper dropshipPalletizationMapper;
|
||||
@Resource
|
||||
private DomesticPdaOperatorClient domesticPdaOperators;
|
||||
@Value("${baiyue.security.internal-service-token:}")
|
||||
private String internalToken;
|
||||
|
||||
/** Opens the field task after planning and ocean release; the real container number may be filled later. */
|
||||
@Transactional
|
||||
@@ -238,6 +244,15 @@ public class LoadingPickService {
|
||||
return view(requiredTask(taskId), warehouseId);
|
||||
}
|
||||
|
||||
/** PDA reads require the actual assigned operator; backstage dispatch/review retains oversight. */
|
||||
public void requireVisibleTo(long taskId, long operatorId, boolean dispatcherOrReviewer) {
|
||||
if (dispatcherOrReviewer) return;
|
||||
var assignment = repository.assignment(taskId);
|
||||
if (assignment == null || !Long.valueOf(operatorId).equals(assignment.getAssigneeId())) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "PDA任务不存在或未分配给当前作业员");
|
||||
}
|
||||
}
|
||||
|
||||
public List<PendingTask> pendingTasks(long operatorId, long warehouseId, String keyword) {
|
||||
String normalizedKeyword = StringUtils.hasText(keyword) ? keyword.trim() : null;
|
||||
return repository.pendingTasks(operatorId, warehouseId, normalizedKeyword).stream()
|
||||
@@ -267,11 +282,7 @@ public class LoadingPickService {
|
||||
if (replay != null) return replay;
|
||||
LoadingPickAssignmentEntity existing = repository.assignment(taskId);
|
||||
if (existing == null) {
|
||||
if (request.expectedAssignmentVersion() != 0
|
||||
|| repository.createAssignment(taskId, operatorId, operatorName, operatorId,
|
||||
operatorName, "CLAIMED") != 1) {
|
||||
throw conflict("PDA任务已被其他作业员领取");
|
||||
}
|
||||
throw conflict("PDA任务尚未指定作业员,请联系客服或仓管分配");
|
||||
} else {
|
||||
if (!Long.valueOf(operatorId).equals(existing.getAssigneeId())) {
|
||||
throw conflict("PDA任务已分配给其他作业员");
|
||||
@@ -289,12 +300,20 @@ public class LoadingPickService {
|
||||
public Task assign(long taskId, AssignRequest request, long operatorId, String operatorName) {
|
||||
inboundHoldGuard.requireTaskNotHeld(taskId);
|
||||
String operationId = operation(request.operationId());
|
||||
String assigneeName = request.assigneeName().trim();
|
||||
LoadingPickTaskEntity task = requiredLoadingTaskForUpdate(taskId);
|
||||
var booking = repository.activeBookingByTask(taskId);
|
||||
var operatorResponse = domesticPdaOperators.required(internalToken, request.assigneeId(), booking == null ? 0L : booking.getWarehouseId());
|
||||
var assignee = operatorResponse == null ? null : operatorResponse.data();
|
||||
if (operatorResponse == null || !operatorResponse.successful() || assignee == null
|
||||
|| !request.assigneeId().equals(assignee.userId())
|
||||
|| !StringUtils.hasText(assignee.displayName())) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "所选国内头程 PDA 作业员无效");
|
||||
}
|
||||
String assigneeName = assignee.displayName().trim();
|
||||
String requestHash = hash("ASSIGN|" + taskId + '|' + request.assigneeId() + '|'
|
||||
+ assigneeName + '|' + request.expectedAssignmentVersion());
|
||||
Task replay = replayTaskOperation(taskId, operationId, requestHash, operatorId);
|
||||
if (replay != null) return replay;
|
||||
LoadingPickTaskEntity task = requiredLoadingTaskForUpdate(taskId);
|
||||
replay = replayTaskOperationForUpdate(taskId, operationId, requestHash, operatorId);
|
||||
if (replay != null) return replay;
|
||||
LoadingPickAssignmentEntity existing = repository.assignment(taskId);
|
||||
|
||||
@@ -65,9 +65,9 @@ paths:
|
||||
'200': {$ref: '#/components/responses/PreloadDetailResponse'}
|
||||
/api/platform/v1/first-leg/loading-plans/{planId}/confirm:
|
||||
post:
|
||||
summary: 原子确认整份预装计划
|
||||
description: 以 loadingPlanId 为唯一范围,原子确认全部已分配入库预装票据。要求计划已 push 且仍为 EDITING、稳定 bridge 完整、真实订舱已关联并为 RELEASED,且真实集装箱号已经补录。任一条件不满足均不写入,也不创建 PDA task。成功后唯一创建待领取 PDA task,claim 后才推进 LOADING。同一 Idempotency-Key 与相同请求精确重放原响应,不同 Key 重复确认返回业务冲突。
|
||||
x-permission: platform:firstleg:preload:edit
|
||||
summary: 原子确认预装并分配国内 PDA 作业员
|
||||
description: 以 loadingPlanId 为唯一范围,校验指定人员为启用的国内头程 PDA 作业员,原子确认预装、创建任务并分配。未分配任务不向 PDA 展示;同一 Idempotency-Key 与相同请求精确重放原响应。
|
||||
x-permission: platform:firstleg:preload:edit, platform:firstleg:pda:assign
|
||||
parameters: [{$ref: '#/components/parameters/PlanId'}, {$ref: '#/components/parameters/IdempotencyKey'}]
|
||||
requestBody:
|
||||
required: true
|
||||
@@ -336,10 +336,11 @@ components:
|
||||
items: {type: string, enum: [LOADING_PLAN_NOT_EDITING, PRELOAD_LINK_MISSING, CONTAINER_NO_MISSING, OCEAN_BOOKING_NOT_LINKED, OCEAN_BOOKING_NOT_RELEASED, PDA_TASK_ALREADY_CREATED, PRELOAD_ALREADY_CONFIRMED]}
|
||||
ConfirmPreloadRequest:
|
||||
type: object
|
||||
required: [expectedVersion, operationReason]
|
||||
required: [expectedVersion, assigneeId]
|
||||
additionalProperties: false
|
||||
properties:
|
||||
expectedVersion: {type: integer, format: int32, minimum: 0, description: 当前装柜计划 version}
|
||||
assigneeId: {$ref: '#/components/schemas/LongId'}
|
||||
PreloadConfirmation:
|
||||
type: object
|
||||
required: [planId, containerBookingId, confirmationStatus, version, readiness]
|
||||
|
||||
+4
-2
@@ -33,8 +33,10 @@ class LoadingListControllerContractTest {
|
||||
authority("lifecycle", "platform:firstleg:preload:list", AuthenticatedUser.class, long.class, long.class);
|
||||
authority("updateTransport", "platform:firstleg:preload:edit", AuthenticatedUser.class,
|
||||
long.class, long.class, String.class, TransportUpdateRequest.class);
|
||||
authority("confirmPreload", "platform:firstleg:preload:edit", AuthenticatedUser.class,
|
||||
long.class, String.class, ConfirmPreloadRequest.class);
|
||||
Method confirm = PlatformLoadingListController.class.getDeclaredMethod("confirmPreload",
|
||||
AuthenticatedUser.class, long.class, String.class, ConfirmPreloadRequest.class);
|
||||
assertThat(confirm.getAnnotation(PreAuthorize.class).value())
|
||||
.isEqualTo("hasAuthority('platform:firstleg:preload:edit') and hasAuthority('platform:firstleg:pda:assign')");
|
||||
authority("confirmedPage", "platform:firstleg:loading-list:list", AuthenticatedUser.class,
|
||||
ConfirmedQuery.class);
|
||||
authority("confirmedDetail", "platform:firstleg:loading-list:list", AuthenticatedUser.class, long.class, long.class);
|
||||
|
||||
+3
-2
@@ -35,7 +35,7 @@ class LoadingPickPdaTaskSqlContractTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void pendingTasksAreLimitedToUnassignedOrCurrentOperatorWithoutCustomerInput() throws Exception {
|
||||
void pendingTasksAreLimitedToAssignedOperatorWithoutCustomerInput() throws Exception {
|
||||
String sql = LoadingPickTaskMapper.class
|
||||
.getDeclaredMethod("selectPendingForOperator", long.class, long.class, String.class)
|
||||
.getAnnotation(Select.class).value()[0];
|
||||
@@ -54,7 +54,8 @@ class LoadingPickPdaTaskSqlContractTest {
|
||||
.contains("pick_item.item_status IN ('PENDING', 'PICKING')")
|
||||
.contains("THEN 'AWAITING_REVIEW'")
|
||||
.contains("ELSE 'IN_PROGRESS' END AS workflowStatus")
|
||||
.contains("assignment.pick_task_id IS NULL OR assignment.assignee_id = #{operatorId}")
|
||||
.contains("AND assignment.assignee_id = #{operatorId}")
|
||||
.doesNotContain("assignment.pick_task_id IS NULL OR assignment.assignee_id = #{operatorId}")
|
||||
.contains("booking.warehouse_id AS warehouseId")
|
||||
.doesNotContain("tenantId");
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ import com.baiyue.firstleg.dto.LoadingListDtos.PreloadReadiness;
|
||||
import com.baiyue.firstleg.dto.LoadingListDtos.DocumentAttachRequest;
|
||||
import com.baiyue.firstleg.dto.LoadingListDtos.DocumentCompleteRequest;
|
||||
import com.baiyue.firstleg.client.LoadingDocumentFileClient;
|
||||
import com.baiyue.firstleg.client.DomesticPdaOperatorClient;
|
||||
import com.baiyue.firstleg.entity.LoadingPickTaskEntity;
|
||||
import com.baiyue.firstleg.entity.ContainerDocumentEntity;
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.firstleg.mapper.LoadingListQueryMapper.AggregateRow;
|
||||
@@ -63,6 +65,8 @@ class LoadingListServiceTest {
|
||||
@MockBean
|
||||
private LoadingDocumentFileClient documentFiles;
|
||||
@MockBean
|
||||
private DomesticPdaOperatorClient domesticPdaOperators;
|
||||
@MockBean
|
||||
private LoadingCompletedOutboxService loadingCompletedOutboxService;
|
||||
@MockBean
|
||||
private FbaPricingService fbaPricingService;
|
||||
@@ -784,6 +788,18 @@ class LoadingListServiceTest {
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void rejectsInvalidPdaOperatorBeforeConfirmingAnyPreload() {
|
||||
when(domesticPdaOperators.required(any(), eq(77L)))
|
||||
.thenReturn(ApiResponse.success(new DomesticPdaOperatorClient.Operator(88L, "其他人")));
|
||||
|
||||
assertThatThrownBy(() -> service.confirmPreload(10L, 9L, "operator", "invalid-operator",
|
||||
new ConfirmPreloadRequest(3, 77L)))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("作业员无效");
|
||||
verify(repository, never()).touchForPreloadConfirmation(anyLong(), anyInt(), anyLong(), anyString());
|
||||
verify(loadingPickService, never()).beginIfReady(anyLong(), anyLong(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void confirmsEveryLinkedPreloadAndCreatesSinglePdaTaskAtomically() {
|
||||
AggregateRow before = row("EDITING", 3);
|
||||
@@ -810,9 +826,16 @@ class LoadingListServiceTest {
|
||||
when(repository.confirmLinkedPreload(301L, "PENDING", 2, 9L, "operator")).thenReturn(1);
|
||||
when(repository.confirmLinkedPreload(302L, "CONFIRMING", 5, 9L, "operator")).thenReturn(1);
|
||||
when(repository.preloadDetail(after)).thenReturn(detail);
|
||||
when(domesticPdaOperators.required(any(), eq(77L)))
|
||||
.thenReturn(ApiResponse.success(new DomesticPdaOperatorClient.Operator(77L, "作业员")));
|
||||
LoadingPickTaskEntity pickTask = new LoadingPickTaskEntity();
|
||||
pickTask.setId(80L);
|
||||
when(pickRepository.taskByPlan(10L)).thenReturn(pickTask);
|
||||
when(pickRepository.createAssignment(80L, 77L, "作业员", 9L, "operator", "ASSIGNED"))
|
||||
.thenReturn(1);
|
||||
|
||||
PreloadConfirmationResponse response = service.confirmPreload(
|
||||
10L, 9L, "operator", "confirm-key", new ConfirmPreloadRequest(3));
|
||||
10L, 9L, "operator", "confirm-key", new ConfirmPreloadRequest(3, 77L));
|
||||
|
||||
assertThat(response.confirmationStatus()).isEqualTo("CONFIRMED");
|
||||
assertThat(response.version()).isEqualTo(4);
|
||||
@@ -822,6 +845,7 @@ class LoadingListServiceTest {
|
||||
verify(fbaPricingService).recalculateUnlockedContainerCustomerFees(eq(30L), eq(101L), anyString());
|
||||
verify(fbaPricingService).recalculateUnlockedContainerCustomerFees(eq(30L), eq(102L), anyString());
|
||||
verify(loadingPickService, times(1)).beginIfReady(30L, 9L, "operator");
|
||||
verify(pickRepository).createAssignment(80L, 77L, "作业员", 9L, "operator", "ASSIGNED");
|
||||
verify(repository).appendHistory(eq(10L), anyString(), eq("CONFIRM_PRELOAD"),
|
||||
eq("EDITING"), eq("EDITING"), anyString(), anyString(), eq(9L),
|
||||
eq("operator"), org.mockito.ArgumentMatchers.isNull());
|
||||
@@ -838,9 +862,11 @@ class LoadingListServiceTest {
|
||||
when(fbaPricingService.recalculateUnlockedContainerCustomerFees(eq(30L), eq(101L), anyString()))
|
||||
.thenReturn(new FbaPricingService.ContainerFeeRecalculationResult(
|
||||
FbaPricingService.ContainerFeeRecalculationStatus.NOT_READY, 0));
|
||||
when(domesticPdaOperators.required(any(), eq(77L)))
|
||||
.thenReturn(ApiResponse.success(new DomesticPdaOperatorClient.Operator(77L, "作业员")));
|
||||
|
||||
assertThatThrownBy(() -> service.confirmPreload(
|
||||
10L, 9L, "operator", "confirm-key", new ConfirmPreloadRequest(3)))
|
||||
10L, 9L, "operator", "confirm-key", new ConfirmPreloadRequest(3, 77L)))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("初始费用");
|
||||
|
||||
verify(repository, never()).touchForPreloadConfirmation(anyLong(), anyInt(), anyLong(), anyString());
|
||||
@@ -852,7 +878,7 @@ class LoadingListServiceTest {
|
||||
void exactConfirmationReplayReturnsFrozenResponseWithoutTouchingPlan() throws Exception {
|
||||
PreloadConfirmationResponse expected = new PreloadConfirmationResponse(
|
||||
10L, 30L, "CONFIRMED", 4, readiness("CONFIRMED", false, true, 80L));
|
||||
String requestHash = sha256("CONFIRM_PRELOAD|10|3");
|
||||
String requestHash = sha256("CONFIRM_PRELOAD|10|3|77");
|
||||
HistoryRow replay = new HistoryRow();
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
replay.setAfterSnapshot(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(
|
||||
@@ -860,7 +886,7 @@ class LoadingListServiceTest {
|
||||
when(repository.history(anyString())).thenReturn(replay);
|
||||
|
||||
PreloadConfirmationResponse actual = service.confirmPreload(
|
||||
10L, 9L, "operator", "confirm-key", new ConfirmPreloadRequest(3));
|
||||
10L, 9L, "operator", "confirm-key", new ConfirmPreloadRequest(3, 77L));
|
||||
|
||||
assertThat(actual).isEqualTo(expected);
|
||||
verify(loadingPickService, never()).requirePreloadConfirmationReady(anyLong());
|
||||
@@ -876,14 +902,14 @@ class LoadingListServiceTest {
|
||||
void rejectsConfirmationReplayWhenExpectedVersionDiffers() throws Exception {
|
||||
PreloadConfirmationResponse expected = new PreloadConfirmationResponse(
|
||||
10L, 30L, "CONFIRMED", 4, readiness("CONFIRMED", false, true, 80L));
|
||||
String originalHash = sha256("CONFIRM_PRELOAD|10|3");
|
||||
String originalHash = sha256("CONFIRM_PRELOAD|10|3|77");
|
||||
HistoryRow replay = new HistoryRow();
|
||||
replay.setAfterSnapshot(new ObjectMapper().writeValueAsString(
|
||||
java.util.Map.of("requestHash", originalHash, "response", expected)));
|
||||
when(repository.history(anyString())).thenReturn(replay);
|
||||
|
||||
assertThatThrownBy(() -> service.confirmPreload(
|
||||
10L, 9L, "operator", "confirm-key", new ConfirmPreloadRequest(4)))
|
||||
10L, 9L, "operator", "confirm-key", new ConfirmPreloadRequest(4, 77L)))
|
||||
.isInstanceOf(BizException.class)
|
||||
.extracting(error -> ((BizException) error).getResultCode())
|
||||
.isEqualTo(ResultCode.CONFLICT);
|
||||
@@ -898,11 +924,13 @@ class LoadingListServiceTest {
|
||||
AggregateRow confirmed = row("EDITING", 4);
|
||||
when(repository.history(anyString())).thenReturn(null);
|
||||
when(repository.aggregate(10L)).thenReturn(confirmed);
|
||||
when(domesticPdaOperators.required(any(), eq(77L)))
|
||||
.thenReturn(ApiResponse.success(new DomesticPdaOperatorClient.Operator(77L, "作业员")));
|
||||
when(loadingPickService.requirePreloadConfirmationReady(10L))
|
||||
.thenThrow(new BizException(ResultCode.CONFLICT, "预装计划已确认"));
|
||||
|
||||
assertThatThrownBy(() -> service.confirmPreload(
|
||||
10L, 9L, "operator", "new-confirm-key", new ConfirmPreloadRequest(4)))
|
||||
10L, 9L, "operator", "new-confirm-key", new ConfirmPreloadRequest(4, 77L)))
|
||||
.isInstanceOf(BizException.class)
|
||||
.extracting(error -> ((BizException) error).getResultCode())
|
||||
.isEqualTo(ResultCode.CONFLICT);
|
||||
@@ -919,11 +947,13 @@ class LoadingListServiceTest {
|
||||
AggregateRow before = row("EDITING", 3);
|
||||
when(repository.history(anyString())).thenReturn(null);
|
||||
when(repository.aggregate(10L)).thenReturn(before);
|
||||
when(domesticPdaOperators.required(any(), eq(77L)))
|
||||
.thenReturn(ApiResponse.success(new DomesticPdaOperatorClient.Operator(77L, "作业员")));
|
||||
when(loadingPickService.requirePreloadConfirmationReady(10L))
|
||||
.thenThrow(new BizException(ResultCode.CONFLICT, "第二票订舱尚未放舱"));
|
||||
|
||||
assertThatThrownBy(() -> service.confirmPreload(
|
||||
10L, 9L, "operator", "confirm-key", new ConfirmPreloadRequest(3)))
|
||||
10L, 9L, "operator", "confirm-key", new ConfirmPreloadRequest(3, 77L)))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("第二票");
|
||||
|
||||
verify(repository, never()).touchForPreloadConfirmation(anyLong(),
|
||||
@@ -940,7 +970,7 @@ class LoadingListServiceTest {
|
||||
when(repository.history(anyString())).thenReturn(replay);
|
||||
|
||||
assertThatThrownBy(() -> service.confirmPreload(
|
||||
10L, 9L, "operator", "confirm-key", new ConfirmPreloadRequest(4)))
|
||||
10L, 9L, "operator", "confirm-key", new ConfirmPreloadRequest(4, 77L)))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("不同请求");
|
||||
|
||||
verify(loadingPickService, never()).requirePreloadConfirmationReady(anyLong());
|
||||
|
||||
@@ -11,6 +11,8 @@ import com.baiyue.firstleg.dto.LoadingPickDtos.ClaimRequest;
|
||||
import com.baiyue.firstleg.dto.LoadingPickDtos.AssignRequest;
|
||||
import com.baiyue.firstleg.dto.LoadingPickDtos.CompleteLoadingRequest;
|
||||
import com.baiyue.firstleg.dto.LoadingPickDtos.Task;
|
||||
import com.baiyue.firstleg.client.DomesticPdaOperatorClient;
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.firstleg.entity.InboundPalletAllocationEntity;
|
||||
import com.baiyue.firstleg.entity.LoadingPickAssignmentEntity;
|
||||
import com.baiyue.firstleg.entity.LoadingPickHistoryEntity;
|
||||
@@ -106,6 +108,8 @@ class LoadingPickServiceTest {
|
||||
private InboundHoldGuard inboundHoldGuard;
|
||||
@MockBean
|
||||
private DropshipPalletizationMapper dropshipPallets;
|
||||
@MockBean
|
||||
private DomesticPdaOperatorClient domesticPdaOperators;
|
||||
@Resource
|
||||
private LoadingPickService service;
|
||||
|
||||
@@ -565,8 +569,7 @@ class LoadingPickServiceTest {
|
||||
when(repository.operation("claim-2")).thenReturn(null);
|
||||
when(repository.task(20L)).thenReturn(task);
|
||||
when(repository.taskForUpdate(20L)).thenReturn(task);
|
||||
when(repository.assignment(20L)).thenReturn(null, assignment);
|
||||
when(repository.createAssignment(20L, 9L, "operator", 9L, "operator", "CLAIMED")).thenReturn(1);
|
||||
when(repository.assignment(20L)).thenReturn(assignment);
|
||||
when(smartLoadingRepository.startPicking(10L, 9L, "operator")).thenReturn(1, 0);
|
||||
doAnswer(invocation -> {
|
||||
LoadingPickOperationEntity value = invocation.getArgument(0);
|
||||
@@ -574,9 +577,9 @@ class LoadingPickServiceTest {
|
||||
return null;
|
||||
}).when(repository).insert(any(LoadingPickOperationEntity.class));
|
||||
|
||||
service.claim(20L, new ClaimRequest(0, "claim-1"), 9L, "operator");
|
||||
service.claim(20L, new ClaimRequest(0, "claim-1"), 9L, "operator");
|
||||
service.claim(20L, new ClaimRequest(0, "claim-2"), 9L, "operator");
|
||||
service.claim(20L, new ClaimRequest(assignment.getVersion(), "claim-1"), 9L, "operator");
|
||||
service.claim(20L, new ClaimRequest(assignment.getVersion(), "claim-1"), 9L, "operator");
|
||||
service.claim(20L, new ClaimRequest(assignment.getVersion(), "claim-2"), 9L, "operator");
|
||||
|
||||
verify(smartLoadingRepository, times(2)).startPicking(10L, 9L, "operator");
|
||||
verify(smartLoadingRepository, times(1)).insert(argThat(
|
||||
@@ -585,7 +588,8 @@ class LoadingPickServiceTest {
|
||||
&& "EDITING".equals(history.getFromStatus())
|
||||
&& "LOADING".equals(history.getToStatus())
|
||||
&& "PDA-START-20".equals(history.getOperationId())));
|
||||
verify(repository, times(1)).createAssignment(20L, 9L, "operator", 9L, "operator", "CLAIMED");
|
||||
verify(repository, never()).createAssignment(anyLong(), anyLong(), anyString(), anyLong(),
|
||||
anyString(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -606,6 +610,17 @@ class LoadingPickServiceTest {
|
||||
verify(smartLoadingRepository, never()).startPicking(anyLong(), anyLong(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void unassignedTaskCannotBeClaimedFromPda() {
|
||||
when(repository.taskForUpdate(20L)).thenReturn(task());
|
||||
|
||||
assertThatThrownBy(() -> service.claim(20L, new ClaimRequest(0, "claim-unassigned"),
|
||||
9L, "operator"))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("尚未指定作业员");
|
||||
verify(repository, never()).createAssignment(anyLong(), anyLong(), anyString(),
|
||||
anyLong(), anyString(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void assignRechecksSameOperationAfterWaitingForTaskLock() {
|
||||
LoadingPickTaskEntity task = task();
|
||||
@@ -615,6 +630,8 @@ class LoadingPickServiceTest {
|
||||
when(repository.taskForUpdate(20L)).thenReturn(task);
|
||||
when(repository.task(20L)).thenReturn(task);
|
||||
when(repository.items(20L)).thenReturn(List.of());
|
||||
when(domesticPdaOperators.required(any(), eq(11L)))
|
||||
.thenReturn(ApiResponse.success(new DomesticPdaOperatorClient.Operator(11L, "picker")));
|
||||
|
||||
Task replay = service.assign(20L, new AssignRequest(11L, "picker", 0, "assign-race"),
|
||||
9L, "operator");
|
||||
|
||||
Reference in New Issue
Block a user