From 2555eda797b38625610b5a84d5b3d5932a5f7b29 Mon Sep 17 00:00:00 2001 From: liuyihe <2513762445@qq.com> Date: Tue, 22 Sep 2026 17:27:32 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B4=A7=E6=9F=9Cpda=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E6=93=8D=E4=BD=9C=E4=BA=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/client/AuthorizationSnapshot.java | 10 +++- .../com/baiyue/auth/service/AuthService.java | 3 ++ deploy/sql/头程数据库统一增量-MySQL8.sql | 4 ++ .../client/DomesticPdaOperatorClient.java | 19 ++++++++ .../PlatformLoadingListController.java | 4 +- .../PlatformLoadingPdaController.java | 12 ++++- .../baiyue/firstleg/dto/LoadingListDtos.java | 3 +- .../mapper/LoadingPickTaskMapper.java | 2 +- .../firstleg/service/LoadingListService.java | 20 +++++++- .../firstleg/service/LoadingPickService.java | 33 ++++++++++--- .../resources/openapi/loading-lists-v1.yaml | 9 ++-- .../LoadingListControllerContractTest.java | 6 ++- .../LoadingPickPdaTaskSqlContractTest.java | 5 +- .../service/LoadingListServiceTest.java | 48 +++++++++++++++---- .../service/LoadingPickServiceTest.java | 29 ++++++++--- .../controller/InternalTenantController.java | 12 +++++ .../PlatformUserManagementController.java | 16 +++++++ .../com/baiyue/iam/dto/AuthorizationDtos.java | 3 +- .../iam/dto/DomesticPdaOperatorDtos.java | 8 ++++ .../iam/dto/PlatformUserManagementDtos.java | 14 ++++-- .../com/baiyue/iam/entity/IamMembership.java | 2 +- .../iam/mapper/AuthorizationQueryMapper.java | 4 ++ .../iam/mapper/IamMembershipRoleMapper.java | 28 +++++++++++ .../service/AuthorizationSnapshotService.java | 3 +- .../service/DomesticPdaOperatorService.java | 32 +++++++++++++ .../PlatformUserManagementService.java | 28 +++++++---- iam/src/main/resources/db/iam.sql | 1 + .../resources/db/phase3-warehouse-roles.sql | 31 ++++++++++-- iam/src/main/resources/openapi/iam-v1.yaml | 28 +++++++++++ .../DomesticPdaOperatorSqlContractTest.java | 20 ++++++++ .../WarehouseRoleSqlContractTest.java | 6 +-- 31 files changed, 384 insertions(+), 59 deletions(-) create mode 100644 firstleg/src/main/java/com/baiyue/firstleg/client/DomesticPdaOperatorClient.java create mode 100644 iam/src/main/java/com/baiyue/iam/dto/DomesticPdaOperatorDtos.java create mode 100644 iam/src/main/java/com/baiyue/iam/service/DomesticPdaOperatorService.java create mode 100644 iam/src/test/java/com/baiyue/iam/repository/DomesticPdaOperatorSqlContractTest.java diff --git a/auth/src/main/java/com/baiyue/auth/client/AuthorizationSnapshot.java b/auth/src/main/java/com/baiyue/auth/client/AuthorizationSnapshot.java index 1061534be..a7cae146c 100644 --- a/auth/src/main/java/com/baiyue/auth/client/AuthorizationSnapshot.java +++ b/auth/src/main/java/com/baiyue/auth/client/AuthorizationSnapshot.java @@ -22,11 +22,19 @@ public class AuthorizationSnapshot { private Set permissions; private Long permissionVersion; private Long overseasWarehouseId; + private Long domesticWarehouseId; public AuthorizationSnapshot(Long userId, Long tenantId, Long membershipId, String appCode, String clientType, Set roles, Set permissions, Long permissionVersion) { 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 roles, Set permissions, + Long permissionVersion, Long overseasWarehouseId) { + this(userId, tenantId, membershipId, appCode, clientType, roles, permissions, + permissionVersion, overseasWarehouseId, null); } } diff --git a/auth/src/main/java/com/baiyue/auth/service/AuthService.java b/auth/src/main/java/com/baiyue/auth/service/AuthService.java index 6ff1a3fa6..15144bf9f 100644 --- a/auth/src/main/java/com/baiyue/auth/service/AuthService.java +++ b/auth/src/main/java/com/baiyue/auth/service/AuthService.java @@ -135,6 +135,9 @@ public class AuthService { if (authorization.getOverseasWarehouseId() != null) { claimsBuilder.claim("overseas_warehouse_id", authorization.getOverseasWarehouseId()); } + if (authorization.getDomesticWarehouseId() != null) { + claimsBuilder.claim("domestic_warehouse_id", authorization.getDomesticWarehouseId()); + } JwtClaimsSet claims = claimsBuilder.build(); String accessToken = jwtEncoder.encode(JwtEncoderParameters.from( signingHeader(), claims)).getTokenValue(); diff --git a/deploy/sql/头程数据库统一增量-MySQL8.sql b/deploy/sql/头程数据库统一增量-MySQL8.sql index 08b793951..a93cf2944 100644 --- a/deploy/sql/头程数据库统一增量-MySQL8.sql +++ b/deploy/sql/头程数据库统一增量-MySQL8.sql @@ -124,6 +124,7 @@ CREATE TABLE iam_membership ( member_type varchar(20) NOT NULL DEFAULT 'EMPLOYEE' COMMENT '成员类型:EMPLOYEE企业员工等', member_no varchar(64) DEFAULT NULL COMMENT '客户企业内部成员编号', 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已离开', joined_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,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,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,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)); diff --git a/firstleg/src/main/java/com/baiyue/firstleg/client/DomesticPdaOperatorClient.java b/firstleg/src/main/java/com/baiyue/firstleg/client/DomesticPdaOperatorClient.java new file mode 100644 index 000000000..f154064ea --- /dev/null +++ b/firstleg/src/main/java/com/baiyue/firstleg/client/DomesticPdaOperatorClient.java @@ -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 required(@RequestHeader("X-Internal-Service-Token") String token, + @PathVariable long authUserId, + @RequestParam long warehouseId); + + record Operator(Long userId, String displayName) { } +} diff --git a/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformLoadingListController.java b/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformLoadingListController.java index 31408a188..e4f5b0dd8 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformLoadingListController.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformLoadingListController.java @@ -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 confirmPreload( @AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long planId, diff --git a/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformLoadingPdaController.java b/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformLoadingPdaController.java index 0344746e7..96b687425 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformLoadingPdaController.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformLoadingPdaController.java @@ -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> 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"); + } + } diff --git a/firstleg/src/main/java/com/baiyue/firstleg/dto/LoadingListDtos.java b/firstleg/src/main/java/com/baiyue/firstleg/dto/LoadingListDtos.java index 3b270601c..383ffb6bb 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/dto/LoadingListDtos.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/dto/LoadingListDtos.java @@ -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( diff --git a/firstleg/src/main/java/com/baiyue/firstleg/mapper/LoadingPickTaskMapper.java b/firstleg/src/main/java/com/baiyue/firstleg/mapper/LoadingPickTaskMapper.java index e3f64662c..809628c83 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/mapper/LoadingPickTaskMapper.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/mapper/LoadingPickTaskMapper.java @@ -108,7 +108,7 @@ public interface LoadingPickTaskMapper extends BaseMapper 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} AND (booking.container_no LIKE CONCAT('%', #{keyword}, '%') OR booking.warehouse_name LIKE CONCAT('%', #{keyword}, '%')) diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/LoadingListService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/LoadingListService.java index f2a26f9ce..98c15ed52 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/LoadingListService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/LoadingListService.java @@ -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 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, diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/LoadingPickService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/LoadingPickService.java index 53a8af32a..9af36a61d 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/LoadingPickService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/LoadingPickService.java @@ -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 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); diff --git a/firstleg/src/main/resources/openapi/loading-lists-v1.yaml b/firstleg/src/main/resources/openapi/loading-lists-v1.yaml index 5756c25c2..ddea318e6 100644 --- a/firstleg/src/main/resources/openapi/loading-lists-v1.yaml +++ b/firstleg/src/main/resources/openapi/loading-lists-v1.yaml @@ -65,9 +65,9 @@ paths: '200': {$ref: '#/components/responses/PreloadDetailResponse'} /api/platform/v1/first-leg/loading-plans/{planId}/confirm: post: - summary: 原子确认整份预装计划 - description: 以 loadingPlanId 为唯一范围,原子确认全部已分配入库预装票据。要求计划已 push 且仍为 EDITING、稳定 bridge 完整、真实订舱已关联并为 RELEASED,且真实集装箱号已经补录。任一条件不满足均不写入,也不创建 PDA task。成功后唯一创建待领取 PDA task,claim 后才推进 LOADING。同一 Idempotency-Key 与相同请求精确重放原响应,不同 Key 重复确认返回业务冲突。 - x-permission: platform:firstleg:preload:edit + summary: 原子确认预装并分配国内 PDA 作业员 + description: 以 loadingPlanId 为唯一范围,校验指定人员为启用的国内头程 PDA 作业员,原子确认预装、创建任务并分配。未分配任务不向 PDA 展示;同一 Idempotency-Key 与相同请求精确重放原响应。 + x-permission: platform:firstleg:preload:edit, platform:firstleg:pda:assign parameters: [{$ref: '#/components/parameters/PlanId'}, {$ref: '#/components/parameters/IdempotencyKey'}] requestBody: required: true @@ -336,10 +336,11 @@ components: items: {type: string, enum: [LOADING_PLAN_NOT_EDITING, PRELOAD_LINK_MISSING, CONTAINER_NO_MISSING, OCEAN_BOOKING_NOT_LINKED, OCEAN_BOOKING_NOT_RELEASED, PDA_TASK_ALREADY_CREATED, PRELOAD_ALREADY_CONFIRMED]} ConfirmPreloadRequest: type: object - required: [expectedVersion, operationReason] + required: [expectedVersion, assigneeId] additionalProperties: false properties: expectedVersion: {type: integer, format: int32, minimum: 0, description: 当前装柜计划 version} + assigneeId: {$ref: '#/components/schemas/LongId'} PreloadConfirmation: type: object required: [planId, containerBookingId, confirmationStatus, version, readiness] diff --git a/firstleg/src/test/java/com/baiyue/firstleg/controller/LoadingListControllerContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/controller/LoadingListControllerContractTest.java index f1fb2e42f..a596d16a0 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/controller/LoadingListControllerContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/controller/LoadingListControllerContractTest.java @@ -33,8 +33,10 @@ class LoadingListControllerContractTest { authority("lifecycle", "platform:firstleg:preload:list", AuthenticatedUser.class, long.class, long.class); authority("updateTransport", "platform:firstleg:preload:edit", AuthenticatedUser.class, long.class, long.class, String.class, TransportUpdateRequest.class); - authority("confirmPreload", "platform:firstleg:preload:edit", AuthenticatedUser.class, - long.class, String.class, ConfirmPreloadRequest.class); + Method confirm = PlatformLoadingListController.class.getDeclaredMethod("confirmPreload", + AuthenticatedUser.class, long.class, String.class, ConfirmPreloadRequest.class); + assertThat(confirm.getAnnotation(PreAuthorize.class).value()) + .isEqualTo("hasAuthority('platform:firstleg:preload:edit') and hasAuthority('platform:firstleg:pda:assign')"); authority("confirmedPage", "platform:firstleg:loading-list:list", AuthenticatedUser.class, ConfirmedQuery.class); authority("confirmedDetail", "platform:firstleg:loading-list:list", AuthenticatedUser.class, long.class, long.class); diff --git a/firstleg/src/test/java/com/baiyue/firstleg/repository/LoadingPickPdaTaskSqlContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/repository/LoadingPickPdaTaskSqlContractTest.java index 33025c9b1..f1b6eabe3 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/repository/LoadingPickPdaTaskSqlContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/repository/LoadingPickPdaTaskSqlContractTest.java @@ -35,7 +35,7 @@ class LoadingPickPdaTaskSqlContractTest { } @Test - void pendingTasksAreLimitedToUnassignedOrCurrentOperatorWithoutCustomerInput() throws Exception { + void pendingTasksAreLimitedToAssignedOperatorWithoutCustomerInput() throws Exception { String sql = LoadingPickTaskMapper.class .getDeclaredMethod("selectPendingForOperator", long.class, long.class, String.class) .getAnnotation(Select.class).value()[0]; @@ -54,7 +54,8 @@ class LoadingPickPdaTaskSqlContractTest { .contains("pick_item.item_status IN ('PENDING', 'PICKING')") .contains("THEN 'AWAITING_REVIEW'") .contains("ELSE 'IN_PROGRESS' END AS workflowStatus") - .contains("assignment.pick_task_id IS NULL OR assignment.assignee_id = #{operatorId}") + .contains("AND assignment.assignee_id = #{operatorId}") + .doesNotContain("assignment.pick_task_id IS NULL OR assignment.assignee_id = #{operatorId}") .contains("booking.warehouse_id AS warehouseId") .doesNotContain("tenantId"); } diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/LoadingListServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/LoadingListServiceTest.java index aeff90aba..a5a61f743 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/LoadingListServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/LoadingListServiceTest.java @@ -12,6 +12,8 @@ import com.baiyue.firstleg.dto.LoadingListDtos.PreloadReadiness; import com.baiyue.firstleg.dto.LoadingListDtos.DocumentAttachRequest; import com.baiyue.firstleg.dto.LoadingListDtos.DocumentCompleteRequest; import com.baiyue.firstleg.client.LoadingDocumentFileClient; +import com.baiyue.firstleg.client.DomesticPdaOperatorClient; +import com.baiyue.firstleg.entity.LoadingPickTaskEntity; import com.baiyue.firstleg.entity.ContainerDocumentEntity; import com.baiyue.common.core.api.ApiResponse; import com.baiyue.firstleg.mapper.LoadingListQueryMapper.AggregateRow; @@ -63,6 +65,8 @@ class LoadingListServiceTest { @MockBean private LoadingDocumentFileClient documentFiles; @MockBean + private DomesticPdaOperatorClient domesticPdaOperators; + @MockBean private LoadingCompletedOutboxService loadingCompletedOutboxService; @MockBean private FbaPricingService fbaPricingService; @@ -784,6 +788,18 @@ class LoadingListServiceTest { } + @Test + void rejectsInvalidPdaOperatorBeforeConfirmingAnyPreload() { + when(domesticPdaOperators.required(any(), eq(77L))) + .thenReturn(ApiResponse.success(new DomesticPdaOperatorClient.Operator(88L, "其他人"))); + + assertThatThrownBy(() -> service.confirmPreload(10L, 9L, "operator", "invalid-operator", + new ConfirmPreloadRequest(3, 77L))) + .isInstanceOf(BizException.class).hasMessageContaining("作业员无效"); + verify(repository, never()).touchForPreloadConfirmation(anyLong(), anyInt(), anyLong(), anyString()); + verify(loadingPickService, never()).beginIfReady(anyLong(), anyLong(), anyString()); + } + @Test void confirmsEveryLinkedPreloadAndCreatesSinglePdaTaskAtomically() { AggregateRow before = row("EDITING", 3); @@ -810,9 +826,16 @@ class LoadingListServiceTest { when(repository.confirmLinkedPreload(301L, "PENDING", 2, 9L, "operator")).thenReturn(1); when(repository.confirmLinkedPreload(302L, "CONFIRMING", 5, 9L, "operator")).thenReturn(1); when(repository.preloadDetail(after)).thenReturn(detail); + when(domesticPdaOperators.required(any(), eq(77L))) + .thenReturn(ApiResponse.success(new DomesticPdaOperatorClient.Operator(77L, "作业员"))); + LoadingPickTaskEntity pickTask = new LoadingPickTaskEntity(); + pickTask.setId(80L); + when(pickRepository.taskByPlan(10L)).thenReturn(pickTask); + when(pickRepository.createAssignment(80L, 77L, "作业员", 9L, "operator", "ASSIGNED")) + .thenReturn(1); PreloadConfirmationResponse response = service.confirmPreload( - 10L, 9L, "operator", "confirm-key", new ConfirmPreloadRequest(3)); + 10L, 9L, "operator", "confirm-key", new ConfirmPreloadRequest(3, 77L)); assertThat(response.confirmationStatus()).isEqualTo("CONFIRMED"); assertThat(response.version()).isEqualTo(4); @@ -822,6 +845,7 @@ class LoadingListServiceTest { verify(fbaPricingService).recalculateUnlockedContainerCustomerFees(eq(30L), eq(101L), anyString()); verify(fbaPricingService).recalculateUnlockedContainerCustomerFees(eq(30L), eq(102L), anyString()); verify(loadingPickService, times(1)).beginIfReady(30L, 9L, "operator"); + verify(pickRepository).createAssignment(80L, 77L, "作业员", 9L, "operator", "ASSIGNED"); verify(repository).appendHistory(eq(10L), anyString(), eq("CONFIRM_PRELOAD"), eq("EDITING"), eq("EDITING"), anyString(), anyString(), eq(9L), eq("operator"), org.mockito.ArgumentMatchers.isNull()); @@ -838,9 +862,11 @@ class LoadingListServiceTest { when(fbaPricingService.recalculateUnlockedContainerCustomerFees(eq(30L), eq(101L), anyString())) .thenReturn(new FbaPricingService.ContainerFeeRecalculationResult( FbaPricingService.ContainerFeeRecalculationStatus.NOT_READY, 0)); + when(domesticPdaOperators.required(any(), eq(77L))) + .thenReturn(ApiResponse.success(new DomesticPdaOperatorClient.Operator(77L, "作业员"))); assertThatThrownBy(() -> service.confirmPreload( - 10L, 9L, "operator", "confirm-key", new ConfirmPreloadRequest(3))) + 10L, 9L, "operator", "confirm-key", new ConfirmPreloadRequest(3, 77L))) .isInstanceOf(BizException.class).hasMessageContaining("初始费用"); verify(repository, never()).touchForPreloadConfirmation(anyLong(), anyInt(), anyLong(), anyString()); @@ -852,7 +878,7 @@ class LoadingListServiceTest { void exactConfirmationReplayReturnsFrozenResponseWithoutTouchingPlan() throws Exception { PreloadConfirmationResponse expected = new PreloadConfirmationResponse( 10L, 30L, "CONFIRMED", 4, readiness("CONFIRMED", false, true, 80L)); - String requestHash = sha256("CONFIRM_PRELOAD|10|3"); + String requestHash = sha256("CONFIRM_PRELOAD|10|3|77"); HistoryRow replay = new HistoryRow(); ObjectMapper mapper = new ObjectMapper(); replay.setAfterSnapshot(mapper.writerWithDefaultPrettyPrinter().writeValueAsString( @@ -860,7 +886,7 @@ class LoadingListServiceTest { when(repository.history(anyString())).thenReturn(replay); PreloadConfirmationResponse actual = service.confirmPreload( - 10L, 9L, "operator", "confirm-key", new ConfirmPreloadRequest(3)); + 10L, 9L, "operator", "confirm-key", new ConfirmPreloadRequest(3, 77L)); assertThat(actual).isEqualTo(expected); verify(loadingPickService, never()).requirePreloadConfirmationReady(anyLong()); @@ -876,14 +902,14 @@ class LoadingListServiceTest { void rejectsConfirmationReplayWhenExpectedVersionDiffers() throws Exception { PreloadConfirmationResponse expected = new PreloadConfirmationResponse( 10L, 30L, "CONFIRMED", 4, readiness("CONFIRMED", false, true, 80L)); - String originalHash = sha256("CONFIRM_PRELOAD|10|3"); + String originalHash = sha256("CONFIRM_PRELOAD|10|3|77"); HistoryRow replay = new HistoryRow(); replay.setAfterSnapshot(new ObjectMapper().writeValueAsString( java.util.Map.of("requestHash", originalHash, "response", expected))); when(repository.history(anyString())).thenReturn(replay); assertThatThrownBy(() -> service.confirmPreload( - 10L, 9L, "operator", "confirm-key", new ConfirmPreloadRequest(4))) + 10L, 9L, "operator", "confirm-key", new ConfirmPreloadRequest(4, 77L))) .isInstanceOf(BizException.class) .extracting(error -> ((BizException) error).getResultCode()) .isEqualTo(ResultCode.CONFLICT); @@ -898,11 +924,13 @@ class LoadingListServiceTest { AggregateRow confirmed = row("EDITING", 4); when(repository.history(anyString())).thenReturn(null); when(repository.aggregate(10L)).thenReturn(confirmed); + when(domesticPdaOperators.required(any(), eq(77L))) + .thenReturn(ApiResponse.success(new DomesticPdaOperatorClient.Operator(77L, "作业员"))); when(loadingPickService.requirePreloadConfirmationReady(10L)) .thenThrow(new BizException(ResultCode.CONFLICT, "预装计划已确认")); assertThatThrownBy(() -> service.confirmPreload( - 10L, 9L, "operator", "new-confirm-key", new ConfirmPreloadRequest(4))) + 10L, 9L, "operator", "new-confirm-key", new ConfirmPreloadRequest(4, 77L))) .isInstanceOf(BizException.class) .extracting(error -> ((BizException) error).getResultCode()) .isEqualTo(ResultCode.CONFLICT); @@ -919,11 +947,13 @@ class LoadingListServiceTest { AggregateRow before = row("EDITING", 3); when(repository.history(anyString())).thenReturn(null); when(repository.aggregate(10L)).thenReturn(before); + when(domesticPdaOperators.required(any(), eq(77L))) + .thenReturn(ApiResponse.success(new DomesticPdaOperatorClient.Operator(77L, "作业员"))); when(loadingPickService.requirePreloadConfirmationReady(10L)) .thenThrow(new BizException(ResultCode.CONFLICT, "第二票订舱尚未放舱")); assertThatThrownBy(() -> service.confirmPreload( - 10L, 9L, "operator", "confirm-key", new ConfirmPreloadRequest(3))) + 10L, 9L, "operator", "confirm-key", new ConfirmPreloadRequest(3, 77L))) .isInstanceOf(BizException.class).hasMessageContaining("第二票"); verify(repository, never()).touchForPreloadConfirmation(anyLong(), @@ -940,7 +970,7 @@ class LoadingListServiceTest { when(repository.history(anyString())).thenReturn(replay); assertThatThrownBy(() -> service.confirmPreload( - 10L, 9L, "operator", "confirm-key", new ConfirmPreloadRequest(4))) + 10L, 9L, "operator", "confirm-key", new ConfirmPreloadRequest(4, 77L))) .isInstanceOf(BizException.class).hasMessageContaining("不同请求"); verify(loadingPickService, never()).requirePreloadConfirmationReady(anyLong()); diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/LoadingPickServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/LoadingPickServiceTest.java index f21014288..4dba707d7 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/LoadingPickServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/LoadingPickServiceTest.java @@ -11,6 +11,8 @@ import com.baiyue.firstleg.dto.LoadingPickDtos.ClaimRequest; import com.baiyue.firstleg.dto.LoadingPickDtos.AssignRequest; import com.baiyue.firstleg.dto.LoadingPickDtos.CompleteLoadingRequest; import com.baiyue.firstleg.dto.LoadingPickDtos.Task; +import com.baiyue.firstleg.client.DomesticPdaOperatorClient; +import com.baiyue.common.core.api.ApiResponse; import com.baiyue.firstleg.entity.InboundPalletAllocationEntity; import com.baiyue.firstleg.entity.LoadingPickAssignmentEntity; import com.baiyue.firstleg.entity.LoadingPickHistoryEntity; @@ -106,6 +108,8 @@ class LoadingPickServiceTest { private InboundHoldGuard inboundHoldGuard; @MockBean private DropshipPalletizationMapper dropshipPallets; + @MockBean + private DomesticPdaOperatorClient domesticPdaOperators; @Resource private LoadingPickService service; @@ -565,8 +569,7 @@ class LoadingPickServiceTest { when(repository.operation("claim-2")).thenReturn(null); when(repository.task(20L)).thenReturn(task); when(repository.taskForUpdate(20L)).thenReturn(task); - when(repository.assignment(20L)).thenReturn(null, assignment); - when(repository.createAssignment(20L, 9L, "operator", 9L, "operator", "CLAIMED")).thenReturn(1); + when(repository.assignment(20L)).thenReturn(assignment); when(smartLoadingRepository.startPicking(10L, 9L, "operator")).thenReturn(1, 0); doAnswer(invocation -> { LoadingPickOperationEntity value = invocation.getArgument(0); @@ -574,9 +577,9 @@ class LoadingPickServiceTest { return null; }).when(repository).insert(any(LoadingPickOperationEntity.class)); - service.claim(20L, new ClaimRequest(0, "claim-1"), 9L, "operator"); - service.claim(20L, new ClaimRequest(0, "claim-1"), 9L, "operator"); - service.claim(20L, new ClaimRequest(0, "claim-2"), 9L, "operator"); + service.claim(20L, new ClaimRequest(assignment.getVersion(), "claim-1"), 9L, "operator"); + service.claim(20L, new ClaimRequest(assignment.getVersion(), "claim-1"), 9L, "operator"); + service.claim(20L, new ClaimRequest(assignment.getVersion(), "claim-2"), 9L, "operator"); verify(smartLoadingRepository, times(2)).startPicking(10L, 9L, "operator"); verify(smartLoadingRepository, times(1)).insert(argThat( @@ -585,7 +588,8 @@ class LoadingPickServiceTest { && "EDITING".equals(history.getFromStatus()) && "LOADING".equals(history.getToStatus()) && "PDA-START-20".equals(history.getOperationId()))); - verify(repository, times(1)).createAssignment(20L, 9L, "operator", 9L, "operator", "CLAIMED"); + verify(repository, never()).createAssignment(anyLong(), anyLong(), anyString(), anyLong(), + anyString(), anyString()); } @Test @@ -606,6 +610,17 @@ class LoadingPickServiceTest { verify(smartLoadingRepository, never()).startPicking(anyLong(), anyLong(), anyString()); } + @Test + void unassignedTaskCannotBeClaimedFromPda() { + when(repository.taskForUpdate(20L)).thenReturn(task()); + + assertThatThrownBy(() -> service.claim(20L, new ClaimRequest(0, "claim-unassigned"), + 9L, "operator")) + .isInstanceOf(BizException.class).hasMessageContaining("尚未指定作业员"); + verify(repository, never()).createAssignment(anyLong(), anyLong(), anyString(), + anyLong(), anyString(), anyString()); + } + @Test void assignRechecksSameOperationAfterWaitingForTaskLock() { LoadingPickTaskEntity task = task(); @@ -615,6 +630,8 @@ class LoadingPickServiceTest { when(repository.taskForUpdate(20L)).thenReturn(task); when(repository.task(20L)).thenReturn(task); when(repository.items(20L)).thenReturn(List.of()); + when(domesticPdaOperators.required(any(), eq(11L))) + .thenReturn(ApiResponse.success(new DomesticPdaOperatorClient.Operator(11L, "picker"))); Task replay = service.assign(20L, new AssignRequest(11L, "picker", 0, "assign-race"), 9L, "operator"); diff --git a/iam/src/main/java/com/baiyue/iam/controller/InternalTenantController.java b/iam/src/main/java/com/baiyue/iam/controller/InternalTenantController.java index 044f022c2..17348d05b 100644 --- a/iam/src/main/java/com/baiyue/iam/controller/InternalTenantController.java +++ b/iam/src/main/java/com/baiyue/iam/controller/InternalTenantController.java @@ -18,6 +18,8 @@ import com.baiyue.iam.service.TenantService; import com.baiyue.iam.dto.CustomerExportDtos.CustomerExportBatch; import com.baiyue.common.core.api.PageResult; 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.Resource; import jakarta.validation.Valid; @@ -38,6 +40,7 @@ public class InternalTenantController { @Resource private MerchantOnboardingService onboardingService; @Resource private ProfileResourceService profileResourceService; @Resource private TenantService tenantService; + @Resource private DomesticPdaOperatorService domesticPdaOperatorService; @Value("${baiyue.security.internal-service-token}") private String internalTokenValue; private byte[] internalToken; @@ -128,6 +131,15 @@ public class InternalTenantController { return ApiResponse.success(tenantService.pageFinanceCustomers(page, size, keyword, status, customerId)); } + @GetMapping("/domestic-pda-operators/{authUserId}") + public ApiResponse 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") public ApiResponse exportCustomers( @RequestHeader(value = "X-Internal-Service-Token", required = false) String token, diff --git a/iam/src/main/java/com/baiyue/iam/controller/PlatformUserManagementController.java b/iam/src/main/java/com/baiyue/iam/controller/PlatformUserManagementController.java index 8ec2775ac..3c289e875 100644 --- a/iam/src/main/java/com/baiyue/iam/controller/PlatformUserManagementController.java +++ b/iam/src/main/java/com/baiyue/iam/controller/PlatformUserManagementController.java @@ -6,6 +6,9 @@ import com.baiyue.common.security.AuthenticatedUser; import com.baiyue.iam.dto.PlatformUserManagementDtos.*; import com.baiyue.iam.service.PlatformUserManagementService; 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.tags.Tag; import jakarta.validation.Valid; @@ -26,6 +29,19 @@ import org.springframework.web.bind.annotation.*; public class PlatformUserManagementController { @Resource private PlatformUserManagementService service; + @Resource + private DomesticPdaOperatorService domesticPdaOperators; + + @GetMapping("/domestic-pda-operators") + @PreAuthorize("hasAuthority('platform:firstleg:pda:assign')") + @Operation(summary = "查询启用的国内头程 PDA 作业员候选") + public ApiResponse> domesticPdaOperators( + @RequestParam long warehouseId, + @RequestParam(required = false) String keyword, + @AuthenticationPrincipal AuthenticatedUser user) { + TenantGuard.tenant(user, "PLATFORM"); + return ApiResponse.success(domesticPdaOperators.options(warehouseId, keyword)); + } @GetMapping @PreAuthorize("hasAuthority('platform:user:list')") diff --git a/iam/src/main/java/com/baiyue/iam/dto/AuthorizationDtos.java b/iam/src/main/java/com/baiyue/iam/dto/AuthorizationDtos.java index acc77d13d..727c6991b 100644 --- a/iam/src/main/java/com/baiyue/iam/dto/AuthorizationDtos.java +++ b/iam/src/main/java/com/baiyue/iam/dto/AuthorizationDtos.java @@ -41,12 +41,13 @@ public final class AuthorizationDtos { private Set permissions; private Long permissionVersion; private Long overseasWarehouseId; + private Long domesticWarehouseId; public SnapshotResponse(Long userId, Long tenantId, Long membershipId, String appCode, String clientType, Set roles, Set permissions, Long permissionVersion) { this(userId, tenantId, membershipId, appCode, clientType, roles, permissions, - permissionVersion, null); + permissionVersion, null, null); } } } diff --git a/iam/src/main/java/com/baiyue/iam/dto/DomesticPdaOperatorDtos.java b/iam/src/main/java/com/baiyue/iam/dto/DomesticPdaOperatorDtos.java new file mode 100644 index 000000000..0f58ddb4c --- /dev/null +++ b/iam/src/main/java/com/baiyue/iam/dto/DomesticPdaOperatorDtos.java @@ -0,0 +1,8 @@ +package com.baiyue.iam.dto; + +/** 国内头程 PDA 作业员候选契约。 */ +public final class DomesticPdaOperatorDtos { + private DomesticPdaOperatorDtos() { } + + public record OperatorOption(Long userId, String displayName) { } +} diff --git a/iam/src/main/java/com/baiyue/iam/dto/PlatformUserManagementDtos.java b/iam/src/main/java/com/baiyue/iam/dto/PlatformUserManagementDtos.java index a90f46028..e70974a6d 100644 --- a/iam/src/main/java/com/baiyue/iam/dto/PlatformUserManagementDtos.java +++ b/iam/src/main/java/com/baiyue/iam/dto/PlatformUserManagementDtos.java @@ -23,12 +23,13 @@ public final class PlatformUserManagementDtos { @NotNull Long tenantId, @NotNull List roleIds, Long overseasWarehouseId, + Long domesticWarehouseId, @NotBlank @Size(min = 8, max = 64) @Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d).+$", message = "密码必须同时包含字母和数字") String password, @NotBlank @Pattern(regexp = "ACTIVE|DISABLED") String status) { public CreateUserRequest(String username, String displayName, String phone, String email, Long tenantId, List 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, @NotNull Long tenantId, @NotNull List roleIds, - Long overseasWarehouseId) { + Long overseasWarehouseId, + Long domesticWarehouseId) { + public UpdateUserRequest(String displayName, String phone, String email, + Long tenantId, List roleIds, Long overseasWarehouseId) { + this(displayName, phone, email, tenantId, roleIds, overseasWarehouseId, null); + } public UpdateUserRequest(String displayName, String phone, String email, Long tenantId, List 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 tenantName, String memberType, String memberNo, String status, List roleIds, List roleNames, LocalDateTime lastLoginAt, - boolean protectedAccount, Long overseasWarehouseId) { } + boolean protectedAccount, Long overseasWarehouseId, Long domesticWarehouseId) { } public record TenantOption(Long id, String name, String status) { } public record RoleOption(Long id, Long tenantId, String name, String status) { } diff --git a/iam/src/main/java/com/baiyue/iam/entity/IamMembership.java b/iam/src/main/java/com/baiyue/iam/entity/IamMembership.java index dfdaa658a..a615bc7de 100644 --- a/iam/src/main/java/com/baiyue/iam/entity/IamMembership.java +++ b/iam/src/main/java/com/baiyue/iam/entity/IamMembership.java @@ -9,6 +9,6 @@ import com.baiyue.common.core.model.BaseEntity; import com.baomidou.mybatisplus. @Getter @Setter @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; } diff --git a/iam/src/main/java/com/baiyue/iam/mapper/AuthorizationQueryMapper.java b/iam/src/main/java/com/baiyue/iam/mapper/AuthorizationQueryMapper.java index a403399f9..e1a487122 100644 --- a/iam/src/main/java/com/baiyue/iam/mapper/AuthorizationQueryMapper.java +++ b/iam/src/main/java/com/baiyue/iam/mapper/AuthorizationQueryMapper.java @@ -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") Long findOverseasWarehouseId(@Param("tenantId") long tenantId, @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); } diff --git a/iam/src/main/java/com/baiyue/iam/mapper/IamMembershipRoleMapper.java b/iam/src/main/java/com/baiyue/iam/mapper/IamMembershipRoleMapper.java index d15438b9b..e16dda80a 100644 --- a/iam/src/main/java/com/baiyue/iam/mapper/IamMembershipRoleMapper.java +++ b/iam/src/main/java/com/baiyue/iam/mapper/IamMembershipRoleMapper.java @@ -10,6 +10,7 @@ import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; import java.util.List; +import com.baiyue.iam.dto.DomesticPdaOperatorDtos.OperatorOption; @Mapper public interface IamMembershipRoleMapper extends BaseMapper { @@ -26,4 +27,31 @@ public interface IamMembershipRoleMapper extends BaseMapper { """) List findAssignedAuthUserIds(@Param("tenantId") long tenantId, @Param("roleId") Long roleId); + + @Select(""" + + """) + List findActiveUsersByRoleCode(@Param("tenantId") long tenantId, + @Param("roleCode") String roleCode, + @Param("keyword") String keyword, + @Param("authUserId") Long authUserId, + @Param("warehouseId") Long warehouseId); } diff --git a/iam/src/main/java/com/baiyue/iam/service/AuthorizationSnapshotService.java b/iam/src/main/java/com/baiyue/iam/service/AuthorizationSnapshotService.java index df90f8b1e..6bfca6f98 100644 --- a/iam/src/main/java/com/baiyue/iam/service/AuthorizationSnapshotService.java +++ b/iam/src/main/java/com/baiyue/iam/service/AuthorizationSnapshotService.java @@ -48,7 +48,8 @@ public class AuthorizationSnapshotService { return new SnapshotResponse(request.getUserId(), request.getTenantId(), request.getMembershipId(), appCode, appCode, Set.copyOf(roles), Set.copyOf(permissions), mapper.findPermissionVersion(request.getUserId()), - mapper.findOverseasWarehouseId(request.getTenantId(), request.getMembershipId())); + mapper.findOverseasWarehouseId(request.getTenantId(), request.getMembershipId()), + mapper.findDomesticWarehouseId(request.getTenantId(), request.getMembershipId())); } /** diff --git a/iam/src/main/java/com/baiyue/iam/service/DomesticPdaOperatorService.java b/iam/src/main/java/com/baiyue/iam/service/DomesticPdaOperatorService.java new file mode 100644 index 000000000..b573a2a53 --- /dev/null +++ b/iam/src/main/java/com/baiyue/iam/service/DomesticPdaOperatorService.java @@ -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 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 作业员")); + } +} diff --git a/iam/src/main/java/com/baiyue/iam/service/PlatformUserManagementService.java b/iam/src/main/java/com/baiyue/iam/service/PlatformUserManagementService.java index fa73ece19..9433ffab0 100644 --- a/iam/src/main/java/com/baiyue/iam/service/PlatformUserManagementService.java +++ b/iam/src/main/java/com/baiyue/iam/service/PlatformUserManagementService.java @@ -75,7 +75,7 @@ public class PlatformUserManagementService { long tenantId = request.tenantId(); requirePlatformTenant(tenantId); List roleIds = validateRoles(tenantId, request.roleIds()); - Long overseasWarehouseId = validateWarehouseBinding(roleIds, request.overseasWarehouseId()); + WarehouseBindings warehouses = validateWarehouseBinding(roleIds, request.overseasWarehouseId(), request.domesticWarehouseId()); long authUserId = IdWorker.getId(); long userId = IdWorker.getId(); long membershipId = IdWorker.getId(); @@ -100,7 +100,8 @@ public class PlatformUserManagementService { membership.setUserId(userId); membership.setMemberType("EMPLOYEE"); membership.setMemberNo(memberNo(tenantId, membershipId)); - membership.setOverseasWarehouseId(overseasWarehouseId); + membership.setOverseasWarehouseId(warehouses.overseasWarehouseId()); + membership.setDomesticWarehouseId(warehouses.domesticWarehouseId()); membership.setStatus(request.status()); membership.setJoinedAt(LocalDateTime.now()); membership.setLeftAt("DISABLED".equals(request.status()) ? LocalDateTime.now() : null); @@ -125,7 +126,7 @@ public class PlatformUserManagementService { long newTenantId = request.tenantId(); requirePlatformTenant(newTenantId); List roleIds = validateRoles(newTenantId, request.roleIds()); - Long overseasWarehouseId = validateWarehouseBinding(roleIds, request.overseasWarehouseId()); + WarehouseBindings warehouses = validateWarehouseBinding(roleIds, request.overseasWarehouseId(), request.domesticWarehouseId()); List oldRoleIds = members.findRoleIds(oldTenantId, membershipId); if (isProtectedPlatformAdmin(membership, oldRoleIds) && (newTenantId != PLATFORM_TENANT_ID || !containsPlatformAdminRole(roleIds))) { @@ -140,7 +141,8 @@ public class PlatformUserManagementService { boolean phoneChanged = !Objects.equals(user.getPhone(), request.phone().trim()); boolean scopeChanged = oldTenantId != newTenantId; 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(), new AuthAccountClient.ProfileRequest(request.displayName().trim()))); @@ -154,7 +156,8 @@ public class PlatformUserManagementService { user.setEmail(trimToNull(request.email())); user.setUserType(userType(newTenantId)); membership.setTenantId(newTenantId); - membership.setOverseasWarehouseId(overseasWarehouseId); + membership.setOverseasWarehouseId(warehouses.overseasWarehouseId()); + membership.setDomesticWarehouseId(warehouses.domesticWarehouseId()); if (scopeChanged && members.existsMemberNo(newTenantId, membership.getMemberNo(), membershipId)) { membership.setMemberNo(memberNo(newTenantId, membershipId)); } @@ -239,7 +242,7 @@ public class PlatformUserManagementService { user.getDisplayName(), user.getPhone(), user.getEmail(), membership.getTenantId(), tenantName, membership.getMemberType(), membership.getMemberNo(), membership.getStatus(), roleIds, roleNames, account.lastLoginAt(), isProtectedPlatformAdmin(membership, roleIds), - membership.getOverseasWarehouseId()); + membership.getOverseasWarehouseId(), membership.getDomesticWarehouseId()); } private List platformRoles() { @@ -262,7 +265,8 @@ public class PlatformUserManagementService { return roleIds; } - private Long validateWarehouseBinding(List roleIds, Long overseasWarehouseId) { + private WarehouseBindings validateWarehouseBinding(List roleIds, Long overseasWarehouseId, + Long domesticWarehouseId) { Set roleCodes = roleIds.stream().map(roleMapper::selectById).filter(Objects::nonNull) .map(IamRole::getRoleCode).collect(Collectors.toSet()); boolean overseasPda = roleCodes.contains("OVERSEAS_WAREHOUSE_PDA_OPERATOR"); @@ -276,9 +280,17 @@ public class PlatformUserManagementService { if (!overseasPda && overseasWarehouseId != null) { 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 roleIds) { return membership.getTenantId() == PLATFORM_TENANT_ID && containsPlatformAdminRole(roleIds); } diff --git a/iam/src/main/resources/db/iam.sql b/iam/src/main/resources/db/iam.sql index 333c94552..e15c0eb20 100644 --- a/iam/src/main/resources/db/iam.sql +++ b/iam/src/main/resources/db/iam.sql @@ -149,6 +149,7 @@ CREATE TABLE IF NOT EXISTS iam_membership ( member_type VARCHAR(20) NOT NULL DEFAULT 'EMPLOYEE' COMMENT '成员类型:EMPLOYEE企业员工等', member_no VARCHAR(64) NULL COMMENT '客户企业内部成员编号', 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已离开', joined_at DATETIME(3) NULL COMMENT '加入客户企业时间', left_at DATETIME(3) NULL COMMENT '离开客户企业时间', diff --git a/iam/src/main/resources/db/phase3-warehouse-roles.sql b/iam/src/main/resources/db/phase3-warehouse-roles.sql index 63424653a..028279ff8 100644 --- a/iam/src/main/resources/db/phase3-warehouse-roles.sql +++ b/iam/src/main/resources/db/phase3-warehouse-roles.sql @@ -3,6 +3,26 @@ USE baiyue_iam; 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 SET role_code = 'OVERSEAS_WAREHOUSE_PDA_OPERATOR', role_name = '海外仓 PDA 作业员', @@ -72,13 +92,16 @@ CREATE TEMPORARY TABLE tmp_phase3_warehouse_role_permission ( PRIMARY KEY (role_code, permission_code) ) ENGINE=MEMORY; --- 操作员:查看入仓业务并维护后台装柜证据,不授予完成装柜、任务分派或出仓动作。 +-- 仓管:可查看并确认预装、指定国内 PDA 作业员,不授予 PDA 执行或出仓动作。 INSERT INTO tmp_phase3_warehouse_role_permission (role_code, permission_code) VALUES ('WAREHOUSE_OPERATOR', 'platform:order'), ('WAREHOUSE_OPERATOR', 'platform:firstleg:inbound:list'), ('WAREHOUSE_OPERATOR', 'platform:firstleg:inbound: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 作业员:只允许国内任务查询、作业和现场照片,不授予后台、海外、分派或复核权限。 INSERT INTO tmp_phase3_warehouse_role_permission (role_code, permission_code) VALUES @@ -112,7 +135,7 @@ BEGIN COALESCE(SUM(role_code = 'WAREHOUSE_TALLY'), 0) INTO operator_permission_count, domestic_pda_permission_count, tally_permission_count FROM tmp_phase3_warehouse_role_permission; - IF operator_permission_count <> 5 + IF operator_permission_count <> 8 OR domestic_pda_permission_count <> 2 OR tally_permission_count <> 12 THEN SIGNAL SQLSTATE '45000' @@ -208,7 +231,7 @@ BEGIN JOIN iam_role_permission grant_row ON grant_row.role_id = role.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' SET MESSAGE_TEXT = '仓库角色初始化失败:最小权限授权未完整写入'; END IF; diff --git a/iam/src/main/resources/openapi/iam-v1.yaml b/iam/src/main/resources/openapi/iam-v1.yaml index 46af4d962..63643eabb 100644 --- a/iam/src/main/resources/openapi/iam-v1.yaml +++ b/iam/src/main/resources/openapi/iam-v1.yaml @@ -435,6 +435,18 @@ paths: responses: '200': {$ref: '#/components/responses/Success'} '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}: parameters: - {$ref: '#/components/parameters/MembershipId'} @@ -993,6 +1005,19 @@ paths: total: {type: integer, format: int64} pages: {type: integer} '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: post: tags: [Internal Export Provider] @@ -1550,6 +1575,7 @@ components: tenantId: {type: integer, format: int64, enum: [0], description: 保留的兼容字段,只允许平台技术作用域 0} 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 角色不能同时分配} + 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} status: {type: string, enum: [ACTIVE, DISABLED]} UpdatePlatformManagedUserRequest: @@ -1562,6 +1588,7 @@ components: tenantId: {type: integer, format: int64, enum: [0], description: 保留的兼容字段,只允许平台技术作用域 0} 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 角色不能同时分配} + domesticWarehouseId: {type: integer, format: int64, nullable: true, description: 选择 CN_FIRSTLEG_PDA_OPERATOR 时必填且只能绑定一个有效国内作业仓;非国内头程 PDA 角色时必须为空;两个 PDA 角色不能同时分配} CreateInternalUserRequest: type: object required: [displayName] @@ -1635,6 +1662,7 @@ components: lastLoginAt: {type: string, format: date-time, nullable: true} protectedAccount: {type: boolean} overseasWarehouseId: {type: string, pattern: '^\d+$', nullable: true, description: 海外仓PDA操作员绑定的唯一实际海外仓ID} + domesticWarehouseId: {type: string, pattern: '^\d+$', nullable: true, description: 国内头程PDA操作员绑定的唯一国内作业仓ID} CreateMemberRequest: type: object required: [username, password, displayName, memberType] diff --git a/iam/src/test/java/com/baiyue/iam/repository/DomesticPdaOperatorSqlContractTest.java b/iam/src/test/java/com/baiyue/iam/repository/DomesticPdaOperatorSqlContractTest.java new file mode 100644 index 000000000..6ae14375c --- /dev/null +++ b/iam/src/test/java/com/baiyue/iam/repository/DomesticPdaOperatorSqlContractTest.java @@ -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"); + } +} diff --git a/iam/src/test/java/com/baiyue/iam/repository/WarehouseRoleSqlContractTest.java b/iam/src/test/java/com/baiyue/iam/repository/WarehouseRoleSqlContractTest.java index b8acb4b52..22865c6a1 100644 --- a/iam/src/test/java/com/baiyue/iam/repository/WarehouseRoleSqlContractTest.java +++ b/iam/src/test/java/com/baiyue/iam/repository/WarehouseRoleSqlContractTest.java @@ -46,7 +46,7 @@ class WarehouseRoleSqlContractTest { .contains("'WAREHOUSE_TALLY', 'platform:firstleg:pda:review'") .contains("'WAREHOUSE_TALLY', '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("tally_permission_count <> 12") .contains("依赖的三期平台权限尚未完整部署") @@ -54,7 +54,7 @@ class WarehouseRoleSqlContractTest { .contains("expected.permission_code IS NULL") .contains("role.role_code = 'OVERSEAS_WAREHOUSE_PDA_OPERATOR' AND permission.permission_code = 'lastleg:pda:operate'") .contains("最小权限授权未完整写入") - .contains("grant_row.tenant_id = 0) <> 19") + .contains("grant_row.tenant_id = 0) <> 22") .contains("(6000004, 4000004, 0, 'TENANT', NULL)") .contains("(6000005, 4000005, 0, 'TENANT', NULL)") .contains("(6000006, 4000006, 0, 'TENANT', NULL)") @@ -80,7 +80,7 @@ class WarehouseRoleSqlContractTest { .doesNotContain("platform:firstleg:inbound:picking:view") .doesNotContain("platform:firstleg:outbound-dispatch:confirm"); 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:outbound-dispatch:confirm'") .doesNotContain("'WAREHOUSE_OPERATOR', 'platform:firstleg:outbound-dispatch:reverse'")