feat(iam): split domestic and overseas container permissions

This commit is contained in:
2026-09-08 19:11:29 +08:00
parent b0c81e337d
commit 19d9a89651
4 changed files with 258 additions and 1 deletions
@@ -0,0 +1,139 @@
-- 国内送柜报关与国外清关提柜双工作台权限;国内沿用现有货柜权限,国外直接收敛到独立权限族。
USE baiyue_iam;
SET NAMES utf8mb4;
DROP PROCEDURE IF EXISTS migrate_container_workspace_permissions;
DELIMITER $$
CREATE PROCEDURE migrate_container_workspace_permissions()
BEGIN
IF EXISTS (
SELECT 1 FROM iam_permission
WHERE (id = 5102090 AND permission_code <> 'platform:firstleg:container:list')
OR (id = 5102092 AND permission_code <> 'platform:firstleg:container:create')
OR (id = 5102093 AND permission_code <> 'platform:firstleg:container:edit')
OR (id = 5102200 AND permission_code <> 'platform:firstleg:terminal-drayage:list')
OR (id = 5102201 AND permission_code <> 'platform:firstleg:terminal-drayage:document:edit')
OR (id = 5102202 AND permission_code <> 'platform:firstleg:terminal-drayage:fleet:edit')
OR (id = 5102203 AND permission_code <> 'platform:firstleg:terminal-drayage:pickup-appointment:edit')
OR (id = 5102204 AND permission_code <> 'platform:firstleg:terminal-drayage:pickup:confirm')
OR (id = 5102205 AND permission_code <> 'platform:firstleg:terminal-drayage:empty-return:confirm')
) OR EXISTS (
SELECT 1 FROM iam_permission
WHERE (permission_code = 'platform:firstleg:container:list' AND id <> 5102090)
OR (permission_code = 'platform:firstleg:container:create' AND id <> 5102092)
OR (permission_code = 'platform:firstleg:container:edit' AND id <> 5102093)
OR (permission_code = 'platform:firstleg:terminal-drayage:list' AND id <> 5102200)
OR (permission_code = 'platform:firstleg:terminal-drayage:document:edit' AND id <> 5102201)
OR (permission_code = 'platform:firstleg:terminal-drayage:fleet:edit' AND id <> 5102202)
OR (permission_code = 'platform:firstleg:terminal-drayage:pickup-appointment:edit' AND id <> 5102203)
OR (permission_code = 'platform:firstleg:terminal-drayage:pickup:confirm' AND id <> 5102204)
OR (permission_code = 'platform:firstleg:terminal-drayage:empty-return:confirm' AND id <> 5102205)
) THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='货柜双工作台权限初始化失败:权限ID或编码冲突';
END IF;
INSERT INTO iam_permission
(id, tenant_id, app_code, permission_type, parent_id, permission_code,
permission_name, route_path, http_method, sort_no, status, built_in, deleted)
VALUES
(5102090,0,'PLATFORM','MENU',5102000,'platform:firstleg:container:list',
'国内送柜报关','/container-loading/domestic','GET',40,'ACTIVE',1,0),
(5102092,0,'PLATFORM','BUTTON',5102090,'platform:firstleg:container:create',
'建立国内货柜',NULL,'POST',20,'ACTIVE',1,0),
(5102093,0,'PLATFORM','BUTTON',5102090,'platform:firstleg:container:edit',
'维护国内货柜资料',NULL,'PUT',30,'ACTIVE',1,0),
(5102200,0,'PLATFORM','MENU',5102000,'platform:firstleg:terminal-drayage:list',
'国外清关提柜','/container-loading/overseas','GET',41,'ACTIVE',1,0),
(5102201,0,'PLATFORM','BUTTON',5102200,'platform:firstleg:terminal-drayage:document:edit',
'维护目的港清关与DO/PU',NULL,'POST',10,'ACTIVE',1,0),
(5102202,0,'PLATFORM','BUTTON',5102200,'platform:firstleg:terminal-drayage:fleet:edit',
'维护目的港码头车队',NULL,'POST',20,'ACTIVE',1,0),
(5102203,0,'PLATFORM','BUTTON',5102200,'platform:firstleg:terminal-drayage:pickup-appointment:edit',
'预约目的港提柜',NULL,'POST',30,'ACTIVE',1,0),
(5102204,0,'PLATFORM','BUTTON',5102200,'platform:firstleg:terminal-drayage:pickup:confirm',
'确认目的港实际提柜',NULL,'POST',40,'ACTIVE',1,0),
(5102205,0,'PLATFORM','BUTTON',5102200,'platform:firstleg:terminal-drayage:empty-return:confirm',
'确认目的港实际还柜',NULL,'POST',50,'ACTIVE',1,0)
ON DUPLICATE KEY UPDATE
permission_type=VALUES(permission_type), parent_id=VALUES(parent_id),
permission_name=VALUES(permission_name), route_path=VALUES(route_path),
http_method=VALUES(http_method), sort_no=VALUES(sort_no), status='ACTIVE',
built_in=1, deleted=0;
-- list 已包含详情只读;旧 container:view 不再作为别名或双权限入口。
DELETE grant_row FROM iam_role_permission grant_row
JOIN iam_permission permission
ON permission.id=grant_row.permission_id AND permission.tenant_id=grant_row.tenant_id
WHERE permission.tenant_id=0
AND permission.permission_code='platform:firstleg:container:view';
UPDATE iam_permission
SET status='INACTIVE', deleted=1
WHERE tenant_id=0 AND permission_code='platform:firstleg:container:view';
-- 平台超级管理员和现有订单运营保持货柜全链路能力;其他角色按两个菜单独立授权。
INSERT IGNORE INTO iam_role_permission (tenant_id, role_id, permission_id, created_at)
SELECT 0, role.id, permission.id, CURRENT_TIMESTAMP(3)
FROM iam_role role
JOIN iam_permission permission
ON permission.tenant_id=0 AND permission.app_code='PLATFORM'
AND permission.permission_code IN (
'platform:firstleg:container:list',
'platform:firstleg:container:create',
'platform:firstleg:container:edit',
'platform:firstleg:terminal-drayage:list',
'platform:firstleg:terminal-drayage:document:edit',
'platform:firstleg:terminal-drayage:fleet:edit',
'platform:firstleg:terminal-drayage:pickup-appointment:edit',
'platform:firstleg:terminal-drayage:pickup:confirm',
'platform:firstleg:terminal-drayage:empty-return:confirm')
WHERE role.tenant_id=0 AND role.app_code='PLATFORM'
AND role.role_code IN ('PLATFORM_ADMIN','ORDER_OPERATOR')
AND role.status='ACTIVE' AND role.deleted=0
AND permission.status='ACTIVE' AND permission.deleted=0;
IF (SELECT COUNT(*) FROM iam_permission
WHERE tenant_id=0 AND app_code='PLATFORM' AND status='ACTIVE' AND deleted=0
AND permission_code IN (
'platform:firstleg:container:list',
'platform:firstleg:container:create',
'platform:firstleg:container:edit',
'platform:firstleg:terminal-drayage:list',
'platform:firstleg:terminal-drayage:document:edit',
'platform:firstleg:terminal-drayage:fleet:edit',
'platform:firstleg:terminal-drayage:pickup-appointment:edit',
'platform:firstleg:terminal-drayage:pickup:confirm',
'platform:firstleg:terminal-drayage:empty-return:confirm')) <> 9 THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='货柜双工作台权限初始化失败:权限目录不完整';
END IF;
IF (SELECT COUNT(DISTINCT CONCAT(role.role_code, ':', permission.permission_code))
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 role.tenant_id=0 AND role.app_code='PLATFORM'
AND role.role_code IN ('PLATFORM_ADMIN','ORDER_OPERATOR')
AND permission.permission_code IN (
'platform:firstleg:container:list',
'platform:firstleg:container:create',
'platform:firstleg:container:edit',
'platform:firstleg:terminal-drayage:list',
'platform:firstleg:terminal-drayage:document:edit',
'platform:firstleg:terminal-drayage:fleet:edit',
'platform:firstleg:terminal-drayage:pickup-appointment:edit',
'platform:firstleg:terminal-drayage:pickup:confirm',
'platform:firstleg:terminal-drayage:empty-return:confirm')) <> 18 THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='货柜双工作台权限初始化失败:平台默认角色授权不完整';
END IF;
IF EXISTS (SELECT 1 FROM iam_permission
WHERE tenant_id=0 AND permission_code='platform:firstleg:container:view'
AND (status='ACTIVE' OR deleted=0)) THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='货柜双工作台权限初始化失败:旧查看权限仍然有效';
END IF;
END$$
DELIMITER ;
CALL migrate_container_workspace_permissions();
DROP PROCEDURE IF EXISTS migrate_container_workspace_permissions;
+1 -1
View File
@@ -124,7 +124,7 @@ paths:
get:
tags: [IAM Resources / IAM 资源]
summary: 查询当前用户有效权限
description: 返回当前有效平台成员权限编码;只要求有效 PLATFORM JWT,不依赖 `platform:role:list`。入库单扣货与解除扣货返回独立写权限 `platform:firstleg:inbound:hold`,不会由 `platform:firstleg:inbound:list` 查看权限自动获得。资金账户充值审核与手工调账分别返回 `platform:finance:recharge:review`、`platform:finance:customer-account:adjust`,并与 `platform:finance:customer-account:list` 只读权限分离。海外仓返回 `platform:overseas-warehouse:dispatch:list`、`platform:overseas-warehouse:dispatch:edit`、`platform:overseas-warehouse:dispatch:cancel`、`platform:overseas-warehouse:dispatch:transfer-delivery`、`platform:overseas-warehouse:outbound:list`、`platform:overseas-warehouse:outbound:create`、`platform:overseas-warehouse:outbound:cancel`、`platform:overseas-warehouse:delivery:list`、`platform:overseas-warehouse:delivery:return`、`platform:overseas-warehouse:delivery:status`、`platform:overseas-warehouse:exception-order:list`、`platform:overseas-warehouse:exception-order:create`、`platform:overseas-warehouse:exception-order:process`、`platform:overseas-warehouse:customer-visit:list`、`platform:overseas-warehouse:customer-visit:edit`;海外 PDA 盘点、移库、换标和现场查询统一返回 `lastleg:pda:operate`,且仅限 `OVERSEAS_WAREHOUSE_PDA_OPERATOR`。列表权限包含详情和只读辅助接口,写操作独立授权。
description: 返回当前有效平台成员权限编码;只要求有效 PLATFORM JWT,不依赖 `platform:role:list`。入库单扣货与解除扣货返回独立写权限 `platform:firstleg:inbound:hold`,不会由 `platform:firstleg:inbound:list` 查看权限自动获得。资金账户充值审核与手工调账分别返回 `platform:finance:recharge:review`、`platform:finance:customer-account:adjust`,并与 `platform:finance:customer-account:list` 只读权限分离。海外仓返回 `platform:overseas-warehouse:dispatch:list`、`platform:overseas-warehouse:dispatch:edit`、`platform:overseas-warehouse:dispatch:cancel`、`platform:overseas-warehouse:dispatch:transfer-delivery`、`platform:overseas-warehouse:outbound:list`、`platform:overseas-warehouse:outbound:create`、`platform:overseas-warehouse:outbound:cancel`、`platform:overseas-warehouse:delivery:list`、`platform:overseas-warehouse:delivery:return`、`platform:overseas-warehouse:delivery:status`、`platform:overseas-warehouse:exception-order:list`、`platform:overseas-warehouse:exception-order:create`、`platform:overseas-warehouse:exception-order:process`、`platform:overseas-warehouse:customer-visit:list`、`platform:overseas-warehouse:customer-visit:edit`;海外 PDA 盘点、移库、换标和现场查询统一返回 `lastleg:pda:operate`,且仅限 `OVERSEAS_WAREHOUSE_PDA_OPERATOR`。货柜双工作台以 `platform:firstleg:container:list/create/edit` 维护国内送柜报关,国外清关提柜独立返回 `platform:firstleg:terminal-drayage:list`、`platform:firstleg:terminal-drayage:document:edit`、`platform:firstleg:terminal-drayage:fleet:edit`、`platform:firstleg:terminal-drayage:pickup-appointment:edit`、`platform:firstleg:terminal-drayage:pickup:confirm`、`platform:firstleg:terminal-drayage:empty-return:confirm`,不以 `container:list/edit` 兼容放行国外操作。列表权限包含详情和只读辅助接口,写操作独立授权。
responses:
'200':
description: 当前平台成员权限查询成功
@@ -35,6 +35,14 @@ class IamOpenApiContractTest {
.contains("platform:overseas-warehouse:customer-visit:list")
.contains("platform:overseas-warehouse:customer-visit:edit")
.contains("platform:overseas-warehouse:outbound:cancel")
.contains("platform:firstleg:container:list/create/edit")
.contains("platform:firstleg:terminal-drayage:list")
.contains("platform:firstleg:terminal-drayage:document:edit")
.contains("platform:firstleg:terminal-drayage:fleet:edit")
.contains("platform:firstleg:terminal-drayage:pickup-appointment:edit")
.contains("platform:firstleg:terminal-drayage:pickup:confirm")
.contains("platform:firstleg:terminal-drayage:empty-return:confirm")
.contains("不以 `container:list/edit` 兼容放行国外操作")
.contains("merchant:warehouse:delivery:list")
.contains("merchant:warehouse:delivery:create")
.contains("merchant:cross-border:procurement")
@@ -0,0 +1,110 @@
package com.baiyue.iam.repository;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassPathResource;
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.regex.Pattern;
import static org.assertj.core.api.Assertions.assertThat;
class ContainerWorkspacePermissionSqlContractTest {
private static final Pattern CODE = Pattern.compile("platform:firstleg:(?:container|terminal-drayage):[a-z0-9:-]+");
private static final Set<String> REQUIRED = Set.of(
"platform:firstleg:container:list",
"platform:firstleg:container:create",
"platform:firstleg:container:edit",
"platform:firstleg:terminal-drayage:list",
"platform:firstleg:terminal-drayage:document:edit",
"platform:firstleg:terminal-drayage:fleet:edit",
"platform:firstleg:terminal-drayage:pickup-appointment:edit",
"platform:firstleg:terminal-drayage:pickup:confirm",
"platform:firstleg:terminal-drayage:empty-return:confirm");
@Test
void definesSeparateDomesticAndOverseasMenusWithoutAliases() throws Exception {
String sql = sql();
assertThat(codes(catalog(sql))).containsExactlyInAnyOrderElementsOf(REQUIRED);
assertThat(sql).contains(
"'platform:firstleg:container:list',\n '国内送柜报关','/container-loading/domestic','GET'",
"'platform:firstleg:terminal-drayage:list',\n '国外清关提柜','/container-loading/overseas','GET'",
"permission.permission_code='platform:firstleg:container:view'",
"SET status='INACTIVE', deleted=1");
assertThat(codes(catalog(sql))).noneMatch(code -> code.endsWith(":view"));
assertThat(sql).doesNotContain(
"platform:firstleg:domestic-container:",
"platform:firstleg:overseas-container:",
"platform:firstleg:terminal-drayage:edit");
}
@Test
void keepsDomesticWritesAndDefinesFineGrainedOverseasWrites() throws Exception {
assertThat(sql()).contains(
"'platform:firstleg:container:create',\n '建立国内货柜',NULL,'POST'",
"'platform:firstleg:container:edit',\n '维护国内货柜资料',NULL,'PUT'",
"'platform:firstleg:terminal-drayage:document:edit',\n '维护目的港清关与DO/PU',NULL,'POST'",
"'platform:firstleg:terminal-drayage:fleet:edit',\n '维护目的港码头车队',NULL,'POST'",
"'platform:firstleg:terminal-drayage:pickup-appointment:edit',\n '预约目的港提柜',NULL,'POST'",
"'platform:firstleg:terminal-drayage:pickup:confirm',\n '确认目的港实际提柜',NULL,'POST'",
"'platform:firstleg:terminal-drayage:empty-return:confirm',\n '确认目的港实际还柜',NULL,'POST'");
}
@Test
void grantsTheCompleteMatrixOnlyToTheExistingDefaultPlatformRoles() throws Exception {
String sql = sql();
int grantStart = sql.indexOf("-- 平台超级管理员和现有订单运营保持货柜全链路能力");
int assertionStart = sql.indexOf("IF (SELECT COUNT(*) FROM iam_permission", grantStart);
String grant = sql.substring(grantStart, assertionStart);
assertThat(grant).contains(
"role.role_code IN ('PLATFORM_ADMIN','ORDER_OPERATOR')",
"platform:firstleg:container:list",
"platform:firstleg:terminal-drayage:list",
"platform:firstleg:terminal-drayage:empty-return:confirm");
assertThat(grant).doesNotContain(
"PLATFORM_CUSTOM_ADMIN",
"WAREHOUSE_OPERATOR",
"WAREHOUSE_TALLY",
"OVERSEAS_WAREHOUSE_PDA_OPERATOR",
"CN_FIRSTLEG_PDA_OPERATOR",
"MERCHANT_OWNER");
assertThat(sql).contains(
"permission_code IN (",
")) <> 9 THEN",
")) <> 18 THEN");
}
@Test
void failsClosedForFixedIdentityConflictsAndRemainsReplaySafe() throws Exception {
assertThat(sql()).contains(
"migrate_container_workspace_permissions",
"id = 5102090 AND permission_code <> 'platform:firstleg:container:list'",
"id = 5102205 AND permission_code <> 'platform:firstleg:terminal-drayage:empty-return:confirm'",
"permission_code = 'platform:firstleg:terminal-drayage:list' AND id <> 5102200",
"SIGNAL SQLSTATE '45000'",
"ON DUPLICATE KEY UPDATE",
"INSERT IGNORE INTO iam_role_permission");
}
private static String sql() throws Exception {
return new ClassPathResource("db/container-workspace-permissions.sql")
.getContentAsString(StandardCharsets.UTF_8);
}
private static Set<String> codes(String source) {
Set<String> values = new LinkedHashSet<>();
CODE.matcher(source).results().map(result -> result.group()).forEach(values::add);
return values;
}
private static String catalog(String sql) {
int start = sql.indexOf("INSERT INTO iam_permission");
int end = sql.indexOf("ON DUPLICATE KEY UPDATE", start);
assertThat(start).isGreaterThanOrEqualTo(0);
assertThat(end).isGreaterThan(start);
return sql.substring(start, end);
}
}