diff --git a/src/main/java/com/yau/digitalrmb/platformintegration/application/PlatformIdentityRepository.java b/src/main/java/com/yau/digitalrmb/platformintegration/application/PlatformIdentityRepository.java new file mode 100644 index 0000000..d5b935d --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/platformintegration/application/PlatformIdentityRepository.java @@ -0,0 +1,11 @@ +package com.yau.digitalrmb.platformintegration.application; + +import com.yau.digitalrmb.platformintegration.domain.PlatformActor; + +import java.util.Optional; + +public interface PlatformIdentityRepository { + Optional findByPlatformUserId(long platformUserId); + + Optional findBySchoolAccount(String schoolAccount); +} diff --git a/src/main/java/com/yau/digitalrmb/platformintegration/domain/PlatformActor.java b/src/main/java/com/yau/digitalrmb/platformintegration/domain/PlatformActor.java new file mode 100644 index 0000000..024db8b --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/platformintegration/domain/PlatformActor.java @@ -0,0 +1,11 @@ +package com.yau.digitalrmb.platformintegration.domain; + +import java.time.Instant; + +public record PlatformActor( + long platformUserId, + String account, + String displayName, + PlatformRole role, + Instant tokenSigningTime) { +} diff --git a/src/main/java/com/yau/digitalrmb/platformintegration/infrastructure/JdbcPlatformIdentityRepository.java b/src/main/java/com/yau/digitalrmb/platformintegration/infrastructure/JdbcPlatformIdentityRepository.java new file mode 100644 index 0000000..239f7fb --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/platformintegration/infrastructure/JdbcPlatformIdentityRepository.java @@ -0,0 +1,65 @@ +package com.yau.digitalrmb.platformintegration.infrastructure; + +import com.yau.digitalrmb.platformintegration.application.PlatformIdentityRepository; +import com.yau.digitalrmb.platformintegration.domain.PlatformActor; +import com.yau.digitalrmb.platformintegration.domain.PlatformRole; +import org.springframework.jdbc.core.simple.JdbcClient; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Repository; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Optional; + +@Repository +public class JdbcPlatformIdentityRepository implements PlatformIdentityRepository { + private static final String TEACHER_QUERY = """ + SELECT cu.ID, cu.CODE, cu.NAME, t.add_time AS signing_time + FROM core_user cu JOIN teacher t ON t.user_id = cu.ID + WHERE cu.JOB_TYPE1 = 'JT_S_02' AND cu.STATE = 'S1' AND cu.DEL_FLAG = 0 + AND t.teacher_status = 1 AND t.add_time IS NOT NULL AND %s + """; + private static final String STUDENT_QUERY = """ + SELECT cu.ID, cu.CODE, cu.NAME, s.add_time AS signing_time + FROM core_user cu JOIN student s ON s.user_id = cu.ID + WHERE cu.JOB_TYPE1 = 'JT_S_03' AND cu.STATE = 'S1' AND cu.DEL_FLAG = 0 + AND s.student_status = 1 AND s.add_time IS NOT NULL AND %s + """; + + private final JdbcClient jdbcClient; + + public JdbcPlatformIdentityRepository(@Qualifier("platformJdbcClient") JdbcClient jdbcClient) { + this.jdbcClient = jdbcClient; + } + + @Override + public Optional findByPlatformUserId(long platformUserId) { + return findBy("cu.ID = :value", platformUserId); + } + + @Override + public Optional findBySchoolAccount(String schoolAccount) { + return findBy("cu.CODE = :value", schoolAccount); + } + + private Optional findBy(String predicate, Object value) { + Optional teacher = query(TEACHER_QUERY.formatted(predicate), value, PlatformRole.TEACHER); + return teacher.isPresent() ? teacher : query(STUDENT_QUERY.formatted(predicate), value, PlatformRole.STUDENT); + } + + private Optional query(String sql, Object value, PlatformRole role) { + return jdbcClient.sql(sql) + .param("value", value) + .query((resultSet, rowNumber) -> map(resultSet, role)) + .optional(); + } + + private PlatformActor map(ResultSet resultSet, PlatformRole role) throws SQLException { + return new PlatformActor( + resultSet.getLong("ID"), + resultSet.getString("CODE"), + resultSet.getString("NAME"), + role, + resultSet.getTimestamp("signing_time").toInstant()); + } +} diff --git a/src/main/java/com/yau/digitalrmb/platformintegration/infrastructure/PlatformReadOnlyDataSourceConfig.java b/src/main/java/com/yau/digitalrmb/platformintegration/infrastructure/PlatformReadOnlyDataSourceConfig.java new file mode 100644 index 0000000..e3adf36 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/platformintegration/infrastructure/PlatformReadOnlyDataSourceConfig.java @@ -0,0 +1,31 @@ +package com.yau.digitalrmb.platformintegration.infrastructure; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import com.yau.digitalrmb.platformintegration.config.PlatformIntegrationProperties; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.core.simple.JdbcClient; + +import javax.sql.DataSource; + +@Configuration(proxyBeanMethods = false) +public class PlatformReadOnlyDataSourceConfig { + @Bean(name = "platformReadOnlyDataSource", destroyMethod = "close") + public HikariDataSource platformReadOnlyDataSource(PlatformIntegrationProperties properties) { + HikariConfig config = new HikariConfig(); + config.setJdbcUrl(properties.getDatasource().getUrl()); + config.setUsername(properties.getDatasource().getUsername()); + config.setPassword(properties.getDatasource().getPassword()); + config.setReadOnly(true); + config.setMaximumPoolSize(5); + config.setPoolName("platform-readonly"); + return new HikariDataSource(config); + } + + @Bean(name = "platformJdbcClient") + public JdbcClient platformJdbcClient(@Qualifier("platformReadOnlyDataSource") DataSource dataSource) { + return JdbcClient.create(dataSource); + } +} diff --git a/src/test/java/com/yau/digitalrmb/platformintegration/infrastructure/JdbcPlatformIdentityRepositoryTest.java b/src/test/java/com/yau/digitalrmb/platformintegration/infrastructure/JdbcPlatformIdentityRepositoryTest.java new file mode 100644 index 0000000..d54a8f7 --- /dev/null +++ b/src/test/java/com/yau/digitalrmb/platformintegration/infrastructure/JdbcPlatformIdentityRepositoryTest.java @@ -0,0 +1,65 @@ +package com.yau.digitalrmb.platformintegration.infrastructure; + +import com.yau.digitalrmb.platformintegration.application.PlatformIdentityRepository; +import com.yau.digitalrmb.platformintegration.domain.PlatformActor; +import com.yau.digitalrmb.platformintegration.domain.PlatformRole; +import org.h2.jdbcx.JdbcDataSource; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.jdbc.core.simple.JdbcClient; + +import javax.sql.DataSource; +import java.sql.Timestamp; +import java.time.Instant; + +import static org.assertj.core.api.Assertions.assertThat; + +class JdbcPlatformIdentityRepositoryTest { + private DataSource dataSource; + private PlatformIdentityRepository repository; + + @BeforeEach + void setUp() throws Exception { + JdbcDataSource source = new JdbcDataSource(); + source.setURL("jdbc:h2:mem:platform_identity;MODE=MySQL;DB_CLOSE_DELAY=-1"); + source.setUser("sa"); + dataSource = source; + execute("DROP ALL OBJECTS"); + createSchema(); + repository = new JdbcPlatformIdentityRepository(JdbcClient.create(dataSource)); + } + + @Test + void resolvesEnabledTeacherFromCoreUserAndTeacherProfile() throws Exception { + execute("INSERT INTO core_user(ID, CODE, NAME, STATE, JOB_TYPE1, DEL_FLAG) VALUES (101, 't001', '教师甲', 'S1', 'JT_S_02', 0)"); + execute("INSERT INTO teacher(teacher_id, user_id, teacher_status, add_time) VALUES (1, 101, 1, '2026-01-01 00:00:00')"); + + PlatformActor actor = repository.findByPlatformUserId(101L).orElseThrow(); + + assertThat(actor.account()).isEqualTo("t001"); + assertThat(actor.role()).isEqualTo(PlatformRole.TEACHER); + assertThat(actor.tokenSigningTime()).isEqualTo(Instant.parse("2025-12-31T16:00:00Z")); + } + + @Test + void excludesDisabledOrUnsupportedUsers() throws Exception { + execute("INSERT INTO core_user(ID, CODE, NAME, STATE, JOB_TYPE1, DEL_FLAG) VALUES (201, 's001', '学生甲', 'S1', 'JT_S_03', 0)"); + execute("INSERT INTO student(student_id, user_id, student_status, add_time) VALUES (1, 201, 2, '2026-01-01 00:00:00')"); + execute("INSERT INTO core_user(ID, CODE, NAME, STATE, JOB_TYPE1, DEL_FLAG) VALUES (202, 'admin', '管理员', 'S1', 'JT_S_01', 0)"); + + assertThat(repository.findByPlatformUserId(201L)).isEmpty(); + assertThat(repository.findByPlatformUserId(202L)).isEmpty(); + } + + private void createSchema() throws Exception { + execute("CREATE TABLE core_user(ID BIGINT PRIMARY KEY, CODE VARCHAR(64), NAME VARCHAR(64), STATE VARCHAR(16), JOB_TYPE1 VARCHAR(16), DEL_FLAG INT)"); + execute("CREATE TABLE teacher(teacher_id BIGINT PRIMARY KEY, user_id BIGINT, teacher_status INT, add_time TIMESTAMP)"); + execute("CREATE TABLE student(student_id BIGINT PRIMARY KEY, user_id BIGINT, student_status INT, add_time TIMESTAMP)"); + } + + private void execute(String sql) throws Exception { + try (var connection = dataSource.getConnection(); var statement = connection.createStatement()) { + statement.execute(sql); + } + } +} diff --git a/src/test/java/com/yau/digitalrmb/platformintegration/infrastructure/PlatformReadOnlyDataSourceConfigTest.java b/src/test/java/com/yau/digitalrmb/platformintegration/infrastructure/PlatformReadOnlyDataSourceConfigTest.java new file mode 100644 index 0000000..8b3a25e --- /dev/null +++ b/src/test/java/com/yau/digitalrmb/platformintegration/infrastructure/PlatformReadOnlyDataSourceConfigTest.java @@ -0,0 +1,43 @@ +package com.yau.digitalrmb.platformintegration.infrastructure; + +import com.zaxxer.hikari.HikariDataSource; +import com.yau.digitalrmb.platformintegration.config.PlatformIntegrationProperties; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.core.simple.JdbcClient; + +import static org.assertj.core.api.Assertions.assertThat; + +class PlatformReadOnlyDataSourceConfigTest { + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(PropertiesConfiguration.class, PlatformReadOnlyDataSourceConfig.class)) + .withPropertyValues( + "platform-integration.datasource.url=jdbc:h2:mem:platform_config", + "platform-integration.datasource.username=sa", + "platform-integration.datasource.password=test-password", + "platform-integration.token.max-age=PT2M", + "platform-integration.token.teacher-claim-value=teacher", + "platform-integration.token.student-claim-value=student", + "platform-integration.cas.login-url=https://sso.example.edu/login", + "platform-integration.cas.validate-url=https://sso.example.edu/p3/serviceValidate", + "platform-integration.cas.callback-url=https://rmb.example.edu/api/v1/auth/cas/callback", + "platform-integration.frontend.callback-url=https://rmb.example.edu/sso-callback"); + + @Test + void createsNamedReadOnlyDatasourceAndJdbcClient() { + contextRunner.run(context -> { + HikariDataSource dataSource = context.getBean("platformReadOnlyDataSource", HikariDataSource.class); + + assertThat(dataSource.isReadOnly()).isTrue(); + assertThat(context.getBean("platformJdbcClient", JdbcClient.class)).isNotNull(); + }); + } + + @Configuration(proxyBeanMethods = false) + @EnableConfigurationProperties(PlatformIntegrationProperties.class) + static class PropertiesConfiguration { + } +}