diff --git a/iam/src/main/resources/db/container-workspace-permissions.sql b/iam/src/main/resources/db/container-workspace-permissions.sql new file mode 100644 index 000000000..57b15c1fd --- /dev/null +++ b/iam/src/main/resources/db/container-workspace-permissions.sql @@ -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; diff --git a/iam/src/main/resources/openapi/iam-v1.yaml b/iam/src/main/resources/openapi/iam-v1.yaml index 1260e76f6..a7783983a 100644 --- a/iam/src/main/resources/openapi/iam-v1.yaml +++ b/iam/src/main/resources/openapi/iam-v1.yaml @@ -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: 当前平台成员权限查询成功 diff --git a/iam/src/test/java/com/baiyue/iam/controller/IamOpenApiContractTest.java b/iam/src/test/java/com/baiyue/iam/controller/IamOpenApiContractTest.java index b7a4f5438..84116ee8f 100644 --- a/iam/src/test/java/com/baiyue/iam/controller/IamOpenApiContractTest.java +++ b/iam/src/test/java/com/baiyue/iam/controller/IamOpenApiContractTest.java @@ -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") diff --git a/iam/src/test/java/com/baiyue/iam/repository/ContainerWorkspacePermissionSqlContractTest.java b/iam/src/test/java/com/baiyue/iam/repository/ContainerWorkspacePermissionSqlContractTest.java new file mode 100644 index 000000000..c1864ab61 --- /dev/null +++ b/iam/src/test/java/com/baiyue/iam/repository/ContainerWorkspacePermissionSqlContractTest.java @@ -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 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 codes(String source) { + Set 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); + } +}