From 1200ef54ad4902d541032a8f3a83d247feaef9e2 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Mon, 10 Aug 2026 11:54:58 +0800 Subject: [PATCH] =?UTF-8?q?feat(iam):=20=E5=88=9D=E5=A7=8B=E5=8C=96?= =?UTF-8?q?=E4=B8=89=E6=9C=9F=E4=BB=93=E5=BA=93=E4=BD=9C=E4=B8=9A=E8=A7=92?= =?UTF-8?q?=E8=89=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/db/phase3-warehouse-roles.sql | 249 ++++++++++++++++++ iam/src/main/resources/openapi/iam-v1.yaml | 4 +- .../WarehouseRoleSqlContractTest.java | 66 +++++ 3 files changed, 317 insertions(+), 2 deletions(-) create mode 100644 iam/src/main/resources/db/phase3-warehouse-roles.sql create mode 100644 iam/src/test/java/com/baiyue/iam/repository/WarehouseRoleSqlContractTest.java diff --git a/iam/src/main/resources/db/phase3-warehouse-roles.sql b/iam/src/main/resources/db/phase3-warehouse-roles.sql new file mode 100644 index 000000000..95e2c1416 --- /dev/null +++ b/iam/src/main/resources/db/phase3-warehouse-roles.sql @@ -0,0 +1,249 @@ +-- 三期仓库常用系统角色:仓库操作员、仓库拣货员、仓库理货员。 +-- 本脚本只组合已经存在的权限资源,需在三期 Firstleg/IAM 权限脚本之后执行,可重复部署。 +USE baiyue_iam; +SET NAMES utf8mb4; + +DROP PROCEDURE IF EXISTS assert_phase3_warehouse_role_ids_available; +DELIMITER $$ +CREATE PROCEDURE assert_phase3_warehouse_role_ids_available() +BEGIN + IF EXISTS ( + 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 = 4000006 AND (tenant_id <> 0 OR app_code <> 'PLATFORM' OR role_code <> 'WAREHOUSE_TALLY')) + 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 = 'WAREHOUSE_TALLY' AND id <> 4000006) + ) THEN + SIGNAL SQLSTATE '45000' + SET MESSAGE_TEXT = '仓库角色初始化失败:4000004-4000006 或角色编码已被占用'; + END IF; + IF EXISTS ( + SELECT 1 + FROM iam_data_scope + WHERE (id = 6000004 AND role_id <> 4000004) + OR (id = 6000005 AND role_id <> 4000005) + OR (id = 6000006 AND role_id <> 4000006) + OR (role_id = 4000004 AND id <> 6000004) + OR (role_id = 4000005 AND id <> 6000005) + OR (role_id = 4000006 AND id <> 6000006) + ) THEN + SIGNAL SQLSTATE '45000' + SET MESSAGE_TEXT = '仓库角色初始化失败:6000004-6000006 数据范围ID已被占用'; + END IF; +END$$ +DELIMITER ; +CALL assert_phase3_warehouse_role_ids_available(); +DROP PROCEDURE assert_phase3_warehouse_role_ids_available; + +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', + '负责排柜、预装、任务分派、装柜完成和国内出仓确认', 0, 0), + (4000005, 0, 'PLATFORM', 'WAREHOUSE_PICKER', '仓库拣货员', 'SYSTEM', 'ACTIVE', + '负责使用PDA执行柜次拣货和现场拍照', 0, 0), + (4000006, 0, 'PLATFORM', 'WAREHOUSE_TALLY', '仓库理货员', 'SYSTEM', 'ACTIVE', + '负责入库测量、PDA复核和装柜证据维护', 0, 0) +ON DUPLICATE KEY UPDATE + role_name = VALUES(role_name), role_type = 'SYSTEM', status = 'ACTIVE', + description = VALUES(description), deleted = 0; + +DROP TEMPORARY TABLE IF EXISTS tmp_phase3_warehouse_role_permission; +CREATE TEMPORARY TABLE tmp_phase3_warehouse_role_permission ( + role_code VARCHAR(64) NOT NULL, + permission_code VARCHAR(128) NOT NULL, + PRIMARY KEY (role_code, permission_code) +) ENGINE=MEMORY; + +-- 操作员:编排装柜并确认国内出仓,不授予财务、系统管理或出仓撤销权限。 +INSERT INTO tmp_phase3_warehouse_role_permission (role_code, permission_code) VALUES + ('WAREHOUSE_OPERATOR', 'platform:order'), + ('WAREHOUSE_OPERATOR', 'platform:master-data'), + ('WAREHOUSE_OPERATOR', 'platform:warehouse:list'), + ('WAREHOUSE_OPERATOR', 'platform:pallet:list'), + ('WAREHOUSE_OPERATOR', 'platform:location:list'), + ('WAREHOUSE_OPERATOR', 'platform:firstleg:inbound:list'), + ('WAREHOUSE_OPERATOR', 'platform:firstleg:inbound:view'), + ('WAREHOUSE_OPERATOR', 'platform:firstleg:container:list'), + ('WAREHOUSE_OPERATOR', 'platform:firstleg:container:view'), + ('WAREHOUSE_OPERATOR', 'platform:firstleg:container:create'), + ('WAREHOUSE_OPERATOR', 'platform:firstleg:container:edit'), + ('WAREHOUSE_OPERATOR', 'platform:firstleg:smart-loading:list'), + ('WAREHOUSE_OPERATOR', 'platform:firstleg:smart-loading:create'), + ('WAREHOUSE_OPERATOR', 'platform:firstleg:smart-loading:assign'), + ('WAREHOUSE_OPERATOR', 'platform:firstleg:smart-loading:delete'), + ('WAREHOUSE_OPERATOR', 'platform:firstleg:smart-loading:push'), + ('WAREHOUSE_OPERATOR', 'platform:firstleg:preload:list'), + ('WAREHOUSE_OPERATOR', 'platform:firstleg:preload:view'), + ('WAREHOUSE_OPERATOR', 'platform:firstleg:preload:edit'), + ('WAREHOUSE_OPERATOR', 'platform:firstleg:preload:complete'), + ('WAREHOUSE_OPERATOR', 'platform:firstleg:preload:confirm'), + ('WAREHOUSE_OPERATOR', 'platform:firstleg:loading-list:list'), + ('WAREHOUSE_OPERATOR', 'platform:firstleg:loading-list:view'), + ('WAREHOUSE_OPERATOR', 'platform:firstleg:pda:view'), + ('WAREHOUSE_OPERATOR', 'platform:firstleg:pda:assign'), + ('WAREHOUSE_OPERATOR', 'platform:firstleg:pda:review'), + ('WAREHOUSE_OPERATOR', 'platform:firstleg:inbound:loading:view'), + ('WAREHOUSE_OPERATOR', 'platform:firstleg:inbound:loading:edit'), + ('WAREHOUSE_OPERATOR', 'platform:firstleg:inbound:loading:complete'), + ('WAREHOUSE_OPERATOR', 'platform:firstleg:inbound:picking:view'), + ('WAREHOUSE_OPERATOR', 'platform:firstleg:outbound-dispatch:list'), + ('WAREHOUSE_OPERATOR', 'platform:firstleg:outbound-dispatch:view'), + ('WAREHOUSE_OPERATOR', 'platform:firstleg:outbound-dispatch:check'), + ('WAREHOUSE_OPERATOR', 'platform:firstleg:outbound-dispatch:confirm'); + +-- 拣货员:只允许柜次查询、PDA作业、拣货记录及现场照片,不授予任务分派或复核权限。 +INSERT INTO tmp_phase3_warehouse_role_permission (role_code, permission_code) VALUES + ('WAREHOUSE_PICKER', 'platform:order'), + ('WAREHOUSE_PICKER', 'platform:master-data'), + ('WAREHOUSE_PICKER', 'platform:warehouse:list'), + ('WAREHOUSE_PICKER', 'platform:pallet:list'), + ('WAREHOUSE_PICKER', 'platform:location:list'), + ('WAREHOUSE_PICKER', 'platform:firstleg:inbound:list'), + ('WAREHOUSE_PICKER', 'platform:firstleg:inbound:view'), + ('WAREHOUSE_PICKER', 'platform:firstleg:container:list'), + ('WAREHOUSE_PICKER', 'platform:firstleg:container:view'), + ('WAREHOUSE_PICKER', 'platform:firstleg:preload:list'), + ('WAREHOUSE_PICKER', 'platform:firstleg:preload:view'), + ('WAREHOUSE_PICKER', 'platform:firstleg:loading-list:list'), + ('WAREHOUSE_PICKER', 'platform:firstleg:loading-list:view'), + ('WAREHOUSE_PICKER', 'platform:firstleg:pda:view'), + ('WAREHOUSE_PICKER', 'platform:firstleg:pda:work'), + ('WAREHOUSE_PICKER', 'platform:firstleg:inbound:loading:view'), + ('WAREHOUSE_PICKER', 'platform:firstleg:inbound:picking:edit'), + ('WAREHOUSE_PICKER', 'platform:firstleg:inbound:picking:view'); + +-- 理货员:负责到仓测量、装柜证据维护和PDA复核,不允许完成装柜或确认出仓。 +INSERT INTO tmp_phase3_warehouse_role_permission (role_code, permission_code) VALUES + ('WAREHOUSE_TALLY', 'platform:order'), + ('WAREHOUSE_TALLY', 'platform:master-data'), + ('WAREHOUSE_TALLY', 'platform:warehouse:list'), + ('WAREHOUSE_TALLY', 'platform:pallet:list'), + ('WAREHOUSE_TALLY', 'platform:location:list'), + ('WAREHOUSE_TALLY', 'platform:firstleg:inbound:list'), + ('WAREHOUSE_TALLY', 'platform:firstleg:inbound:view'), + ('WAREHOUSE_TALLY', 'platform:firstleg:inbound:measure:view'), + ('WAREHOUSE_TALLY', 'platform:firstleg:inbound:measure:edit'), + ('WAREHOUSE_TALLY', 'platform:firstleg:inbound:measure:submit'), + ('WAREHOUSE_TALLY', 'platform:firstleg:inbound:weight:edit'), + ('WAREHOUSE_TALLY', 'platform:firstleg:inbound:weight:confirm'), + ('WAREHOUSE_TALLY', 'platform:firstleg:container:list'), + ('WAREHOUSE_TALLY', 'platform:firstleg:container:view'), + ('WAREHOUSE_TALLY', 'platform:firstleg:preload:list'), + ('WAREHOUSE_TALLY', 'platform:firstleg:preload:view'), + ('WAREHOUSE_TALLY', 'platform:firstleg:loading-list:list'), + ('WAREHOUSE_TALLY', 'platform:firstleg:loading-list:view'), + ('WAREHOUSE_TALLY', 'platform:firstleg:pda:view'), + ('WAREHOUSE_TALLY', 'platform:firstleg:pda:review'), + ('WAREHOUSE_TALLY', 'platform:firstleg:inbound:loading:view'), + ('WAREHOUSE_TALLY', 'platform:firstleg:inbound:loading:edit'), + ('WAREHOUSE_TALLY', 'platform:firstleg:inbound:picking:view'); + +DROP PROCEDURE IF EXISTS assert_phase3_warehouse_role_permissions_available; +DELIMITER $$ +CREATE PROCEDURE assert_phase3_warehouse_role_permissions_available() +BEGIN + IF (SELECT COUNT(*) FROM tmp_phase3_warehouse_role_permission + WHERE role_code = 'WAREHOUSE_OPERATOR') <> 34 + OR (SELECT COUNT(*) FROM tmp_phase3_warehouse_role_permission + WHERE role_code = 'WAREHOUSE_PICKER') <> 18 + OR (SELECT COUNT(*) FROM tmp_phase3_warehouse_role_permission + WHERE role_code = 'WAREHOUSE_TALLY') <> 23 THEN + SIGNAL SQLSTATE '45000' + SET MESSAGE_TEXT = '仓库角色初始化失败:角色权限组合数量不符合预期'; + END IF; + IF EXISTS ( + SELECT 1 + FROM tmp_phase3_warehouse_role_permission expected + LEFT JOIN iam_permission permission + ON permission.tenant_id = 0 + AND permission.app_code = 'PLATFORM' + AND permission.permission_code = expected.permission_code + AND permission.status = 'ACTIVE' + AND permission.deleted = 0 + WHERE permission.id IS NULL + ) THEN + SIGNAL SQLSTATE '45000' + SET MESSAGE_TEXT = '仓库角色初始化失败:依赖的三期平台权限尚未完整部署'; + END IF; +END$$ +DELIMITER ; +CALL assert_phase3_warehouse_role_permissions_available(); +DROP PROCEDURE assert_phase3_warehouse_role_permissions_available; + +INSERT IGNORE INTO iam_role_permission (role_id, permission_id, tenant_id) +SELECT role.id, permission.id, 0 + FROM tmp_phase3_warehouse_role_permission expected + JOIN iam_role role + ON role.tenant_id = 0 + AND role.app_code = 'PLATFORM' + AND role.role_code = expected.role_code + AND role.status = 'ACTIVE' + AND role.deleted = 0 + JOIN iam_permission permission + ON permission.tenant_id = 0 + AND permission.app_code = 'PLATFORM' + AND permission.permission_code = expected.permission_code + AND permission.status = 'ACTIVE' + AND permission.deleted = 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) +ON DUPLICATE KEY UPDATE + tenant_id = 0, scope_type = 'TENANT', scope_expression = NULL; + +DROP PROCEDURE IF EXISTS assert_phase3_warehouse_roles_ready; +DELIMITER $$ +CREATE PROCEDURE assert_phase3_warehouse_roles_ready() +BEGIN + IF (SELECT COUNT(*) + FROM iam_role + WHERE tenant_id = 0 + AND app_code = 'PLATFORM' + AND role_type = 'SYSTEM' + AND status = 'ACTIVE' + AND deleted = 0 + AND role_code IN ('WAREHOUSE_OPERATOR', 'WAREHOUSE_PICKER', 'WAREHOUSE_TALLY')) <> 3 THEN + SIGNAL SQLSTATE '45000' + SET MESSAGE_TEXT = '仓库角色初始化失败:系统角色未完整启用'; + END IF; + IF (SELECT COUNT(*) + FROM tmp_phase3_warehouse_role_permission expected + JOIN iam_role role + ON role.tenant_id = 0 + AND role.app_code = 'PLATFORM' + AND role.role_code = expected.role_code + JOIN iam_permission permission + ON permission.tenant_id = 0 + AND permission.app_code = 'PLATFORM' + AND permission.permission_code = expected.permission_code + JOIN iam_role_permission grant_row + ON grant_row.role_id = role.id + AND grant_row.permission_id = permission.id + AND grant_row.tenant_id = 0) <> 75 THEN + SIGNAL SQLSTATE '45000' + SET MESSAGE_TEXT = '仓库角色初始化失败:最小权限授权未完整写入'; + END IF; + IF (SELECT COUNT(*) + FROM iam_data_scope scope_row + 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 scope_row.tenant_id = 0 + AND scope_row.scope_type = 'TENANT') <> 3 THEN + SIGNAL SQLSTATE '45000' + SET MESSAGE_TEXT = '仓库角色初始化失败:数据范围未完整写入'; + END IF; +END$$ +DELIMITER ; +CALL assert_phase3_warehouse_roles_ready(); +DROP PROCEDURE assert_phase3_warehouse_roles_ready; + +DROP TEMPORARY TABLE IF EXISTS tmp_phase3_warehouse_role_permission; diff --git a/iam/src/main/resources/openapi/iam-v1.yaml b/iam/src/main/resources/openapi/iam-v1.yaml index 38d10e5b4..255c4c083 100644 --- a/iam/src/main/resources/openapi/iam-v1.yaml +++ b/iam/src/main/resources/openapi/iam-v1.yaml @@ -585,7 +585,7 @@ paths: get: tags: [RBAC] summary: List roles / 查询角色 - description: List roles in the selected application and current customer or platform technical scope. / 查询指定应用及当前客户范围或平台技术作用域内的角色。 + 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. / 查询指定应用及当前客户范围或平台技术作用域内的角色;三期角色迁移后,平台作用域包含仓库操作员、仓库拣货员和仓库理货员三个内置角色。 responses: '200': {$ref: '#/components/responses/Success'} '403': {$ref: '#/components/responses/Forbidden'} @@ -606,7 +606,7 @@ paths: put: tags: [RBAC] summary: Assign roles to member / 设置成员角色 - description: Replace member role assignments in the same tenant and application. / 在同一租户和应用域内替换成员角色。 + description: Replace member role assignments in the same tenant and application. Platform administrators can assign the built-in warehouse roles to active internal-user memberships; the API does not create accounts or fixed credentials. / 在同一客户和应用域内替换成员角色;平台管理员可将内置仓库角色分配给已启用的内部用户成员,本接口不会创建账号或固定密码。 parameters: - {$ref: '#/components/parameters/App'} requestBody: diff --git a/iam/src/test/java/com/baiyue/iam/repository/WarehouseRoleSqlContractTest.java b/iam/src/test/java/com/baiyue/iam/repository/WarehouseRoleSqlContractTest.java new file mode 100644 index 000000000..dda4140ff --- /dev/null +++ b/iam/src/test/java/com/baiyue/iam/repository/WarehouseRoleSqlContractTest.java @@ -0,0 +1,66 @@ +package com.baiyue.iam.repository; + +import org.junit.jupiter.api.Test; +import org.springframework.core.io.ClassPathResource; + +import java.nio.charset.StandardCharsets; + +import static org.assertj.core.api.Assertions.assertThat; + +class WarehouseRoleSqlContractTest { + @Test + void initializesWarehouseRolesWithLeastPrivilegeAndStableIds() throws Exception { + String sql = new ClassPathResource("db/phase3-warehouse-roles.sql") + .getContentAsString(StandardCharsets.UTF_8); + + assertThat(sql) + .contains("(4000004, 0, 'PLATFORM', 'WAREHOUSE_OPERATOR', '仓库操作员', 'SYSTEM'") + .contains("(4000005, 0, 'PLATFORM', 'WAREHOUSE_PICKER', '仓库拣货员', 'SYSTEM'") + .contains("(4000006, 0, 'PLATFORM', 'WAREHOUSE_TALLY', '仓库理货员', 'SYSTEM'") + .contains("ON DUPLICATE KEY UPDATE") + .contains("INSERT IGNORE INTO iam_role_permission") + .contains("'WAREHOUSE_PICKER', 'platform:firstleg:pda:work'") + .contains("'WAREHOUSE_TALLY', 'platform:firstleg:pda:review'") + .contains("'WAREHOUSE_TALLY', 'platform:firstleg:inbound:loading:edit'") + .contains("'WAREHOUSE_OPERATOR', 'platform:firstleg:outbound-dispatch:confirm'") + .contains("WHERE role_code = 'WAREHOUSE_OPERATOR') <> 34") + .contains("WHERE role_code = 'WAREHOUSE_PICKER') <> 18") + .contains("WHERE role_code = 'WAREHOUSE_TALLY') <> 23") + .contains("依赖的三期平台权限尚未完整部署") + .contains("最小权限授权未完整写入") + .contains("(6000004, 4000004, 0, 'TENANT', NULL)") + .contains("(6000005, 4000005, 0, 'TENANT', NULL)") + .contains("(6000006, 4000006, 0, 'TENANT', NULL)"); + + assertRoleGrant(sql, "WAREHOUSE_PICKER", "platform:firstleg:pda:work"); + assertRoleGrant(sql, "WAREHOUSE_TALLY", "platform:firstleg:pda:review"); + assertRoleGrant(sql, "WAREHOUSE_OPERATOR", "platform:firstleg:pda:assign"); + + assertThat(roleGrantBlock(sql, "WAREHOUSE_PICKER")) + .doesNotContain("platform:firstleg:pda:assign") + .doesNotContain("platform:firstleg:pda:review") + .doesNotContain("platform:firstleg:outbound-dispatch:confirm"); + assertThat(roleGrantBlock(sql, "WAREHOUSE_TALLY")) + .doesNotContain("platform:firstleg:inbound:loading:complete") + .doesNotContain("platform:firstleg:outbound-dispatch:confirm"); + assertThat(sql) + .doesNotContain("'WAREHOUSE_OPERATOR', 'platform:firstleg:outbound-dispatch:reverse'") + .doesNotContain("'WAREHOUSE_PICKER', 'platform:firstleg:smart-loading:delete'") + .doesNotContain("'WAREHOUSE_TALLY', 'firstleg:inbound:fee-detail:edit'"); + } + + private static void assertRoleGrant(String sql, String roleCode, String permissionCode) { + assertThat(sql).contains("('" + roleCode + "', '" + permissionCode + "')"); + } + + private static String roleGrantBlock(String sql, String roleCode) { + String marker = "-- " + switch (roleCode) { + case "WAREHOUSE_PICKER" -> "拣货员:"; + case "WAREHOUSE_TALLY" -> "理货员:"; + default -> throw new IllegalArgumentException("unsupported role " + roleCode); + }; + int start = sql.indexOf(marker); + int end = sql.indexOf("\n-- ", start + marker.length()); + return sql.substring(start, end > start ? end : sql.length()); + } +}