From 3809d2a028049dfe932acd43a11dec42c3669145 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Tue, 11 Aug 2026 19:35:18 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/sql/头程数据库统一增量-MySQL8.sql | 88 ++++++++++++++++++- .../FinancePhase1PermissionContractTest.java | 42 +++++++++ .../db/finance-g1-g2-permissions.sql | 86 ++++++++++++++++++ .../FinanceG1G2PermissionSqlContractTest.java | 87 ++++++++++++++++++ 4 files changed, 301 insertions(+), 2 deletions(-) create mode 100644 finance/src/test/java/com/baiyue/finance/FinancePhase1PermissionContractTest.java create mode 100644 iam/src/main/resources/db/finance-g1-g2-permissions.sql create mode 100644 iam/src/test/java/com/baiyue/iam/repository/FinanceG1G2PermissionSqlContractTest.java diff --git a/deploy/sql/头程数据库统一增量-MySQL8.sql b/deploy/sql/头程数据库统一增量-MySQL8.sql index d3d0bef5c..18892e41a 100644 --- a/deploy/sql/头程数据库统一增量-MySQL8.sql +++ b/deploy/sql/头程数据库统一增量-MySQL8.sql @@ -13760,10 +13760,91 @@ VALUES (5205011, 10001, 'MERCHANT', 'BUTTON', 5205000, 'merchant:finance:statement:view', '查看账单详情及收款', NULL, 'GET', 20, 'ACTIVE', 0) ON DUPLICATE KEY UPDATE permission_name=VALUES(permission_name), parent_id=VALUES(parent_id), route_path=VALUES(route_path), http_method=VALUES(http_method), status='ACTIVE', deleted=0; +-- Finance G1/G2 新权限只进入可分配权限目录,不默认授予平台管理员。 +DROP PROCEDURE IF EXISTS migrate_finance_g1_g2_permissions; +DELIMITER $$ +CREATE PROCEDURE migrate_finance_g1_g2_permissions() +BEGIN + IF EXISTS ( + SELECT 1 + FROM baiyue_iam.iam_permission + WHERE (id = 5105022 AND permission_code <> 'platform:finance:account:view') + OR (id = 5105023 AND permission_code <> 'platform:finance:fee-source:view') + OR (id = 5105024 AND permission_code <> 'platform:finance:fee-source:review') + OR (id = 5105025 AND permission_code <> 'platform:finance:fee-source:settlement') + OR (permission_code = 'platform:finance:account:view' AND id <> 5105022) + OR (permission_code = 'platform:finance:fee-source:view' AND id <> 5105023) + OR (permission_code = 'platform:finance:fee-source:review' AND id <> 5105024) + OR (permission_code = 'platform:finance:fee-source:settlement' AND id <> 5105025) + OR (id IN (5105022, 5105023, 5105024, 5105025) + AND (tenant_id <> 0 OR app_code <> 'PLATFORM')) + ) THEN + SIGNAL SQLSTATE '45000' + SET MESSAGE_TEXT = 'finance G1/G2 permission id/code conflict'; + END IF; + + INSERT INTO baiyue_iam.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 + (5105022, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:account:view', + '查看客户财务账户及流水', NULL, 'GET', 130, 'ACTIVE', 1, 0), + (5105023, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:fee-source:view', + '查看财务费用来源', NULL, 'GET', 140, 'ACTIVE', 1, 0), + (5105024, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:fee-source:review', + '审核财务费用来源', NULL, 'POST', 150, 'ACTIVE', 1, 0), + (5105025, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:fee-source:settlement', + '设置费用来源参与结算', NULL, 'PUT', 160, '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; + + IF (SELECT COUNT(*) + FROM baiyue_iam.iam_permission + WHERE tenant_id = 0 + AND app_code = 'PLATFORM' + AND permission_type = 'BUTTON' + AND parent_id = 5105000 + AND status = 'ACTIVE' + AND built_in = 1 + AND deleted = 0 + AND permission_code IN ( + 'platform:finance:account:view', + 'platform:finance:fee-source:view', + 'platform:finance:fee-source:review', + 'platform:finance:fee-source:settlement')) <> 4 THEN + SIGNAL SQLSTATE '45000' + SET MESSAGE_TEXT = 'finance G1/G2 permission catalog is incomplete'; + END IF; +END$$ +DELIMITER ; +CALL migrate_finance_g1_g2_permissions(); +DROP PROCEDURE IF EXISTS migrate_finance_g1_g2_permissions; + INSERT INTO baiyue_iam.iam_role_permission (role_id, permission_id, tenant_id) SELECT 4000001, id, 0 FROM baiyue_iam.iam_permission - WHERE tenant_id=0 AND app_code='PLATFORM' AND (permission_code='platform:finance' OR permission_code LIKE 'platform:finance:%') + WHERE tenant_id=0 AND app_code='PLATFORM' AND permission_code IN ( + 'platform:finance', + 'platform:finance:bill:view', 'platform:finance:bill:create', + 'platform:finance:bill:update', 'platform:finance:bill:issue', + 'platform:finance:bill:adjust', 'platform:finance:bill:void', + 'platform:finance:receipt:view', 'platform:finance:receipt:create', + 'platform:finance:cost:view', 'platform:finance:cost:create', + 'platform:finance:customer-terms:view', 'platform:finance:customer-terms:update') ON DUPLICATE KEY UPDATE tenant_id=VALUES(tenant_id); +DELETE rp + FROM baiyue_iam.iam_role_permission rp + JOIN baiyue_iam.iam_permission p + ON p.id = rp.permission_id AND p.tenant_id = rp.tenant_id + WHERE rp.role_id = 4000001 + AND rp.tenant_id = 0 + AND p.permission_code IN ( + 'platform:finance:account:view', + 'platform:finance:fee-source:view', + 'platform:finance:fee-source:review', + 'platform:finance:fee-source:settlement'); INSERT INTO baiyue_iam.iam_role_permission (role_id, permission_id, tenant_id) SELECT 4000101, id, 10001 FROM baiyue_iam.iam_permission WHERE tenant_id=10001 AND app_code='MERCHANT' AND (permission_code='merchant:finance:statement' OR permission_code LIKE 'merchant:finance:%') @@ -13773,7 +13854,7 @@ ON DUPLICATE KEY UPDATE tenant_id=VALUES(tenant_id); DELIMITER $$ CREATE PROCEDURE assert_finance_permissions() BEGIN - IF (SELECT COUNT(*) FROM baiyue_iam.iam_permission WHERE tenant_id=0 AND app_code='PLATFORM' AND permission_code LIKE 'platform:finance:%' AND status='ACTIVE' AND deleted=0) <> 12 THEN + IF (SELECT COUNT(*) FROM baiyue_iam.iam_permission WHERE tenant_id=0 AND app_code='PLATFORM' AND permission_code LIKE 'platform:finance:%' AND status='ACTIVE' AND deleted=0) <> 16 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='统一迁移失败:平台 Finance 权限不完整'; END IF; IF (SELECT COUNT(*) FROM baiyue_iam.iam_permission WHERE tenant_id=10001 AND app_code='MERCHANT' AND permission_code IN ('merchant:finance:statement:list','merchant:finance:statement:view') AND status='ACTIVE' AND deleted=0) <> 2 THEN @@ -13782,6 +13863,9 @@ BEGIN IF (SELECT COUNT(*) FROM baiyue_iam.iam_role_permission rp JOIN baiyue_iam.iam_permission p ON p.id=rp.permission_id AND p.tenant_id=rp.tenant_id WHERE rp.role_id=4000001 AND rp.tenant_id=0 AND (p.permission_code='platform:finance' OR p.permission_code LIKE 'platform:finance:%')) <> 13 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='统一迁移失败:平台管理员 Finance 授权不完整'; END IF; + IF (SELECT COUNT(*) FROM baiyue_iam.iam_role_permission rp JOIN baiyue_iam.iam_permission p ON p.id=rp.permission_id AND p.tenant_id=rp.tenant_id WHERE rp.role_id=4000001 AND rp.tenant_id=0 AND p.permission_code IN ('platform:finance:account:view','platform:finance:fee-source:view','platform:finance:fee-source:review','platform:finance:fee-source:settlement')) <> 0 THEN + SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='统一迁移失败:Finance G1/G2 新权限不得默认授予平台管理员'; + END IF; IF (SELECT COUNT(*) FROM baiyue_iam.iam_role_permission rp JOIN baiyue_iam.iam_permission p ON p.id=rp.permission_id AND p.tenant_id=rp.tenant_id WHERE rp.role_id=4000101 AND rp.tenant_id=10001 AND (p.permission_code='merchant:finance:statement' OR p.permission_code LIKE 'merchant:finance:%')) <> 3 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='统一迁移失败:客户管理员 Finance 只读授权不完整'; END IF; diff --git a/finance/src/test/java/com/baiyue/finance/FinancePhase1PermissionContractTest.java b/finance/src/test/java/com/baiyue/finance/FinancePhase1PermissionContractTest.java new file mode 100644 index 000000000..e1960d11c --- /dev/null +++ b/finance/src/test/java/com/baiyue/finance/FinancePhase1PermissionContractTest.java @@ -0,0 +1,42 @@ +package com.baiyue.finance; + +import com.baiyue.finance.controller.FinancePhase1OperationController; +import org.junit.jupiter.api.Test; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; + +import java.lang.reflect.Method; + +import static org.assertj.core.api.Assertions.assertThat; + +class FinancePhase1PermissionContractTest { + @Test + void freezesG1AndG2ControllerPermissionsAndHttpActions() throws Exception { + assertEndpoint("listDerivedCustomerAccounts", GetMapping.class, + "hasAuthority('platform:finance:account:view')", int.class, int.class, String.class, String.class); + assertEndpoint("listDerivedCustomerAccountLedger", GetMapping.class, + "hasAuthority('platform:finance:account:view')", long.class, int.class, int.class, + String.class, java.time.LocalDate.class, java.time.LocalDate.class); + assertEndpoint("getFinanceFeeSource", GetMapping.class, + "hasAuthority('platform:finance:fee-source:view')", long.class); + assertEndpoint("reviewFinanceFeeSource", PostMapping.class, + "hasAuthority('platform:finance:fee-source:review')", + com.baiyue.common.security.AuthenticatedUser.class, long.class, String.class, + com.baiyue.finance.dto.FinancePhase1Dtos.ReviewRequest.class); + assertEndpoint("updateFeeSourceSettlementInclusion", PutMapping.class, + "hasAuthority('platform:finance:fee-source:settlement')", + com.baiyue.common.security.AuthenticatedUser.class, long.class, String.class, + com.baiyue.finance.dto.FinancePhase1Dtos.SettlementInclusionRequest.class); + } + + private static void assertEndpoint(String methodName, + Class mapping, + String authority, + Class... parameterTypes) throws Exception { + Method method = FinancePhase1OperationController.class.getMethod(methodName, parameterTypes); + assertThat(method.isAnnotationPresent(mapping)).isTrue(); + assertThat(method.getAnnotation(PreAuthorize.class).value()).isEqualTo(authority); + } +} diff --git a/iam/src/main/resources/db/finance-g1-g2-permissions.sql b/iam/src/main/resources/db/finance-g1-g2-permissions.sql new file mode 100644 index 000000000..1f2cc46be --- /dev/null +++ b/iam/src/main/resources/db/finance-g1-g2-permissions.sql @@ -0,0 +1,86 @@ +-- Finance G1/G2 permissions exposed by FinancePhase1OperationController. +-- These resources are assignable to custom platform roles; this migration does not grant them by default. +USE baiyue_iam; +SET NAMES utf8mb4; + +DROP PROCEDURE IF EXISTS migrate_finance_g1_g2_permissions; +DELIMITER $$ +CREATE PROCEDURE migrate_finance_g1_g2_permissions() +BEGIN + IF EXISTS ( + SELECT 1 + FROM iam_permission + WHERE (id = 5105000 AND permission_code <> 'platform:finance') + OR (id = 5105022 AND permission_code <> 'platform:finance:account:view') + OR (id = 5105023 AND permission_code <> 'platform:finance:fee-source:view') + OR (id = 5105024 AND permission_code <> 'platform:finance:fee-source:review') + OR (id = 5105025 AND permission_code <> 'platform:finance:fee-source:settlement') + OR (permission_code = 'platform:finance' AND id <> 5105000) + OR (permission_code = 'platform:finance:account:view' AND id <> 5105022) + OR (permission_code = 'platform:finance:fee-source:view' AND id <> 5105023) + OR (permission_code = 'platform:finance:fee-source:review' AND id <> 5105024) + OR (permission_code = 'platform:finance:fee-source:settlement' AND id <> 5105025) + OR (id IN (5105000, 5105022, 5105023, 5105024, 5105025) + AND (tenant_id <> 0 OR app_code <> 'PLATFORM')) + ) THEN + SIGNAL SQLSTATE '45000' + SET MESSAGE_TEXT = 'finance G1/G2 permission id/code conflict'; + 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 + (5105000, 0, 'PLATFORM', 'MENU', 0, 'platform:finance', + '财务管理', '/finance', NULL, 60, 'ACTIVE', 1, 0), + (5105022, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:account:view', + '查看客户财务账户及流水', NULL, 'GET', 130, 'ACTIVE', 1, 0), + (5105023, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:fee-source:view', + '查看财务费用来源', NULL, 'GET', 140, 'ACTIVE', 1, 0), + (5105024, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:fee-source:review', + '审核财务费用来源', NULL, 'POST', 150, 'ACTIVE', 1, 0), + (5105025, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:fee-source:settlement', + '设置费用来源参与结算', NULL, 'PUT', 160, '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; + + IF (SELECT COUNT(*) + FROM iam_permission + WHERE tenant_id = 0 + AND app_code = 'PLATFORM' + AND permission_type = 'BUTTON' + AND parent_id = 5105000 + AND status = 'ACTIVE' + AND built_in = 1 + AND deleted = 0 + AND permission_code IN ( + 'platform:finance:account:view', + 'platform:finance:fee-source:view', + 'platform:finance:fee-source:review', + 'platform:finance:fee-source:settlement')) <> 4 THEN + SIGNAL SQLSTATE '45000' + SET MESSAGE_TEXT = 'finance G1/G2 permission catalog is incomplete'; + END IF; + + IF (SELECT COUNT(*) + FROM iam_permission + WHERE id = 5105000 + AND tenant_id = 0 + AND app_code = 'PLATFORM' + AND permission_type = 'MENU' + AND parent_id = 0 + AND permission_code = 'platform:finance' + AND status = 'ACTIVE' + AND built_in = 1 + AND deleted = 0) <> 1 THEN + SIGNAL SQLSTATE '45000' + SET MESSAGE_TEXT = 'finance permission parent is invalid'; + END IF; +END$$ +DELIMITER ; + +CALL migrate_finance_g1_g2_permissions(); +DROP PROCEDURE IF EXISTS migrate_finance_g1_g2_permissions; diff --git a/iam/src/test/java/com/baiyue/iam/repository/FinanceG1G2PermissionSqlContractTest.java b/iam/src/test/java/com/baiyue/iam/repository/FinanceG1G2PermissionSqlContractTest.java new file mode 100644 index 000000000..45921f858 --- /dev/null +++ b/iam/src/test/java/com/baiyue/iam/repository/FinanceG1G2PermissionSqlContractTest.java @@ -0,0 +1,87 @@ +package com.baiyue.iam.repository; + +import org.junit.jupiter.api.Test; +import org.springframework.core.io.ClassPathResource; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.assertj.core.api.Assertions.assertThat; + +class FinanceG1G2PermissionSqlContractTest { + @Test + void declaresTheFourControllerPermissionsUnderFinanceWithoutDefaultRoleGrants() throws Exception { + String sql = new ClassPathResource("db/finance-g1-g2-permissions.sql") + .getContentAsString(StandardCharsets.UTF_8); + + assertThat(sql) + .contains("(5105022, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:account:view'") + .contains("(5105023, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:fee-source:view'") + .contains("(5105024, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:fee-source:review'") + .contains("(5105025, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:fee-source:settlement'") + .contains("NULL, 'GET', 130, 'ACTIVE', 1, 0") + .contains("NULL, 'GET', 140, 'ACTIVE', 1, 0") + .contains("NULL, 'POST', 150, 'ACTIVE', 1, 0") + .contains("NULL, 'PUT', 160, 'ACTIVE', 1, 0") + .contains("ON DUPLICATE KEY UPDATE") + .contains("CALL migrate_finance_g1_g2_permissions()") + .contains("DROP PROCEDURE IF EXISTS migrate_finance_g1_g2_permissions") + .doesNotContain("INSERT INTO iam_role_permission"); + } + + @Test + void failsClosedOnPermissionIdentityConflictsAndAssertsCatalogCompleteness() throws Exception { + String sql = new ClassPathResource("db/finance-g1-g2-permissions.sql") + .getContentAsString(StandardCharsets.UTF_8); + + assertThat(sql) + .contains("SIGNAL SQLSTATE '45000'") + .contains("id = 5105022 AND permission_code <> 'platform:finance:account:view'") + .contains("permission_code = 'platform:finance:fee-source:settlement' AND id <> 5105025") + .contains("tenant_id <> 0 OR app_code <> 'PLATFORM'") + .contains("parent_id = 5105000") + .contains("permission_type = 'MENU'") + .contains("built_in = 1") + .contains("finance permission parent is invalid") + .contains("permission catalog is incomplete"); + } + + @Test + void unifiedSqlMirrorsCatalogButDoesNotAutoGrantTheNewPermissions() throws Exception { + String sql = Files.readString(resolveUnifiedSql(), StandardCharsets.UTF_8); + String financeSection = sql.substring(sql.indexOf("-- Finance F1 人工财务权限"), + sql.indexOf("CALL assert_unified_final_schema()")); + String platformAdminGrant = financeSection.substring( + financeSection.indexOf("SELECT 4000001, id, 0 FROM baiyue_iam.iam_permission"), + financeSection.indexOf("ON DUPLICATE KEY UPDATE tenant_id=VALUES(tenant_id);")); + + assertThat(financeSection) + .contains("(5105022, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:account:view'") + .contains("(5105023, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:fee-source:view'") + .contains("(5105024, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:fee-source:review'") + .contains("(5105025, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:fee-source:settlement'") + .contains("NULL, 'GET', 130, 'ACTIVE', 1, 0") + .contains("NULL, 'GET', 140, 'ACTIVE', 1, 0") + .contains("NULL, 'POST', 150, 'ACTIVE', 1, 0") + .contains("NULL, 'PUT', 160, 'ACTIVE', 1, 0") + .contains("permission_code IN (\n 'platform:finance',") + .contains("permission_code LIKE 'platform:finance:%' AND status='ACTIVE' AND deleted=0) <> 16") + .contains("p.permission_code IN ('platform:finance:account:view','platform:finance:fee-source:view','platform:finance:fee-source:review','platform:finance:fee-source:settlement')) <> 0") + .contains("平台管理员 Finance 授权不完整") + .contains("Finance G1/G2 新权限不得默认授予平台管理员"); + assertThat(platformAdminGrant) + .contains("'platform:finance'") + .contains("'platform:finance:customer-terms:update'") + .doesNotContain("LIKE 'platform:finance:%'") + .doesNotContain("platform:finance:account:view") + .doesNotContain("platform:finance:fee-source:view") + .doesNotContain("platform:finance:fee-source:review") + .doesNotContain("platform:finance:fee-source:settlement"); + } + + private static Path resolveUnifiedSql() { + Path direct = Path.of("deploy", "sql", "头程数据库统一增量-MySQL8.sql"); + return Files.exists(direct) ? direct : Path.of("..", "deploy", "sql", "头程数据库统一增量-MySQL8.sql"); + } +}