feat: add readonly platform identity projection
parent
977d7eb6ce
commit
e84d424c42
@ -0,0 +1,85 @@
|
||||
package com.yau.digitalrmb.identity.application;
|
||||
|
||||
import com.yau.digitalrmb.identity.infrastructure.persistence.entity.PlatformUserSnapshotEntity;
|
||||
import com.yau.digitalrmb.identity.infrastructure.persistence.entity.UserEntity;
|
||||
import com.yau.digitalrmb.identity.infrastructure.persistence.mapper.PlatformUserSnapshotMapper;
|
||||
import com.yau.digitalrmb.identity.infrastructure.persistence.mapper.UserMapper;
|
||||
import com.yau.digitalrmb.platformintegration.domain.PlatformActor;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
|
||||
@Service
|
||||
public class PlatformIdentityProjectionService {
|
||||
private static final String EXTERNAL_SSO_ONLY_PASSWORD = "{noop}EXTERNAL_SSO_ONLY";
|
||||
|
||||
private final UserMapper userMapper;
|
||||
private final PlatformUserSnapshotMapper snapshotMapper;
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
public PlatformIdentityProjectionService(UserMapper userMapper,
|
||||
PlatformUserSnapshotMapper snapshotMapper,
|
||||
JdbcTemplate jdbcTemplate) {
|
||||
this.userMapper = userMapper;
|
||||
this.snapshotMapper = snapshotMapper;
|
||||
this.jdbcTemplate = jdbcTemplate;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void project(PlatformActor actor) {
|
||||
projectUser(actor);
|
||||
projectSnapshot(actor);
|
||||
projectRole(actor);
|
||||
}
|
||||
|
||||
private void projectUser(PlatformActor actor) {
|
||||
UserEntity user = userMapper.selectById(actor.platformUserId());
|
||||
if (user == null) {
|
||||
user = new UserEntity();
|
||||
user.setId(actor.platformUserId());
|
||||
user.setUsername(actor.account());
|
||||
user.setPasswordHash(EXTERNAL_SSO_ONLY_PASSWORD);
|
||||
user.setEnabled(true);
|
||||
userMapper.insert(user);
|
||||
return;
|
||||
}
|
||||
user.setUsername(actor.account());
|
||||
user.setEnabled(true);
|
||||
userMapper.updateById(user);
|
||||
}
|
||||
|
||||
private void projectSnapshot(PlatformActor actor) {
|
||||
PlatformUserSnapshotEntity snapshot = snapshotMapper.selectById(actor.platformUserId());
|
||||
boolean newSnapshot = snapshot == null;
|
||||
if (newSnapshot) {
|
||||
snapshot = new PlatformUserSnapshotEntity();
|
||||
snapshot.setPlatformUserId(actor.platformUserId());
|
||||
}
|
||||
snapshot.setAccount(actor.account());
|
||||
snapshot.setDisplayName(actor.displayName());
|
||||
snapshot.setRoleKey(actor.role().name());
|
||||
snapshot.setSourceUpdatedAt(LocalDateTime.ofInstant(actor.tokenSigningTime(), ZoneOffset.UTC));
|
||||
snapshot.setSyncedAt(LocalDateTime.now(ZoneOffset.UTC));
|
||||
if (newSnapshot) {
|
||||
snapshotMapper.insert(snapshot);
|
||||
} else {
|
||||
snapshotMapper.updateById(snapshot);
|
||||
}
|
||||
}
|
||||
|
||||
private void projectRole(PlatformActor actor) {
|
||||
jdbcTemplate.update("DELETE FROM sys_user_role WHERE user_id = ?", actor.platformUserId());
|
||||
jdbcTemplate.update("INSERT INTO sys_user_role (user_id, role_id) VALUES (?, ?)",
|
||||
actor.platformUserId(), roleId(actor));
|
||||
}
|
||||
|
||||
private long roleId(PlatformActor actor) {
|
||||
return switch (actor.role()) {
|
||||
case TEACHER -> 1001L;
|
||||
case STUDENT -> 1002L;
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
package com.yau.digitalrmb.identity.infrastructure.persistence.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@TableName("platform_user_snapshot")
|
||||
public class PlatformUserSnapshotEntity {
|
||||
@TableId(value = "platform_user_id", type = IdType.INPUT)
|
||||
private Long platformUserId;
|
||||
|
||||
private String account;
|
||||
|
||||
private String displayName;
|
||||
|
||||
private String roleKey;
|
||||
|
||||
private LocalDateTime sourceUpdatedAt;
|
||||
|
||||
@TableField("synced_at")
|
||||
private LocalDateTime syncedAt;
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
package com.yau.digitalrmb.identity.infrastructure.persistence.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.yau.digitalrmb.identity.infrastructure.persistence.entity.PlatformUserSnapshotEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface PlatformUserSnapshotMapper extends BaseMapper<PlatformUserSnapshotEntity> {
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
CREATE TABLE platform_user_snapshot (
|
||||
platform_user_id BIGINT PRIMARY KEY,
|
||||
account VARCHAR(64) NOT NULL,
|
||||
display_name VARCHAR(64) NOT NULL,
|
||||
role_key VARCHAR(16) NOT NULL,
|
||||
source_updated_at TIMESTAMP NOT NULL,
|
||||
synced_at TIMESTAMP NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE auth_login_exchange_code (
|
||||
code_hash CHAR(64) PRIMARY KEY,
|
||||
platform_user_id BIGINT NOT NULL,
|
||||
expires_at TIMESTAMP NOT NULL,
|
||||
consumed_at TIMESTAMP NULL
|
||||
);
|
||||
|
||||
CREATE TABLE auth_refresh_token (
|
||||
token_hash CHAR(64) PRIMARY KEY,
|
||||
platform_user_id BIGINT NOT NULL,
|
||||
expires_at TIMESTAMP NOT NULL,
|
||||
revoked_at TIMESTAMP NULL
|
||||
);
|
||||
|
||||
INSERT INTO sys_role (id, name, created_at, updated_at, created_by, updated_by, deleted)
|
||||
VALUES (1001, 'TEACHER', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'SYSTEM', 'SYSTEM', FALSE);
|
||||
|
||||
INSERT INTO sys_role (id, name, created_at, updated_at, created_by, updated_by, deleted)
|
||||
VALUES (1002, 'STUDENT', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'SYSTEM', 'SYSTEM', FALSE);
|
||||
@ -0,0 +1,43 @@
|
||||
package com.yau.digitalrmb.identity;
|
||||
|
||||
import com.yau.digitalrmb.identity.application.PlatformIdentityProjectionService;
|
||||
import com.yau.digitalrmb.platformintegration.domain.PlatformActor;
|
||||
import com.yau.digitalrmb.platformintegration.domain.PlatformRole;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.time.Instant;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@SpringBootTest
|
||||
@ActiveProfiles("test")
|
||||
class PlatformIdentityProjectionServiceTest {
|
||||
@Autowired
|
||||
private PlatformIdentityProjectionService projectionService;
|
||||
|
||||
@Autowired
|
||||
private DataSource dataSource;
|
||||
|
||||
@Test
|
||||
void projectionIsIdempotentAndOwnsExactlyOneRole() {
|
||||
PlatformActor teacher = new PlatformActor(101L, 1L, "t001", "教师甲", PlatformRole.TEACHER,
|
||||
Instant.parse("2026-01-01T00:00:00Z"));
|
||||
|
||||
projectionService.project(teacher);
|
||||
projectionService.project(teacher);
|
||||
|
||||
JdbcTemplate jdbc = new JdbcTemplate(dataSource);
|
||||
Integer associationCount = jdbc.queryForObject("SELECT COUNT(*) FROM sys_user_role WHERE user_id = 101", Integer.class);
|
||||
String role = jdbc.queryForObject("SELECT role_key FROM platform_user_snapshot WHERE platform_user_id = 101", String.class);
|
||||
String userName = jdbc.queryForObject("SELECT username FROM sys_user WHERE id = 101", String.class);
|
||||
|
||||
assertThat(associationCount).isEqualTo(1);
|
||||
assertThat(role).isEqualTo("TEACHER");
|
||||
assertThat(userName).isEqualTo("t001");
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue