货柜pda 添加操作人
This commit is contained in:
@@ -22,11 +22,19 @@ public class AuthorizationSnapshot {
|
|||||||
private Set<String> permissions;
|
private Set<String> permissions;
|
||||||
private Long permissionVersion;
|
private Long permissionVersion;
|
||||||
private Long overseasWarehouseId;
|
private Long overseasWarehouseId;
|
||||||
|
private Long domesticWarehouseId;
|
||||||
|
|
||||||
public AuthorizationSnapshot(Long userId, Long tenantId, Long membershipId, String appCode,
|
public AuthorizationSnapshot(Long userId, Long tenantId, Long membershipId, String appCode,
|
||||||
String clientType, Set<String> roles, Set<String> permissions,
|
String clientType, Set<String> roles, Set<String> permissions,
|
||||||
Long permissionVersion) {
|
Long permissionVersion) {
|
||||||
this(userId, tenantId, membershipId, appCode, clientType, roles, permissions,
|
this(userId, tenantId, membershipId, appCode, clientType, roles, permissions,
|
||||||
permissionVersion, null);
|
permissionVersion, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AuthorizationSnapshot(Long userId, Long tenantId, Long membershipId, String appCode,
|
||||||
|
String clientType, Set<String> roles, Set<String> permissions,
|
||||||
|
Long permissionVersion, Long overseasWarehouseId) {
|
||||||
|
this(userId, tenantId, membershipId, appCode, clientType, roles, permissions,
|
||||||
|
permissionVersion, overseasWarehouseId, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,6 +135,9 @@ public class AuthService {
|
|||||||
if (authorization.getOverseasWarehouseId() != null) {
|
if (authorization.getOverseasWarehouseId() != null) {
|
||||||
claimsBuilder.claim("overseas_warehouse_id", authorization.getOverseasWarehouseId());
|
claimsBuilder.claim("overseas_warehouse_id", authorization.getOverseasWarehouseId());
|
||||||
}
|
}
|
||||||
|
if (authorization.getDomesticWarehouseId() != null) {
|
||||||
|
claimsBuilder.claim("domestic_warehouse_id", authorization.getDomesticWarehouseId());
|
||||||
|
}
|
||||||
JwtClaimsSet claims = claimsBuilder.build();
|
JwtClaimsSet claims = claimsBuilder.build();
|
||||||
String accessToken = jwtEncoder.encode(JwtEncoderParameters.from(
|
String accessToken = jwtEncoder.encode(JwtEncoderParameters.from(
|
||||||
signingHeader(), claims)).getTokenValue();
|
signingHeader(), claims)).getTokenValue();
|
||||||
|
|||||||
@@ -124,6 +124,7 @@ CREATE TABLE iam_membership (
|
|||||||
member_type varchar(20) NOT NULL DEFAULT 'EMPLOYEE' COMMENT '成员类型:EMPLOYEE企业员工等',
|
member_type varchar(20) NOT NULL DEFAULT 'EMPLOYEE' COMMENT '成员类型:EMPLOYEE企业员工等',
|
||||||
member_no varchar(64) DEFAULT NULL COMMENT '客户企业内部成员编号',
|
member_no varchar(64) DEFAULT NULL COMMENT '客户企业内部成员编号',
|
||||||
overseas_warehouse_id bigint DEFAULT NULL COMMENT '海外仓PDA操作员绑定的唯一实际海外仓ID',
|
overseas_warehouse_id bigint DEFAULT NULL COMMENT '海外仓PDA操作员绑定的唯一实际海外仓ID',
|
||||||
|
domestic_warehouse_id bigint DEFAULT NULL COMMENT '国内头程PDA操作员绑定的唯一国内作业仓ID',
|
||||||
status varchar(20) NOT NULL DEFAULT 'ACTIVE' COMMENT '成员状态:ACTIVE在职、DISABLED停用、LEFT已离开',
|
status varchar(20) NOT NULL DEFAULT 'ACTIVE' COMMENT '成员状态:ACTIVE在职、DISABLED停用、LEFT已离开',
|
||||||
joined_at datetime(3) DEFAULT NULL COMMENT '加入客户企业时间',
|
joined_at datetime(3) DEFAULT NULL COMMENT '加入客户企业时间',
|
||||||
left_at datetime(3) DEFAULT NULL COMMENT '离开客户企业时间',
|
left_at datetime(3) DEFAULT NULL COMMENT '离开客户企业时间',
|
||||||
@@ -6814,6 +6815,9 @@ INSERT INTO iam_role_permission (role_id, permission_id, tenant_id, created_at)
|
|||||||
INSERT INTO iam_role_permission (role_id, permission_id, tenant_id, created_at) VALUES (4000004,5102031,0,CURRENT_TIMESTAMP(3));
|
INSERT INTO iam_role_permission (role_id, permission_id, tenant_id, created_at) VALUES (4000004,5102031,0,CURRENT_TIMESTAMP(3));
|
||||||
INSERT INTO iam_role_permission (role_id, permission_id, tenant_id, created_at) VALUES (4000004,5102124,0,CURRENT_TIMESTAMP(3));
|
INSERT INTO iam_role_permission (role_id, permission_id, tenant_id, created_at) VALUES (4000004,5102124,0,CURRENT_TIMESTAMP(3));
|
||||||
INSERT INTO iam_role_permission (role_id, permission_id, tenant_id, created_at) VALUES (4000004,5102125,0,CURRENT_TIMESTAMP(3));
|
INSERT INTO iam_role_permission (role_id, permission_id, tenant_id, created_at) VALUES (4000004,5102125,0,CURRENT_TIMESTAMP(3));
|
||||||
|
INSERT INTO iam_role_permission (role_id, permission_id, tenant_id, created_at) VALUES (4000004,5102100,0,CURRENT_TIMESTAMP(3));
|
||||||
|
INSERT INTO iam_role_permission (role_id, permission_id, tenant_id, created_at) VALUES (4000004,5102103,0,CURRENT_TIMESTAMP(3));
|
||||||
|
INSERT INTO iam_role_permission (role_id, permission_id, tenant_id, created_at) VALUES (4000004,5102191,0,CURRENT_TIMESTAMP(3));
|
||||||
INSERT INTO iam_role_permission (role_id, permission_id, tenant_id, created_at) VALUES (4000004,5107000,0,CURRENT_TIMESTAMP(3));
|
INSERT INTO iam_role_permission (role_id, permission_id, tenant_id, created_at) VALUES (4000004,5107000,0,CURRENT_TIMESTAMP(3));
|
||||||
INSERT INTO iam_role_permission (role_id, permission_id, tenant_id, created_at) VALUES (4000004,5107010,0,CURRENT_TIMESTAMP(3));
|
INSERT INTO iam_role_permission (role_id, permission_id, tenant_id, created_at) VALUES (4000004,5107010,0,CURRENT_TIMESTAMP(3));
|
||||||
INSERT INTO iam_role_permission (role_id, permission_id, tenant_id, created_at) VALUES (4000004,5107016,0,CURRENT_TIMESTAMP(3));
|
INSERT INTO iam_role_permission (role_id, permission_id, tenant_id, created_at) VALUES (4000004,5107016,0,CURRENT_TIMESTAMP(3));
|
||||||
|
|||||||
@@ -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));
|
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")
|
@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(
|
public ApiResponse<PreloadConfirmationResponse> confirmPreload(
|
||||||
@AuthenticationPrincipal AuthenticatedUser user,
|
@AuthenticationPrincipal AuthenticatedUser user,
|
||||||
@PathVariable @Positive long planId,
|
@PathVariable @Positive long planId,
|
||||||
|
|||||||
+11
-1
@@ -64,7 +64,9 @@ public class PlatformLoadingPdaController {
|
|||||||
@RequestParam(required = false) Long loadingPlanId,
|
@RequestParam(required = false) Long loadingPlanId,
|
||||||
@RequestParam @Positive long warehouseId) {
|
@RequestParam @Positive long warehouseId) {
|
||||||
FirstlegScopeGuard.platform(user);
|
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")
|
@GetMapping("/tasks/pending")
|
||||||
@@ -90,6 +92,7 @@ public class PlatformLoadingPdaController {
|
|||||||
@PathVariable @Positive long taskId,
|
@PathVariable @Positive long taskId,
|
||||||
@RequestParam @Positive long warehouseId) {
|
@RequestParam @Positive long warehouseId) {
|
||||||
FirstlegScopeGuard.platform(user);
|
FirstlegScopeGuard.platform(user);
|
||||||
|
service.requireVisibleTo(taskId, user.userId(), canManage(user));
|
||||||
return ApiResponse.success(service.task(taskId, warehouseId));
|
return ApiResponse.success(service.task(taskId, warehouseId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,6 +146,7 @@ public class PlatformLoadingPdaController {
|
|||||||
@PathVariable @Positive long taskId,
|
@PathVariable @Positive long taskId,
|
||||||
@RequestParam @Positive long warehouseId) {
|
@RequestParam @Positive long warehouseId) {
|
||||||
FirstlegScopeGuard.platform(user);
|
FirstlegScopeGuard.platform(user);
|
||||||
|
service.requireVisibleTo(taskId, user.userId(), canManage(user));
|
||||||
return ApiResponse.success(service.inventorySnapshot(taskId, warehouseId));
|
return ApiResponse.success(service.inventorySnapshot(taskId, warehouseId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,7 +242,13 @@ public class PlatformLoadingPdaController {
|
|||||||
public ApiResponse<List<Operation>> operations(@AuthenticationPrincipal AuthenticatedUser user,
|
public ApiResponse<List<Operation>> operations(@AuthenticationPrincipal AuthenticatedUser user,
|
||||||
@PathVariable @Positive long taskId) {
|
@PathVariable @Positive long taskId) {
|
||||||
FirstlegScopeGuard.platform(user);
|
FirstlegScopeGuard.platform(user);
|
||||||
|
service.requireVisibleTo(taskId, user.userId(), canManage(user));
|
||||||
return ApiResponse.success(service.operations(taskId));
|
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(
|
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
|
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'
|
WHERE task.task_status IN ('LOADING','COMPLETING','COMPLETION_FAILED') AND booking.record_status = 'ACTIVE'
|
||||||
AND booking.warehouse_id = #{warehouseId}
|
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 != ''">
|
<if test="keyword != null and keyword != ''">
|
||||||
AND (booking.container_no LIKE CONCAT('%', #{keyword}, '%')
|
AND (booking.container_no LIKE CONCAT('%', #{keyword}, '%')
|
||||||
OR booking.warehouse_name 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.DocumentCompleteRequest;
|
||||||
import com.baiyue.firstleg.dto.LoadingListDtos.LifecycleResolution;
|
import com.baiyue.firstleg.dto.LoadingListDtos.LifecycleResolution;
|
||||||
import com.baiyue.firstleg.client.LoadingDocumentFileClient;
|
import com.baiyue.firstleg.client.LoadingDocumentFileClient;
|
||||||
|
import com.baiyue.firstleg.client.DomesticPdaOperatorClient;
|
||||||
import com.baiyue.firstleg.entity.ContainerDocumentEntity;
|
import com.baiyue.firstleg.entity.ContainerDocumentEntity;
|
||||||
import com.baiyue.firstleg.mapper.LoadingListQueryMapper.AggregateRow;
|
import com.baiyue.firstleg.mapper.LoadingListQueryMapper.AggregateRow;
|
||||||
import com.baiyue.firstleg.mapper.LoadingListQueryMapper.HistoryRow;
|
import com.baiyue.firstleg.mapper.LoadingListQueryMapper.HistoryRow;
|
||||||
@@ -78,6 +79,8 @@ public class LoadingListService {
|
|||||||
private ObjectMapper objectMapper;
|
private ObjectMapper objectMapper;
|
||||||
@Resource
|
@Resource
|
||||||
private LoadingDocumentFileClient documentFiles;
|
private LoadingDocumentFileClient documentFiles;
|
||||||
|
@Resource
|
||||||
|
private DomesticPdaOperatorClient domesticPdaOperators;
|
||||||
@Value("${baiyue.security.internal-service-token:}")
|
@Value("${baiyue.security.internal-service-token:}")
|
||||||
private String internalToken;
|
private String internalToken;
|
||||||
|
|
||||||
@@ -509,12 +512,21 @@ public class LoadingListService {
|
|||||||
String idempotencyKey,
|
String idempotencyKey,
|
||||||
ConfirmPreloadRequest request) {
|
ConfirmPreloadRequest request) {
|
||||||
inboundHoldGuard.requirePlanNotHeld(planId);
|
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);
|
String operationId = operationId("CONFIRM_PRELOAD", planId, idempotencyKey);
|
||||||
HistoryRow replay = repository.history(operationId);
|
HistoryRow replay = repository.history(operationId);
|
||||||
if (replay != null) return confirmationReplay(replay, requestHash);
|
if (replay != null) return confirmationReplay(replay, requestHash);
|
||||||
|
|
||||||
AggregateRow before = requirePlan(planId);
|
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);
|
List<Link> links = loadingPickService.requirePreloadConfirmationReady(planId);
|
||||||
requireContainerFeesReadyForPreloadConfirmation(links, operationId);
|
requireContainerFeesReadyForPreloadConfirmation(links, operationId);
|
||||||
if (repository.touchForPreloadConfirmation(planId, request.expectedVersion(),
|
if (repository.touchForPreloadConfirmation(planId, request.expectedVersion(),
|
||||||
@@ -533,6 +545,12 @@ public class LoadingListService {
|
|||||||
.forEach(containerBookingId -> loadingPickService.beginIfReady(
|
.forEach(containerBookingId -> loadingPickService.beginIfReady(
|
||||||
containerBookingId, operatorId, operatorName));
|
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);
|
AggregateRow after = requirePlan(planId);
|
||||||
PreloadDetail detail = repository.preloadDetail(after);
|
PreloadDetail detail = repository.preloadDetail(after);
|
||||||
PreloadConfirmationResponse response = new PreloadConfirmationResponse(planId,
|
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.InventoryUnit;
|
||||||
import com.baiyue.firstleg.dto.LoadingPickDtos.PendingTask;
|
import com.baiyue.firstleg.dto.LoadingPickDtos.PendingTask;
|
||||||
import com.baiyue.firstleg.domain.Iso6346ContainerNumber;
|
import com.baiyue.firstleg.domain.Iso6346ContainerNumber;
|
||||||
|
import com.baiyue.firstleg.client.DomesticPdaOperatorClient;
|
||||||
import com.baiyue.firstleg.entity.LoadingPickAssignmentEntity;
|
import com.baiyue.firstleg.entity.LoadingPickAssignmentEntity;
|
||||||
import com.baiyue.firstleg.entity.LoadingPickHistoryEntity;
|
import com.baiyue.firstleg.entity.LoadingPickHistoryEntity;
|
||||||
import com.baiyue.firstleg.entity.LoadingPickItemEntity;
|
import com.baiyue.firstleg.entity.LoadingPickItemEntity;
|
||||||
@@ -54,6 +55,7 @@ import com.baiyue.firstleg.oceanbooking.repository.OceanBookingRepository;
|
|||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import org.springframework.dao.DuplicateKeyException;
|
import org.springframework.dao.DuplicateKeyException;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.transaction.support.TransactionTemplate;
|
import org.springframework.transaction.support.TransactionTemplate;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
@@ -110,6 +112,10 @@ public class LoadingPickService {
|
|||||||
private InboundHoldGuard inboundHoldGuard;
|
private InboundHoldGuard inboundHoldGuard;
|
||||||
@Resource
|
@Resource
|
||||||
private com.baiyue.firstleg.mapper.DropshipPalletizationMapper dropshipPalletizationMapper;
|
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. */
|
/** Opens the field task after planning and ocean release; the real container number may be filled later. */
|
||||||
@Transactional
|
@Transactional
|
||||||
@@ -238,6 +244,15 @@ public class LoadingPickService {
|
|||||||
return view(requiredTask(taskId), warehouseId);
|
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) {
|
public List<PendingTask> pendingTasks(long operatorId, long warehouseId, String keyword) {
|
||||||
String normalizedKeyword = StringUtils.hasText(keyword) ? keyword.trim() : null;
|
String normalizedKeyword = StringUtils.hasText(keyword) ? keyword.trim() : null;
|
||||||
return repository.pendingTasks(operatorId, warehouseId, normalizedKeyword).stream()
|
return repository.pendingTasks(operatorId, warehouseId, normalizedKeyword).stream()
|
||||||
@@ -267,11 +282,7 @@ public class LoadingPickService {
|
|||||||
if (replay != null) return replay;
|
if (replay != null) return replay;
|
||||||
LoadingPickAssignmentEntity existing = repository.assignment(taskId);
|
LoadingPickAssignmentEntity existing = repository.assignment(taskId);
|
||||||
if (existing == null) {
|
if (existing == null) {
|
||||||
if (request.expectedAssignmentVersion() != 0
|
throw conflict("PDA任务尚未指定作业员,请联系客服或仓管分配");
|
||||||
|| repository.createAssignment(taskId, operatorId, operatorName, operatorId,
|
|
||||||
operatorName, "CLAIMED") != 1) {
|
|
||||||
throw conflict("PDA任务已被其他作业员领取");
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (!Long.valueOf(operatorId).equals(existing.getAssigneeId())) {
|
if (!Long.valueOf(operatorId).equals(existing.getAssigneeId())) {
|
||||||
throw conflict("PDA任务已分配给其他作业员");
|
throw conflict("PDA任务已分配给其他作业员");
|
||||||
@@ -289,12 +300,20 @@ public class LoadingPickService {
|
|||||||
public Task assign(long taskId, AssignRequest request, long operatorId, String operatorName) {
|
public Task assign(long taskId, AssignRequest request, long operatorId, String operatorName) {
|
||||||
inboundHoldGuard.requireTaskNotHeld(taskId);
|
inboundHoldGuard.requireTaskNotHeld(taskId);
|
||||||
String operationId = operation(request.operationId());
|
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() + '|'
|
String requestHash = hash("ASSIGN|" + taskId + '|' + request.assigneeId() + '|'
|
||||||
+ assigneeName + '|' + request.expectedAssignmentVersion());
|
+ assigneeName + '|' + request.expectedAssignmentVersion());
|
||||||
Task replay = replayTaskOperation(taskId, operationId, requestHash, operatorId);
|
Task replay = replayTaskOperation(taskId, operationId, requestHash, operatorId);
|
||||||
if (replay != null) return replay;
|
if (replay != null) return replay;
|
||||||
LoadingPickTaskEntity task = requiredLoadingTaskForUpdate(taskId);
|
|
||||||
replay = replayTaskOperationForUpdate(taskId, operationId, requestHash, operatorId);
|
replay = replayTaskOperationForUpdate(taskId, operationId, requestHash, operatorId);
|
||||||
if (replay != null) return replay;
|
if (replay != null) return replay;
|
||||||
LoadingPickAssignmentEntity existing = repository.assignment(taskId);
|
LoadingPickAssignmentEntity existing = repository.assignment(taskId);
|
||||||
|
|||||||
@@ -65,9 +65,9 @@ paths:
|
|||||||
'200': {$ref: '#/components/responses/PreloadDetailResponse'}
|
'200': {$ref: '#/components/responses/PreloadDetailResponse'}
|
||||||
/api/platform/v1/first-leg/loading-plans/{planId}/confirm:
|
/api/platform/v1/first-leg/loading-plans/{planId}/confirm:
|
||||||
post:
|
post:
|
||||||
summary: 原子确认整份预装计划
|
summary: 原子确认预装并分配国内 PDA 作业员
|
||||||
description: 以 loadingPlanId 为唯一范围,原子确认全部已分配入库预装票据。要求计划已 push 且仍为 EDITING、稳定 bridge 完整、真实订舱已关联并为 RELEASED,且真实集装箱号已经补录。任一条件不满足均不写入,也不创建 PDA task。成功后唯一创建待领取 PDA task,claim 后才推进 LOADING。同一 Idempotency-Key 与相同请求精确重放原响应,不同 Key 重复确认返回业务冲突。
|
description: 以 loadingPlanId 为唯一范围,校验指定人员为启用的国内头程 PDA 作业员,原子确认预装、创建任务并分配。未分配任务不向 PDA 展示;同一 Idempotency-Key 与相同请求精确重放原响应。
|
||||||
x-permission: platform:firstleg:preload:edit
|
x-permission: platform:firstleg:preload:edit, platform:firstleg:pda:assign
|
||||||
parameters: [{$ref: '#/components/parameters/PlanId'}, {$ref: '#/components/parameters/IdempotencyKey'}]
|
parameters: [{$ref: '#/components/parameters/PlanId'}, {$ref: '#/components/parameters/IdempotencyKey'}]
|
||||||
requestBody:
|
requestBody:
|
||||||
required: true
|
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]}
|
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:
|
ConfirmPreloadRequest:
|
||||||
type: object
|
type: object
|
||||||
required: [expectedVersion, operationReason]
|
required: [expectedVersion, assigneeId]
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
expectedVersion: {type: integer, format: int32, minimum: 0, description: 当前装柜计划 version}
|
expectedVersion: {type: integer, format: int32, minimum: 0, description: 当前装柜计划 version}
|
||||||
|
assigneeId: {$ref: '#/components/schemas/LongId'}
|
||||||
PreloadConfirmation:
|
PreloadConfirmation:
|
||||||
type: object
|
type: object
|
||||||
required: [planId, containerBookingId, confirmationStatus, version, readiness]
|
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("lifecycle", "platform:firstleg:preload:list", AuthenticatedUser.class, long.class, long.class);
|
||||||
authority("updateTransport", "platform:firstleg:preload:edit", AuthenticatedUser.class,
|
authority("updateTransport", "platform:firstleg:preload:edit", AuthenticatedUser.class,
|
||||||
long.class, long.class, String.class, TransportUpdateRequest.class);
|
long.class, long.class, String.class, TransportUpdateRequest.class);
|
||||||
authority("confirmPreload", "platform:firstleg:preload:edit", AuthenticatedUser.class,
|
Method confirm = PlatformLoadingListController.class.getDeclaredMethod("confirmPreload",
|
||||||
long.class, String.class, ConfirmPreloadRequest.class);
|
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,
|
authority("confirmedPage", "platform:firstleg:loading-list:list", AuthenticatedUser.class,
|
||||||
ConfirmedQuery.class);
|
ConfirmedQuery.class);
|
||||||
authority("confirmedDetail", "platform:firstleg:loading-list:list", AuthenticatedUser.class, long.class, long.class);
|
authority("confirmedDetail", "platform:firstleg:loading-list:list", AuthenticatedUser.class, long.class, long.class);
|
||||||
|
|||||||
+3
-2
@@ -35,7 +35,7 @@ class LoadingPickPdaTaskSqlContractTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void pendingTasksAreLimitedToUnassignedOrCurrentOperatorWithoutCustomerInput() throws Exception {
|
void pendingTasksAreLimitedToAssignedOperatorWithoutCustomerInput() throws Exception {
|
||||||
String sql = LoadingPickTaskMapper.class
|
String sql = LoadingPickTaskMapper.class
|
||||||
.getDeclaredMethod("selectPendingForOperator", long.class, long.class, String.class)
|
.getDeclaredMethod("selectPendingForOperator", long.class, long.class, String.class)
|
||||||
.getAnnotation(Select.class).value()[0];
|
.getAnnotation(Select.class).value()[0];
|
||||||
@@ -54,7 +54,8 @@ class LoadingPickPdaTaskSqlContractTest {
|
|||||||
.contains("pick_item.item_status IN ('PENDING', 'PICKING')")
|
.contains("pick_item.item_status IN ('PENDING', 'PICKING')")
|
||||||
.contains("THEN 'AWAITING_REVIEW'")
|
.contains("THEN 'AWAITING_REVIEW'")
|
||||||
.contains("ELSE 'IN_PROGRESS' END AS workflowStatus")
|
.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")
|
.contains("booking.warehouse_id AS warehouseId")
|
||||||
.doesNotContain("tenantId");
|
.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.DocumentAttachRequest;
|
||||||
import com.baiyue.firstleg.dto.LoadingListDtos.DocumentCompleteRequest;
|
import com.baiyue.firstleg.dto.LoadingListDtos.DocumentCompleteRequest;
|
||||||
import com.baiyue.firstleg.client.LoadingDocumentFileClient;
|
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.firstleg.entity.ContainerDocumentEntity;
|
||||||
import com.baiyue.common.core.api.ApiResponse;
|
import com.baiyue.common.core.api.ApiResponse;
|
||||||
import com.baiyue.firstleg.mapper.LoadingListQueryMapper.AggregateRow;
|
import com.baiyue.firstleg.mapper.LoadingListQueryMapper.AggregateRow;
|
||||||
@@ -63,6 +65,8 @@ class LoadingListServiceTest {
|
|||||||
@MockBean
|
@MockBean
|
||||||
private LoadingDocumentFileClient documentFiles;
|
private LoadingDocumentFileClient documentFiles;
|
||||||
@MockBean
|
@MockBean
|
||||||
|
private DomesticPdaOperatorClient domesticPdaOperators;
|
||||||
|
@MockBean
|
||||||
private LoadingCompletedOutboxService loadingCompletedOutboxService;
|
private LoadingCompletedOutboxService loadingCompletedOutboxService;
|
||||||
@MockBean
|
@MockBean
|
||||||
private FbaPricingService fbaPricingService;
|
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
|
@Test
|
||||||
void confirmsEveryLinkedPreloadAndCreatesSinglePdaTaskAtomically() {
|
void confirmsEveryLinkedPreloadAndCreatesSinglePdaTaskAtomically() {
|
||||||
AggregateRow before = row("EDITING", 3);
|
AggregateRow before = row("EDITING", 3);
|
||||||
@@ -810,9 +826,16 @@ class LoadingListServiceTest {
|
|||||||
when(repository.confirmLinkedPreload(301L, "PENDING", 2, 9L, "operator")).thenReturn(1);
|
when(repository.confirmLinkedPreload(301L, "PENDING", 2, 9L, "operator")).thenReturn(1);
|
||||||
when(repository.confirmLinkedPreload(302L, "CONFIRMING", 5, 9L, "operator")).thenReturn(1);
|
when(repository.confirmLinkedPreload(302L, "CONFIRMING", 5, 9L, "operator")).thenReturn(1);
|
||||||
when(repository.preloadDetail(after)).thenReturn(detail);
|
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(
|
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.confirmationStatus()).isEqualTo("CONFIRMED");
|
||||||
assertThat(response.version()).isEqualTo(4);
|
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(101L), anyString());
|
||||||
verify(fbaPricingService).recalculateUnlockedContainerCustomerFees(eq(30L), eq(102L), anyString());
|
verify(fbaPricingService).recalculateUnlockedContainerCustomerFees(eq(30L), eq(102L), anyString());
|
||||||
verify(loadingPickService, times(1)).beginIfReady(30L, 9L, "operator");
|
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"),
|
verify(repository).appendHistory(eq(10L), anyString(), eq("CONFIRM_PRELOAD"),
|
||||||
eq("EDITING"), eq("EDITING"), anyString(), anyString(), eq(9L),
|
eq("EDITING"), eq("EDITING"), anyString(), anyString(), eq(9L),
|
||||||
eq("operator"), org.mockito.ArgumentMatchers.isNull());
|
eq("operator"), org.mockito.ArgumentMatchers.isNull());
|
||||||
@@ -838,9 +862,11 @@ class LoadingListServiceTest {
|
|||||||
when(fbaPricingService.recalculateUnlockedContainerCustomerFees(eq(30L), eq(101L), anyString()))
|
when(fbaPricingService.recalculateUnlockedContainerCustomerFees(eq(30L), eq(101L), anyString()))
|
||||||
.thenReturn(new FbaPricingService.ContainerFeeRecalculationResult(
|
.thenReturn(new FbaPricingService.ContainerFeeRecalculationResult(
|
||||||
FbaPricingService.ContainerFeeRecalculationStatus.NOT_READY, 0));
|
FbaPricingService.ContainerFeeRecalculationStatus.NOT_READY, 0));
|
||||||
|
when(domesticPdaOperators.required(any(), eq(77L)))
|
||||||
|
.thenReturn(ApiResponse.success(new DomesticPdaOperatorClient.Operator(77L, "作业员")));
|
||||||
|
|
||||||
assertThatThrownBy(() -> service.confirmPreload(
|
assertThatThrownBy(() -> service.confirmPreload(
|
||||||
10L, 9L, "operator", "confirm-key", new ConfirmPreloadRequest(3)))
|
10L, 9L, "operator", "confirm-key", new ConfirmPreloadRequest(3, 77L)))
|
||||||
.isInstanceOf(BizException.class).hasMessageContaining("初始费用");
|
.isInstanceOf(BizException.class).hasMessageContaining("初始费用");
|
||||||
|
|
||||||
verify(repository, never()).touchForPreloadConfirmation(anyLong(), anyInt(), anyLong(), anyString());
|
verify(repository, never()).touchForPreloadConfirmation(anyLong(), anyInt(), anyLong(), anyString());
|
||||||
@@ -852,7 +878,7 @@ class LoadingListServiceTest {
|
|||||||
void exactConfirmationReplayReturnsFrozenResponseWithoutTouchingPlan() throws Exception {
|
void exactConfirmationReplayReturnsFrozenResponseWithoutTouchingPlan() throws Exception {
|
||||||
PreloadConfirmationResponse expected = new PreloadConfirmationResponse(
|
PreloadConfirmationResponse expected = new PreloadConfirmationResponse(
|
||||||
10L, 30L, "CONFIRMED", 4, readiness("CONFIRMED", false, true, 80L));
|
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();
|
HistoryRow replay = new HistoryRow();
|
||||||
ObjectMapper mapper = new ObjectMapper();
|
ObjectMapper mapper = new ObjectMapper();
|
||||||
replay.setAfterSnapshot(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(
|
replay.setAfterSnapshot(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(
|
||||||
@@ -860,7 +886,7 @@ class LoadingListServiceTest {
|
|||||||
when(repository.history(anyString())).thenReturn(replay);
|
when(repository.history(anyString())).thenReturn(replay);
|
||||||
|
|
||||||
PreloadConfirmationResponse actual = service.confirmPreload(
|
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);
|
assertThat(actual).isEqualTo(expected);
|
||||||
verify(loadingPickService, never()).requirePreloadConfirmationReady(anyLong());
|
verify(loadingPickService, never()).requirePreloadConfirmationReady(anyLong());
|
||||||
@@ -876,14 +902,14 @@ class LoadingListServiceTest {
|
|||||||
void rejectsConfirmationReplayWhenExpectedVersionDiffers() throws Exception {
|
void rejectsConfirmationReplayWhenExpectedVersionDiffers() throws Exception {
|
||||||
PreloadConfirmationResponse expected = new PreloadConfirmationResponse(
|
PreloadConfirmationResponse expected = new PreloadConfirmationResponse(
|
||||||
10L, 30L, "CONFIRMED", 4, readiness("CONFIRMED", false, true, 80L));
|
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();
|
HistoryRow replay = new HistoryRow();
|
||||||
replay.setAfterSnapshot(new ObjectMapper().writeValueAsString(
|
replay.setAfterSnapshot(new ObjectMapper().writeValueAsString(
|
||||||
java.util.Map.of("requestHash", originalHash, "response", expected)));
|
java.util.Map.of("requestHash", originalHash, "response", expected)));
|
||||||
when(repository.history(anyString())).thenReturn(replay);
|
when(repository.history(anyString())).thenReturn(replay);
|
||||||
|
|
||||||
assertThatThrownBy(() -> service.confirmPreload(
|
assertThatThrownBy(() -> service.confirmPreload(
|
||||||
10L, 9L, "operator", "confirm-key", new ConfirmPreloadRequest(4)))
|
10L, 9L, "operator", "confirm-key", new ConfirmPreloadRequest(4, 77L)))
|
||||||
.isInstanceOf(BizException.class)
|
.isInstanceOf(BizException.class)
|
||||||
.extracting(error -> ((BizException) error).getResultCode())
|
.extracting(error -> ((BizException) error).getResultCode())
|
||||||
.isEqualTo(ResultCode.CONFLICT);
|
.isEqualTo(ResultCode.CONFLICT);
|
||||||
@@ -898,11 +924,13 @@ class LoadingListServiceTest {
|
|||||||
AggregateRow confirmed = row("EDITING", 4);
|
AggregateRow confirmed = row("EDITING", 4);
|
||||||
when(repository.history(anyString())).thenReturn(null);
|
when(repository.history(anyString())).thenReturn(null);
|
||||||
when(repository.aggregate(10L)).thenReturn(confirmed);
|
when(repository.aggregate(10L)).thenReturn(confirmed);
|
||||||
|
when(domesticPdaOperators.required(any(), eq(77L)))
|
||||||
|
.thenReturn(ApiResponse.success(new DomesticPdaOperatorClient.Operator(77L, "作业员")));
|
||||||
when(loadingPickService.requirePreloadConfirmationReady(10L))
|
when(loadingPickService.requirePreloadConfirmationReady(10L))
|
||||||
.thenThrow(new BizException(ResultCode.CONFLICT, "预装计划已确认"));
|
.thenThrow(new BizException(ResultCode.CONFLICT, "预装计划已确认"));
|
||||||
|
|
||||||
assertThatThrownBy(() -> service.confirmPreload(
|
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)
|
.isInstanceOf(BizException.class)
|
||||||
.extracting(error -> ((BizException) error).getResultCode())
|
.extracting(error -> ((BizException) error).getResultCode())
|
||||||
.isEqualTo(ResultCode.CONFLICT);
|
.isEqualTo(ResultCode.CONFLICT);
|
||||||
@@ -919,11 +947,13 @@ class LoadingListServiceTest {
|
|||||||
AggregateRow before = row("EDITING", 3);
|
AggregateRow before = row("EDITING", 3);
|
||||||
when(repository.history(anyString())).thenReturn(null);
|
when(repository.history(anyString())).thenReturn(null);
|
||||||
when(repository.aggregate(10L)).thenReturn(before);
|
when(repository.aggregate(10L)).thenReturn(before);
|
||||||
|
when(domesticPdaOperators.required(any(), eq(77L)))
|
||||||
|
.thenReturn(ApiResponse.success(new DomesticPdaOperatorClient.Operator(77L, "作业员")));
|
||||||
when(loadingPickService.requirePreloadConfirmationReady(10L))
|
when(loadingPickService.requirePreloadConfirmationReady(10L))
|
||||||
.thenThrow(new BizException(ResultCode.CONFLICT, "第二票订舱尚未放舱"));
|
.thenThrow(new BizException(ResultCode.CONFLICT, "第二票订舱尚未放舱"));
|
||||||
|
|
||||||
assertThatThrownBy(() -> service.confirmPreload(
|
assertThatThrownBy(() -> service.confirmPreload(
|
||||||
10L, 9L, "operator", "confirm-key", new ConfirmPreloadRequest(3)))
|
10L, 9L, "operator", "confirm-key", new ConfirmPreloadRequest(3, 77L)))
|
||||||
.isInstanceOf(BizException.class).hasMessageContaining("第二票");
|
.isInstanceOf(BizException.class).hasMessageContaining("第二票");
|
||||||
|
|
||||||
verify(repository, never()).touchForPreloadConfirmation(anyLong(),
|
verify(repository, never()).touchForPreloadConfirmation(anyLong(),
|
||||||
@@ -940,7 +970,7 @@ class LoadingListServiceTest {
|
|||||||
when(repository.history(anyString())).thenReturn(replay);
|
when(repository.history(anyString())).thenReturn(replay);
|
||||||
|
|
||||||
assertThatThrownBy(() -> service.confirmPreload(
|
assertThatThrownBy(() -> service.confirmPreload(
|
||||||
10L, 9L, "operator", "confirm-key", new ConfirmPreloadRequest(4)))
|
10L, 9L, "operator", "confirm-key", new ConfirmPreloadRequest(4, 77L)))
|
||||||
.isInstanceOf(BizException.class).hasMessageContaining("不同请求");
|
.isInstanceOf(BizException.class).hasMessageContaining("不同请求");
|
||||||
|
|
||||||
verify(loadingPickService, never()).requirePreloadConfirmationReady(anyLong());
|
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.AssignRequest;
|
||||||
import com.baiyue.firstleg.dto.LoadingPickDtos.CompleteLoadingRequest;
|
import com.baiyue.firstleg.dto.LoadingPickDtos.CompleteLoadingRequest;
|
||||||
import com.baiyue.firstleg.dto.LoadingPickDtos.Task;
|
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.InboundPalletAllocationEntity;
|
||||||
import com.baiyue.firstleg.entity.LoadingPickAssignmentEntity;
|
import com.baiyue.firstleg.entity.LoadingPickAssignmentEntity;
|
||||||
import com.baiyue.firstleg.entity.LoadingPickHistoryEntity;
|
import com.baiyue.firstleg.entity.LoadingPickHistoryEntity;
|
||||||
@@ -106,6 +108,8 @@ class LoadingPickServiceTest {
|
|||||||
private InboundHoldGuard inboundHoldGuard;
|
private InboundHoldGuard inboundHoldGuard;
|
||||||
@MockBean
|
@MockBean
|
||||||
private DropshipPalletizationMapper dropshipPallets;
|
private DropshipPalletizationMapper dropshipPallets;
|
||||||
|
@MockBean
|
||||||
|
private DomesticPdaOperatorClient domesticPdaOperators;
|
||||||
@Resource
|
@Resource
|
||||||
private LoadingPickService service;
|
private LoadingPickService service;
|
||||||
|
|
||||||
@@ -565,8 +569,7 @@ class LoadingPickServiceTest {
|
|||||||
when(repository.operation("claim-2")).thenReturn(null);
|
when(repository.operation("claim-2")).thenReturn(null);
|
||||||
when(repository.task(20L)).thenReturn(task);
|
when(repository.task(20L)).thenReturn(task);
|
||||||
when(repository.taskForUpdate(20L)).thenReturn(task);
|
when(repository.taskForUpdate(20L)).thenReturn(task);
|
||||||
when(repository.assignment(20L)).thenReturn(null, assignment);
|
when(repository.assignment(20L)).thenReturn(assignment);
|
||||||
when(repository.createAssignment(20L, 9L, "operator", 9L, "operator", "CLAIMED")).thenReturn(1);
|
|
||||||
when(smartLoadingRepository.startPicking(10L, 9L, "operator")).thenReturn(1, 0);
|
when(smartLoadingRepository.startPicking(10L, 9L, "operator")).thenReturn(1, 0);
|
||||||
doAnswer(invocation -> {
|
doAnswer(invocation -> {
|
||||||
LoadingPickOperationEntity value = invocation.getArgument(0);
|
LoadingPickOperationEntity value = invocation.getArgument(0);
|
||||||
@@ -574,9 +577,9 @@ class LoadingPickServiceTest {
|
|||||||
return null;
|
return null;
|
||||||
}).when(repository).insert(any(LoadingPickOperationEntity.class));
|
}).when(repository).insert(any(LoadingPickOperationEntity.class));
|
||||||
|
|
||||||
service.claim(20L, new ClaimRequest(0, "claim-1"), 9L, "operator");
|
service.claim(20L, new ClaimRequest(assignment.getVersion(), "claim-1"), 9L, "operator");
|
||||||
service.claim(20L, new ClaimRequest(0, "claim-1"), 9L, "operator");
|
service.claim(20L, new ClaimRequest(assignment.getVersion(), "claim-1"), 9L, "operator");
|
||||||
service.claim(20L, new ClaimRequest(0, "claim-2"), 9L, "operator");
|
service.claim(20L, new ClaimRequest(assignment.getVersion(), "claim-2"), 9L, "operator");
|
||||||
|
|
||||||
verify(smartLoadingRepository, times(2)).startPicking(10L, 9L, "operator");
|
verify(smartLoadingRepository, times(2)).startPicking(10L, 9L, "operator");
|
||||||
verify(smartLoadingRepository, times(1)).insert(argThat(
|
verify(smartLoadingRepository, times(1)).insert(argThat(
|
||||||
@@ -585,7 +588,8 @@ class LoadingPickServiceTest {
|
|||||||
&& "EDITING".equals(history.getFromStatus())
|
&& "EDITING".equals(history.getFromStatus())
|
||||||
&& "LOADING".equals(history.getToStatus())
|
&& "LOADING".equals(history.getToStatus())
|
||||||
&& "PDA-START-20".equals(history.getOperationId())));
|
&& "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
|
@Test
|
||||||
@@ -606,6 +610,17 @@ class LoadingPickServiceTest {
|
|||||||
verify(smartLoadingRepository, never()).startPicking(anyLong(), anyLong(), anyString());
|
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
|
@Test
|
||||||
void assignRechecksSameOperationAfterWaitingForTaskLock() {
|
void assignRechecksSameOperationAfterWaitingForTaskLock() {
|
||||||
LoadingPickTaskEntity task = task();
|
LoadingPickTaskEntity task = task();
|
||||||
@@ -615,6 +630,8 @@ class LoadingPickServiceTest {
|
|||||||
when(repository.taskForUpdate(20L)).thenReturn(task);
|
when(repository.taskForUpdate(20L)).thenReturn(task);
|
||||||
when(repository.task(20L)).thenReturn(task);
|
when(repository.task(20L)).thenReturn(task);
|
||||||
when(repository.items(20L)).thenReturn(List.of());
|
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"),
|
Task replay = service.assign(20L, new AssignRequest(11L, "picker", 0, "assign-race"),
|
||||||
9L, "operator");
|
9L, "operator");
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import com.baiyue.iam.service.TenantService;
|
|||||||
import com.baiyue.iam.dto.CustomerExportDtos.CustomerExportBatch;
|
import com.baiyue.iam.dto.CustomerExportDtos.CustomerExportBatch;
|
||||||
import com.baiyue.common.core.api.PageResult;
|
import com.baiyue.common.core.api.PageResult;
|
||||||
import com.baiyue.iam.dto.TenantDtos.FinanceCustomerSummary;
|
import com.baiyue.iam.dto.TenantDtos.FinanceCustomerSummary;
|
||||||
|
import com.baiyue.iam.dto.DomesticPdaOperatorDtos.OperatorOption;
|
||||||
|
import com.baiyue.iam.service.DomesticPdaOperatorService;
|
||||||
import jakarta.annotation.PostConstruct;
|
import jakarta.annotation.PostConstruct;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
@@ -38,6 +40,7 @@ public class InternalTenantController {
|
|||||||
@Resource private MerchantOnboardingService onboardingService;
|
@Resource private MerchantOnboardingService onboardingService;
|
||||||
@Resource private ProfileResourceService profileResourceService;
|
@Resource private ProfileResourceService profileResourceService;
|
||||||
@Resource private TenantService tenantService;
|
@Resource private TenantService tenantService;
|
||||||
|
@Resource private DomesticPdaOperatorService domesticPdaOperatorService;
|
||||||
@Value("${baiyue.security.internal-service-token}") private String internalTokenValue;
|
@Value("${baiyue.security.internal-service-token}") private String internalTokenValue;
|
||||||
private byte[] internalToken;
|
private byte[] internalToken;
|
||||||
|
|
||||||
@@ -128,6 +131,15 @@ public class InternalTenantController {
|
|||||||
return ApiResponse.success(tenantService.pageFinanceCustomers(page, size, keyword, status, customerId));
|
return ApiResponse.success(tenantService.pageFinanceCustomers(page, size, keyword, status, customerId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/domestic-pda-operators/{authUserId}")
|
||||||
|
public ApiResponse<OperatorOption> domesticPdaOperator(
|
||||||
|
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
|
||||||
|
@PathVariable long authUserId,
|
||||||
|
@RequestParam long warehouseId) {
|
||||||
|
verify(token);
|
||||||
|
return ApiResponse.success(domesticPdaOperatorService.required(authUserId, warehouseId));
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/exports/customers")
|
@GetMapping("/exports/customers")
|
||||||
public ApiResponse<CustomerExportBatch> exportCustomers(
|
public ApiResponse<CustomerExportBatch> exportCustomers(
|
||||||
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
|
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ import com.baiyue.common.security.AuthenticatedUser;
|
|||||||
import com.baiyue.iam.dto.PlatformUserManagementDtos.*;
|
import com.baiyue.iam.dto.PlatformUserManagementDtos.*;
|
||||||
import com.baiyue.iam.service.PlatformUserManagementService;
|
import com.baiyue.iam.service.PlatformUserManagementService;
|
||||||
import com.baiyue.iam.service.TenantGuard;
|
import com.baiyue.iam.service.TenantGuard;
|
||||||
|
import com.baiyue.iam.service.DomesticPdaOperatorService;
|
||||||
|
import com.baiyue.iam.dto.DomesticPdaOperatorDtos.OperatorOption;
|
||||||
|
import java.util.List;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
@@ -26,6 +29,19 @@ import org.springframework.web.bind.annotation.*;
|
|||||||
public class PlatformUserManagementController {
|
public class PlatformUserManagementController {
|
||||||
@Resource
|
@Resource
|
||||||
private PlatformUserManagementService service;
|
private PlatformUserManagementService service;
|
||||||
|
@Resource
|
||||||
|
private DomesticPdaOperatorService domesticPdaOperators;
|
||||||
|
|
||||||
|
@GetMapping("/domestic-pda-operators")
|
||||||
|
@PreAuthorize("hasAuthority('platform:firstleg:pda:assign')")
|
||||||
|
@Operation(summary = "查询启用的国内头程 PDA 作业员候选")
|
||||||
|
public ApiResponse<List<OperatorOption>> domesticPdaOperators(
|
||||||
|
@RequestParam long warehouseId,
|
||||||
|
@RequestParam(required = false) String keyword,
|
||||||
|
@AuthenticationPrincipal AuthenticatedUser user) {
|
||||||
|
TenantGuard.tenant(user, "PLATFORM");
|
||||||
|
return ApiResponse.success(domesticPdaOperators.options(warehouseId, keyword));
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
@PreAuthorize("hasAuthority('platform:user:list')")
|
@PreAuthorize("hasAuthority('platform:user:list')")
|
||||||
|
|||||||
@@ -41,12 +41,13 @@ public final class AuthorizationDtos {
|
|||||||
private Set<String> permissions;
|
private Set<String> permissions;
|
||||||
private Long permissionVersion;
|
private Long permissionVersion;
|
||||||
private Long overseasWarehouseId;
|
private Long overseasWarehouseId;
|
||||||
|
private Long domesticWarehouseId;
|
||||||
|
|
||||||
public SnapshotResponse(Long userId, Long tenantId, Long membershipId, String appCode,
|
public SnapshotResponse(Long userId, Long tenantId, Long membershipId, String appCode,
|
||||||
String clientType, Set<String> roles, Set<String> permissions,
|
String clientType, Set<String> roles, Set<String> permissions,
|
||||||
Long permissionVersion) {
|
Long permissionVersion) {
|
||||||
this(userId, tenantId, membershipId, appCode, clientType, roles, permissions,
|
this(userId, tenantId, membershipId, appCode, clientType, roles, permissions,
|
||||||
permissionVersion, null);
|
permissionVersion, null, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.baiyue.iam.dto;
|
||||||
|
|
||||||
|
/** 国内头程 PDA 作业员候选契约。 */
|
||||||
|
public final class DomesticPdaOperatorDtos {
|
||||||
|
private DomesticPdaOperatorDtos() { }
|
||||||
|
|
||||||
|
public record OperatorOption(Long userId, String displayName) { }
|
||||||
|
}
|
||||||
@@ -23,12 +23,13 @@ public final class PlatformUserManagementDtos {
|
|||||||
@NotNull Long tenantId,
|
@NotNull Long tenantId,
|
||||||
@NotNull List<Long> roleIds,
|
@NotNull List<Long> roleIds,
|
||||||
Long overseasWarehouseId,
|
Long overseasWarehouseId,
|
||||||
|
Long domesticWarehouseId,
|
||||||
@NotBlank @Size(min = 8, max = 64)
|
@NotBlank @Size(min = 8, max = 64)
|
||||||
@Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d).+$", message = "密码必须同时包含字母和数字") String password,
|
@Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d).+$", message = "密码必须同时包含字母和数字") String password,
|
||||||
@NotBlank @Pattern(regexp = "ACTIVE|DISABLED") String status) {
|
@NotBlank @Pattern(regexp = "ACTIVE|DISABLED") String status) {
|
||||||
public CreateUserRequest(String username, String displayName, String phone, String email,
|
public CreateUserRequest(String username, String displayName, String phone, String email,
|
||||||
Long tenantId, List<Long> roleIds, String password, String status) {
|
Long tenantId, List<Long> roleIds, String password, String status) {
|
||||||
this(username, displayName, phone, email, tenantId, roleIds, null, password, status);
|
this(username, displayName, phone, email, tenantId, roleIds, null, null, password, status);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,10 +40,15 @@ public final class PlatformUserManagementDtos {
|
|||||||
@Email @Size(max = 128) String email,
|
@Email @Size(max = 128) String email,
|
||||||
@NotNull Long tenantId,
|
@NotNull Long tenantId,
|
||||||
@NotNull List<Long> roleIds,
|
@NotNull List<Long> roleIds,
|
||||||
Long overseasWarehouseId) {
|
Long overseasWarehouseId,
|
||||||
|
Long domesticWarehouseId) {
|
||||||
|
public UpdateUserRequest(String displayName, String phone, String email,
|
||||||
|
Long tenantId, List<Long> roleIds, Long overseasWarehouseId) {
|
||||||
|
this(displayName, phone, email, tenantId, roleIds, overseasWarehouseId, null);
|
||||||
|
}
|
||||||
public UpdateUserRequest(String displayName, String phone, String email,
|
public UpdateUserRequest(String displayName, String phone, String email,
|
||||||
Long tenantId, List<Long> roleIds) {
|
Long tenantId, List<Long> roleIds) {
|
||||||
this(displayName, phone, email, tenantId, roleIds, null);
|
this(displayName, phone, email, tenantId, roleIds, null, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,7 +61,7 @@ public final class PlatformUserManagementDtos {
|
|||||||
String displayName, String phone, String email, Long tenantId,
|
String displayName, String phone, String email, Long tenantId,
|
||||||
String tenantName, String memberType, String memberNo, String status,
|
String tenantName, String memberType, String memberNo, String status,
|
||||||
List<Long> roleIds, List<String> roleNames, LocalDateTime lastLoginAt,
|
List<Long> roleIds, List<String> roleNames, LocalDateTime lastLoginAt,
|
||||||
boolean protectedAccount, Long overseasWarehouseId) { }
|
boolean protectedAccount, Long overseasWarehouseId, Long domesticWarehouseId) { }
|
||||||
|
|
||||||
public record TenantOption(Long id, String name, String status) { }
|
public record TenantOption(Long id, String name, String status) { }
|
||||||
public record RoleOption(Long id, Long tenantId, String name, String status) { }
|
public record RoleOption(Long id, Long tenantId, String name, String status) { }
|
||||||
|
|||||||
@@ -9,6 +9,6 @@ import com.baiyue.common.core.model.BaseEntity; import com.baomidou.mybatisplus.
|
|||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@TableName("iam_membership") public class IamMembership extends BaseEntity {
|
@TableName("iam_membership") public class IamMembership extends BaseEntity {
|
||||||
@TableId private Long id; private Long tenantId; private Long userId; private String memberType; private String memberNo; private Long overseasWarehouseId; private String status; private LocalDateTime joinedAt; private LocalDateTime leftAt;
|
@TableId private Long id; private Long tenantId; private Long userId; private String memberType; private String memberNo; private Long overseasWarehouseId; private Long domesticWarehouseId; private String status; private LocalDateTime joinedAt; private LocalDateTime leftAt;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,4 +105,8 @@ public interface AuthorizationQueryMapper {
|
|||||||
@Select("SELECT overseas_warehouse_id FROM iam_membership WHERE id=#{membershipId} AND tenant_id=#{tenantId} AND status='ACTIVE' AND deleted=0")
|
@Select("SELECT overseas_warehouse_id FROM iam_membership WHERE id=#{membershipId} AND tenant_id=#{tenantId} AND status='ACTIVE' AND deleted=0")
|
||||||
Long findOverseasWarehouseId(@Param("tenantId") long tenantId,
|
Long findOverseasWarehouseId(@Param("tenantId") long tenantId,
|
||||||
@Param("membershipId") long membershipId);
|
@Param("membershipId") long membershipId);
|
||||||
|
|
||||||
|
@Select("SELECT domestic_warehouse_id FROM iam_membership WHERE id=#{membershipId} AND tenant_id=#{tenantId} AND status='ACTIVE' AND deleted=0")
|
||||||
|
Long findDomesticWarehouseId(@Param("tenantId") long tenantId,
|
||||||
|
@Param("membershipId") long membershipId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import org.apache.ibatis.annotations.Param;
|
|||||||
import org.apache.ibatis.annotations.Select;
|
import org.apache.ibatis.annotations.Select;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import com.baiyue.iam.dto.DomesticPdaOperatorDtos.OperatorOption;
|
||||||
|
|
||||||
@Mapper
|
@Mapper
|
||||||
public interface IamMembershipRoleMapper extends BaseMapper<IamMembershipRole> {
|
public interface IamMembershipRoleMapper extends BaseMapper<IamMembershipRole> {
|
||||||
@@ -26,4 +27,31 @@ public interface IamMembershipRoleMapper extends BaseMapper<IamMembershipRole> {
|
|||||||
""")
|
""")
|
||||||
List<Long> findAssignedAuthUserIds(@Param("tenantId") long tenantId,
|
List<Long> findAssignedAuthUserIds(@Param("tenantId") long tenantId,
|
||||||
@Param("roleId") Long roleId);
|
@Param("roleId") Long roleId);
|
||||||
|
|
||||||
|
@Select("""
|
||||||
|
<script>
|
||||||
|
SELECT DISTINCT u.auth_user_id AS userId, u.display_name AS displayName
|
||||||
|
FROM iam_membership_role mr
|
||||||
|
JOIN iam_role role ON role.id = mr.role_id AND role.tenant_id = mr.tenant_id
|
||||||
|
JOIN iam_membership m ON m.id = mr.membership_id AND m.tenant_id = mr.tenant_id
|
||||||
|
JOIN iam_user u ON u.id = m.user_id
|
||||||
|
WHERE mr.tenant_id = #{tenantId}
|
||||||
|
AND role.role_code = #{roleCode} AND role.app_code = 'PLATFORM'
|
||||||
|
AND role.status = 'ACTIVE' AND role.deleted = 0
|
||||||
|
AND m.status = 'ACTIVE' AND m.deleted = 0
|
||||||
|
AND u.status = 'ACTIVE' AND u.deleted = 0
|
||||||
|
<if test="warehouseId != null">AND m.domestic_warehouse_id = #{warehouseId}</if>
|
||||||
|
<if test="authUserId != null">AND u.auth_user_id = #{authUserId}</if>
|
||||||
|
<if test="keyword != null and keyword != ''">
|
||||||
|
AND u.display_name LIKE CONCAT('%', #{keyword}, '%')
|
||||||
|
</if>
|
||||||
|
ORDER BY u.display_name, u.auth_user_id
|
||||||
|
LIMIT 200
|
||||||
|
</script>
|
||||||
|
""")
|
||||||
|
List<OperatorOption> findActiveUsersByRoleCode(@Param("tenantId") long tenantId,
|
||||||
|
@Param("roleCode") String roleCode,
|
||||||
|
@Param("keyword") String keyword,
|
||||||
|
@Param("authUserId") Long authUserId,
|
||||||
|
@Param("warehouseId") Long warehouseId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,8 @@ public class AuthorizationSnapshotService {
|
|||||||
return new SnapshotResponse(request.getUserId(), request.getTenantId(), request.getMembershipId(),
|
return new SnapshotResponse(request.getUserId(), request.getTenantId(), request.getMembershipId(),
|
||||||
appCode, appCode, Set.copyOf(roles), Set.copyOf(permissions),
|
appCode, appCode, Set.copyOf(roles), Set.copyOf(permissions),
|
||||||
mapper.findPermissionVersion(request.getUserId()),
|
mapper.findPermissionVersion(request.getUserId()),
|
||||||
mapper.findOverseasWarehouseId(request.getTenantId(), request.getMembershipId()));
|
mapper.findOverseasWarehouseId(request.getTenantId(), request.getMembershipId()),
|
||||||
|
mapper.findDomesticWarehouseId(request.getTenantId(), request.getMembershipId()));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package com.baiyue.iam.service;
|
||||||
|
|
||||||
|
import com.baiyue.common.core.api.ResultCode;
|
||||||
|
import com.baiyue.common.core.exception.BizException;
|
||||||
|
import com.baiyue.iam.dto.DomesticPdaOperatorDtos.OperatorOption;
|
||||||
|
import com.baiyue.iam.mapper.IamMembershipRoleMapper;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** 提供启用的国内头程 PDA 作业员,并作为派单时的权威角色校验。 */
|
||||||
|
@Service
|
||||||
|
public class DomesticPdaOperatorService {
|
||||||
|
@Resource
|
||||||
|
private IamMembershipRoleMapper membershipRoles;
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<OperatorOption> options(long warehouseId, String keyword) {
|
||||||
|
String normalized = keyword == null || keyword.isBlank() ? null : keyword.trim();
|
||||||
|
return membershipRoles.findActiveUsersByRoleCode(0L, "CN_FIRSTLEG_PDA_OPERATOR", normalized, null, warehouseId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public OperatorOption required(long authUserId, long warehouseId) {
|
||||||
|
return membershipRoles.findActiveUsersByRoleCode(
|
||||||
|
0L, "CN_FIRSTLEG_PDA_OPERATOR", null, authUserId, warehouseId).stream().findFirst()
|
||||||
|
.orElseThrow(() -> new BizException(ResultCode.BAD_REQUEST,
|
||||||
|
"所选人员不是该仓库启用的国内头程 PDA 作业员"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -75,7 +75,7 @@ public class PlatformUserManagementService {
|
|||||||
long tenantId = request.tenantId();
|
long tenantId = request.tenantId();
|
||||||
requirePlatformTenant(tenantId);
|
requirePlatformTenant(tenantId);
|
||||||
List<Long> roleIds = validateRoles(tenantId, request.roleIds());
|
List<Long> roleIds = validateRoles(tenantId, request.roleIds());
|
||||||
Long overseasWarehouseId = validateWarehouseBinding(roleIds, request.overseasWarehouseId());
|
WarehouseBindings warehouses = validateWarehouseBinding(roleIds, request.overseasWarehouseId(), request.domesticWarehouseId());
|
||||||
long authUserId = IdWorker.getId();
|
long authUserId = IdWorker.getId();
|
||||||
long userId = IdWorker.getId();
|
long userId = IdWorker.getId();
|
||||||
long membershipId = IdWorker.getId();
|
long membershipId = IdWorker.getId();
|
||||||
@@ -100,7 +100,8 @@ public class PlatformUserManagementService {
|
|||||||
membership.setUserId(userId);
|
membership.setUserId(userId);
|
||||||
membership.setMemberType("EMPLOYEE");
|
membership.setMemberType("EMPLOYEE");
|
||||||
membership.setMemberNo(memberNo(tenantId, membershipId));
|
membership.setMemberNo(memberNo(tenantId, membershipId));
|
||||||
membership.setOverseasWarehouseId(overseasWarehouseId);
|
membership.setOverseasWarehouseId(warehouses.overseasWarehouseId());
|
||||||
|
membership.setDomesticWarehouseId(warehouses.domesticWarehouseId());
|
||||||
membership.setStatus(request.status());
|
membership.setStatus(request.status());
|
||||||
membership.setJoinedAt(LocalDateTime.now());
|
membership.setJoinedAt(LocalDateTime.now());
|
||||||
membership.setLeftAt("DISABLED".equals(request.status()) ? LocalDateTime.now() : null);
|
membership.setLeftAt("DISABLED".equals(request.status()) ? LocalDateTime.now() : null);
|
||||||
@@ -125,7 +126,7 @@ public class PlatformUserManagementService {
|
|||||||
long newTenantId = request.tenantId();
|
long newTenantId = request.tenantId();
|
||||||
requirePlatformTenant(newTenantId);
|
requirePlatformTenant(newTenantId);
|
||||||
List<Long> roleIds = validateRoles(newTenantId, request.roleIds());
|
List<Long> roleIds = validateRoles(newTenantId, request.roleIds());
|
||||||
Long overseasWarehouseId = validateWarehouseBinding(roleIds, request.overseasWarehouseId());
|
WarehouseBindings warehouses = validateWarehouseBinding(roleIds, request.overseasWarehouseId(), request.domesticWarehouseId());
|
||||||
List<Long> oldRoleIds = members.findRoleIds(oldTenantId, membershipId);
|
List<Long> oldRoleIds = members.findRoleIds(oldTenantId, membershipId);
|
||||||
if (isProtectedPlatformAdmin(membership, oldRoleIds)
|
if (isProtectedPlatformAdmin(membership, oldRoleIds)
|
||||||
&& (newTenantId != PLATFORM_TENANT_ID || !containsPlatformAdminRole(roleIds))) {
|
&& (newTenantId != PLATFORM_TENANT_ID || !containsPlatformAdminRole(roleIds))) {
|
||||||
@@ -140,7 +141,8 @@ public class PlatformUserManagementService {
|
|||||||
boolean phoneChanged = !Objects.equals(user.getPhone(), request.phone().trim());
|
boolean phoneChanged = !Objects.equals(user.getPhone(), request.phone().trim());
|
||||||
boolean scopeChanged = oldTenantId != newTenantId;
|
boolean scopeChanged = oldTenantId != newTenantId;
|
||||||
boolean rolesChanged = !new java.util.HashSet<>(oldRoleIds).equals(new java.util.HashSet<>(roleIds));
|
boolean rolesChanged = !new java.util.HashSet<>(oldRoleIds).equals(new java.util.HashSet<>(roleIds));
|
||||||
boolean warehouseChanged = !Objects.equals(membership.getOverseasWarehouseId(), overseasWarehouseId);
|
boolean warehouseChanged = !Objects.equals(membership.getOverseasWarehouseId(), warehouses.overseasWarehouseId())
|
||||||
|
|| !Objects.equals(membership.getDomesticWarehouseId(), warehouses.domesticWarehouseId());
|
||||||
|
|
||||||
if (profileChanged) call(() -> authClient.profile(internalToken, user.getAuthUserId(),
|
if (profileChanged) call(() -> authClient.profile(internalToken, user.getAuthUserId(),
|
||||||
new AuthAccountClient.ProfileRequest(request.displayName().trim())));
|
new AuthAccountClient.ProfileRequest(request.displayName().trim())));
|
||||||
@@ -154,7 +156,8 @@ public class PlatformUserManagementService {
|
|||||||
user.setEmail(trimToNull(request.email()));
|
user.setEmail(trimToNull(request.email()));
|
||||||
user.setUserType(userType(newTenantId));
|
user.setUserType(userType(newTenantId));
|
||||||
membership.setTenantId(newTenantId);
|
membership.setTenantId(newTenantId);
|
||||||
membership.setOverseasWarehouseId(overseasWarehouseId);
|
membership.setOverseasWarehouseId(warehouses.overseasWarehouseId());
|
||||||
|
membership.setDomesticWarehouseId(warehouses.domesticWarehouseId());
|
||||||
if (scopeChanged && members.existsMemberNo(newTenantId, membership.getMemberNo(), membershipId)) {
|
if (scopeChanged && members.existsMemberNo(newTenantId, membership.getMemberNo(), membershipId)) {
|
||||||
membership.setMemberNo(memberNo(newTenantId, membershipId));
|
membership.setMemberNo(memberNo(newTenantId, membershipId));
|
||||||
}
|
}
|
||||||
@@ -239,7 +242,7 @@ public class PlatformUserManagementService {
|
|||||||
user.getDisplayName(), user.getPhone(), user.getEmail(), membership.getTenantId(), tenantName,
|
user.getDisplayName(), user.getPhone(), user.getEmail(), membership.getTenantId(), tenantName,
|
||||||
membership.getMemberType(), membership.getMemberNo(), membership.getStatus(), roleIds,
|
membership.getMemberType(), membership.getMemberNo(), membership.getStatus(), roleIds,
|
||||||
roleNames, account.lastLoginAt(), isProtectedPlatformAdmin(membership, roleIds),
|
roleNames, account.lastLoginAt(), isProtectedPlatformAdmin(membership, roleIds),
|
||||||
membership.getOverseasWarehouseId());
|
membership.getOverseasWarehouseId(), membership.getDomesticWarehouseId());
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<IamRole> platformRoles() {
|
private List<IamRole> platformRoles() {
|
||||||
@@ -262,7 +265,8 @@ public class PlatformUserManagementService {
|
|||||||
return roleIds;
|
return roleIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Long validateWarehouseBinding(List<Long> roleIds, Long overseasWarehouseId) {
|
private WarehouseBindings validateWarehouseBinding(List<Long> roleIds, Long overseasWarehouseId,
|
||||||
|
Long domesticWarehouseId) {
|
||||||
Set<String> roleCodes = roleIds.stream().map(roleMapper::selectById).filter(Objects::nonNull)
|
Set<String> roleCodes = roleIds.stream().map(roleMapper::selectById).filter(Objects::nonNull)
|
||||||
.map(IamRole::getRoleCode).collect(Collectors.toSet());
|
.map(IamRole::getRoleCode).collect(Collectors.toSet());
|
||||||
boolean overseasPda = roleCodes.contains("OVERSEAS_WAREHOUSE_PDA_OPERATOR");
|
boolean overseasPda = roleCodes.contains("OVERSEAS_WAREHOUSE_PDA_OPERATOR");
|
||||||
@@ -276,9 +280,17 @@ public class PlatformUserManagementService {
|
|||||||
if (!overseasPda && overseasWarehouseId != null) {
|
if (!overseasPda && overseasWarehouseId != null) {
|
||||||
throw new BizException(ResultCode.BAD_REQUEST, "仅海外仓 PDA 作业员可绑定海外仓");
|
throw new BizException(ResultCode.BAD_REQUEST, "仅海外仓 PDA 作业员可绑定海外仓");
|
||||||
}
|
}
|
||||||
return overseasWarehouseId;
|
if (domesticPda && (domesticWarehouseId == null || domesticWarehouseId <= 0)) {
|
||||||
|
throw new BizException(ResultCode.BAD_REQUEST, "国内头程 PDA 作业员必须绑定一个有效国内仓");
|
||||||
|
}
|
||||||
|
if (!domesticPda && domesticWarehouseId != null) {
|
||||||
|
throw new BizException(ResultCode.BAD_REQUEST, "仅国内头程 PDA 作业员可绑定国内仓");
|
||||||
|
}
|
||||||
|
return new WarehouseBindings(overseasWarehouseId, domesticWarehouseId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private record WarehouseBindings(Long overseasWarehouseId, Long domesticWarehouseId) { }
|
||||||
|
|
||||||
private boolean isProtectedPlatformAdmin(IamMembership membership, List<Long> roleIds) {
|
private boolean isProtectedPlatformAdmin(IamMembership membership, List<Long> roleIds) {
|
||||||
return membership.getTenantId() == PLATFORM_TENANT_ID && containsPlatformAdminRole(roleIds);
|
return membership.getTenantId() == PLATFORM_TENANT_ID && containsPlatformAdminRole(roleIds);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -149,6 +149,7 @@ CREATE TABLE IF NOT EXISTS iam_membership (
|
|||||||
member_type VARCHAR(20) NOT NULL DEFAULT 'EMPLOYEE' COMMENT '成员类型:EMPLOYEE企业员工等',
|
member_type VARCHAR(20) NOT NULL DEFAULT 'EMPLOYEE' COMMENT '成员类型:EMPLOYEE企业员工等',
|
||||||
member_no VARCHAR(64) NULL COMMENT '客户企业内部成员编号',
|
member_no VARCHAR(64) NULL COMMENT '客户企业内部成员编号',
|
||||||
overseas_warehouse_id BIGINT NULL COMMENT '海外仓PDA操作员绑定的唯一实际海外仓ID',
|
overseas_warehouse_id BIGINT NULL COMMENT '海外仓PDA操作员绑定的唯一实际海外仓ID',
|
||||||
|
domestic_warehouse_id BIGINT NULL COMMENT '国内头程PDA操作员绑定的唯一国内作业仓ID',
|
||||||
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE' COMMENT '成员状态:ACTIVE在职、DISABLED停用、LEFT已离开',
|
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE' COMMENT '成员状态:ACTIVE在职、DISABLED停用、LEFT已离开',
|
||||||
joined_at DATETIME(3) NULL COMMENT '加入客户企业时间',
|
joined_at DATETIME(3) NULL COMMENT '加入客户企业时间',
|
||||||
left_at DATETIME(3) NULL COMMENT '离开客户企业时间',
|
left_at DATETIME(3) NULL COMMENT '离开客户企业时间',
|
||||||
|
|||||||
@@ -3,6 +3,26 @@
|
|||||||
USE baiyue_iam;
|
USE baiyue_iam;
|
||||||
SET NAMES utf8mb4;
|
SET NAMES utf8mb4;
|
||||||
|
|
||||||
|
DROP PROCEDURE IF EXISTS ensure_domestic_pda_operator_warehouse_column;
|
||||||
|
DELIMITER $$
|
||||||
|
CREATE PROCEDURE ensure_domestic_pda_operator_warehouse_column()
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_schema = DATABASE() AND table_name = 'iam_membership'
|
||||||
|
AND column_name = 'domestic_warehouse_id'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE iam_membership
|
||||||
|
ADD COLUMN domestic_warehouse_id BIGINT NULL
|
||||||
|
COMMENT '国内头程PDA操作员绑定的唯一国内作业仓ID'
|
||||||
|
AFTER overseas_warehouse_id,
|
||||||
|
ADD KEY idx_membership_domestic_warehouse (tenant_id, domestic_warehouse_id, status);
|
||||||
|
END IF;
|
||||||
|
END$$
|
||||||
|
DELIMITER ;
|
||||||
|
CALL ensure_domestic_pda_operator_warehouse_column();
|
||||||
|
DROP PROCEDURE ensure_domestic_pda_operator_warehouse_column;
|
||||||
|
|
||||||
UPDATE iam_role
|
UPDATE iam_role
|
||||||
SET role_code = 'OVERSEAS_WAREHOUSE_PDA_OPERATOR',
|
SET role_code = 'OVERSEAS_WAREHOUSE_PDA_OPERATOR',
|
||||||
role_name = '海外仓 PDA 作业员',
|
role_name = '海外仓 PDA 作业员',
|
||||||
@@ -72,13 +92,16 @@ CREATE TEMPORARY TABLE tmp_phase3_warehouse_role_permission (
|
|||||||
PRIMARY KEY (role_code, permission_code)
|
PRIMARY KEY (role_code, permission_code)
|
||||||
) ENGINE=MEMORY;
|
) ENGINE=MEMORY;
|
||||||
|
|
||||||
-- 操作员:查看入仓业务并维护后台装柜证据,不授予完成装柜、任务分派或出仓动作。
|
-- 仓管:可查看并确认预装、指定国内 PDA 作业员,不授予 PDA 执行或出仓动作。
|
||||||
INSERT INTO tmp_phase3_warehouse_role_permission (role_code, permission_code) VALUES
|
INSERT INTO tmp_phase3_warehouse_role_permission (role_code, permission_code) VALUES
|
||||||
('WAREHOUSE_OPERATOR', 'platform:order'),
|
('WAREHOUSE_OPERATOR', 'platform:order'),
|
||||||
('WAREHOUSE_OPERATOR', 'platform:firstleg:inbound:list'),
|
('WAREHOUSE_OPERATOR', 'platform:firstleg:inbound:list'),
|
||||||
('WAREHOUSE_OPERATOR', 'platform:firstleg:inbound:view'),
|
('WAREHOUSE_OPERATOR', 'platform:firstleg:inbound:view'),
|
||||||
('WAREHOUSE_OPERATOR', 'platform:firstleg:inbound:loading:view'),
|
('WAREHOUSE_OPERATOR', 'platform:firstleg:inbound:loading:view'),
|
||||||
('WAREHOUSE_OPERATOR', 'platform:firstleg:inbound:loading:edit');
|
('WAREHOUSE_OPERATOR', 'platform:firstleg:inbound:loading:edit'),
|
||||||
|
('WAREHOUSE_OPERATOR', 'platform:firstleg:preload:list'),
|
||||||
|
('WAREHOUSE_OPERATOR', 'platform:firstleg:preload:edit'),
|
||||||
|
('WAREHOUSE_OPERATOR', 'platform:firstleg:pda:assign');
|
||||||
|
|
||||||
-- 国内头程 PDA 作业员:只允许国内任务查询、作业和现场照片,不授予后台、海外、分派或复核权限。
|
-- 国内头程 PDA 作业员:只允许国内任务查询、作业和现场照片,不授予后台、海外、分派或复核权限。
|
||||||
INSERT INTO tmp_phase3_warehouse_role_permission (role_code, permission_code) VALUES
|
INSERT INTO tmp_phase3_warehouse_role_permission (role_code, permission_code) VALUES
|
||||||
@@ -112,7 +135,7 @@ BEGIN
|
|||||||
COALESCE(SUM(role_code = 'WAREHOUSE_TALLY'), 0)
|
COALESCE(SUM(role_code = 'WAREHOUSE_TALLY'), 0)
|
||||||
INTO operator_permission_count, domestic_pda_permission_count, tally_permission_count
|
INTO operator_permission_count, domestic_pda_permission_count, tally_permission_count
|
||||||
FROM tmp_phase3_warehouse_role_permission;
|
FROM tmp_phase3_warehouse_role_permission;
|
||||||
IF operator_permission_count <> 5
|
IF operator_permission_count <> 8
|
||||||
OR domestic_pda_permission_count <> 2
|
OR domestic_pda_permission_count <> 2
|
||||||
OR tally_permission_count <> 12 THEN
|
OR tally_permission_count <> 12 THEN
|
||||||
SIGNAL SQLSTATE '45000'
|
SIGNAL SQLSTATE '45000'
|
||||||
@@ -208,7 +231,7 @@ BEGIN
|
|||||||
JOIN iam_role_permission grant_row
|
JOIN iam_role_permission grant_row
|
||||||
ON grant_row.role_id = role.id
|
ON grant_row.role_id = role.id
|
||||||
AND grant_row.permission_id = permission.id
|
AND grant_row.permission_id = permission.id
|
||||||
AND grant_row.tenant_id = 0) <> 19 THEN
|
AND grant_row.tenant_id = 0) <> 22 THEN
|
||||||
SIGNAL SQLSTATE '45000'
|
SIGNAL SQLSTATE '45000'
|
||||||
SET MESSAGE_TEXT = '仓库角色初始化失败:最小权限授权未完整写入';
|
SET MESSAGE_TEXT = '仓库角色初始化失败:最小权限授权未完整写入';
|
||||||
END IF;
|
END IF;
|
||||||
|
|||||||
@@ -435,6 +435,18 @@ paths:
|
|||||||
responses:
|
responses:
|
||||||
'200': {$ref: '#/components/responses/Success'}
|
'200': {$ref: '#/components/responses/Success'}
|
||||||
'403': {$ref: '#/components/responses/Forbidden'}
|
'403': {$ref: '#/components/responses/Forbidden'}
|
||||||
|
/api/platform/v1/iam/users/domestic-pda-operators:
|
||||||
|
get:
|
||||||
|
tags: [IAM Resources / IAM 资源]
|
||||||
|
summary: 查询启用的国内头程 PDA 作业员候选
|
||||||
|
description: 仅返回具有 CN_FIRSTLEG_PDA_OPERATOR 角色且账号、成员、角色均启用的平台人员。
|
||||||
|
x-permission: platform:firstleg:pda:assign
|
||||||
|
parameters:
|
||||||
|
- {name: warehouseId, in: query, required: true, schema: {type: integer, format: int64, minimum: 1, description: 仅返回绑定到该国内仓的国内头程 PDA 作业员}}
|
||||||
|
- {name: keyword, in: query, required: false, schema: {type: string}}
|
||||||
|
responses:
|
||||||
|
'200': {$ref: '#/components/responses/Success'}
|
||||||
|
'403': {$ref: '#/components/responses/Forbidden'}
|
||||||
/api/platform/v1/iam/users/{membershipId}:
|
/api/platform/v1/iam/users/{membershipId}:
|
||||||
parameters:
|
parameters:
|
||||||
- {$ref: '#/components/parameters/MembershipId'}
|
- {$ref: '#/components/parameters/MembershipId'}
|
||||||
@@ -993,6 +1005,19 @@ paths:
|
|||||||
total: {type: integer, format: int64}
|
total: {type: integer, format: int64}
|
||||||
pages: {type: integer}
|
pages: {type: integer}
|
||||||
'403': {$ref: '#/components/responses/Forbidden'}
|
'403': {$ref: '#/components/responses/Forbidden'}
|
||||||
|
/internal/iam/domestic-pda-operators/{authUserId}:
|
||||||
|
get:
|
||||||
|
tags: [IAM Resources / IAM 资源]
|
||||||
|
summary: 校验并返回启用的国内头程 PDA 作业员
|
||||||
|
security: []
|
||||||
|
parameters:
|
||||||
|
- {$ref: '#/components/parameters/InternalServiceToken'}
|
||||||
|
- {name: authUserId, in: path, required: true, schema: {type: integer, format: int64, minimum: 1}}
|
||||||
|
- {name: warehouseId, in: query, required: true, schema: {type: integer, format: int64, minimum: 1}}
|
||||||
|
responses:
|
||||||
|
'200': {$ref: '#/components/responses/Success'}
|
||||||
|
'403': {$ref: '#/components/responses/Forbidden'}
|
||||||
|
'404': {$ref: '#/components/responses/NotFound'}
|
||||||
/internal/exports/v1/CUSTOMER_LIST/batches:
|
/internal/exports/v1/CUSTOMER_LIST/batches:
|
||||||
post:
|
post:
|
||||||
tags: [Internal Export Provider]
|
tags: [Internal Export Provider]
|
||||||
@@ -1550,6 +1575,7 @@ components:
|
|||||||
tenantId: {type: integer, format: int64, enum: [0], description: 保留的兼容字段,只允许平台技术作用域 0}
|
tenantId: {type: integer, format: int64, enum: [0], description: 保留的兼容字段,只允许平台技术作用域 0}
|
||||||
roleIds: {type: array, minItems: 1, items: {type: integer, format: int64}}
|
roleIds: {type: array, minItems: 1, items: {type: integer, format: int64}}
|
||||||
overseasWarehouseId: {type: integer, format: int64, nullable: true, description: 选择 OVERSEAS_WAREHOUSE_PDA_OPERATOR 时必填且只能绑定一个实际海外仓;CN_FIRSTLEG_PDA_OPERATOR 与其他角色必须为空;两个 PDA 角色不能同时分配}
|
overseasWarehouseId: {type: integer, format: int64, nullable: true, description: 选择 OVERSEAS_WAREHOUSE_PDA_OPERATOR 时必填且只能绑定一个实际海外仓;CN_FIRSTLEG_PDA_OPERATOR 与其他角色必须为空;两个 PDA 角色不能同时分配}
|
||||||
|
domesticWarehouseId: {type: integer, format: int64, nullable: true, description: 选择 CN_FIRSTLEG_PDA_OPERATOR 时必填且只能绑定一个有效国内作业仓;非国内头程 PDA 角色时必须为空;两个 PDA 角色不能同时分配}
|
||||||
password: {type: string, format: password, minLength: 8, maxLength: 64, writeOnly: true}
|
password: {type: string, format: password, minLength: 8, maxLength: 64, writeOnly: true}
|
||||||
status: {type: string, enum: [ACTIVE, DISABLED]}
|
status: {type: string, enum: [ACTIVE, DISABLED]}
|
||||||
UpdatePlatformManagedUserRequest:
|
UpdatePlatformManagedUserRequest:
|
||||||
@@ -1562,6 +1588,7 @@ components:
|
|||||||
tenantId: {type: integer, format: int64, enum: [0], description: 保留的兼容字段,只允许平台技术作用域 0}
|
tenantId: {type: integer, format: int64, enum: [0], description: 保留的兼容字段,只允许平台技术作用域 0}
|
||||||
roleIds: {type: array, minItems: 1, items: {type: integer, format: int64}}
|
roleIds: {type: array, minItems: 1, items: {type: integer, format: int64}}
|
||||||
overseasWarehouseId: {type: integer, format: int64, nullable: true, description: 选择 OVERSEAS_WAREHOUSE_PDA_OPERATOR 时必填且只能绑定一个实际海外仓;CN_FIRSTLEG_PDA_OPERATOR 与其他角色必须为空;两个 PDA 角色不能同时分配}
|
overseasWarehouseId: {type: integer, format: int64, nullable: true, description: 选择 OVERSEAS_WAREHOUSE_PDA_OPERATOR 时必填且只能绑定一个实际海外仓;CN_FIRSTLEG_PDA_OPERATOR 与其他角色必须为空;两个 PDA 角色不能同时分配}
|
||||||
|
domesticWarehouseId: {type: integer, format: int64, nullable: true, description: 选择 CN_FIRSTLEG_PDA_OPERATOR 时必填且只能绑定一个有效国内作业仓;非国内头程 PDA 角色时必须为空;两个 PDA 角色不能同时分配}
|
||||||
CreateInternalUserRequest:
|
CreateInternalUserRequest:
|
||||||
type: object
|
type: object
|
||||||
required: [displayName]
|
required: [displayName]
|
||||||
@@ -1635,6 +1662,7 @@ components:
|
|||||||
lastLoginAt: {type: string, format: date-time, nullable: true}
|
lastLoginAt: {type: string, format: date-time, nullable: true}
|
||||||
protectedAccount: {type: boolean}
|
protectedAccount: {type: boolean}
|
||||||
overseasWarehouseId: {type: string, pattern: '^\d+$', nullable: true, description: 海外仓PDA操作员绑定的唯一实际海外仓ID}
|
overseasWarehouseId: {type: string, pattern: '^\d+$', nullable: true, description: 海外仓PDA操作员绑定的唯一实际海外仓ID}
|
||||||
|
domesticWarehouseId: {type: string, pattern: '^\d+$', nullable: true, description: 国内头程PDA操作员绑定的唯一国内作业仓ID}
|
||||||
CreateMemberRequest:
|
CreateMemberRequest:
|
||||||
type: object
|
type: object
|
||||||
required: [username, password, displayName, memberType]
|
required: [username, password, displayName, memberType]
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.baiyue.iam.repository;
|
||||||
|
|
||||||
|
import com.baiyue.iam.mapper.IamMembershipRoleMapper;
|
||||||
|
import org.apache.ibatis.annotations.Select;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class DomesticPdaOperatorSqlContractTest {
|
||||||
|
@Test
|
||||||
|
void candidatesUseActivePlatformRoleAndAuthAccountIdentity() throws Exception {
|
||||||
|
String sql = IamMembershipRoleMapper.class.getDeclaredMethod("findActiveUsersByRoleCode",
|
||||||
|
long.class, String.class, String.class, Long.class).getAnnotation(Select.class).value()[0];
|
||||||
|
|
||||||
|
assertThat(sql).contains("u.auth_user_id AS userId", "role.role_code = #{roleCode}",
|
||||||
|
"role.app_code = 'PLATFORM'", "role.status = 'ACTIVE'", "m.status = 'ACTIVE'",
|
||||||
|
"u.status = 'ACTIVE'", "u.auth_user_id = #{authUserId}")
|
||||||
|
.doesNotContain("baiyue_auth");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -46,7 +46,7 @@ class WarehouseRoleSqlContractTest {
|
|||||||
.contains("'WAREHOUSE_TALLY', 'platform:firstleg:pda:review'")
|
.contains("'WAREHOUSE_TALLY', 'platform:firstleg:pda:review'")
|
||||||
.contains("'WAREHOUSE_TALLY', 'platform:firstleg:inbound:loading:edit'")
|
.contains("'WAREHOUSE_TALLY', 'platform:firstleg:inbound:loading:edit'")
|
||||||
.contains("'WAREHOUSE_OPERATOR', 'platform:firstleg:inbound:loading:edit'")
|
.contains("'WAREHOUSE_OPERATOR', 'platform:firstleg:inbound:loading:edit'")
|
||||||
.contains("operator_permission_count <> 5")
|
.contains("operator_permission_count <> 8")
|
||||||
.contains("domestic_pda_permission_count <> 2")
|
.contains("domestic_pda_permission_count <> 2")
|
||||||
.contains("tally_permission_count <> 12")
|
.contains("tally_permission_count <> 12")
|
||||||
.contains("依赖的三期平台权限尚未完整部署")
|
.contains("依赖的三期平台权限尚未完整部署")
|
||||||
@@ -54,7 +54,7 @@ class WarehouseRoleSqlContractTest {
|
|||||||
.contains("expected.permission_code IS NULL")
|
.contains("expected.permission_code IS NULL")
|
||||||
.contains("role.role_code = 'OVERSEAS_WAREHOUSE_PDA_OPERATOR' AND permission.permission_code = 'lastleg:pda:operate'")
|
.contains("role.role_code = 'OVERSEAS_WAREHOUSE_PDA_OPERATOR' AND permission.permission_code = 'lastleg:pda:operate'")
|
||||||
.contains("最小权限授权未完整写入")
|
.contains("最小权限授权未完整写入")
|
||||||
.contains("grant_row.tenant_id = 0) <> 19")
|
.contains("grant_row.tenant_id = 0) <> 22")
|
||||||
.contains("(6000004, 4000004, 0, 'TENANT', NULL)")
|
.contains("(6000004, 4000004, 0, 'TENANT', NULL)")
|
||||||
.contains("(6000005, 4000005, 0, 'TENANT', NULL)")
|
.contains("(6000005, 4000005, 0, 'TENANT', NULL)")
|
||||||
.contains("(6000006, 4000006, 0, 'TENANT', NULL)")
|
.contains("(6000006, 4000006, 0, 'TENANT', NULL)")
|
||||||
@@ -80,7 +80,7 @@ class WarehouseRoleSqlContractTest {
|
|||||||
.doesNotContain("platform:firstleg:inbound:picking:view")
|
.doesNotContain("platform:firstleg:inbound:picking:view")
|
||||||
.doesNotContain("platform:firstleg:outbound-dispatch:confirm");
|
.doesNotContain("platform:firstleg:outbound-dispatch:confirm");
|
||||||
assertThat(sql)
|
assertThat(sql)
|
||||||
.doesNotContain("'WAREHOUSE_OPERATOR', 'platform:firstleg:pda:assign'")
|
.contains("'WAREHOUSE_OPERATOR', 'platform:firstleg:pda:assign'")
|
||||||
.doesNotContain("'WAREHOUSE_OPERATOR', 'platform:firstleg:inbound:loading:complete'")
|
.doesNotContain("'WAREHOUSE_OPERATOR', 'platform:firstleg:inbound:loading:complete'")
|
||||||
.doesNotContain("'WAREHOUSE_OPERATOR', 'platform:firstleg:outbound-dispatch:confirm'")
|
.doesNotContain("'WAREHOUSE_OPERATOR', 'platform:firstleg:outbound-dispatch:confirm'")
|
||||||
.doesNotContain("'WAREHOUSE_OPERATOR', 'platform:firstleg:outbound-dispatch:reverse'")
|
.doesNotContain("'WAREHOUSE_OPERATOR', 'platform:firstleg:outbound-dispatch:reverse'")
|
||||||
|
|||||||
Reference in New Issue
Block a user