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