优化
This commit is contained in:
@@ -63,7 +63,7 @@ class AuthServiceSmallJwtTest {
|
||||
account.setDefaultAppCode("PLATFORM");
|
||||
account.setTokenVersion(1);
|
||||
AuthorizationSnapshot snapshot = new AuthorizationSnapshot(42L, 0L, 84L, "PLATFORM",
|
||||
"WEB", Set.of("WAREHOUSE_PICKER"), Set.of("platform:order:list"), 1L, 900L);
|
||||
"WEB", Set.of("OVERSEAS_WAREHOUSE_PDA_OPERATOR"), Set.of("platform:order:list"), 1L, 900L);
|
||||
Jwt encoded = org.mockito.Mockito.mock(Jwt.class);
|
||||
|
||||
when(repository.findByUsername("operator")).thenReturn(account);
|
||||
@@ -91,7 +91,7 @@ class AuthServiceSmallJwtTest {
|
||||
assertThat(jwt.getValue().getJwsHeader().getAlgorithm()).isEqualTo(MacAlgorithm.HS256);
|
||||
assertThat(jwt.getValue().getJwsHeader().getKeyId()).isNull();
|
||||
verify(tokenSessionStore).store(any(), org.mockito.ArgumentMatchers.eq(42L),
|
||||
org.mockito.ArgumentMatchers.eq(Set.of("WAREHOUSE_PICKER")),
|
||||
org.mockito.ArgumentMatchers.eq(Set.of("OVERSEAS_WAREHOUSE_PDA_OPERATOR")),
|
||||
org.mockito.ArgumentMatchers.eq(Set.of("platform:order:list")), any(),
|
||||
org.mockito.ArgumentMatchers.eq(Duration.ofMinutes(30)),
|
||||
org.mockito.ArgumentMatchers.eq(Duration.ofDays(7)));
|
||||
|
||||
+1
-1
@@ -66,7 +66,7 @@ class OverseasSessionValidationServiceTest {
|
||||
|
||||
private static AuthorizationSnapshot snapshot(Long version, Long warehouseId) {
|
||||
return new AuthorizationSnapshot(42L, 0L, 84L, "PLATFORM", "PDA",
|
||||
Set.of("WAREHOUSE_PICKER"), Set.of("lastleg:pda:operate"), version, warehouseId);
|
||||
Set.of("OVERSEAS_WAREHOUSE_PDA_OPERATOR"), Set.of("lastleg:pda:operate"), version, warehouseId);
|
||||
}
|
||||
|
||||
private void inject(String fieldName, Object value) throws Exception {
|
||||
|
||||
@@ -52,7 +52,7 @@ class OverseasTokenServiceTest {
|
||||
String lastlegAuthority = "lastleg:pda:operate";
|
||||
String domesticAuthority = "platform:firstleg:order:list";
|
||||
AuthorizationSnapshot snapshot = new AuthorizationSnapshot(42L, 0L, 84L, "PLATFORM", "WEB",
|
||||
Set.of("WAREHOUSE_PICKER"), Set.of(authority, lastlegAuthority, domesticAuthority), 7L, 900L);
|
||||
Set.of("OVERSEAS_WAREHOUSE_PDA_OPERATOR"), Set.of(authority, lastlegAuthority, domesticAuthority), 7L, 900L);
|
||||
Jwt encoded = org.mockito.Mockito.mock(Jwt.class);
|
||||
when(accounts.findById(42L)).thenReturn(account);
|
||||
when(authorizationLoader.load(account)).thenReturn(snapshot);
|
||||
@@ -74,7 +74,7 @@ class OverseasTokenServiceTest {
|
||||
.containsEntry("permission_version", 7L)
|
||||
.containsEntry("token_version", 0)
|
||||
.containsEntry("overseas_warehouse_id", 900L)
|
||||
.containsEntry("roles", Set.of("WAREHOUSE_PICKER"))
|
||||
.containsEntry("roles", Set.of("OVERSEAS_WAREHOUSE_PDA_OPERATOR"))
|
||||
.containsEntry("authorities", Set.of(authority, lastlegAuthority))
|
||||
.doesNotContainKeys("userId", "tenantId", "appCode", "permissionVersion");
|
||||
assertThat(response.expiresIn()).isEqualTo(900);
|
||||
@@ -87,9 +87,9 @@ class OverseasTokenServiceTest {
|
||||
account.setDefaultMembershipId(85L); account.setDefaultAppCode("PLATFORM");
|
||||
account.setTokenVersion(8);
|
||||
AuthorizationSnapshot oldBinding = new AuthorizationSnapshot(43L, 0L, 85L, "PLATFORM", "PDA",
|
||||
Set.of("WAREHOUSE_PICKER"), Set.of("lastleg:pda:operate"), 8L, 900L);
|
||||
Set.of("OVERSEAS_WAREHOUSE_PDA_OPERATOR"), Set.of("lastleg:pda:operate"), 8L, 900L);
|
||||
AuthorizationSnapshot newBinding = new AuthorizationSnapshot(43L, 0L, 85L, "PLATFORM", "PDA",
|
||||
Set.of("WAREHOUSE_PICKER"), Set.of("lastleg:pda:operate"), 9L, 901L);
|
||||
Set.of("OVERSEAS_WAREHOUSE_PDA_OPERATOR"), Set.of("lastleg:pda:operate"), 9L, 901L);
|
||||
Jwt first = org.mockito.Mockito.mock(Jwt.class);
|
||||
Jwt second = org.mockito.Mockito.mock(Jwt.class);
|
||||
when(encoder.encode(any())).thenReturn(first, second);
|
||||
@@ -117,7 +117,7 @@ class OverseasTokenServiceTest {
|
||||
AuthAccount account = new AuthAccount();
|
||||
account.setId(44L); account.setAccountStatus("ACTIVE");
|
||||
AuthorizationSnapshot unbound = new AuthorizationSnapshot(44L, 0L, 86L, "PLATFORM", "PDA",
|
||||
Set.of("WAREHOUSE_PICKER"), Set.of("lastleg:pda:operate"), 10L, null);
|
||||
Set.of("OVERSEAS_WAREHOUSE_PDA_OPERATOR"), Set.of("lastleg:pda:operate"), 10L, null);
|
||||
Jwt encoded = org.mockito.Mockito.mock(Jwt.class);
|
||||
when(encoder.encode(any())).thenReturn(encoded);
|
||||
when(encoded.getTokenValue()).thenReturn("overseas-unbound");
|
||||
|
||||
@@ -10817,11 +10817,18 @@ CALL ensure_carrier_schedule_retry_check();
|
||||
DROP PROCEDURE IF EXISTS ensure_carrier_schedule_retry_check;
|
||||
|
||||
-- 来源:iam/src/main/resources/db/phase3-warehouse-roles.sql
|
||||
-- 三期仓库常用系统角色:仓库操作员、仓库拣货员、仓库理货员。
|
||||
-- 三期仓库系统角色:国内仓后台、国内头程 PDA、海外仓 PDA、国内仓理货复核。
|
||||
-- 本脚本只组合已经存在的权限资源,需在三期 Firstleg/IAM 权限脚本之后执行,可重复部署。
|
||||
USE baiyue_iam;
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
UPDATE iam_role
|
||||
SET role_code = 'OVERSEAS_WAREHOUSE_PDA_OPERATOR',
|
||||
role_name = '海外仓 PDA 作业员',
|
||||
description = '负责使用海外仓 PDA 执行接柜、入仓和库内作业,必须绑定一个海外仓'
|
||||
WHERE id = 4000005 AND tenant_id = 0 AND app_code = 'PLATFORM'
|
||||
AND role_code = 'WAREHOUSE_PICKER';
|
||||
|
||||
DROP PROCEDURE IF EXISTS assert_phase3_warehouse_role_ids_available;
|
||||
DELIMITER $$
|
||||
CREATE PROCEDURE assert_phase3_warehouse_role_ids_available()
|
||||
@@ -10830,14 +10837,16 @@ BEGIN
|
||||
SELECT 1
|
||||
FROM iam_role
|
||||
WHERE (id = 4000004 AND (tenant_id <> 0 OR app_code <> 'PLATFORM' OR role_code <> 'WAREHOUSE_OPERATOR'))
|
||||
OR (id = 4000005 AND (tenant_id <> 0 OR app_code <> 'PLATFORM' OR role_code <> 'WAREHOUSE_PICKER'))
|
||||
OR (id = 4000005 AND (tenant_id <> 0 OR app_code <> 'PLATFORM' OR role_code <> 'OVERSEAS_WAREHOUSE_PDA_OPERATOR'))
|
||||
OR (id = 4000006 AND (tenant_id <> 0 OR app_code <> 'PLATFORM' OR role_code <> 'WAREHOUSE_TALLY'))
|
||||
OR (id = 4000007 AND (tenant_id <> 0 OR app_code <> 'PLATFORM' OR role_code <> 'CN_FIRSTLEG_PDA_OPERATOR'))
|
||||
OR (tenant_id = 0 AND app_code = 'PLATFORM' AND role_code = 'WAREHOUSE_OPERATOR' AND id <> 4000004)
|
||||
OR (tenant_id = 0 AND app_code = 'PLATFORM' AND role_code = 'WAREHOUSE_PICKER' AND id <> 4000005)
|
||||
OR (tenant_id = 0 AND app_code = 'PLATFORM' AND role_code = 'OVERSEAS_WAREHOUSE_PDA_OPERATOR' AND id <> 4000005)
|
||||
OR (tenant_id = 0 AND app_code = 'PLATFORM' AND role_code = 'WAREHOUSE_TALLY' AND id <> 4000006)
|
||||
OR (tenant_id = 0 AND app_code = 'PLATFORM' AND role_code = 'CN_FIRSTLEG_PDA_OPERATOR' AND id <> 4000007)
|
||||
) THEN
|
||||
SIGNAL SQLSTATE '45000'
|
||||
SET MESSAGE_TEXT = '仓库角色初始化失败:4000004-4000006 或角色编码已被占用';
|
||||
SET MESSAGE_TEXT = '仓库角色初始化失败:4000004-4000007 或角色编码已被占用';
|
||||
END IF;
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
@@ -10845,12 +10854,14 @@ BEGIN
|
||||
WHERE (id = 6000004 AND role_id <> 4000004)
|
||||
OR (id = 6000005 AND role_id <> 4000005)
|
||||
OR (id = 6000006 AND role_id <> 4000006)
|
||||
OR (id = 6000007 AND role_id <> 4000007)
|
||||
OR (role_id = 4000004 AND id <> 6000004)
|
||||
OR (role_id = 4000005 AND id <> 6000005)
|
||||
OR (role_id = 4000006 AND id <> 6000006)
|
||||
OR (role_id = 4000007 AND id <> 6000007)
|
||||
) THEN
|
||||
SIGNAL SQLSTATE '45000'
|
||||
SET MESSAGE_TEXT = '仓库角色初始化失败:6000004-6000006 数据范围ID已被占用';
|
||||
SET MESSAGE_TEXT = '仓库角色初始化失败:6000004-6000007 数据范围ID已被占用';
|
||||
END IF;
|
||||
END$$
|
||||
DELIMITER ;
|
||||
@@ -10861,12 +10872,14 @@ INSERT INTO iam_role (
|
||||
id, tenant_id, app_code, role_code, role_name, role_type, status,
|
||||
description, deleted, version
|
||||
) VALUES
|
||||
(4000004, 0, 'PLATFORM', 'WAREHOUSE_OPERATOR', '仓库操作员', 'SYSTEM', 'ACTIVE',
|
||||
(4000004, 0, 'PLATFORM', 'WAREHOUSE_OPERATOR', '国内仓后台操作员', 'SYSTEM', 'ACTIVE',
|
||||
'负责查看入仓业务并在后台维护装柜证据', 0, 0),
|
||||
(4000005, 0, 'PLATFORM', 'WAREHOUSE_PICKER', '仓库拣货员', 'SYSTEM', 'ACTIVE',
|
||||
'负责使用PDA执行柜次拣货和现场拍照', 0, 0),
|
||||
(4000006, 0, 'PLATFORM', 'WAREHOUSE_TALLY', '仓库理货员', 'SYSTEM', 'ACTIVE',
|
||||
'负责入库测量、PDA复核和装柜证据维护', 0, 0)
|
||||
(4000005, 0, 'PLATFORM', 'OVERSEAS_WAREHOUSE_PDA_OPERATOR', '海外仓 PDA 作业员', 'SYSTEM', 'ACTIVE',
|
||||
'负责使用海外仓 PDA 执行接柜、入仓和库内作业,必须绑定一个海外仓', 0, 0),
|
||||
(4000006, 0, 'PLATFORM', 'WAREHOUSE_TALLY', '国内仓理货复核员', 'SYSTEM', 'ACTIVE',
|
||||
'负责国内入库测量、PDA复核和装柜证据维护', 0, 0),
|
||||
(4000007, 0, 'PLATFORM', 'CN_FIRSTLEG_PDA_OPERATOR', '国内头程 PDA 作业员', 'SYSTEM', 'ACTIVE',
|
||||
'负责使用国内 PDA 执行柜次拣货、现场取证和装柜完成', 0, 0)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
role_name = VALUES(role_name), role_type = 'SYSTEM', status = 'ACTIVE',
|
||||
description = VALUES(description), deleted = 0;
|
||||
@@ -10886,10 +10899,10 @@ INSERT INTO tmp_phase3_warehouse_role_permission (role_code, permission_code) VA
|
||||
('WAREHOUSE_OPERATOR', 'platform:firstleg:inbound:loading:view'),
|
||||
('WAREHOUSE_OPERATOR', 'platform:firstleg:inbound:loading:edit');
|
||||
|
||||
-- 拣货员:只允许PDA任务查询、作业和现场照片,不授予后台页面、任务分派或复核权限。
|
||||
-- 国内头程 PDA 作业员:只允许国内任务查询、作业和现场照片,不授予后台、海外、分派或复核权限。
|
||||
INSERT INTO tmp_phase3_warehouse_role_permission (role_code, permission_code) VALUES
|
||||
('WAREHOUSE_PICKER', 'platform:firstleg:pda:view'),
|
||||
('WAREHOUSE_PICKER', 'platform:firstleg:pda:work');
|
||||
('CN_FIRSTLEG_PDA_OPERATOR', 'platform:firstleg:pda:view'),
|
||||
('CN_FIRSTLEG_PDA_OPERATOR', 'platform:firstleg:pda:work');
|
||||
|
||||
-- 理货员:负责到仓测量、装柜证据维护和PDA复核,不允许完成装柜或确认出仓。
|
||||
INSERT INTO tmp_phase3_warehouse_role_permission (role_code, permission_code) VALUES
|
||||
@@ -10911,15 +10924,15 @@ DELIMITER $$
|
||||
CREATE PROCEDURE assert_phase3_warehouse_role_permissions_available()
|
||||
BEGIN
|
||||
DECLARE operator_permission_count INT DEFAULT 0;
|
||||
DECLARE picker_permission_count INT DEFAULT 0;
|
||||
DECLARE domestic_pda_permission_count INT DEFAULT 0;
|
||||
DECLARE tally_permission_count INT DEFAULT 0;
|
||||
SELECT COALESCE(SUM(role_code = 'WAREHOUSE_OPERATOR'), 0),
|
||||
COALESCE(SUM(role_code = 'WAREHOUSE_PICKER'), 0),
|
||||
COALESCE(SUM(role_code = 'CN_FIRSTLEG_PDA_OPERATOR'), 0),
|
||||
COALESCE(SUM(role_code = 'WAREHOUSE_TALLY'), 0)
|
||||
INTO operator_permission_count, picker_permission_count, tally_permission_count
|
||||
INTO operator_permission_count, domestic_pda_permission_count, tally_permission_count
|
||||
FROM tmp_phase3_warehouse_role_permission;
|
||||
IF operator_permission_count <> 5
|
||||
OR picker_permission_count <> 2
|
||||
OR domestic_pda_permission_count <> 2
|
||||
OR tally_permission_count <> 12 THEN
|
||||
SIGNAL SQLSTATE '45000'
|
||||
SET MESSAGE_TEXT = '仓库角色初始化失败:角色权限组合数量不符合预期';
|
||||
@@ -10943,7 +10956,7 @@ DELIMITER ;
|
||||
CALL assert_phase3_warehouse_role_permissions_available();
|
||||
DROP PROCEDURE assert_phase3_warehouse_role_permissions_available;
|
||||
|
||||
-- 三个系统角色的权限集合由迁移维护。重复执行时移除历史版本或人工误授的越权项,
|
||||
-- 四个系统角色的权限集合由初始化维护。重复执行时移除人工误授的越权项,
|
||||
-- 避免最小权限角色因脚本升级只增不减。
|
||||
DELETE role_permission
|
||||
FROM iam_role_permission role_permission
|
||||
@@ -10958,8 +10971,8 @@ DELETE role_permission
|
||||
AND expected.permission_code = permission.permission_code
|
||||
WHERE role.tenant_id = 0
|
||||
AND role.app_code = 'PLATFORM'
|
||||
AND role.role_code IN ('WAREHOUSE_OPERATOR', 'WAREHOUSE_PICKER', 'WAREHOUSE_TALLY')
|
||||
AND NOT (role.role_code = 'WAREHOUSE_PICKER' AND permission.permission_code = 'lastleg:pda:operate')
|
||||
AND role.role_code IN ('WAREHOUSE_OPERATOR', 'OVERSEAS_WAREHOUSE_PDA_OPERATOR', 'WAREHOUSE_TALLY', 'CN_FIRSTLEG_PDA_OPERATOR')
|
||||
AND NOT (role.role_code = 'OVERSEAS_WAREHOUSE_PDA_OPERATOR' AND permission.permission_code = 'lastleg:pda:operate')
|
||||
AND expected.permission_code IS NULL;
|
||||
|
||||
INSERT IGNORE INTO iam_role_permission (role_id, permission_id, tenant_id)
|
||||
@@ -10981,7 +10994,8 @@ SELECT role.id, permission.id, 0
|
||||
INSERT INTO iam_data_scope (id, role_id, tenant_id, scope_type, scope_expression) VALUES
|
||||
(6000004, 4000004, 0, 'TENANT', NULL),
|
||||
(6000005, 4000005, 0, 'TENANT', NULL),
|
||||
(6000006, 4000006, 0, 'TENANT', NULL)
|
||||
(6000006, 4000006, 0, 'TENANT', NULL),
|
||||
(6000007, 4000007, 0, 'TENANT', NULL)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
tenant_id = 0, scope_type = 'TENANT', scope_expression = NULL;
|
||||
|
||||
@@ -10996,7 +11010,7 @@ BEGIN
|
||||
AND role_type = 'SYSTEM'
|
||||
AND status = 'ACTIVE'
|
||||
AND deleted = 0
|
||||
AND role_code IN ('WAREHOUSE_OPERATOR', 'WAREHOUSE_PICKER', 'WAREHOUSE_TALLY')) <> 3 THEN
|
||||
AND role_code IN ('WAREHOUSE_OPERATOR', 'OVERSEAS_WAREHOUSE_PDA_OPERATOR', 'WAREHOUSE_TALLY', 'CN_FIRSTLEG_PDA_OPERATOR')) <> 4 THEN
|
||||
SIGNAL SQLSTATE '45000'
|
||||
SET MESSAGE_TEXT = '仓库角色初始化失败:系统角色未完整启用';
|
||||
END IF;
|
||||
@@ -11022,9 +11036,9 @@ BEGIN
|
||||
JOIN iam_role role ON role.id = scope_row.role_id
|
||||
WHERE role.tenant_id = 0
|
||||
AND role.app_code = 'PLATFORM'
|
||||
AND role.role_code IN ('WAREHOUSE_OPERATOR', 'WAREHOUSE_PICKER', 'WAREHOUSE_TALLY')
|
||||
AND role.role_code IN ('WAREHOUSE_OPERATOR', 'OVERSEAS_WAREHOUSE_PDA_OPERATOR', 'WAREHOUSE_TALLY', 'CN_FIRSTLEG_PDA_OPERATOR')
|
||||
AND scope_row.tenant_id = 0
|
||||
AND scope_row.scope_type = 'TENANT') <> 3 THEN
|
||||
AND scope_row.scope_type = 'TENANT') <> 4 THEN
|
||||
SIGNAL SQLSTATE '45000'
|
||||
SET MESSAGE_TEXT = '仓库角色初始化失败:数据范围未完整写入';
|
||||
END IF;
|
||||
@@ -11036,7 +11050,6 @@ DROP PROCEDURE assert_phase3_warehouse_roles_ready;
|
||||
DROP TEMPORARY TABLE IF EXISTS tmp_phase3_warehouse_role_permission;
|
||||
|
||||
|
||||
|
||||
-- BEGIN FINANCE FEE SOURCE MODULE MIGRATIONS
|
||||
-- 来源:finance/src/main/resources/db/finance.sql
|
||||
CREATE DATABASE IF NOT EXISTS baiyue_finance
|
||||
@@ -16340,14 +16353,14 @@ DELETE role_permission FROM baiyue_iam.iam_role_permission role_permission
|
||||
JOIN baiyue_iam.iam_role role ON role.id=role_permission.role_id AND role.tenant_id=role_permission.tenant_id
|
||||
JOIN baiyue_iam.iam_permission permission ON permission.id=role_permission.permission_id AND permission.tenant_id=role_permission.tenant_id
|
||||
WHERE permission.permission_code='lastleg:pda:operate'
|
||||
AND NOT (role.id=4000005 AND role.tenant_id=0 AND role.app_code='PLATFORM' AND role.role_code='WAREHOUSE_PICKER');
|
||||
AND NOT (role.id=4000005 AND role.tenant_id=0 AND role.app_code='PLATFORM' AND role.role_code='OVERSEAS_WAREHOUSE_PDA_OPERATOR');
|
||||
|
||||
INSERT IGNORE INTO baiyue_iam.iam_role_permission (tenant_id, role_id, permission_id, created_at)
|
||||
SELECT 0, role.id, permission.id, NOW(3)
|
||||
FROM baiyue_iam.iam_role role
|
||||
JOIN baiyue_iam.iam_permission permission ON permission.tenant_id=0 AND permission.app_code='PLATFORM'
|
||||
AND permission.permission_code='lastleg:pda:operate'
|
||||
WHERE role.id=4000005 AND role.tenant_id=0 AND role.app_code='PLATFORM' AND role.role_code='WAREHOUSE_PICKER'
|
||||
WHERE role.id=4000005 AND role.tenant_id=0 AND role.app_code='PLATFORM' AND role.role_code='OVERSEAS_WAREHOUSE_PDA_OPERATOR'
|
||||
AND role.status='ACTIVE' AND role.deleted=0 AND permission.status='ACTIVE' AND permission.deleted=0;
|
||||
|
||||
DROP PROCEDURE IF EXISTS baiyue_iam.assert_overseas_pda_picker_permission;
|
||||
@@ -16358,15 +16371,15 @@ BEGIN
|
||||
JOIN baiyue_iam.iam_role role ON role.id=grant_row.role_id AND role.tenant_id=grant_row.tenant_id
|
||||
JOIN baiyue_iam.iam_permission permission ON permission.id=grant_row.permission_id AND permission.tenant_id=grant_row.tenant_id
|
||||
WHERE permission.permission_code='lastleg:pda:operate'
|
||||
AND role.id=4000005 AND role.app_code='PLATFORM' AND role.role_code='WAREHOUSE_PICKER') <> 1 THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='海外PDA权限初始化失败:WAREHOUSE_PICKER未精确授权';
|
||||
AND role.id=4000005 AND role.app_code='PLATFORM' AND role.role_code='OVERSEAS_WAREHOUSE_PDA_OPERATOR') <> 1 THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='海外PDA权限初始化失败:海外仓 PDA 作业员未精确授权';
|
||||
END IF;
|
||||
IF EXISTS (SELECT 1 FROM baiyue_iam.iam_role_permission grant_row
|
||||
JOIN baiyue_iam.iam_role role ON role.id=grant_row.role_id AND role.tenant_id=grant_row.tenant_id
|
||||
JOIN baiyue_iam.iam_permission permission ON permission.id=grant_row.permission_id AND permission.tenant_id=grant_row.tenant_id
|
||||
WHERE permission.permission_code='lastleg:pda:operate'
|
||||
AND NOT (role.id=4000005 AND role.tenant_id=0 AND role.app_code='PLATFORM' AND role.role_code='WAREHOUSE_PICKER')) THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='海外PDA权限初始化失败:存在非仓库拣货员授权';
|
||||
AND NOT (role.id=4000005 AND role.tenant_id=0 AND role.app_code='PLATFORM' AND role.role_code='OVERSEAS_WAREHOUSE_PDA_OPERATOR')) THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='海外PDA权限初始化失败:存在非海外仓 PDA 作业员授权';
|
||||
END IF;
|
||||
END$$
|
||||
DELIMITER ;
|
||||
@@ -16409,7 +16422,7 @@ JOIN baiyue_iam.iam_permission permission ON permission.id=grant_row.permission_
|
||||
LEFT JOIN tmp_phase4_warehouse_role_permission expected
|
||||
ON expected.role_code=role.role_code AND expected.permission_code=permission.permission_code
|
||||
WHERE role.tenant_id=0 AND role.app_code='PLATFORM'
|
||||
AND role.role_code IN ('WAREHOUSE_OPERATOR','WAREHOUSE_PICKER','WAREHOUSE_TALLY')
|
||||
AND role.role_code IN ('WAREHOUSE_OPERATOR','OVERSEAS_WAREHOUSE_PDA_OPERATOR','WAREHOUSE_TALLY','CN_FIRSTLEG_PDA_OPERATOR')
|
||||
AND permission.permission_code LIKE 'platform:overseas-%'
|
||||
AND expected.permission_code IS NULL;
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ public interface LoadingCompletedEventMapper {
|
||||
@Select("""
|
||||
SELECT p.id AS loading_plan_id, p.plan_no, p.container_booking_id, p.version AS aggregate_version,
|
||||
b.container_no, b.cabinet_no, b.warehouse_id, b.warehouse_name,
|
||||
b.customer_id AS booking_customer_id, b.customer_name,
|
||||
i.customer_name,
|
||||
b.estimated_port_arrival_at, b.actual_port_arrival_at,
|
||||
b.estimated_warehouse_arrival_at,
|
||||
i.account_id, i.inbound_id AS source_inbound_id,
|
||||
@@ -84,8 +84,8 @@ public interface LoadingCompletedEventMapper {
|
||||
class Row {
|
||||
private Long loadingPlanId; private String planNo; private Long containerBookingId;
|
||||
private Integer aggregateVersion; private String containerNo; private String cabinetNo;
|
||||
private Long warehouseId; private String warehouseName; private Long bookingCustomerId;
|
||||
private String customerName; private Long accountId;
|
||||
private Long warehouseId; private String warehouseName; private String customerName;
|
||||
private Long accountId;
|
||||
private LocalDateTime estimatedPortArrivalAt; private LocalDateTime actualPortArrivalAt;
|
||||
private LocalDateTime estimatedWarehouseArrivalAt;
|
||||
private Long sourceInboundId; private Long rootInboundId; private Long sourceOrderId;
|
||||
|
||||
+3
-3
@@ -120,11 +120,11 @@ public class LoadingCompletedOutboxService {
|
||||
|
||||
private void validateCustomerSnapshots(List<Row> rows) {
|
||||
for (Row row : rows) {
|
||||
if (row.getAccountId() == null || !Objects.equals(row.getAccountId(), row.getBookingCustomerId())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "装柜计划客户与柜次客户快照不一致,不能生成尾程交接事件");
|
||||
if (row.getAccountId() == null || row.getAccountId() <= 0) {
|
||||
throw new BizException(ResultCode.CONFLICT, "装柜明细缺少有效客户标识,不能生成尾程交接事件");
|
||||
}
|
||||
if (row.getCustomerName() == null || row.getCustomerName().isBlank()) {
|
||||
throw new BizException(ResultCode.CONFLICT, "柜次缺少客户名称快照,不能生成尾程交接事件");
|
||||
throw new BizException(ResultCode.CONFLICT, "装柜明细缺少客户名称快照,不能生成尾程交接事件");
|
||||
}
|
||||
}
|
||||
rows.stream().collect(java.util.stream.Collectors.groupingBy(Row::getAccountId)).forEach((accountId, values) -> {
|
||||
|
||||
@@ -50,7 +50,7 @@ public class LoadingCompletionWorker {
|
||||
String traceId = cause instanceof PdaPickSettlementService.SettlementFailure remote
|
||||
? remote.traceId() : null;
|
||||
int attempt = command.getAttemptCount() == null ? 1 : command.getAttemptCount() + 1;
|
||||
boolean terminal = attempt >= MAX_ATTEMPTS;
|
||||
boolean terminal = isDeterministicLocalBusinessFailure(stage, cause) || attempt >= MAX_ATTEMPTS;
|
||||
LocalDateTime retryAt = LocalDateTime.now().plusSeconds(Math.min(300L, 15L << Math.min(attempt - 1, 4)));
|
||||
boolean recorded = commands.fail(command.getId(), workerId, terminal ? "FAILED" : "RETRYABLE", retryAt,
|
||||
stage, code, requestId, traceId);
|
||||
@@ -61,4 +61,10 @@ public class LoadingCompletionWorker {
|
||||
+ "requestId={} traceId={} attempt={} terminal={}", command.getId(), command.getPickTaskId(),
|
||||
command.getOperationId(), stage, code, requestId, traceId, attempt, terminal, cause);
|
||||
}
|
||||
|
||||
private static boolean isDeterministicLocalBusinessFailure(String stage, Throwable cause) {
|
||||
return cause instanceof BizException && java.util.Set.of(
|
||||
"OWNER_DISCOVERY", "OWNER_READY", "PLAN_GATE", "TASK_REVIEW",
|
||||
"PLAN_COMPLETE", "COMPLETED_EVENT").contains(stage);
|
||||
}
|
||||
}
|
||||
|
||||
+15
-7
@@ -85,7 +85,7 @@ class LoadingCompletedOutboxServiceTest {
|
||||
.hasMessageContaining("缺少 SKU");
|
||||
}
|
||||
|
||||
@Test void rejectsMissingTrustedBookingCustomerName() {
|
||||
@Test void rejectsMissingTrustedPlanItemCustomerName() {
|
||||
Row value = row(11, 100, 101);
|
||||
value.setCustomerName(" ");
|
||||
when(snapshots.rows(90L)).thenReturn(List.of(value));
|
||||
@@ -93,12 +93,19 @@ class LoadingCompletedOutboxServiceTest {
|
||||
assertThatThrownBy(() -> service.publish(90L)).hasMessageContaining("缺少客户名称快照");
|
||||
}
|
||||
|
||||
@Test void rejectsBookingCustomerIdThatDoesNotMatchAccountEvent() {
|
||||
@Test void publishesFrozenPlanItemCustomerSnapshotWithoutDependingOnBookingOwner() throws Exception {
|
||||
Row value = row(11, 100, 101);
|
||||
value.setBookingCustomerId(22L);
|
||||
value.setCustomerName("南玻万");
|
||||
value.setDestinationType("FBA");
|
||||
when(snapshots.rows(90L)).thenReturn(List.of(value));
|
||||
|
||||
assertThatThrownBy(() -> service.publish(90L)).hasMessageContaining("客户与柜次客户快照不一致");
|
||||
service.publish(90L);
|
||||
|
||||
ArgumentCaptor<InboundOutboxEntity> captor = ArgumentCaptor.forClass(InboundOutboxEntity.class);
|
||||
verify(outbox).insert(captor.capture());
|
||||
EventEnvelope event = mapper.readValue(captor.getValue().getPayloadJson(), EventEnvelope.class);
|
||||
assertThat(event.tenantId()).isEqualTo("11");
|
||||
assertThat(event.payload().at("/orders/0/customerName").asText()).isEqualTo("南玻万");
|
||||
}
|
||||
|
||||
@Test void fbaFreezesPackageCountWithoutRequiringCartonLabels() throws Exception {
|
||||
@@ -209,8 +216,9 @@ class LoadingCompletedOutboxServiceTest {
|
||||
|
||||
assertThat(rowsSql).contains("p.plan_status = 'COMPLETED'")
|
||||
.contains("o.process_status = 'LOADED'")
|
||||
.contains("b.customer_id AS booking_customer_id")
|
||||
.contains("b.customer_name")
|
||||
.contains("i.customer_name")
|
||||
.doesNotContain("b.customer_id AS booking_customer_id")
|
||||
.doesNotContain("b.customer_name")
|
||||
.contains("b.estimated_port_arrival_at", "b.actual_port_arrival_at")
|
||||
.contains("b.estimated_warehouse_arrival_at");
|
||||
for (String sql : List.of(rowsSql, palletsSql)) {
|
||||
@@ -288,7 +296,7 @@ class LoadingCompletedOutboxServiceTest {
|
||||
private Row row(long account,long inbound,long detail) {
|
||||
Row r=new Row(); r.setLoadingPlanId(90L); r.setPlanNo("P90"); r.setContainerBookingId(91L);
|
||||
r.setAggregateVersion(8); r.setContainerNo("CONT"); r.setCabinetNo("CAB"); r.setWarehouseId(92L);
|
||||
r.setWarehouseName("WH"); r.setBookingCustomerId(account); r.setCustomerName("客户-" + account);
|
||||
r.setWarehouseName("WH"); r.setCustomerName("客户-" + account);
|
||||
r.setEstimatedPortArrivalAt(java.time.LocalDateTime.of(2026, 9, 30, 12, 0));
|
||||
r.setActualPortArrivalAt(java.time.LocalDateTime.of(2026, 10, 1, 8, 30));
|
||||
r.setEstimatedWarehouseArrivalAt(java.time.LocalDateTime.of(2026, 10, 3, 9, 0));
|
||||
|
||||
@@ -63,6 +63,24 @@ class LoadingCompletionWorkerTest {
|
||||
verify(picks).failCompletion(20L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void completedEventBusinessFailureIsTerminalOnFirstAttempt() {
|
||||
LoadingCompletionCommandEntity command = command(0);
|
||||
when(commands.due(10)).thenReturn(List.of(command));
|
||||
when(commands.claim(eq(900L), any(String.class), any(LocalDateTime.class))).thenReturn(true);
|
||||
doThrow(new LoadingPickService.CompletionProcessingException("COMPLETED_EVENT",
|
||||
new BizException(ResultCode.CONFLICT, "customer snapshot conflict")))
|
||||
.when(loadingPicks).processCompletion(eq(command), any(String.class));
|
||||
when(commands.fail(eq(900L), any(String.class), eq("FAILED"), any(LocalDateTime.class),
|
||||
eq("COMPLETED_EVENT"), eq(ResultCode.CONFLICT.code()), eq(null), eq(null))).thenReturn(true);
|
||||
|
||||
worker.dispatch();
|
||||
|
||||
verify(commands).fail(eq(900L), any(String.class), eq("FAILED"), any(LocalDateTime.class),
|
||||
eq("COMPLETED_EVENT"), eq(ResultCode.CONFLICT.code()), eq(null), eq(null));
|
||||
verify(picks).failCompletion(20L);
|
||||
}
|
||||
|
||||
private static LoadingCompletionCommandEntity command(int attemptCount) {
|
||||
LoadingCompletionCommandEntity command = new LoadingCompletionCommandEntity();
|
||||
command.setId(900L);
|
||||
|
||||
+1
-1
@@ -101,7 +101,7 @@ class PdaSessionValidationFilterTest {
|
||||
MockServerHttpRequest.get(path).build());
|
||||
return exchange.mutate()
|
||||
.principal(Mono.just(new JwtAuthenticationToken(jwt.build(),
|
||||
List.of(new SimpleGrantedAuthority("ROLE_WAREHOUSE_PICKER")))))
|
||||
List.of(new SimpleGrantedAuthority("ROLE_OVERSEAS_WAREHOUSE_PDA_OPERATOR")))))
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -14,7 +14,7 @@
|
||||
| 客户资料/审核 | `controller/PlatformTenantController.java`、`service/TenantService.java` |
|
||||
| 客户默认尺寸初始化、重试与补偿 | `service/CustomerDimensionRuleInitializationService.java`、`CustomerDimensionInitializationRelay.java`、`repository/CustomerDimensionInitializationRepository.java`、`client/CustomerDimensionRuleClient.java`、`model/CustomerDimensionInitializationTask.java`;持久任务异步调用 Firstleg `/internal/firstleg/v1/customer-dimension-configs/defaults` |
|
||||
| 商户成员与个人资料 | `controller/MerchantTenantController.java`、`service/MemberService.java`、`service/PersonalProfileService.java` |
|
||||
| 平台用户与海外 PDA 仓绑定 | `controller/PlatformUserManagementController.java`、`service/PlatformUserManagementService.java`、`dto/PlatformUserManagementDtos.java`、`entity/IamMembership.java`;`WAREHOUSE_PICKER` 必须且只能绑定一个 `overseasWarehouseId` |
|
||||
| 平台用户与海外 PDA 仓绑定 | `controller/PlatformUserManagementController.java`、`service/PlatformUserManagementService.java`、`dto/PlatformUserManagementDtos.java`、`entity/IamMembership.java`;`OVERSEAS_WAREHOUSE_PDA_OPERATOR` 必须且只能绑定一个 `overseasWarehouseId` |
|
||||
| 角色/权限/RBAC | `controller/RbacController.java`、`service/RbacService.java` |
|
||||
| 授权快照/内部资格 | `controller/AuthorizationController.java`、`InternalAuthorizationController.java`、`service/AuthorizationSnapshotService.java` |
|
||||
| 海外安全状态事件 Outbox | `service/SecurityEventOutboxService.java`、`SecurityEventOutboxRelay.java`、`client/SecurityEventBridgeClient.java`;账号、授权或客户状态变更经版本化事件投影到 `../lastleg-service/security/` |
|
||||
@@ -30,7 +30,7 @@
|
||||
- 成员/账号跨 IAM/Auth 保持幂等补偿;资质跨 IAM/File 不保存文件内容。
|
||||
- 客户建档只在 IAM 事务内落持久初始化意图,远端投递由带租约、退避和最大次数的 Relay 完成;只有状态为 `READY` 才能审核通过。注册补偿必须投递 `COMPENSATE`,不得在建档事务中同步依赖 Firstleg。
|
||||
- 权限资源 code/type 不可变;按钮隐藏不能替代后端权限校验。
|
||||
- 只有平台 `WAREHOUSE_PICKER` 可保存 `overseasWarehouseId`,且必须为单个有效海外仓;非拣货员必须为空。仓绑定、账号状态、角色权限或客户状态变化要在同一业务事务内推进安全事件 Outbox,不得依赖海外服务回查国内 IAM。
|
||||
- 国内头程 PDA 使用 `CN_FIRSTLEG_PDA_OPERATOR`,海外仓 PDA 使用 `OVERSEAS_WAREHOUSE_PDA_OPERATOR`;两个角色互斥且权限不得交叉。只有海外角色可保存 `overseasWarehouseId`,且必须为单个有效海外仓;国内角色及其他角色必须为空。仓绑定、账号状态、角色权限或客户状态变化要在同一业务事务内推进安全事件 Outbox,不得依赖海外服务回查国内 IAM。
|
||||
- `IamTenantStatusChanged` 是安全可用性快照,不暴露客户建档/审核流程状态:仅 IAM 业务状态 `ACTIVE` 发布 `ACTIVE`,`PROFILE_REQUIRED`、`PENDING`、`REJECTED`、`DISABLED`、`ARCHIVED` 及其他不可用状态统一发布 `DISABLED`。
|
||||
|
||||
## 全量验收导航
|
||||
|
||||
@@ -254,13 +254,18 @@ public class PlatformUserManagementService {
|
||||
}
|
||||
|
||||
private Long validateWarehouseBinding(List<Long> roleIds, Long overseasWarehouseId) {
|
||||
boolean picker = roleIds.stream().map(roleMapper::selectById).filter(Objects::nonNull)
|
||||
.anyMatch(role -> "WAREHOUSE_PICKER".equals(role.getRoleCode()));
|
||||
if (picker && (overseasWarehouseId == null || overseasWarehouseId <= 0)) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "海外仓拣货员必须绑定一个有效海外仓");
|
||||
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");
|
||||
boolean domesticPda = roleCodes.contains("CN_FIRSTLEG_PDA_OPERATOR");
|
||||
if (overseasPda && domesticPda) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "国内头程 PDA 与海外仓 PDA 角色不能同时分配");
|
||||
}
|
||||
if (!picker && overseasWarehouseId != null) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "仅海外仓拣货员可绑定海外仓");
|
||||
if (overseasPda && (overseasWarehouseId == null || overseasWarehouseId <= 0)) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "海外仓 PDA 作业员必须绑定一个有效海外仓");
|
||||
}
|
||||
if (!overseasPda && overseasWarehouseId != null) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "仅海外仓 PDA 作业员可绑定海外仓");
|
||||
}
|
||||
return overseasWarehouseId;
|
||||
}
|
||||
|
||||
@@ -80,14 +80,14 @@ DELETE role_permission FROM iam_role_permission role_permission
|
||||
JOIN iam_role role ON role.id=role_permission.role_id AND role.tenant_id=role_permission.tenant_id
|
||||
JOIN iam_permission permission ON permission.id=role_permission.permission_id AND permission.tenant_id=role_permission.tenant_id
|
||||
WHERE permission.permission_code='lastleg:pda:operate'
|
||||
AND NOT (role.id=4000005 AND role.tenant_id=0 AND role.app_code='PLATFORM' AND role.role_code='WAREHOUSE_PICKER');
|
||||
AND NOT (role.id=4000005 AND role.tenant_id=0 AND role.app_code='PLATFORM' AND role.role_code='OVERSEAS_WAREHOUSE_PDA_OPERATOR');
|
||||
|
||||
INSERT IGNORE INTO iam_role_permission (tenant_id, role_id, permission_id, created_at)
|
||||
SELECT 0, role.id, permission.id, NOW(3)
|
||||
FROM iam_role role
|
||||
JOIN iam_permission permission ON permission.tenant_id=0 AND permission.app_code='PLATFORM'
|
||||
AND permission.permission_code='lastleg:pda:operate'
|
||||
WHERE role.id=4000005 AND role.tenant_id=0 AND role.app_code='PLATFORM' AND role.role_code='WAREHOUSE_PICKER'
|
||||
WHERE role.id=4000005 AND role.tenant_id=0 AND role.app_code='PLATFORM' AND role.role_code='OVERSEAS_WAREHOUSE_PDA_OPERATOR'
|
||||
AND role.status='ACTIVE' AND role.deleted=0 AND permission.status='ACTIVE' AND permission.deleted=0;
|
||||
|
||||
DROP PROCEDURE IF EXISTS assert_overseas_pda_picker_permission;
|
||||
@@ -98,15 +98,15 @@ BEGIN
|
||||
JOIN iam_role role ON role.id=grant_row.role_id AND role.tenant_id=grant_row.tenant_id
|
||||
JOIN iam_permission permission ON permission.id=grant_row.permission_id AND permission.tenant_id=grant_row.tenant_id
|
||||
WHERE permission.permission_code='lastleg:pda:operate'
|
||||
AND role.id=4000005 AND role.app_code='PLATFORM' AND role.role_code='WAREHOUSE_PICKER') <> 1 THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='海外PDA权限初始化失败:WAREHOUSE_PICKER未精确授权';
|
||||
AND role.id=4000005 AND role.app_code='PLATFORM' AND role.role_code='OVERSEAS_WAREHOUSE_PDA_OPERATOR') <> 1 THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='海外PDA权限初始化失败:海外仓 PDA 作业员未精确授权';
|
||||
END IF;
|
||||
IF EXISTS (SELECT 1 FROM iam_role_permission grant_row
|
||||
JOIN iam_role role ON role.id=grant_row.role_id AND role.tenant_id=grant_row.tenant_id
|
||||
JOIN iam_permission permission ON permission.id=grant_row.permission_id AND permission.tenant_id=grant_row.tenant_id
|
||||
WHERE permission.permission_code='lastleg:pda:operate'
|
||||
AND NOT (role.id=4000005 AND role.tenant_id=0 AND role.app_code='PLATFORM' AND role.role_code='WAREHOUSE_PICKER')) THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='海外PDA权限初始化失败:存在非仓库拣货员授权';
|
||||
AND NOT (role.id=4000005 AND role.tenant_id=0 AND role.app_code='PLATFORM' AND role.role_code='OVERSEAS_WAREHOUSE_PDA_OPERATOR')) THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='海外PDA权限初始化失败:存在非海外仓 PDA 作业员授权';
|
||||
END IF;
|
||||
END$$
|
||||
DELIMITER ;
|
||||
@@ -148,7 +148,7 @@ JOIN iam_permission permission ON permission.id=grant_row.permission_id AND perm
|
||||
LEFT JOIN tmp_phase4_warehouse_role_permission expected
|
||||
ON expected.role_code=role.role_code AND expected.permission_code=permission.permission_code
|
||||
WHERE role.tenant_id=0 AND role.app_code='PLATFORM'
|
||||
AND role.role_code IN ('WAREHOUSE_OPERATOR','WAREHOUSE_PICKER','WAREHOUSE_TALLY')
|
||||
AND role.role_code IN ('WAREHOUSE_OPERATOR','OVERSEAS_WAREHOUSE_PDA_OPERATOR','WAREHOUSE_TALLY','CN_FIRSTLEG_PDA_OPERATOR')
|
||||
AND permission.permission_code LIKE 'platform:overseas-%'
|
||||
AND expected.permission_code IS NULL;
|
||||
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
-- 三期仓库常用系统角色:仓库操作员、仓库拣货员、仓库理货员。
|
||||
-- 三期仓库系统角色:国内仓后台、国内头程 PDA、海外仓 PDA、国内仓理货复核。
|
||||
-- 本脚本只组合已经存在的权限资源,需在三期 Firstleg/IAM 权限脚本之后执行,可重复部署。
|
||||
USE baiyue_iam;
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
UPDATE iam_role
|
||||
SET role_code = 'OVERSEAS_WAREHOUSE_PDA_OPERATOR',
|
||||
role_name = '海外仓 PDA 作业员',
|
||||
description = '负责使用海外仓 PDA 执行接柜、入仓和库内作业,必须绑定一个海外仓'
|
||||
WHERE id = 4000005 AND tenant_id = 0 AND app_code = 'PLATFORM'
|
||||
AND role_code = 'WAREHOUSE_PICKER';
|
||||
|
||||
DROP PROCEDURE IF EXISTS assert_phase3_warehouse_role_ids_available;
|
||||
DELIMITER $$
|
||||
CREATE PROCEDURE assert_phase3_warehouse_role_ids_available()
|
||||
@@ -11,14 +18,16 @@ BEGIN
|
||||
SELECT 1
|
||||
FROM iam_role
|
||||
WHERE (id = 4000004 AND (tenant_id <> 0 OR app_code <> 'PLATFORM' OR role_code <> 'WAREHOUSE_OPERATOR'))
|
||||
OR (id = 4000005 AND (tenant_id <> 0 OR app_code <> 'PLATFORM' OR role_code <> 'WAREHOUSE_PICKER'))
|
||||
OR (id = 4000005 AND (tenant_id <> 0 OR app_code <> 'PLATFORM' OR role_code <> 'OVERSEAS_WAREHOUSE_PDA_OPERATOR'))
|
||||
OR (id = 4000006 AND (tenant_id <> 0 OR app_code <> 'PLATFORM' OR role_code <> 'WAREHOUSE_TALLY'))
|
||||
OR (id = 4000007 AND (tenant_id <> 0 OR app_code <> 'PLATFORM' OR role_code <> 'CN_FIRSTLEG_PDA_OPERATOR'))
|
||||
OR (tenant_id = 0 AND app_code = 'PLATFORM' AND role_code = 'WAREHOUSE_OPERATOR' AND id <> 4000004)
|
||||
OR (tenant_id = 0 AND app_code = 'PLATFORM' AND role_code = 'WAREHOUSE_PICKER' AND id <> 4000005)
|
||||
OR (tenant_id = 0 AND app_code = 'PLATFORM' AND role_code = 'OVERSEAS_WAREHOUSE_PDA_OPERATOR' AND id <> 4000005)
|
||||
OR (tenant_id = 0 AND app_code = 'PLATFORM' AND role_code = 'WAREHOUSE_TALLY' AND id <> 4000006)
|
||||
OR (tenant_id = 0 AND app_code = 'PLATFORM' AND role_code = 'CN_FIRSTLEG_PDA_OPERATOR' AND id <> 4000007)
|
||||
) THEN
|
||||
SIGNAL SQLSTATE '45000'
|
||||
SET MESSAGE_TEXT = '仓库角色初始化失败:4000004-4000006 或角色编码已被占用';
|
||||
SET MESSAGE_TEXT = '仓库角色初始化失败:4000004-4000007 或角色编码已被占用';
|
||||
END IF;
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
@@ -26,12 +35,14 @@ BEGIN
|
||||
WHERE (id = 6000004 AND role_id <> 4000004)
|
||||
OR (id = 6000005 AND role_id <> 4000005)
|
||||
OR (id = 6000006 AND role_id <> 4000006)
|
||||
OR (id = 6000007 AND role_id <> 4000007)
|
||||
OR (role_id = 4000004 AND id <> 6000004)
|
||||
OR (role_id = 4000005 AND id <> 6000005)
|
||||
OR (role_id = 4000006 AND id <> 6000006)
|
||||
OR (role_id = 4000007 AND id <> 6000007)
|
||||
) THEN
|
||||
SIGNAL SQLSTATE '45000'
|
||||
SET MESSAGE_TEXT = '仓库角色初始化失败:6000004-6000006 数据范围ID已被占用';
|
||||
SET MESSAGE_TEXT = '仓库角色初始化失败:6000004-6000007 数据范围ID已被占用';
|
||||
END IF;
|
||||
END$$
|
||||
DELIMITER ;
|
||||
@@ -42,12 +53,14 @@ INSERT INTO iam_role (
|
||||
id, tenant_id, app_code, role_code, role_name, role_type, status,
|
||||
description, deleted, version
|
||||
) VALUES
|
||||
(4000004, 0, 'PLATFORM', 'WAREHOUSE_OPERATOR', '仓库操作员', 'SYSTEM', 'ACTIVE',
|
||||
(4000004, 0, 'PLATFORM', 'WAREHOUSE_OPERATOR', '国内仓后台操作员', 'SYSTEM', 'ACTIVE',
|
||||
'负责查看入仓业务并在后台维护装柜证据', 0, 0),
|
||||
(4000005, 0, 'PLATFORM', 'WAREHOUSE_PICKER', '仓库拣货员', 'SYSTEM', 'ACTIVE',
|
||||
'负责使用PDA执行柜次拣货和现场拍照', 0, 0),
|
||||
(4000006, 0, 'PLATFORM', 'WAREHOUSE_TALLY', '仓库理货员', 'SYSTEM', 'ACTIVE',
|
||||
'负责入库测量、PDA复核和装柜证据维护', 0, 0)
|
||||
(4000005, 0, 'PLATFORM', 'OVERSEAS_WAREHOUSE_PDA_OPERATOR', '海外仓 PDA 作业员', 'SYSTEM', 'ACTIVE',
|
||||
'负责使用海外仓 PDA 执行接柜、入仓和库内作业,必须绑定一个海外仓', 0, 0),
|
||||
(4000006, 0, 'PLATFORM', 'WAREHOUSE_TALLY', '国内仓理货复核员', 'SYSTEM', 'ACTIVE',
|
||||
'负责国内入库测量、PDA复核和装柜证据维护', 0, 0),
|
||||
(4000007, 0, 'PLATFORM', 'CN_FIRSTLEG_PDA_OPERATOR', '国内头程 PDA 作业员', 'SYSTEM', 'ACTIVE',
|
||||
'负责使用国内 PDA 执行柜次拣货、现场取证和装柜完成', 0, 0)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
role_name = VALUES(role_name), role_type = 'SYSTEM', status = 'ACTIVE',
|
||||
description = VALUES(description), deleted = 0;
|
||||
@@ -67,10 +80,10 @@ INSERT INTO tmp_phase3_warehouse_role_permission (role_code, permission_code) VA
|
||||
('WAREHOUSE_OPERATOR', 'platform:firstleg:inbound:loading:view'),
|
||||
('WAREHOUSE_OPERATOR', 'platform:firstleg:inbound:loading:edit');
|
||||
|
||||
-- 拣货员:只允许PDA任务查询、作业和现场照片,不授予后台页面、任务分派或复核权限。
|
||||
-- 国内头程 PDA 作业员:只允许国内任务查询、作业和现场照片,不授予后台、海外、分派或复核权限。
|
||||
INSERT INTO tmp_phase3_warehouse_role_permission (role_code, permission_code) VALUES
|
||||
('WAREHOUSE_PICKER', 'platform:firstleg:pda:view'),
|
||||
('WAREHOUSE_PICKER', 'platform:firstleg:pda:work');
|
||||
('CN_FIRSTLEG_PDA_OPERATOR', 'platform:firstleg:pda:view'),
|
||||
('CN_FIRSTLEG_PDA_OPERATOR', 'platform:firstleg:pda:work');
|
||||
|
||||
-- 理货员:负责到仓测量、装柜证据维护和PDA复核,不允许完成装柜或确认出仓。
|
||||
INSERT INTO tmp_phase3_warehouse_role_permission (role_code, permission_code) VALUES
|
||||
@@ -92,15 +105,15 @@ DELIMITER $$
|
||||
CREATE PROCEDURE assert_phase3_warehouse_role_permissions_available()
|
||||
BEGIN
|
||||
DECLARE operator_permission_count INT DEFAULT 0;
|
||||
DECLARE picker_permission_count INT DEFAULT 0;
|
||||
DECLARE domestic_pda_permission_count INT DEFAULT 0;
|
||||
DECLARE tally_permission_count INT DEFAULT 0;
|
||||
SELECT COALESCE(SUM(role_code = 'WAREHOUSE_OPERATOR'), 0),
|
||||
COALESCE(SUM(role_code = 'WAREHOUSE_PICKER'), 0),
|
||||
COALESCE(SUM(role_code = 'CN_FIRSTLEG_PDA_OPERATOR'), 0),
|
||||
COALESCE(SUM(role_code = 'WAREHOUSE_TALLY'), 0)
|
||||
INTO operator_permission_count, picker_permission_count, tally_permission_count
|
||||
INTO operator_permission_count, domestic_pda_permission_count, tally_permission_count
|
||||
FROM tmp_phase3_warehouse_role_permission;
|
||||
IF operator_permission_count <> 5
|
||||
OR picker_permission_count <> 2
|
||||
OR domestic_pda_permission_count <> 2
|
||||
OR tally_permission_count <> 12 THEN
|
||||
SIGNAL SQLSTATE '45000'
|
||||
SET MESSAGE_TEXT = '仓库角色初始化失败:角色权限组合数量不符合预期';
|
||||
@@ -124,7 +137,7 @@ DELIMITER ;
|
||||
CALL assert_phase3_warehouse_role_permissions_available();
|
||||
DROP PROCEDURE assert_phase3_warehouse_role_permissions_available;
|
||||
|
||||
-- 三个系统角色的权限集合由迁移维护。重复执行时移除历史版本或人工误授的越权项,
|
||||
-- 四个系统角色的权限集合由初始化维护。重复执行时移除人工误授的越权项,
|
||||
-- 避免最小权限角色因脚本升级只增不减。
|
||||
DELETE role_permission
|
||||
FROM iam_role_permission role_permission
|
||||
@@ -139,8 +152,8 @@ DELETE role_permission
|
||||
AND expected.permission_code = permission.permission_code
|
||||
WHERE role.tenant_id = 0
|
||||
AND role.app_code = 'PLATFORM'
|
||||
AND role.role_code IN ('WAREHOUSE_OPERATOR', 'WAREHOUSE_PICKER', 'WAREHOUSE_TALLY')
|
||||
AND NOT (role.role_code = 'WAREHOUSE_PICKER' AND permission.permission_code = 'lastleg:pda:operate')
|
||||
AND role.role_code IN ('WAREHOUSE_OPERATOR', 'OVERSEAS_WAREHOUSE_PDA_OPERATOR', 'WAREHOUSE_TALLY', 'CN_FIRSTLEG_PDA_OPERATOR')
|
||||
AND NOT (role.role_code = 'OVERSEAS_WAREHOUSE_PDA_OPERATOR' AND permission.permission_code = 'lastleg:pda:operate')
|
||||
AND expected.permission_code IS NULL;
|
||||
|
||||
INSERT IGNORE INTO iam_role_permission (role_id, permission_id, tenant_id)
|
||||
@@ -162,7 +175,8 @@ SELECT role.id, permission.id, 0
|
||||
INSERT INTO iam_data_scope (id, role_id, tenant_id, scope_type, scope_expression) VALUES
|
||||
(6000004, 4000004, 0, 'TENANT', NULL),
|
||||
(6000005, 4000005, 0, 'TENANT', NULL),
|
||||
(6000006, 4000006, 0, 'TENANT', NULL)
|
||||
(6000006, 4000006, 0, 'TENANT', NULL),
|
||||
(6000007, 4000007, 0, 'TENANT', NULL)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
tenant_id = 0, scope_type = 'TENANT', scope_expression = NULL;
|
||||
|
||||
@@ -177,7 +191,7 @@ BEGIN
|
||||
AND role_type = 'SYSTEM'
|
||||
AND status = 'ACTIVE'
|
||||
AND deleted = 0
|
||||
AND role_code IN ('WAREHOUSE_OPERATOR', 'WAREHOUSE_PICKER', 'WAREHOUSE_TALLY')) <> 3 THEN
|
||||
AND role_code IN ('WAREHOUSE_OPERATOR', 'OVERSEAS_WAREHOUSE_PDA_OPERATOR', 'WAREHOUSE_TALLY', 'CN_FIRSTLEG_PDA_OPERATOR')) <> 4 THEN
|
||||
SIGNAL SQLSTATE '45000'
|
||||
SET MESSAGE_TEXT = '仓库角色初始化失败:系统角色未完整启用';
|
||||
END IF;
|
||||
@@ -203,9 +217,9 @@ BEGIN
|
||||
JOIN iam_role role ON role.id = scope_row.role_id
|
||||
WHERE role.tenant_id = 0
|
||||
AND role.app_code = 'PLATFORM'
|
||||
AND role.role_code IN ('WAREHOUSE_OPERATOR', 'WAREHOUSE_PICKER', 'WAREHOUSE_TALLY')
|
||||
AND role.role_code IN ('WAREHOUSE_OPERATOR', 'OVERSEAS_WAREHOUSE_PDA_OPERATOR', 'WAREHOUSE_TALLY', 'CN_FIRSTLEG_PDA_OPERATOR')
|
||||
AND scope_row.tenant_id = 0
|
||||
AND scope_row.scope_type = 'TENANT') <> 3 THEN
|
||||
AND scope_row.scope_type = 'TENANT') <> 4 THEN
|
||||
SIGNAL SQLSTATE '45000'
|
||||
SET MESSAGE_TEXT = '仓库角色初始化失败:数据范围未完整写入';
|
||||
END IF;
|
||||
|
||||
@@ -645,7 +645,7 @@ paths:
|
||||
get:
|
||||
tags: [RBAC]
|
||||
summary: List roles / 查询角色
|
||||
description: List roles in the selected application and current customer or platform technical scope. The platform scope includes the built-in WAREHOUSE_OPERATOR, WAREHOUSE_PICKER and WAREHOUSE_TALLY roles after the phase-three role migration. / 查询指定应用及当前客户范围或平台技术作用域内的角色;三期角色迁移后,平台作用域包含仓库操作员、仓库拣货员和仓库理货员三个内置角色。
|
||||
description: List roles in the selected application and current customer or platform technical scope. Domestic first-leg PDA and overseas warehouse PDA operators are separate, mutually exclusive roles. / 查询指定应用及当前客户范围或平台技术作用域内的角色;国内头程 PDA 作业员与海外仓 PDA 作业员是两批独立人员,对应角色互斥且权限不交叉。
|
||||
responses:
|
||||
'200': {$ref: '#/components/responses/Success'}
|
||||
'403': {$ref: '#/components/responses/Forbidden'}
|
||||
@@ -1323,7 +1323,7 @@ components:
|
||||
email: {type: string, format: email, maxLength: 128}
|
||||
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: 选择WAREHOUSE_PICKER角色时必填且只能绑定一个实际海外仓;其他角色必须为空}
|
||||
overseasWarehouseId: {type: integer, format: int64, nullable: true, description: 选择 OVERSEAS_WAREHOUSE_PDA_OPERATOR 时必填且只能绑定一个实际海外仓;CN_FIRSTLEG_PDA_OPERATOR 与其他角色必须为空;两个 PDA 角色不能同时分配}
|
||||
password: {type: string, format: password, minLength: 8, maxLength: 64, writeOnly: true}
|
||||
status: {type: string, enum: [ACTIVE, DISABLED]}
|
||||
UpdatePlatformManagedUserRequest:
|
||||
@@ -1335,7 +1335,7 @@ components:
|
||||
email: {type: string, format: email, maxLength: 128}
|
||||
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: 选择WAREHOUSE_PICKER角色时必填且只能绑定一个实际海外仓;其他角色必须为空}
|
||||
overseasWarehouseId: {type: integer, format: int64, nullable: true, description: 选择 OVERSEAS_WAREHOUSE_PDA_OPERATOR 时必填且只能绑定一个实际海外仓;CN_FIRSTLEG_PDA_OPERATOR 与其他角色必须为空;两个 PDA 角色不能同时分配}
|
||||
CreateInternalUserRequest:
|
||||
type: object
|
||||
required: [displayName]
|
||||
|
||||
+5
-5
@@ -82,10 +82,10 @@ class OverseasPhase4PermissionSqlContractTest {
|
||||
"platform:overseas-business:purchase:list",
|
||||
"platform:overseas-business:truck:list",
|
||||
"platform:overseas-business:looseboard:list");
|
||||
assertThat(matrix).doesNotContain("WAREHOUSE_PICKER");
|
||||
assertThat(matrix).doesNotContain("OVERSEAS_WAREHOUSE_PDA_OPERATOR");
|
||||
assertThat(matrix).doesNotContain(":create'", ":edit'", ":status'", ":export'", ":adjust'");
|
||||
assertThat(sql).contains("assert_phase4_warehouse_role_minimum", "<> 20",
|
||||
"role.role_code IN ('WAREHOUSE_OPERATOR','WAREHOUSE_PICKER','WAREHOUSE_TALLY')");
|
||||
"role.role_code IN ('WAREHOUSE_OPERATOR','OVERSEAS_WAREHOUSE_PDA_OPERATOR','WAREHOUSE_TALLY','CN_FIRSTLEG_PDA_OPERATOR')");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,12 +125,12 @@ class OverseasPhase4PermissionSqlContractTest {
|
||||
"permission.permission_code<>'lastleg:pda:operate'",
|
||||
"role.id=4000005",
|
||||
"assert_overseas_pda_picker_permission",
|
||||
"存在非仓库拣货员授权",
|
||||
"role.role_code='WAREHOUSE_PICKER'",
|
||||
"存在非海外仓 PDA 作业员授权",
|
||||
"role.role_code='OVERSEAS_WAREHOUSE_PDA_OPERATOR'",
|
||||
"permission.permission_code='lastleg:pda:operate'",
|
||||
"role.role_code='PLATFORM_ADMIN'",
|
||||
"DELETE role_permission FROM");
|
||||
int picker = sql.indexOf("role.role_code='WAREHOUSE_PICKER'");
|
||||
int picker = sql.indexOf("role.role_code='OVERSEAS_WAREHOUSE_PDA_OPERATOR'");
|
||||
int grantStart = sql.lastIndexOf("INSERT IGNORE INTO", picker);
|
||||
int grantEnd = sql.indexOf(';', picker);
|
||||
assertThat(grantStart).isGreaterThanOrEqualTo(0);
|
||||
|
||||
@@ -30,33 +30,35 @@ class WarehouseRoleSqlContractTest {
|
||||
private static void assertWarehouseRoleContract(String sql) {
|
||||
|
||||
assertThat(sql)
|
||||
.contains("(4000004, 0, 'PLATFORM', 'WAREHOUSE_OPERATOR', '仓库操作员', 'SYSTEM'")
|
||||
.contains("(4000005, 0, 'PLATFORM', 'WAREHOUSE_PICKER', '仓库拣货员', 'SYSTEM'")
|
||||
.contains("(4000006, 0, 'PLATFORM', 'WAREHOUSE_TALLY', '仓库理货员', 'SYSTEM'")
|
||||
.contains("(4000004, 0, 'PLATFORM', 'WAREHOUSE_OPERATOR', '国内仓后台操作员', 'SYSTEM'")
|
||||
.contains("(4000005, 0, 'PLATFORM', 'OVERSEAS_WAREHOUSE_PDA_OPERATOR', '海外仓 PDA 作业员', 'SYSTEM'")
|
||||
.contains("(4000006, 0, 'PLATFORM', 'WAREHOUSE_TALLY', '国内仓理货复核员', 'SYSTEM'")
|
||||
.contains("(4000007, 0, 'PLATFORM', 'CN_FIRSTLEG_PDA_OPERATOR', '国内头程 PDA 作业员', 'SYSTEM'")
|
||||
.contains("ON DUPLICATE KEY UPDATE")
|
||||
.contains("INSERT IGNORE INTO iam_role_permission")
|
||||
.contains("'WAREHOUSE_PICKER', 'platform:firstleg:pda:work'")
|
||||
.contains("'CN_FIRSTLEG_PDA_OPERATOR', 'platform:firstleg:pda:work'")
|
||||
.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("picker_permission_count <> 2")
|
||||
.contains("domestic_pda_permission_count <> 2")
|
||||
.contains("tally_permission_count <> 12")
|
||||
.contains("依赖的三期平台权限尚未完整部署")
|
||||
.contains("DELETE role_permission")
|
||||
.contains("expected.permission_code IS NULL")
|
||||
.contains("role.role_code = 'WAREHOUSE_PICKER' AND permission.permission_code = 'lastleg:pda:operate'")
|
||||
.contains("role.role_code = 'OVERSEAS_WAREHOUSE_PDA_OPERATOR' AND permission.permission_code = 'lastleg:pda:operate'")
|
||||
.contains("最小权限授权未完整写入")
|
||||
.contains("grant_row.tenant_id = 0) <> 19")
|
||||
.contains("(6000004, 4000004, 0, 'TENANT', NULL)")
|
||||
.contains("(6000005, 4000005, 0, 'TENANT', NULL)")
|
||||
.contains("(6000006, 4000006, 0, 'TENANT', NULL)");
|
||||
.contains("(6000006, 4000006, 0, 'TENANT', NULL)")
|
||||
.contains("(6000007, 4000007, 0, 'TENANT', NULL)");
|
||||
|
||||
assertRoleGrant(sql, "WAREHOUSE_PICKER", "platform:firstleg:pda:work");
|
||||
assertRoleGrant(sql, "CN_FIRSTLEG_PDA_OPERATOR", "platform:firstleg:pda:work");
|
||||
assertRoleGrant(sql, "WAREHOUSE_TALLY", "platform:firstleg:pda:review");
|
||||
assertRoleGrant(sql, "WAREHOUSE_OPERATOR", "platform:firstleg:inbound:loading:edit");
|
||||
|
||||
assertThat(roleGrantBlock(sql, "WAREHOUSE_PICKER"))
|
||||
assertThat(roleGrantBlock(sql, "CN_FIRSTLEG_PDA_OPERATOR"))
|
||||
.doesNotContain("platform:order")
|
||||
.doesNotContain("platform:master-data")
|
||||
.doesNotContain("platform:warehouse:list")
|
||||
@@ -76,9 +78,9 @@ class WarehouseRoleSqlContractTest {
|
||||
.doesNotContain("'WAREHOUSE_OPERATOR', 'platform:firstleg:inbound:loading:complete'")
|
||||
.doesNotContain("'WAREHOUSE_OPERATOR', 'platform:firstleg:outbound-dispatch:confirm'")
|
||||
.doesNotContain("'WAREHOUSE_OPERATOR', 'platform:firstleg:outbound-dispatch:reverse'")
|
||||
.doesNotContain("'WAREHOUSE_PICKER', 'platform:firstleg:smart-loading:delete'")
|
||||
.doesNotContain("'CN_FIRSTLEG_PDA_OPERATOR', 'platform:firstleg:smart-loading:delete'")
|
||||
.doesNotContain("'WAREHOUSE_TALLY', 'firstleg:inbound:fee-detail:edit'")
|
||||
.doesNotContain("'WAREHOUSE_PICKER', 'merchant:")
|
||||
.doesNotContain("'CN_FIRSTLEG_PDA_OPERATOR', 'merchant:")
|
||||
.doesNotContain("'WAREHOUSE_OPERATOR', 'merchant:")
|
||||
.doesNotContain("'WAREHOUSE_TALLY', 'merchant:");
|
||||
}
|
||||
@@ -89,7 +91,7 @@ class WarehouseRoleSqlContractTest {
|
||||
|
||||
private static String roleGrantBlock(String sql, String roleCode) {
|
||||
String marker = "-- " + switch (roleCode) {
|
||||
case "WAREHOUSE_PICKER" -> "拣货员:";
|
||||
case "CN_FIRSTLEG_PDA_OPERATOR" -> "国内头程 PDA 作业员:";
|
||||
case "WAREHOUSE_TALLY" -> "理货员:";
|
||||
default -> throw new IllegalArgumentException("unsupported role " + roleCode);
|
||||
};
|
||||
|
||||
@@ -146,7 +146,7 @@ class PlatformUserManagementServiceTest {
|
||||
|
||||
@Test
|
||||
void warehousePickerRequiresAndPersistsSingleOverseasWarehouseBinding() {
|
||||
platformRole.setRoleCode("WAREHOUSE_PICKER");
|
||||
platformRole.setRoleCode("OVERSEAS_WAREHOUSE_PDA_OPERATOR");
|
||||
UpdateUserRequest missing = new UpdateUserRequest("张伟", "13800138000", null,
|
||||
0L, List.of(40001L), null);
|
||||
assertThrows(BizException.class,
|
||||
@@ -161,9 +161,31 @@ class PlatformUserManagementServiceTest {
|
||||
verify(auth).revokeSessions(eq("internal-token"), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void domesticAndOverseasPdaRolesAreMutuallyExclusive() {
|
||||
IamRole domestic = new IamRole();
|
||||
domestic.setId(40007L); domestic.setTenantId(0L); domestic.setAppCode("PLATFORM");
|
||||
domestic.setRoleCode("CN_FIRSTLEG_PDA_OPERATOR"); domestic.setRoleName("国内头程 PDA 作业员");
|
||||
domestic.setStatus("ACTIVE");
|
||||
IamRole overseas = new IamRole();
|
||||
overseas.setId(40005L); overseas.setTenantId(0L); overseas.setAppCode("PLATFORM");
|
||||
overseas.setRoleCode("OVERSEAS_WAREHOUSE_PDA_OPERATOR"); overseas.setRoleName("海外仓 PDA 作业员");
|
||||
overseas.setStatus("ACTIVE");
|
||||
when(roles.selectById(40007L)).thenReturn(domestic);
|
||||
when(roles.selectById(40005L)).thenReturn(overseas);
|
||||
|
||||
UpdateUserRequest request = new UpdateUserRequest("张伟", "13800138000", null,
|
||||
0L, List.of(40007L, 40005L), 900L);
|
||||
|
||||
BizException error = assertThrows(BizException.class,
|
||||
() -> service.update(30L, request, Set.of("platform:user:grant-role")));
|
||||
assertTrue(error.getMessage().contains("不能同时分配"));
|
||||
verify(auth, never()).scope(anyString(), anyLong(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void changingWarehouseBindingRevokesAllAuthSessionsSoAccountVersionIsBumped() {
|
||||
platformRole.setRoleCode("WAREHOUSE_PICKER");
|
||||
platformRole.setRoleCode("OVERSEAS_WAREHOUSE_PDA_OPERATOR");
|
||||
membership.setOverseasWarehouseId(900L);
|
||||
UpdateUserRequest changed = new UpdateUserRequest("张伟", "13800138000", null,
|
||||
0L, List.of(40001L), 901L);
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
- Lastleg 安全投影只接受 `ACTIVE`/`DISABLED` 两态安全语义;客户建档和审核的 `PROFILE_REQUIRED`/`PENDING`/`REJECTED`/`ARCHIVED` 由 IAM 在发布前统一折叠为 `DISABLED`,Lastleg 不兼容或解释国内工作流状态。
|
||||
- 开发基线只为国内固定 `authUserId=1000001`、`tenantId=0` 的 `platform_admin` 写 USER/AUTHORIZATION version=0 投影;使用 `INSERT IGNORE`,后续 IAM version>=1 事件单调覆盖。不得扩大为历史账号运行时补齐或缺失投影默认放行。
|
||||
- PDA 入仓任务完成前必须先通过 `/unload-photos` 保存非空且不重复的卸柜照片文件 ID;照片上传时间由服务端记录,预计报空时间固定为照片上传时间加两天。完成入仓时,库存入账、计划/任务状态推进与 `OVERSEAS_CONTAINER_UNLOADED` V1 Outbox 必须处于同一事务;事件绑定来源头程货柜身份,不得从前端请求拼装或回查头程数据库。
|
||||
- 海外 PDA 柜号签收只允许平台 `WAREHOUSE_PICKER` 使用 `lastleg:pda:operate`;PDA 不接收 `customerId`,候选跨客户读取,选柜时从柜内计划唯一解析并冻结真实客户。未完成会话按平台操作账号和柜号分别全局唯一,后续照片、扫码、实收、库存与完成操作只使用会话客户范围;平台管理员和客户账号不得进入该作业入口。
|
||||
- 海外 PDA 柜号签收只允许平台 `OVERSEAS_WAREHOUSE_PDA_OPERATOR` 使用 `lastleg:pda:operate`;国内头程 PDA 角色不得进入。PDA 不接收 `customerId`,候选跨客户读取,选柜时从柜内计划唯一解析并冻结真实客户。未完成会话按平台操作账号和柜号分别全局唯一,后续照片、扫码、实收、库存与完成操作只使用会话客户范围;平台管理员和客户账号不得进入该作业入口。
|
||||
|
||||
## 查询与租户约束
|
||||
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ import java.util.List;
|
||||
@RestController
|
||||
@RequestMapping("/api/pda/v1/last-leg/receiving-containers")
|
||||
@Tag(name = "Overseas PDA container receiving")
|
||||
@PreAuthorize("hasAuthority('lastleg:pda:operate') and hasAuthority('ROLE_WAREHOUSE_PICKER')")
|
||||
@PreAuthorize("hasAuthority('lastleg:pda:operate') and hasAuthority('ROLE_OVERSEAS_WAREHOUSE_PDA_OPERATOR')")
|
||||
public class ReceivingContainerController {
|
||||
public static final String OPERATE_PERMISSION = PdaWarehouseIdentity.OPERATE_PERMISSION;
|
||||
@Resource private ReceivingContainerService service;
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/pda/v1/last-leg/pallet-operations")
|
||||
@PreAuthorize("hasAuthority('lastleg:pda:operate') and hasAuthority('ROLE_WAREHOUSE_PICKER')")
|
||||
@PreAuthorize("hasAuthority('lastleg:pda:operate') and hasAuthority('ROLE_OVERSEAS_WAREHOUSE_PDA_OPERATOR')")
|
||||
public class PdaPalletOperationController {
|
||||
@Resource private PdaPalletOperationService service;
|
||||
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/pda/v1/last-leg/inventory-moves")
|
||||
@PreAuthorize("hasAuthority('lastleg:pda:operate') and hasAuthority('ROLE_WAREHOUSE_PICKER')")
|
||||
@PreAuthorize("hasAuthority('lastleg:pda:operate') and hasAuthority('ROLE_OVERSEAS_WAREHOUSE_PDA_OPERATOR')")
|
||||
public class PdaInventoryMoveController {
|
||||
@Resource private PdaInventoryMoveService service;
|
||||
|
||||
|
||||
+2
-2
@@ -6,7 +6,7 @@ import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
public record PdaWarehouseIdentity(String operatorKey, String operatorName, long overseasWarehouseId) {
|
||||
public static final String OPERATE_PERMISSION = "lastleg:pda:operate";
|
||||
public static final String PICKER_ROLE = "ROLE_WAREHOUSE_PICKER";
|
||||
public static final String PICKER_ROLE = "ROLE_OVERSEAS_WAREHOUSE_PDA_OPERATOR";
|
||||
|
||||
public static PdaWarehouseIdentity require(AuthenticatedUser user) {
|
||||
if (user == null || user.userId() == null || user.tenantId() == null || user.tenantId() != 0
|
||||
@@ -15,7 +15,7 @@ public record PdaWarehouseIdentity(String operatorKey, String operatorName, long
|
||||
"Authenticated platform warehouse PDA identity is required");
|
||||
}
|
||||
if (!user.permissions().contains(PICKER_ROLE)) {
|
||||
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "WAREHOUSE_PICKER role is required");
|
||||
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "OVERSEAS_WAREHOUSE_PDA_OPERATOR role is required");
|
||||
}
|
||||
if (!user.permissions().contains(OPERATE_PERMISSION)) {
|
||||
throw new ResponseStatusException(HttpStatus.FORBIDDEN, OPERATE_PERMISSION + " permission is required");
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/pda/v1/last-leg/stocktakes")
|
||||
@PreAuthorize("hasAuthority('lastleg:pda:operate') and hasAuthority('ROLE_WAREHOUSE_PICKER')")
|
||||
@PreAuthorize("hasAuthority('lastleg:pda:operate') and hasAuthority('ROLE_OVERSEAS_WAREHOUSE_PDA_OPERATOR')")
|
||||
public class PdaStocktakeController {
|
||||
@Resource private PdaStocktakeService service;
|
||||
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/pda/v1/last-leg")
|
||||
@PreAuthorize("hasAuthority('lastleg:pda:operate') and hasAuthority('ROLE_WAREHOUSE_PICKER')")
|
||||
@PreAuthorize("hasAuthority('lastleg:pda:operate') and hasAuthority('ROLE_OVERSEAS_WAREHOUSE_PDA_OPERATOR')")
|
||||
public class PdaWarehouseController {
|
||||
@Resource private PdaWarehouseService service;
|
||||
|
||||
|
||||
+2
-2
@@ -28,7 +28,7 @@ import java.util.List;
|
||||
@PreAuthorize("hasAuthority('lastleg:pda:operate')")
|
||||
public class PdaRelabelController {
|
||||
public static final String OPERATE_PERMISSION = "lastleg:pda:operate";
|
||||
private static final String PICKER_ROLE = "ROLE_WAREHOUSE_PICKER";
|
||||
private static final String PICKER_ROLE = "ROLE_OVERSEAS_WAREHOUSE_PDA_OPERATOR";
|
||||
|
||||
@Resource private PdaRelabelService service;
|
||||
|
||||
@@ -111,7 +111,7 @@ public class PdaRelabelController {
|
||||
if (user.permissions() == null || !user.permissions().contains(OPERATE_PERMISSION)
|
||||
|| !user.permissions().contains(PICKER_ROLE)) {
|
||||
throw new ResponseStatusException(HttpStatus.FORBIDDEN,
|
||||
"WAREHOUSE_PICKER and " + OPERATE_PERMISSION + " are required");
|
||||
"OVERSEAS_WAREHOUSE_PDA_OPERATOR and " + OPERATE_PERMISSION + " are required");
|
||||
}
|
||||
if (user.overseasWarehouseId() == null || user.overseasWarehouseId() <= 0L) {
|
||||
throw new ResponseStatusException(HttpStatus.FORBIDDEN,
|
||||
|
||||
@@ -6,7 +6,7 @@ paths:
|
||||
/api/pda/v1/last-leg/receiving-containers/candidates:
|
||||
get:
|
||||
summary: 查询当前客户待签收柜号
|
||||
description: 权限 lastleg:pda:operate;仅 PLATFORM WAREHOUSE_PICKER 可用;仅返回已单调消费 PICKUP_READY(80) 或更高实际船节点、状态为 RECEIVING_AVAILABLE 的柜,不接收 customerId。
|
||||
description: 权限 lastleg:pda:operate;仅 PLATFORM OVERSEAS_WAREHOUSE_PDA_OPERATOR 可用;仅返回已单调消费 PICKUP_READY(80) 或更高实际船节点、状态为 RECEIVING_AVAILABLE 的柜,不接收 customerId。
|
||||
security: [{ bearerAuth: [] }]
|
||||
responses: { '200': { description: 柜号候选及箱数进度, content: { application/json: { schema: { $ref: '#/components/schemas/ReceivingCandidateListApiResponse' } } } }, '403': { description: 缺少 PDA 操作权限 } }
|
||||
/api/pda/v1/last-leg/receiving-containers/warehouse-profile:
|
||||
@@ -107,7 +107,7 @@ paths:
|
||||
/api/pda/v1/last-leg/inventory/resolve:
|
||||
post:
|
||||
summary: PDA按SKU、箱唛或入仓码扫码查询库存
|
||||
description: 仅 WAREHOUSE_PICKER + lastleg:pda:operate;海外实际仓只取Token overseasWarehouseId,不接收客户或仓库参数。
|
||||
description: 仅 OVERSEAS_WAREHOUSE_PDA_OPERATOR + lastleg:pda:operate;海外实际仓只取Token overseasWarehouseId,不接收客户或仓库参数。
|
||||
security: [{ bearerAuth: [] }]
|
||||
requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/PdaWarehouseScan' } } } }
|
||||
responses: { '200': { description: 同仓跨客户库存与物理仓位事实, content: { application/json: { schema: { $ref: '#/components/schemas/PdaInventoryScanApiResponse' } } } }, '404': { description: A0404:当前绑定仓无匹配库存 } }
|
||||
|
||||
+2
-2
@@ -51,7 +51,7 @@ class ReceivingContainerContractTest {
|
||||
void everyBrowserReachableReceivingMethodUsesOnlyPdaOperatePermission() {
|
||||
PreAuthorize permission = ReceivingContainerController.class.getAnnotation(PreAuthorize.class);
|
||||
assertTrue(permission != null);
|
||||
assertTrue("hasAuthority('lastleg:pda:operate') and hasAuthority('ROLE_WAREHOUSE_PICKER')"
|
||||
assertTrue("hasAuthority('lastleg:pda:operate') and hasAuthority('ROLE_OVERSEAS_WAREHOUSE_PDA_OPERATOR')"
|
||||
.equals(permission.value()));
|
||||
}
|
||||
|
||||
@@ -138,7 +138,7 @@ class ReceivingContainerContractTest {
|
||||
assertEquals(12, successResponses.stream().filter(line -> line.contains("/schemas/Receiving")).count());
|
||||
assertEquals(4, successResponses.stream().filter(line -> line.contains("/schemas/Pda")).count());
|
||||
assertTrue(receivingPaths.contains("返回 WAIT_SIGN、SIGNED 或 RECEIVING 当前柜"));
|
||||
assertTrue(receivingPaths.contains("仅 PLATFORM WAREHOUSE_PICKER 可用"));
|
||||
assertTrue(receivingPaths.contains("仅 PLATFORM OVERSEAS_WAREHOUSE_PDA_OPERATOR 可用"));
|
||||
assertFalse(receivingPaths.contains("客户与操作账号均从 PDA Token 获取"));
|
||||
assertTrue(yaml.contains("enum: [LICENSE_PLATE, CONTAINER_SEAL, DOOR_OPEN]"));
|
||||
assertTrue(yaml.contains("photos: { type: array, minItems: 3, maxItems: 3"));
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ class PdaPalletOperationControllerContractTest {
|
||||
RequestMapping mapping = PdaPalletOperationController.class.getAnnotation(RequestMapping.class);
|
||||
assertArrayEquals(new String[]{"/api/pda/v1/last-leg/pallet-operations"}, mapping.value());
|
||||
PreAuthorize authorize = PdaPalletOperationController.class.getAnnotation(PreAuthorize.class);
|
||||
assertEquals("hasAuthority('lastleg:pda:operate') and hasAuthority('ROLE_WAREHOUSE_PICKER')", authorize.value());
|
||||
assertEquals("hasAuthority('lastleg:pda:operate') and hasAuthority('ROLE_OVERSEAS_WAREHOUSE_PDA_OPERATOR')", authorize.value());
|
||||
Set<String> paths = Arrays.stream(PdaPalletOperationController.class.getDeclaredMethods())
|
||||
.map(Method::getDeclaredAnnotations).flatMap(Arrays::stream)
|
||||
.filter(PostMapping.class::isInstance).map(PostMapping.class::cast)
|
||||
|
||||
+2
-2
@@ -25,9 +25,9 @@ class PdaInventoryOperationContractTest {
|
||||
assertThat(PdaInventoryMoveController.class.getAnnotation(RequestMapping.class).value())
|
||||
.containsExactly("/api/pda/v1/last-leg/inventory-moves");
|
||||
assertThat(PdaStocktakeController.class.getAnnotation(PreAuthorize.class).value())
|
||||
.isEqualTo("hasAuthority('lastleg:pda:operate') and hasAuthority('ROLE_WAREHOUSE_PICKER')");
|
||||
.isEqualTo("hasAuthority('lastleg:pda:operate') and hasAuthority('ROLE_OVERSEAS_WAREHOUSE_PDA_OPERATOR')");
|
||||
assertThat(PdaInventoryMoveController.class.getAnnotation(PreAuthorize.class).value())
|
||||
.isEqualTo("hasAuthority('lastleg:pda:operate') and hasAuthority('ROLE_WAREHOUSE_PICKER')");
|
||||
.isEqualTo("hasAuthority('lastleg:pda:operate') and hasAuthority('ROLE_OVERSEAS_WAREHOUSE_PDA_OPERATOR')");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ class PdaWarehouseContractTest {
|
||||
void controllerRequiresPickerRoleAndPdaAuthority() {
|
||||
PreAuthorize permission = PdaWarehouseController.class.getAnnotation(PreAuthorize.class);
|
||||
assertThat(permission.value()).isEqualTo(
|
||||
"hasAuthority('lastleg:pda:operate') and hasAuthority('ROLE_WAREHOUSE_PICKER')");
|
||||
"hasAuthority('lastleg:pda:operate') and hasAuthority('ROLE_OVERSEAS_WAREHOUSE_PDA_OPERATOR')");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+2
-2
@@ -43,12 +43,12 @@ class PdaRelabelContractTest {
|
||||
|
||||
@Test
|
||||
void identityRequiresPlatformPickerPermissionAndFrozenWarehouse() {
|
||||
AuthenticatedUser valid = user(Set.of("lastleg:pda:operate", "ROLE_WAREHOUSE_PICKER"), 900L);
|
||||
AuthenticatedUser valid = user(Set.of("lastleg:pda:operate", "ROLE_OVERSEAS_WAREHOUSE_PDA_OPERATOR"), 900L);
|
||||
assertEquals(900L, PdaRelabelController.identity(valid).overseasWarehouseId());
|
||||
assertThrows(ResponseStatusException.class,
|
||||
() -> PdaRelabelController.identity(user(Set.of("lastleg:pda:operate"), 900L)));
|
||||
assertThrows(ResponseStatusException.class,
|
||||
() -> PdaRelabelController.identity(user(Set.of("lastleg:pda:operate", "ROLE_WAREHOUSE_PICKER"), null)));
|
||||
() -> PdaRelabelController.identity(user(Set.of("lastleg:pda:operate", "ROLE_OVERSEAS_WAREHOUSE_PDA_OPERATOR"), null)));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -84,7 +84,7 @@ export async function run(context) {
|
||||
|
||||
表中的 `*-fixture.mjs` 是被业务模块复用的测试数据前置,不是新增的独立全量入口。执行 HTTP 时仍从 `../admin-test-env-acceptance.mjs` 进入;`*contract.test.mjs` 只做离线脚本结构和数据归属回归。
|
||||
|
||||
四期 HTTP 验收使用国内登录响应同时签发的短时海外 Token。总入口通过 `--gateway` 访问国内 Gateway,通过 `--overseas-gateway` 访问海外 Gateway。海外仓、海外业务和海外主数据模块只对带本轮 marker 的资源执行已确认的新增、编辑、状态与导出,并在结束前按契约停用;PDA 收货模块使用本轮临时 WAREHOUSE_PICKER 海外 Token,只调用 `/api/pda/v1/last-leg/receiving-containers/**` 柜号收货入口,不接受 `customerId`、`planNo` 或分配 ID。收货成功后由相邻的 `phase4-overseas-pda-operations` 消费仅存在于进程内存、不会进入 JSON 报告的 Token 与库存事实,执行盘点、整托移库、托盘操作和换标;新模块无论成功失败都在 finally 停用两个 picker 并清空临时凭据。两模块均不调用出库、退货出库、派送或内部接口;任何真实柜、箱唛、托盘、库存或换标前置缺失都必须形成 deployment blocker,不能按空场景通过;船节点禁止手工创建任务。
|
||||
四期 HTTP 验收使用国内登录响应同时签发的短时海外 Token。总入口通过 `--gateway` 访问国内 Gateway,通过 `--overseas-gateway` 访问海外 Gateway。海外仓、海外业务和海外主数据模块只对带本轮 marker 的资源执行已确认的新增、编辑、状态与导出,并在结束前按契约停用;PDA 收货模块使用本轮临时 OVERSEAS_WAREHOUSE_PDA_OPERATOR 海外 Token,只调用 `/api/pda/v1/last-leg/receiving-containers/**` 柜号收货入口,不接受 `customerId`、`planNo` 或分配 ID。收货成功后由相邻的 `phase4-overseas-pda-operations` 消费仅存在于进程内存、不会进入 JSON 报告的 Token 与库存事实,执行盘点、整托移库、托盘操作和换标;新模块无论成功失败都在 finally 停用两个 picker 并清空临时凭据。两模块均不调用出库、退货出库、派送或内部接口;任何真实柜、箱唛、托盘、库存或换标前置缺失都必须形成 deployment blocker,不能按空场景通过;船节点禁止手工创建任务。
|
||||
|
||||
节点全链定向入口为 `--modules phase4-overseas-pda`。依赖展开固定经过本轮 `LOADED` 柜次的 `DESTINATION_CLEARANCE`、`CUSTOMS_DECLARATION`、`DELIVERY_ORDER`、`PICKUP_ORDER` 四类资料生命周期,再确认国内出仓、核对自动创建的 `OUTBOUND_CONFIRM` 船节点任务、人工补录至实际 `PICKUP_READY`,最后验证海外 PDA 候选可见。四类资料模块只写本轮 marker 柜次,覆盖上传/完成重放、同键异载荷 `A0409`、冲突后零漂移和聚合回读;任一 fixture 或 Trace 缺失均为 deployment blocker。
|
||||
|
||||
|
||||
@@ -36,22 +36,30 @@ export async function run(context) {
|
||||
const rolesResponse = await context.request('GET', '/api/platform/v1/iam/roles')
|
||||
const roles = context.apiSuccess(MODULE, '查询三期仓库内置角色', rolesResponse, '', OPTIONS)
|
||||
const warehouseRoles = new Map((Array.isArray(roles) ? roles : [])
|
||||
.filter((item) => ['WAREHOUSE_OPERATOR', 'WAREHOUSE_PICKER', 'WAREHOUSE_TALLY'].includes(item.roleCode))
|
||||
.filter((item) => ['WAREHOUSE_OPERATOR', 'OVERSEAS_WAREHOUSE_PDA_OPERATOR', 'WAREHOUSE_TALLY', 'CN_FIRSTLEG_PDA_OPERATOR'].includes(item.roleCode))
|
||||
.map((item) => [item.roleCode, item]))
|
||||
context.assert(MODULE, '操作员、拣货员、理货员三个系统角色已启用', rolesResponse,
|
||||
warehouseRoles.size === 3 && [...warehouseRoles.values()]
|
||||
context.assert(MODULE, '国内后台、国内PDA、海外PDA、理货复核四个系统角色已启用', rolesResponse,
|
||||
warehouseRoles.size === 4 && [...warehouseRoles.values()]
|
||||
.every((role) => role.roleType === 'SYSTEM' && role.status === 'ACTIVE'),
|
||||
`roles=${[...warehouseRoles.keys()].join(',')}`, OPTIONS)
|
||||
|
||||
const roleCodes = (roleCode) => new Set((warehouseRoles.get(roleCode)?.permissionIds || [])
|
||||
.map((permissionId) => permissionById.get(id(permissionId))).filter(Boolean))
|
||||
const pickerCodes = roleCodes('WAREHOUSE_PICKER')
|
||||
context.assert(MODULE, '拣货员只具备国内PDA查询作业与海外PDA入仓操作权限', rolesResponse,
|
||||
pickerCodes.size === 3
|
||||
&& pickerCodes.has('platform:firstleg:pda:view')
|
||||
&& pickerCodes.has('platform:firstleg:pda:work')
|
||||
&& pickerCodes.has('lastleg:pda:operate'),
|
||||
`permissions=${[...pickerCodes].sort().join(',')}`, OPTIONS)
|
||||
const domesticPdaCodes = roleCodes('CN_FIRSTLEG_PDA_OPERATOR')
|
||||
context.assert(MODULE, '国内头程PDA角色只具备国内任务查询与作业权限', rolesResponse,
|
||||
domesticPdaCodes.size === 2
|
||||
&& domesticPdaCodes.has('platform:firstleg:pda:view')
|
||||
&& domesticPdaCodes.has('platform:firstleg:pda:work')
|
||||
&& !domesticPdaCodes.has('lastleg:pda:operate'),
|
||||
`permissions=${[...domesticPdaCodes].sort().join(',')}`, OPTIONS)
|
||||
|
||||
const overseasPdaCodes = roleCodes('OVERSEAS_WAREHOUSE_PDA_OPERATOR')
|
||||
context.assert(MODULE, '海外仓PDA角色只具备海外PDA作业权限', rolesResponse,
|
||||
overseasPdaCodes.size === 1
|
||||
&& overseasPdaCodes.has('lastleg:pda:operate')
|
||||
&& !overseasPdaCodes.has('platform:firstleg:pda:view')
|
||||
&& !overseasPdaCodes.has('platform:firstleg:pda:work'),
|
||||
`permissions=${[...overseasPdaCodes].sort().join(',')}`, OPTIONS)
|
||||
|
||||
const operatorCodes = roleCodes('WAREHOUSE_OPERATOR')
|
||||
context.assert(MODULE, '操作员具备国内入仓权限与四期十项只读权限', rolesResponse,
|
||||
@@ -88,7 +96,7 @@ export async function run(context) {
|
||||
`phase4ReadCodes=${PHASE4_READ_CODES.length}`, OPTIONS)
|
||||
|
||||
context.assert(MODULE, '仓库系统角色不包含任何客户侧权限', rolesResponse,
|
||||
[...operatorCodes, ...pickerCodes, ...tallyCodes]
|
||||
[...operatorCodes, ...domesticPdaCodes, ...overseasPdaCodes, ...tallyCodes]
|
||||
.every((code) => code.startsWith('platform:') || code === 'lastleg:pda:operate'),
|
||||
'warehouse roles may use platform:* plus the single overseas service authority lastleg:pda:operate', OPTIONS)
|
||||
|
||||
|
||||
@@ -32,10 +32,11 @@ test('warehouse operator and tally use the exact phase-four list/detail permissi
|
||||
assert.match(iamSql, /permission_code REGEXP ':\(create\|edit\|status\|export\|adjust\|delete\|review\)\$'/)
|
||||
})
|
||||
|
||||
test('picker stays isolated to the dedicated overseas PDA authority', () => {
|
||||
assert.match(acceptance, /pickerCodes\.size === 3/)
|
||||
assert.match(acceptance, /pickerCodes\.has\('lastleg:pda:operate'\)/)
|
||||
assert.match(iamSql, /role\.role_code='WAREHOUSE_PICKER'/)
|
||||
test('domestic and overseas PDA operators use isolated authorities', () => {
|
||||
assert.match(acceptance, /domesticPdaCodes\.size === 2/)
|
||||
assert.match(acceptance, /overseasPdaCodes\.size === 1/)
|
||||
assert.match(acceptance, /overseasPdaCodes\.has\('lastleg:pda:operate'\)/)
|
||||
assert.match(iamSql, /role\.role_code='OVERSEAS_WAREHOUSE_PDA_OPERATOR'/)
|
||||
assert.match(iamSql, /permission\.permission_code='lastleg:pda:operate'/)
|
||||
})
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ test('PDA module covers the container-number receiving contract and excludes out
|
||||
assert.match(source, /同键异载荷冲突/)
|
||||
assert.match(source, /跨柜切换锁返回409/)
|
||||
assert.match(source, /第二仓库操作员抢占同柜返回409/)
|
||||
assert.match(source, /WAREHOUSE_PICKER/)
|
||||
assert.match(source, /OVERSEAS_WAREHOUSE_PDA_OPERATOR/)
|
||||
assert.match(source, /lastleg:pda:operate/)
|
||||
assert.match(source, /安全投影已收敛/)
|
||||
assert.match(source, /projectionReady = success\(projectionResponse\) \|\| notFound\(projectionResponse\)/)
|
||||
@@ -157,7 +157,7 @@ test('PDA receiving lifecycle is ordered from candidate claim through photos, mo
|
||||
]) assert.ok(source.includes(token), token)
|
||||
})
|
||||
|
||||
test('missing WAREHOUSE_PICKER overseas authority is a deployment blocker', async () => {
|
||||
test('missing OVERSEAS_WAREHOUSE_PDA_OPERATOR overseas authority is a deployment blocker', async () => {
|
||||
const calls = []; const gaps = []; const steps = []
|
||||
const context = {
|
||||
marker: 'PDA-CONTRACT', context: {}, createdResources: {},
|
||||
@@ -192,7 +192,7 @@ test('empty candidate response cannot become a zero-business-request pass', asyn
|
||||
async request(method, requestPath, body) {
|
||||
calls.push({ channel: 'domestic', method, requestPath })
|
||||
if (method === 'GET' && requestPath.endsWith('/roles')) return response([
|
||||
{ id: roleId, roleCode: 'WAREHOUSE_PICKER', status: 'ACTIVE', permissionIds: [permissionId] },
|
||||
{ id: roleId, roleCode: 'OVERSEAS_WAREHOUSE_PDA_OPERATOR', status: 'ACTIVE', permissionIds: [permissionId] },
|
||||
], requestPath, method)
|
||||
if (method === 'GET' && requestPath.endsWith('/permissions')) return response([
|
||||
{ id: permissionId, permissionCode: 'lastleg:pda:operate' },
|
||||
|
||||
@@ -90,15 +90,15 @@ async function loginMerchantForNegativeProbe(context) {
|
||||
async function resolvePickerAccess(context) {
|
||||
const rolesResponse = await context.request('GET', '/api/platform/v1/iam/roles')
|
||||
const roles = context.apiSuccess(MODULE, '查询海外PDA仓库操作员角色', rolesResponse, '', OPTIONS)
|
||||
const role = (Array.isArray(roles) ? roles : []).find((item) => item?.roleCode === 'WAREHOUSE_PICKER' && item?.status === 'ACTIVE')
|
||||
const role = (Array.isArray(roles) ? roles : []).find((item) => item?.roleCode === 'OVERSEAS_WAREHOUSE_PDA_OPERATOR' && item?.status === 'ACTIVE')
|
||||
const permissionsResponse = await context.request('GET', '/api/platform/v1/iam/permissions')
|
||||
const permissions = context.apiSuccess(MODULE, '查询海外PDA权限资源', permissionsResponse, '', OPTIONS)
|
||||
const operate = (Array.isArray(permissions) ? permissions : []).find((item) => item?.permissionCode === 'lastleg:pda:operate')
|
||||
const assigned = Boolean(role && operate && (role.permissionIds || []).map(id).includes(id(operate.id)))
|
||||
context.assert(MODULE, 'WAREHOUSE_PICKER精确具备海外PDA权限', rolesResponse, assigned,
|
||||
context.assert(MODULE, 'OVERSEAS_WAREHOUSE_PDA_OPERATOR精确具备海外PDA权限', rolesResponse, assigned,
|
||||
`roleId=${id(role?.id) || 'missing'}, permissionId=${id(operate?.id) || 'missing'}`, OPTIONS)
|
||||
if (!assigned) {
|
||||
blockerGap(context, '海外PDA仓库操作员权限', 'WAREHOUSE_PICKER未获得lastleg:pda:operate;必须先执行最新国内统一增量SQL并部署IAM')
|
||||
blockerGap(context, '海外PDA仓库操作员权限', 'OVERSEAS_WAREHOUSE_PDA_OPERATOR未获得lastleg:pda:operate;必须先执行最新国内统一增量SQL并部署IAM')
|
||||
return null
|
||||
}
|
||||
return { role, operate }
|
||||
@@ -662,7 +662,7 @@ export async function run(context) {
|
||||
}
|
||||
try {
|
||||
if (!primary?.token || !secondary?.token) {
|
||||
blockerGap(context, '海外PDA双操作员账号', '未能创建并登录两个WAREHOUSE_PICKER平台账号,无法验证全局一柜锁')
|
||||
blockerGap(context, '海外PDA双操作员账号', '未能创建并登录两个OVERSEAS_WAREHOUSE_PDA_OPERATOR平台账号,无法验证全局一柜锁')
|
||||
return
|
||||
}
|
||||
const matrix = context.createdResources?.phase4InboundProducerFixtures
|
||||
|
||||
Reference in New Issue
Block a user