货柜pda 添加操作人

This commit is contained in:
2026-09-22 17:27:32 +08:00
parent abca6882c3
commit 2555eda797
31 changed files with 384 additions and 59 deletions
@@ -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) { }
}
@@ -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,
@@ -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);