diff --git a/yudao-module-education/src/main/java/cn/iocoder/yudao/module/education/config/EducationProtectedTokenRoleValidator.java b/yudao-module-education/src/main/java/cn/iocoder/yudao/module/education/config/EducationProtectedTokenRoleValidator.java new file mode 100644 index 00000000..ea9d46f3 --- /dev/null +++ b/yudao-module-education/src/main/java/cn/iocoder/yudao/module/education/config/EducationProtectedTokenRoleValidator.java @@ -0,0 +1,55 @@ +package cn.iocoder.yudao.module.education.config; + +import lombok.RequiredArgsConstructor; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Component; + +import javax.sql.DataSource; +import java.util.List; + +/** + * Verifies that the runtime database role cannot forge protected lifecycle tokens. + */ +@Component +@RequiredArgsConstructor +@ConditionalOnProperty(prefix = "yudao.education", name = "enabled", havingValue = "true") +public class EducationProtectedTokenRoleValidator implements ApplicationRunner { + + private static final List PROTECTED_TABLES = List.of( + "education_question_lifecycle_transition_token", + "education_content_node_lifecycle_transition_token", + "education_question_collection_lifecycle_transition_token", + "education_question_collection_membership_token"); + + private final DataSource dataSource; + + @Override + public void run(ApplicationArguments args) { + validate(dataSource); + } + + public static void validate(DataSource dataSource) { + JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); + List unsafeTables = jdbcTemplate.queryForList(""" + SELECT protected.table_name + FROM (VALUES (?), (?), (?), (?)) AS protected(table_name) + LEFT JOIN pg_namespace namespace ON namespace.nspname = current_schema() + LEFT JOIN pg_class token_table + ON token_table.relnamespace = namespace.oid + AND token_table.relname = protected.table_name + AND token_table.relkind IN ('r', 'p') + WHERE token_table.oid IS NULL + OR pg_has_role(current_user, token_table.relowner, 'MEMBER') + OR has_table_privilege(current_user, token_table.oid, 'INSERT,UPDATE,DELETE,TRUNCATE') + ORDER BY protected.table_name + """, String.class, PROTECTED_TABLES.toArray()); + if (!unsafeTables.isEmpty()) { + throw new IllegalStateException("Education runtime database role must not own or write protected token tables: " + + String.join(", ", unsafeTables)); + } + } + +} diff --git a/yudao-module-education/src/main/resources/db/migration/education/V4180__validate_adopted_practice_shape.sql b/yudao-module-education/src/main/resources/db/migration/education/V4180__validate_adopted_practice_shape.sql new file mode 100644 index 00000000..98837066 --- /dev/null +++ b/yudao-module-education/src/main/resources/db/migration/education/V4180__validate_adopted_practice_shape.sql @@ -0,0 +1,92 @@ +-- EDU-006 follow-up: fail closed when adopted Practice tables have incompatible scalar shapes. +-- Published migrations V4030-V4060 remain immutable; this validates the post-adoption contract forward-only. + +DO $$ +DECLARE + mismatch TEXT; +BEGIN + WITH expected(table_name, column_name, data_type, nullable, min_length) AS ( + VALUES + ('education_practice_session','id','bigint',false,NULL::INTEGER), + ('education_practice_session','tenant_id','bigint',false,NULL), + ('education_practice_session','user_id','bigint',false,NULL), + ('education_practice_session','client_session_id','character varying',false,36), + ('education_practice_session','status','character varying',false,20), + ('education_practice_session','question_count','integer',false,NULL), + ('education_practice_session','version','integer',false,NULL), + ('education_practice_question','id','bigint',false,NULL), + ('education_practice_question','tenant_id','bigint',false,NULL), + ('education_practice_question','session_id','bigint',false,NULL), + ('education_practice_question','sequence','integer',false,NULL), + ('education_practice_question','question_id','character varying',false,64), + ('education_practice_question','content_version','character varying',false,64), + ('education_practice_question','stem','text',false,NULL), + ('education_practice_question','type','character varying',false,32), + ('education_practice_question','options','jsonb',false,NULL), + ('education_practice_question','is_answered','boolean',false,NULL), + ('education_practice_report','id','bigint',false,NULL), + ('education_practice_report','tenant_id','bigint',false,NULL), + ('education_practice_report','user_id','bigint',false,NULL), + ('education_practice_report','session_id','bigint',false,NULL), + ('education_practice_report','question_count','integer',false,NULL), + ('education_practice_report','score','integer',false,NULL), + ('education_practice_report_detail','id','bigint',false,NULL), + ('education_practice_report_detail','tenant_id','bigint',false,NULL), + ('education_practice_report_detail','user_id','bigint',false,NULL), + ('education_practice_report_detail','report_id','bigint',false,NULL), + ('education_practice_report_detail','session_id','bigint',false,NULL), + ('education_practice_report_detail','question_id','character varying',false,64), + ('education_practice_report_detail','sequence','integer',false,NULL), + ('education_practice_report_detail','stem','text',false,NULL), + ('education_practice_report_detail','type','character varying',false,32), + ('education_practice_report_detail','is_correct','boolean',false,NULL), + ('education_wrong_question','id','bigint',false,NULL), + ('education_wrong_question','tenant_id','bigint',false,NULL), + ('education_wrong_question','user_id','bigint',false,NULL), + ('education_wrong_question','question_id','character varying',false,64), + ('education_wrong_question','stem','text',false,NULL), + ('education_wrong_question','type','character varying',false,32), + ('education_wrong_question','options','jsonb',false,NULL), + ('education_wrong_question','first_wrong_time','timestamp without time zone',false,NULL), + ('education_wrong_question','last_wrong_time','timestamp without time zone',false,NULL), + ('education_wrong_question','wrong_count','integer',false,NULL), + ('education_wrong_question','master_status','character varying',false,20), + ('education_favorite','id','bigint',false,NULL), + ('education_favorite','tenant_id','bigint',false,NULL), + ('education_favorite','user_id','bigint',false,NULL), + ('education_favorite','target_type','character varying',false,32), + ('education_favorite','target_id','character varying',false,64), + ('education_favorite','content_version','character varying',false,64), + ('education_favorite','available','boolean',false,NULL), + ('education_idempotency','id','bigint',false,NULL), + ('education_idempotency','tenant_id','bigint',false,NULL), + ('education_idempotency','user_id','bigint',false,NULL), + ('education_idempotency','operation','character varying',false,32), + ('education_idempotency','idempotency_key','character varying',false,64), + ('education_idempotency','request_hash','character varying',false,64), + ('education_idempotency','session_id','bigint',false,NULL), + ('education_idempotency','status','character varying',false,20), + ('education_idempotency','business_payload','jsonb',true,NULL) + ), actual AS ( + SELECT e.*, c.data_type AS actual_type, c.is_nullable = 'YES' AS actual_nullable, + c.character_maximum_length AS actual_length + FROM expected e + LEFT JOIN information_schema.columns c + ON c.table_schema = current_schema() AND c.table_name = e.table_name + AND c.column_name = e.column_name + ) + SELECT STRING_AGG(format('%s.%s expected %s%s%s but found %s%s', table_name, column_name, data_type, + CASE WHEN nullable THEN '' ELSE ' NOT NULL' END, + CASE WHEN min_length IS NULL THEN '' ELSE format(' length >= %s', min_length) END, + COALESCE(actual_type, 'MISSING'), + CASE WHEN actual_type IS NULL THEN '' ELSE format(' nullable=%s length=%s', actual_nullable, + COALESCE(actual_length::TEXT, 'n/a')) END), '; ' ORDER BY table_name, column_name) + INTO mismatch + FROM actual + WHERE actual_type IS NULL OR actual_type <> data_type OR (NOT nullable AND actual_nullable) + OR (min_length IS NOT NULL AND COALESCE(actual_length, 0) < min_length); + + IF mismatch IS NOT NULL THEN + RAISE EXCEPTION 'Incompatible adopted Education Practice schema: %', mismatch; + END IF; +END $$; diff --git a/yudao-module-education/src/main/resources/db/migration/education/V4190__harden_classroom_and_notify_sequence.sql b/yudao-module-education/src/main/resources/db/migration/education/V4190__harden_classroom_and_notify_sequence.sql new file mode 100644 index 00000000..f701e000 --- /dev/null +++ b/yudao-module-education/src/main/resources/db/migration/education/V4190__harden_classroom_and_notify_sequence.sql @@ -0,0 +1,57 @@ +-- Harden Education classroom tenant triggers and reconcile the System notify-template sequence. +-- V4140 and V4160 remain immutable release history. + +CREATE OR REPLACE FUNCTION education_validate_class_relationship_tenant() +RETURNS TRIGGER +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path = pg_catalog +AS $$ +DECLARE owner_tenant BIGINT; +BEGIN + EXECUTE format('SELECT tenant_id FROM %I.education_class WHERE id = $1 AND deleted = false', TG_TABLE_SCHEMA) + INTO owner_tenant USING NEW.class_id; + IF owner_tenant IS NULL OR owner_tenant <> NEW.tenant_id THEN + RAISE EXCEPTION 'education class relationship cannot cross tenants'; + END IF; + RETURN NEW; +END; +$$; +REVOKE ALL ON FUNCTION education_validate_class_relationship_tenant() FROM PUBLIC; + +CREATE OR REPLACE FUNCTION education_validate_invitation_audit_tenant() +RETURNS TRIGGER +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path = pg_catalog +AS $$ +DECLARE owner_tenant BIGINT; +BEGIN + EXECUTE format('SELECT tenant_id FROM %I.education_class_invitation WHERE id = $1', TG_TABLE_SCHEMA) + INTO owner_tenant USING NEW.invitation_id; + IF owner_tenant IS NULL OR owner_tenant <> NEW.tenant_id THEN + RAISE EXCEPTION 'education invitation audit cannot cross tenants'; + END IF; + RETURN NEW; +END; +$$; +REVOKE ALL ON FUNCTION education_validate_invitation_audit_tenant() FROM PUBLIC; + +DO $$ +DECLARE + sequence_name TEXT; + maximum_id BIGINT; +BEGIN + IF to_regclass('system_notify_template') IS NULL THEN + RETURN; + END IF; + SELECT pg_get_serial_sequence(format('%I.system_notify_template', current_schema()), 'id') INTO sequence_name; + IF sequence_name IS NULL THEN + sequence_name := CASE WHEN to_regclass('system_notify_template_seq') IS NOT NULL + THEN format('%I.system_notify_template_seq', current_schema()) ELSE NULL END; + END IF; + IF sequence_name IS NOT NULL THEN + SELECT COALESCE(MAX(id), 1) INTO maximum_id FROM system_notify_template; + PERFORM setval(sequence_name::regclass, maximum_id, true); + END IF; +END $$; diff --git a/yudao-module-education/src/main/resources/db/migration/education/V4210__protect_public_question_collection_deletion.sql b/yudao-module-education/src/main/resources/db/migration/education/V4210__protect_public_question_collection_deletion.sql new file mode 100644 index 00000000..469d84db --- /dev/null +++ b/yudao-module-education/src/main/resources/db/migration/education/V4210__protect_public_question_collection_deletion.sql @@ -0,0 +1,18 @@ +-- EDU-010 forward fix: PUBLIC and tenant-owned collections are lifecycle-managed records. +-- V4110 already prevented tenant-owned physical deletion; replace the function without +-- changing the published migration checksum so adopted PUBLIC rows receive the same guard. + +CREATE OR REPLACE FUNCTION education_prevent_question_collection_delete() +RETURNS TRIGGER +LANGUAGE plpgsql +SET search_path = pg_catalog, pg_temp +AS $$ +BEGIN + IF OLD.scope IN ('PUBLIC', 'TENANT_OWNED') THEN + RAISE EXCEPTION 'collection cannot be physically deleted' USING ERRCODE = '23514'; + END IF; + RETURN OLD; +END +$$; + +REVOKE ALL ON FUNCTION education_prevent_question_collection_delete() FROM PUBLIC; diff --git a/yudao-module-education/src/test/java/cn/iocoder/yudao/module/education/test/EducationFlywayMigrationIntegrationTest.java b/yudao-module-education/src/test/java/cn/iocoder/yudao/module/education/test/EducationFlywayMigrationIntegrationTest.java index 4ced6e25..a18e36f8 100644 --- a/yudao-module-education/src/test/java/cn/iocoder/yudao/module/education/test/EducationFlywayMigrationIntegrationTest.java +++ b/yudao-module-education/src/test/java/cn/iocoder/yudao/module/education/test/EducationFlywayMigrationIntegrationTest.java @@ -1,8 +1,10 @@ package cn.iocoder.yudao.module.education.test; +import cn.iocoder.yudao.module.education.config.EducationProtectedTokenRoleValidator; import org.flywaydb.core.Flyway; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; +import org.postgresql.ds.PGSimpleDataSource; import java.sql.Connection; import java.sql.DriverManager; @@ -119,7 +121,7 @@ class EducationFlywayMigrationIntegrationTest { assertThat(queryStrings(schema, "SELECT COALESCE(version, 'BASELINE') FROM flyway_schema_history ORDER BY installed_rank")) - .containsExactly("4009", "4010", "4020", "4030", "4040", "4050", "4060", "4070", "4080", "4090", "4100", "4110", "4120", "4130", "4140", "4150", "4160", "4170"); + .containsExactly("4009", "4010", "4020", "4030", "4040", "4050", "4060", "4070", "4080", "4090", "4100", "4110", "4120", "4130", "4140", "4150", "4160", "4170", "4180", "4190", "4200", "4210"); assertThat(queryLong(schema, "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = current_schema() " + "AND table_name = 'education_idempotency'")) @@ -171,6 +173,31 @@ class EducationFlywayMigrationIntegrationTest { .isEqualTo(2L); } + @Test + void shouldRejectIncompatibleAdoptedPracticeScalarShape() throws SQLException { + String schema = createSchema("bad_shape"); + createCompatibleManualPracticeFixture(schema); + execute(schema, "ALTER TABLE education_practice_session ALTER COLUMN tenant_id DROP NOT NULL"); + + assertThatThrownBy(() -> configureFlyway(schema, true).load().migrate()) + .hasMessageContaining("Incompatible adopted Education Practice schema") + .hasMessageContaining("education_practice_session.tenant_id"); + assertThat(queryLong(schema, + "SELECT COUNT(*) FROM flyway_schema_history WHERE version = '4180' AND success = TRUE")) + .isZero(); + } + + @Test + void shouldRejectUndersizedAdoptedPracticeIdentifier() throws SQLException { + String schema = createSchema("bad_length"); + createCompatibleManualPracticeFixture(schema); + execute(schema, "ALTER TABLE education_practice_session ALTER COLUMN client_session_id TYPE VARCHAR(16)"); + + assertThatThrownBy(() -> configureFlyway(schema, true).load().migrate()) + .hasMessageContaining("Incompatible adopted Education Practice schema") + .hasMessageContaining("education_practice_session.client_session_id"); + } + @Test void shouldCreateClassRelationshipSchemaAndEnforceTenantBoundary() throws SQLException { String schema = createSchema("classes"); @@ -211,7 +238,7 @@ class EducationFlywayMigrationIntegrationTest { assertThat(queryStrings(schema, "SELECT version FROM flyway_schema_history WHERE success = TRUE ORDER BY installed_rank")) - .containsExactly("4010", "4020", "4030", "4040", "4050", "4060", "4070", "4080", "4090", "4100", "4110", "4120", "4130", "4140", "4150", "4160", "4170"); + .containsExactly("4010", "4020", "4030", "4040", "4050", "4060", "4070", "4080", "4090", "4100", "4110", "4120", "4130", "4140", "4150", "4160", "4170", "4180", "4190", "4200", "4210"); assertThat(queryStrings(schema, "SELECT table_name FROM information_schema.tables " + "WHERE table_schema = current_schema() AND table_name IN (" + @@ -748,6 +775,62 @@ class EducationFlywayMigrationIntegrationTest { } } + @Test + void shouldRejectPhysicalDeleteOfAdoptedEmptyPublicCollectionForRuntimeRole() throws SQLException { + String schema = createSchema("public_collection_delete"); + configureFlyway(schema, false).target("4100").load().migrate(); + execute(schema, """ + INSERT INTO education_content_entry(id,tenant_id,scope,entry_key,name,entry_type) + VALUES(100,0,'PUBLIC','questions','Questions','question'); + INSERT INTO education_content_node(id,tenant_id,scope,entry_id,name,node_type,publication_status,is_active,is_hidden) + VALUES(200,0,'PUBLIC',100,'Public node','category','ACTIVE',true,false); + INSERT INTO education_question_collection(id,tenant_id,scope,entry_id,node_id,name,collection_type,question_count,is_active,is_hidden) + VALUES(300,0,'PUBLIC',100,200,'Adopted empty','MANUAL',0,true,false); + """); + configureFlyway(schema, false).load().migrate(); + + String runtimeRole = "edu_delete_runtime_" + UUID.randomUUID().toString().replace("-", ""); + try { + execute(schema, "CREATE ROLE " + runtimeRole + " LOGIN PASSWORD 'runtime_test'"); + execute(schema, "GRANT USAGE ON SCHEMA " + schema + " TO " + runtimeRole); + execute(schema, "GRANT SELECT, DELETE ON " + schema + ".education_question_collection TO " + runtimeRole); + try (Connection connection = DriverManager.getConnection(jdbcUrl(schema), runtimeRole, "runtime_test"); + var statement = connection.createStatement()) { + assertThatThrownBy(() -> statement.execute("DELETE FROM education_question_collection WHERE id=300")) + .hasMessageContaining("collection cannot be physically deleted"); + } + } finally { + execute(schema, "DROP OWNED BY " + runtimeRole + "; DROP ROLE " + runtimeRole); + } + } + + @Test + void shouldRejectRuntimeRoleThatOwnsProtectedTokenTablesAndAcceptSeparatedRole() throws SQLException { + String schema = createSchema("token_role_separation"); + configureFlyway(schema, false).load().migrate(); + PGSimpleDataSource ownerDataSource = dataSource(schema, USER, PASSWORD); + assertThatThrownBy(() -> EducationProtectedTokenRoleValidator.validate(ownerDataSource)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("education_question_lifecycle_transition_token") + .hasMessageContaining("education_content_node_lifecycle_transition_token") + .hasMessageContaining("education_question_collection_lifecycle_transition_token") + .hasMessageContaining("education_question_collection_membership_token"); + + String runtimeRole = "edu_safe_runtime_" + UUID.randomUUID().toString().replace("-", ""); + try { + execute(schema, "CREATE ROLE " + runtimeRole + " LOGIN PASSWORD 'runtime_test'"); + execute(schema, "GRANT USAGE ON SCHEMA " + schema + " TO " + runtimeRole); + execute(schema, "GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA " + schema + " TO " + runtimeRole); + execute(schema, "REVOKE ALL ON " + schema + ".education_question_lifecycle_transition_token, " + + schema + ".education_content_node_lifecycle_transition_token, " + + schema + ".education_question_collection_lifecycle_transition_token, " + + schema + ".education_question_collection_membership_token FROM " + runtimeRole); + EducationProtectedTokenRoleValidator.validate(dataSource(schema, runtimeRole, "runtime_test")); + } finally { + execute(schema, "DROP OWNED BY " + runtimeRole + "; DROP ROLE " + runtimeRole); + } + } + @Test void shouldUseMigrationLocksThatBlockMembershipAndQuestionWrites() throws SQLException { String schema = createSchema("collection_lock_modes"); @@ -1335,6 +1418,14 @@ class EducationFlywayMigrationIntegrationTest { } } + private static PGSimpleDataSource dataSource(String schema, String user, String password) { + PGSimpleDataSource dataSource = new PGSimpleDataSource(); + dataSource.setUrl(jdbcUrl(schema)); + dataSource.setUser(user); + dataSource.setPassword(password); + return dataSource; + } + private static String jdbcUrl(String schema) { return adminUrl() + "?currentSchema=" + schema + "&stringtype=unspecified"; } diff --git a/yudao-module-education/src/test/java/cn/iocoder/yudao/module/education/test/PostgreSqlDbIntegrationTest.java b/yudao-module-education/src/test/java/cn/iocoder/yudao/module/education/test/PostgreSqlDbIntegrationTest.java index f0b7fd2a..52d1bd07 100644 --- a/yudao-module-education/src/test/java/cn/iocoder/yudao/module/education/test/PostgreSqlDbIntegrationTest.java +++ b/yudao-module-education/src/test/java/cn/iocoder/yudao/module/education/test/PostgreSqlDbIntegrationTest.java @@ -47,15 +47,21 @@ public abstract class PostgreSqlDbIntegrationTest { private static final String USER = requiredEnv("EDU_TEST_POSTGRES_USER"); private static final String PASSWORD = requiredEnv("EDU_TEST_POSTGRES_PASSWORD"); private static final String SCHEMA = "edu_test_" + UUID.randomUUID().toString().replace("-", ""); + private static final String ROLE_SUFFIX = UUID.randomUUID().toString().replace("-", ""); + private static final String FLYWAY_ROLE = "edu_flyway_" + ROLE_SUFFIX; + private static final String RUNTIME_ROLE = "edu_runtime_" + ROLE_SUFFIX; + private static final String ROLE_PASSWORD = "education_test"; @BeforeAll static void createSchema() throws SQLException { try (var connection = DriverManager.getConnection(adminUrl(), USER, PASSWORD); var statement = connection.createStatement()) { - statement.execute("CREATE SCHEMA " + SCHEMA); + statement.execute("CREATE ROLE " + FLYWAY_ROLE + " LOGIN PASSWORD '" + ROLE_PASSWORD + "'"); + statement.execute("CREATE ROLE " + RUNTIME_ROLE + " LOGIN PASSWORD '" + ROLE_PASSWORD + "'"); + statement.execute("CREATE SCHEMA " + SCHEMA + " AUTHORIZATION " + FLYWAY_ROLE); } Flyway.configure() - .dataSource(jdbcUrl(), USER, PASSWORD) + .dataSource(jdbcUrl(FLYWAY_ROLE), FLYWAY_ROLE, ROLE_PASSWORD) .locations("classpath:db/migration/education") .schemas(SCHEMA) .defaultSchema(SCHEMA) @@ -65,6 +71,16 @@ public abstract class PostgreSqlDbIntegrationTest { .outOfOrder(false) .load() .migrate(); + try (var connection = DriverManager.getConnection(adminUrl(), USER, PASSWORD); + var statement = connection.createStatement()) { + statement.execute("GRANT USAGE ON SCHEMA " + SCHEMA + " TO " + RUNTIME_ROLE); + statement.execute("GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE ON ALL TABLES IN SCHEMA " + SCHEMA + " TO " + RUNTIME_ROLE); + statement.execute("GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA " + SCHEMA + " TO " + RUNTIME_ROLE); + statement.execute("REVOKE ALL ON " + SCHEMA + ".education_question_lifecycle_transition_token, " + + SCHEMA + ".education_content_node_lifecycle_transition_token, " + + SCHEMA + ".education_question_collection_lifecycle_transition_token, " + + SCHEMA + ".education_question_collection_membership_token FROM " + RUNTIME_ROLE); + } } @AfterAll @@ -72,6 +88,10 @@ public abstract class PostgreSqlDbIntegrationTest { try (var connection = DriverManager.getConnection(adminUrl(), USER, PASSWORD); var statement = connection.createStatement()) { statement.execute("DROP SCHEMA IF EXISTS " + SCHEMA + " CASCADE"); + statement.execute("DROP OWNED BY " + RUNTIME_ROLE); + statement.execute("DROP ROLE " + RUNTIME_ROLE); + statement.execute("DROP OWNED BY " + FLYWAY_ROLE); + statement.execute("DROP ROLE " + FLYWAY_ROLE); } } @@ -79,14 +99,18 @@ public abstract class PostgreSqlDbIntegrationTest { static void postgresqlProperties(DynamicPropertyRegistry registry) { registry.add("spring.datasource.url", PostgreSqlDbIntegrationTest::jdbcUrl); registry.add("spring.datasource.driver-class-name", () -> "org.postgresql.Driver"); - registry.add("spring.datasource.username", () -> USER); - registry.add("spring.datasource.password", () -> PASSWORD); + registry.add("spring.datasource.username", () -> RUNTIME_ROLE); + registry.add("spring.datasource.password", () -> ROLE_PASSWORD); registry.add("spring.sql.init.mode", () -> "never"); } private static String jdbcUrl() { + return jdbcUrl(RUNTIME_ROLE); + } + + private static String jdbcUrl(String role) { return "jdbc:postgresql://" + HOST + ":" + PORT + "/" + DATABASE - + "?currentSchema=" + SCHEMA + "&stringtype=unspecified"; + + "?currentSchema=" + SCHEMA + "&stringtype=unspecified&ApplicationName=" + role; } private static String adminUrl() { diff --git a/yudao-module-education/src/test/resources/sql/postgresql/clean.sql b/yudao-module-education/src/test/resources/sql/postgresql/clean.sql index f94c85a4..28f38b52 100644 --- a/yudao-module-education/src/test/resources/sql/postgresql/clean.sql +++ b/yudao-module-education/src/test/resources/sql/postgresql/clean.sql @@ -16,7 +16,6 @@ TRUNCATE TABLE education_content_import_asset, education_question_collection_lifecycle_audit, education_content_node_lifecycle_audit, - education_question_lifecycle_transition_token, education_question_lifecycle_audit, education_question_version, education_question_collection_question, @@ -33,4 +32,4 @@ TRUNCATE TABLE education_content_node, education_content_entry, education_subject - RESTART IDENTITY CASCADE; + CASCADE; diff --git a/yudao-server/src/main/resources/application-dev.yaml b/yudao-server/src/main/resources/application-dev.yaml index 0ef53aad..d6beb39e 100644 --- a/yudao-server/src/main/resources/application-dev.yaml +++ b/yudao-server/src/main/resources/application-dev.yaml @@ -26,8 +26,8 @@ spring: flyway: enabled: true url: ${FLYWAY_URL:${spring.datasource.dynamic.datasource.master.url}} - user: ${FLYWAY_USER:${spring.datasource.dynamic.datasource.master.username}} - password: ${FLYWAY_PASSWORD:${spring.datasource.dynamic.datasource.master.password}} + user: ${FLYWAY_USER} + password: ${FLYWAY_PASSWORD} locations: classpath:db/migration table: flyway_schema_history baseline-on-migrate: true diff --git a/yudao-server/src/main/resources/application-local.yaml b/yudao-server/src/main/resources/application-local.yaml index c691c2ef..2536de6f 100644 --- a/yudao-server/src/main/resources/application-local.yaml +++ b/yudao-server/src/main/resources/application-local.yaml @@ -26,8 +26,8 @@ spring: flyway: enabled: true url: ${FLYWAY_URL:${spring.datasource.dynamic.datasource.master.url}} - user: ${FLYWAY_USER:${spring.datasource.dynamic.datasource.master.username}} - password: ${FLYWAY_PASSWORD:${spring.datasource.dynamic.datasource.master.password}} + user: ${FLYWAY_USER} + password: ${FLYWAY_PASSWORD} locations: classpath:db/migration table: flyway_schema_history baseline-on-migrate: true