diff --git a/pom.xml b/pom.xml
index d170367..605b83f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -57,6 +57,11 @@
mybatis-plus-spring-boot4-starter
${mybatis-plus.version}
+
+ com.baomidou
+ mybatis-plus-jsqlparser
+ ${mybatis-plus.version}
+
com.mysql
mysql-connector-j
diff --git a/src/main/java/com/yau/digitalrmb/identity/infrastructure/persistence/entity/RoleEntity.java b/src/main/java/com/yau/digitalrmb/identity/infrastructure/persistence/entity/RoleEntity.java
new file mode 100644
index 0000000..ed8ce43
--- /dev/null
+++ b/src/main/java/com/yau/digitalrmb/identity/infrastructure/persistence/entity/RoleEntity.java
@@ -0,0 +1,13 @@
+package com.yau.digitalrmb.identity.infrastructure.persistence.entity;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.yau.digitalrmb.shared.infrastructure.persistence.AuditableEntity;
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+@TableName("sys_role")
+public class RoleEntity extends AuditableEntity {
+ private String name;
+}
diff --git a/src/main/java/com/yau/digitalrmb/identity/infrastructure/persistence/entity/UserEntity.java b/src/main/java/com/yau/digitalrmb/identity/infrastructure/persistence/entity/UserEntity.java
new file mode 100644
index 0000000..fbde89b
--- /dev/null
+++ b/src/main/java/com/yau/digitalrmb/identity/infrastructure/persistence/entity/UserEntity.java
@@ -0,0 +1,19 @@
+package com.yau.digitalrmb.identity.infrastructure.persistence.entity;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.yau.digitalrmb.shared.infrastructure.persistence.AuditableEntity;
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+@TableName("sys_user")
+public class UserEntity extends AuditableEntity {
+ private String username;
+
+ @TableField("password_hash")
+ private String passwordHash;
+
+ private Boolean enabled;
+}
diff --git a/src/main/java/com/yau/digitalrmb/identity/infrastructure/persistence/mapper/RoleMapper.java b/src/main/java/com/yau/digitalrmb/identity/infrastructure/persistence/mapper/RoleMapper.java
new file mode 100644
index 0000000..433032b
--- /dev/null
+++ b/src/main/java/com/yau/digitalrmb/identity/infrastructure/persistence/mapper/RoleMapper.java
@@ -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.RoleEntity;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface RoleMapper extends BaseMapper {
+}
diff --git a/src/main/java/com/yau/digitalrmb/identity/infrastructure/persistence/mapper/UserMapper.java b/src/main/java/com/yau/digitalrmb/identity/infrastructure/persistence/mapper/UserMapper.java
new file mode 100644
index 0000000..4d5e01e
--- /dev/null
+++ b/src/main/java/com/yau/digitalrmb/identity/infrastructure/persistence/mapper/UserMapper.java
@@ -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.UserEntity;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface UserMapper extends BaseMapper {
+}
diff --git a/src/main/java/com/yau/digitalrmb/shared/config/MybatisPlusConfig.java b/src/main/java/com/yau/digitalrmb/shared/config/MybatisPlusConfig.java
new file mode 100644
index 0000000..7dfd431
--- /dev/null
+++ b/src/main/java/com/yau/digitalrmb/shared/config/MybatisPlusConfig.java
@@ -0,0 +1,50 @@
+package com.yau.digitalrmb.shared.config;
+
+import com.baomidou.mybatisplus.annotation.FieldFill;
+import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
+import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
+import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
+import com.baomidou.mybatisplus.annotation.DbType;
+import org.apache.ibatis.reflection.MetaObject;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+
+import java.time.LocalDateTime;
+
+@Configuration
+public class MybatisPlusConfig {
+ @Bean
+ public MybatisPlusInterceptor mybatisPlusInterceptor() {
+ MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
+ interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
+ return interceptor;
+ }
+
+ @Bean
+ public MetaObjectHandler metaObjectHandler() {
+ return new MetaObjectHandler() {
+ @Override
+ public void insertFill(MetaObject metaObject) {
+ LocalDateTime now = LocalDateTime.now();
+ String actor = currentActor();
+ strictInsertFill(metaObject, "createdAt", LocalDateTime.class, now);
+ strictInsertFill(metaObject, "updatedAt", LocalDateTime.class, now);
+ strictInsertFill(metaObject, "createdBy", String.class, actor);
+ strictInsertFill(metaObject, "updatedBy", String.class, actor);
+ }
+
+ @Override
+ public void updateFill(MetaObject metaObject) {
+ strictUpdateFill(metaObject, "updatedAt", LocalDateTime.class, LocalDateTime.now());
+ strictUpdateFill(metaObject, "updatedBy", String.class, currentActor());
+ }
+ };
+ }
+
+ private static String currentActor() {
+ Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+ return authentication == null || !authentication.isAuthenticated() ? "SYSTEM" : authentication.getName();
+ }
+}
diff --git a/src/main/java/com/yau/digitalrmb/shared/infrastructure/persistence/AuditableEntity.java b/src/main/java/com/yau/digitalrmb/shared/infrastructure/persistence/AuditableEntity.java
new file mode 100644
index 0000000..8f5c31b
--- /dev/null
+++ b/src/main/java/com/yau/digitalrmb/shared/infrastructure/persistence/AuditableEntity.java
@@ -0,0 +1,33 @@
+package com.yau.digitalrmb.shared.infrastructure.persistence;
+
+import com.baomidou.mybatisplus.annotation.FieldFill;
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.time.LocalDateTime;
+
+@Getter
+@Setter
+public abstract class AuditableEntity {
+ @TableId(type = IdType.ASSIGN_ID)
+ private Long id;
+
+ @TableField(value = "created_at", fill = FieldFill.INSERT)
+ private LocalDateTime createdAt;
+
+ @TableField(value = "updated_at", fill = FieldFill.INSERT_UPDATE)
+ private LocalDateTime updatedAt;
+
+ @TableField(value = "created_by", fill = FieldFill.INSERT)
+ private String createdBy;
+
+ @TableField(value = "updated_by", fill = FieldFill.INSERT_UPDATE)
+ private String updatedBy;
+
+ @TableLogic
+ private Boolean deleted;
+}
diff --git a/src/main/resources/db/migration/V1__create_identity_tables.sql b/src/main/resources/db/migration/V1__create_identity_tables.sql
new file mode 100644
index 0000000..6ed0fd3
--- /dev/null
+++ b/src/main/resources/db/migration/V1__create_identity_tables.sql
@@ -0,0 +1,29 @@
+CREATE TABLE sys_user (
+ id BIGINT PRIMARY KEY,
+ username VARCHAR(64) NOT NULL UNIQUE,
+ password_hash VARCHAR(100) NOT NULL,
+ enabled BOOLEAN NOT NULL,
+ created_at TIMESTAMP NOT NULL,
+ updated_at TIMESTAMP NOT NULL,
+ created_by VARCHAR(64) NOT NULL,
+ updated_by VARCHAR(64) NOT NULL,
+ deleted BOOLEAN NOT NULL DEFAULT FALSE
+);
+
+CREATE TABLE sys_role (
+ id BIGINT PRIMARY KEY,
+ name VARCHAR(64) NOT NULL UNIQUE,
+ created_at TIMESTAMP NOT NULL,
+ updated_at TIMESTAMP NOT NULL,
+ created_by VARCHAR(64) NOT NULL,
+ updated_by VARCHAR(64) NOT NULL,
+ deleted BOOLEAN NOT NULL DEFAULT FALSE
+);
+
+CREATE TABLE sys_user_role (
+ user_id BIGINT NOT NULL,
+ role_id BIGINT NOT NULL,
+ PRIMARY KEY (user_id, role_id),
+ CONSTRAINT fk_user_role_user FOREIGN KEY (user_id) REFERENCES sys_user(id),
+ CONSTRAINT fk_user_role_role FOREIGN KEY (role_id) REFERENCES sys_role(id)
+);
diff --git a/src/test/java/com/yau/digitalrmb/ApplicationContextTest.java b/src/test/java/com/yau/digitalrmb/ApplicationContextTest.java
index 488d509..952a65c 100644
--- a/src/test/java/com/yau/digitalrmb/ApplicationContextTest.java
+++ b/src/test/java/com/yau/digitalrmb/ApplicationContextTest.java
@@ -17,6 +17,8 @@ class ApplicationContextTest {
@Test
void baselineMigrationIsApplied() {
- assertThat(flyway.info().applied()).hasSize(1);
+ assertThat(flyway.info().applied())
+ .extracting(migration -> migration.getVersion().getVersion())
+ .contains("0");
}
}
diff --git a/src/test/java/com/yau/digitalrmb/identity/IdentityPersistenceTest.java b/src/test/java/com/yau/digitalrmb/identity/IdentityPersistenceTest.java
new file mode 100644
index 0000000..9e90465
--- /dev/null
+++ b/src/test/java/com/yau/digitalrmb/identity/IdentityPersistenceTest.java
@@ -0,0 +1,32 @@
+package com.yau.digitalrmb.identity;
+
+import com.yau.digitalrmb.identity.infrastructure.persistence.entity.UserEntity;
+import com.yau.digitalrmb.identity.infrastructure.persistence.mapper.UserMapper;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ActiveProfiles;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@SpringBootTest
+@ActiveProfiles("test")
+class IdentityPersistenceTest {
+ @Autowired
+ private UserMapper userMapper;
+
+ @Test
+ void mapperPersistsAuditedUser() {
+ UserEntity user = new UserEntity();
+ user.setUsername("operator");
+ user.setPasswordHash("hash");
+ user.setEnabled(true);
+
+ userMapper.insert(user);
+
+ assertThat(user.getId()).isNotNull();
+ assertThat(userMapper.selectById(user.getId()).getUsername()).isEqualTo("operator");
+ assertThat(user.getCreatedAt()).isNotNull();
+ assertThat(user.getCreatedBy()).isEqualTo("SYSTEM");
+ }
+}