refactor(security): 重构安全配置并优化测试环境

- 移除旧的测试套件和UAT测试文件
- 更新密码编码器配置使用BCrypt strength=12
- 添加用户角色关联表和相关服务
- 优化前端日期显示格式
- 清理无用资源和配置文件
- 增强测试数据管理和清理功能
This commit is contained in:
张翔
2026-03-27 13:00:22 +08:00
parent ce30893a96
commit af44c23f21
294 changed files with 16057 additions and 22601 deletions
@@ -0,0 +1,30 @@
package cn.novalon.manage.db.config;
import org.flywaydb.core.Flyway;
import org.springframework.boot.autoconfigure.flyway.FlywayProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import javax.sql.DataSource;
@Configuration
public class FlywayConfig {
@Bean
@Profile("!test")
public Flyway flyway(DataSource dataSource, FlywayProperties flywayProperties) {
Flyway flyway = Flyway.configure()
.dataSource(dataSource)
.locations(flywayProperties.getLocations().toArray(new String[0]))
.baselineOnMigrate(true)
.baselineVersion("0")
.table("flyway_schema_history")
.validateOnMigrate(true)
.outOfOrder(false)
.load();
flyway.migrate();
return flyway;
}
}
@@ -0,0 +1,37 @@
package cn.novalon.manage.db.converter;
import cn.novalon.manage.db.entity.UserRoleEntity;
import cn.novalon.manage.sys.core.domain.UserRole;
import org.springframework.stereotype.Component;
@Component
public class UserRoleConverter {
public UserRole toDomain(UserRoleEntity entity) {
if (entity == null) {
return null;
}
UserRole domain = new UserRole();
domain.setId(entity.getId());
domain.setUserId(entity.getUserId());
domain.setRoleId(entity.getRoleId());
domain.setCreatedAt(entity.getCreatedAt());
domain.setCreatedBy(entity.getCreatedBy());
return domain;
}
public UserRoleEntity toEntity(UserRole domain) {
if (domain == null) {
return null;
}
UserRoleEntity entity = new UserRoleEntity();
entity.setId(domain.getId());
entity.setUserId(domain.getUserId());
entity.setRoleId(domain.getRoleId());
entity.setCreatedAt(domain.getCreatedAt());
entity.setCreatedBy(domain.getCreatedBy());
return entity;
}
}
@@ -0,0 +1,26 @@
package cn.novalon.manage.db.dao;
import cn.novalon.manage.db.entity.UserRoleEntity;
import org.springframework.data.domain.Sort;
import org.springframework.data.r2dbc.repository.R2dbcRepository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public interface UserRoleDao extends R2dbcRepository<UserRoleEntity, Long> {
Flux<UserRoleEntity> findByUserId(Long userId);
Flux<UserRoleEntity> findByUserId(Long userId, Sort sort);
Flux<UserRoleEntity> findByRoleId(Long roleId);
Flux<UserRoleEntity> findByRoleId(Long roleId, Sort sort);
Mono<Long> countByUserId(Long userId);
Mono<Long> countByRoleId(Long roleId);
Mono<Void> deleteByUserId(Long userId);
Mono<Void> deleteByRoleId(Long roleId);
}
@@ -0,0 +1,66 @@
package cn.novalon.manage.db.entity;
import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.mapping.Column;
import org.springframework.data.relational.core.mapping.Table;
import java.time.LocalDateTime;
@Table("user_role")
public class UserRoleEntity {
@Id
private Long id;
@Column("user_id")
private Long userId;
@Column("role_id")
private Long roleId;
@Column("created_at")
private LocalDateTime createdAt;
@Column("created_by")
private String createdBy;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public Long getRoleId() {
return roleId;
}
public void setRoleId(Long roleId) {
this.roleId = roleId;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public String getCreatedBy() {
return createdBy;
}
public void setCreatedBy(String createdBy) {
this.createdBy = createdBy;
}
}
@@ -0,0 +1,79 @@
package cn.novalon.manage.db.repository;
import cn.novalon.manage.db.converter.UserRoleConverter;
import cn.novalon.manage.db.dao.UserRoleDao;
import cn.novalon.manage.db.entity.UserRoleEntity;
import cn.novalon.manage.sys.core.domain.UserRole;
import cn.novalon.manage.sys.core.repository.IUserRoleRepository;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@Repository
public class UserRoleRepository implements IUserRoleRepository {
private final UserRoleDao userRoleDao;
private final UserRoleConverter userRoleConverter;
public UserRoleRepository(UserRoleDao userRoleDao, UserRoleConverter userRoleConverter) {
this.userRoleDao = userRoleDao;
this.userRoleConverter = userRoleConverter;
}
@Override
public Mono<UserRole> save(UserRole userRole) {
UserRoleEntity entity = userRoleConverter.toEntity(userRole);
return userRoleDao.save(entity)
.map(userRoleConverter::toDomain);
}
@Override
public Mono<Void> deleteById(Long id) {
return userRoleDao.deleteById(id);
}
@Override
public Mono<Void> deleteByUserId(Long userId) {
return userRoleDao.deleteByUserId(userId);
}
@Override
public Mono<Void> deleteByRoleId(Long roleId) {
return userRoleDao.deleteByRoleId(roleId);
}
@Override
public Flux<UserRole> findByUserId(Long userId) {
return userRoleDao.findByUserId(userId, Sort.by(Sort.Direction.DESC, "created_at"))
.map(userRoleConverter::toDomain);
}
@Override
public Flux<UserRole> findByRoleId(Long roleId) {
return userRoleDao.findByRoleId(roleId, Sort.by(Sort.Direction.DESC, "created_at"))
.map(userRoleConverter::toDomain);
}
@Override
public Mono<Long> countByUserId(Long userId) {
return userRoleDao.countByUserId(userId);
}
@Override
public Mono<Long> countByRoleId(Long roleId) {
return userRoleDao.countByRoleId(roleId);
}
@Override
public Flux<UserRole> findAll() {
return userRoleDao.findAll()
.map(userRoleConverter::toDomain);
}
@Override
public Mono<UserRole> findById(Long id) {
return userRoleDao.findById(id)
.map(userRoleConverter::toDomain);
}
}
@@ -0,0 +1,9 @@
spring:
flyway:
enabled: true
locations: classpath:db/migration
baseline-on-migrate: true
baseline-version: 0
table: flyway_schema_history
validate-on-migrate: true
out-of-order: false
@@ -9,7 +9,6 @@ CREATE TABLE IF NOT EXISTS users (
password VARCHAR(255) NOT NULL,
email VARCHAR(100),
phone VARCHAR(20),
role_id BIGINT,
status INTEGER DEFAULT 1,
create_by VARCHAR(50),
update_by VARCHAR(50),
@@ -32,8 +31,8 @@ CREATE TABLE IF NOT EXISTS roles (
deleted_at TIMESTAMP
);
-- 菜单表
CREATE TABLE IF NOT EXISTS menus (
-- 菜单表(统一使用sys_menu表名)
CREATE TABLE IF NOT EXISTS sys_menu (
id BIGSERIAL PRIMARY KEY,
menu_name VARCHAR(50) NOT NULL,
parent_id BIGINT DEFAULT 0,
@@ -123,7 +122,7 @@ CREATE TABLE IF NOT EXISTS sys_login_log (
login_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 异常日志表(修复后的结构)
-- 异常日志表
CREATE TABLE IF NOT EXISTS sys_exception_log (
id BIGSERIAL PRIMARY KEY,
username VARCHAR(50),
@@ -234,3 +233,6 @@ COMMENT ON COLUMN sys_exception_log.exception_msg IS '异常消息';
COMMENT ON COLUMN sys_exception_log.exception_stack IS '异常堆栈';
COMMENT ON COLUMN sys_exception_log.ip IS 'IP地址';
COMMENT ON COLUMN sys_exception_log.create_time IS '创建时间';
COMMENT ON TABLE sys_menu IS '系统菜单表';
COMMENT ON TABLE sys_login_log IS '登录日志表';
@@ -9,8 +9,8 @@ ON CONFLICT (role_key) DO NOTHING;
-- 插入初始管理员用户
-- BCrypt哈希值对应明文密码: admin123
INSERT INTO users (id, username, password, email, phone, role_id, status, create_by, update_by)
VALUES (1, 'admin', '$2b$12$SFefXlGRFMA0fvxIufpWPuIAl0OPLgRDoCZPThCvjpiJGPYS8yNYy', 'admin@novalon.com', '13800138000', 1, 1, 'system', 'system')
INSERT INTO users (id, username, password, email, phone, status, create_by, update_by)
VALUES (1, 'admin', '$2b$12$SFefXlGRFMA0fvxIufpWPuIAl0OPLgRDoCZPThCvjpiJGPYS8yNYy', 'admin@novalon.com', '13800138000', 1, 'system', 'system')
ON CONFLICT (username) DO UPDATE SET
password = EXCLUDED.password,
status = EXCLUDED.status;
@@ -56,4 +56,4 @@ SELECT setval('users_id_seq', (SELECT COALESCE(MAX(id), 1) FROM users));
SELECT setval('roles_id_seq', (SELECT COALESCE(MAX(id), 1) FROM roles));
SELECT setval('sys_dict_type_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_dict_type));
SELECT setval('sys_dict_data_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_dict_data));
SELECT setval('sys_config_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_config));
SELECT setval('sys_config_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_config));
@@ -0,0 +1,23 @@
-- 创建用户角色关联表(支持多对多关系)
CREATE TABLE IF NOT EXISTS user_role (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
role_id BIGINT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_by VARCHAR(50),
CONSTRAINT fk_user_role_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
CONSTRAINT fk_user_role_role FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE,
CONSTRAINT uk_user_role UNIQUE (user_id, role_id)
);
-- 创建索引
CREATE INDEX IF NOT EXISTS idx_user_role_user_id ON user_role(user_id);
CREATE INDEX IF NOT EXISTS idx_user_role_role_id ON user_role(role_id);
-- 表注释
COMMENT ON TABLE user_role IS '用户角色关联表';
COMMENT ON COLUMN user_role.id IS '主键ID';
COMMENT ON COLUMN user_role.user_id IS '用户ID';
COMMENT ON COLUMN user_role.role_id IS '角色ID';
COMMENT ON COLUMN user_role.created_at IS '创建时间';
COMMENT ON COLUMN user_role.created_by IS '创建人';
@@ -1,11 +1,10 @@
-- Novalon管理系统索引优化脚本
-- 版本: V3
-- 版本: V5
-- 描述: 为表创建必要的索引以提升查询性能
-- 用户表索引
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
CREATE INDEX IF NOT EXISTS idx_users_role_id ON users(role_id);
CREATE INDEX IF NOT EXISTS idx_users_status ON users(status);
CREATE INDEX IF NOT EXISTS idx_users_deleted_at ON users(deleted_at);
@@ -15,9 +14,9 @@ CREATE INDEX IF NOT EXISTS idx_roles_status ON roles(status);
CREATE INDEX IF NOT EXISTS idx_roles_deleted_at ON roles(deleted_at);
-- 菜单表索引
CREATE INDEX IF NOT EXISTS idx_menus_parent_id ON menus(parent_id);
CREATE INDEX IF NOT EXISTS idx_menus_status ON menus(status);
CREATE INDEX IF NOT EXISTS idx_menus_deleted_at ON menus(deleted_at);
CREATE INDEX IF NOT EXISTS idx_sys_menu_parent_id ON sys_menu(parent_id);
CREATE INDEX IF NOT EXISTS idx_sys_menu_status ON sys_menu(status);
CREATE INDEX IF NOT EXISTS idx_sys_menu_deleted_at ON sys_menu(deleted_at);
-- 字典类型表索引
CREATE INDEX IF NOT EXISTS idx_sys_dict_type_dict_type ON sys_dict_type(dict_type);
@@ -76,4 +75,4 @@ CREATE INDEX IF NOT EXISTS idx_sys_file_deleted_at ON sys_file(deleted_at);
-- OAuth2客户端表索引
CREATE INDEX IF NOT EXISTS idx_oauth2_client_client_id ON oauth2_client(client_id);
CREATE INDEX IF NOT EXISTS idx_oauth2_client_enabled ON oauth2_client(enabled);
CREATE INDEX IF NOT EXISTS idx_oauth2_client_deleted_at ON oauth2_client(deleted_at);
CREATE INDEX IF NOT EXISTS idx_oauth2_client_deleted_at ON oauth2_client(deleted_at);
@@ -0,0 +1,90 @@
-- 系统菜单初始化数据
-- 版本: V6
-- 描述: 初始化系统菜单数据
-- 一级菜单
INSERT INTO sys_menu (id, parent_id, menu_name, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(1, 0, '系统管理', 1, 'M', NULL, NULL, 1, NOW(), NOW()),
(2, 0, '审计日志', 2, 'M', NULL, NULL, 1, NOW(), NOW()),
(3, 0, '系统监控', 3, 'M', NULL, NULL, 1, NOW(), NOW());
-- 系统管理子菜单
INSERT INTO sys_menu (id, parent_id, menu_name, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(11, 1, '用户管理', 1, 'C', 'system:user:list', 'system/user/index', 1, NOW(), NOW()),
(12, 1, '角色管理', 2, 'C', 'system:role:list', 'system/role/index', 1, NOW(), NOW()),
(13, 1, '菜单管理', 3, 'C', 'system:menu:list', 'system/menu/index', 1, NOW(), NOW()),
(14, 1, '部门管理', 4, 'C', 'system:dept:list', 'system/dept/index', 1, NOW(), NOW()),
(15, 1, '字典管理', 5, 'C', 'system:dict:list', 'system/dict/index', 1, NOW(), NOW()),
(16, 1, '参数管理', 6, 'C', 'system:config:list', 'system/config/index', 1, NOW(), NOW()),
(17, 1, '通知公告', 7, 'C', 'system:notice:list', 'system/notice/index', 1, NOW(), NOW()),
(18, 1, '文件管理', 8, 'C', 'system:file:list', 'system/file/index', 1, NOW(), NOW());
-- 用户管理按钮权限
INSERT INTO sys_menu (id, parent_id, menu_name, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(111, 11, '用户查询', 1, 'F', 'system:user:query', NULL, 1, NOW(), NOW()),
(112, 11, '用户新增', 2, 'F', 'system:user:add', NULL, 1, NOW(), NOW()),
(113, 11, '用户修改', 3, 'F', 'system:user:edit', NULL, 1, NOW(), NOW()),
(114, 11, '用户删除', 4, 'F', 'system:user:remove', NULL, 1, NOW(), NOW()),
(115, 11, '用户导出', 5, 'F', 'system:user:export', NULL, 1, NOW(), NOW()),
(116, 11, '用户导入', 6, 'F', 'system:user:import', NULL, 1, NOW(), NOW()),
(117, 11, '重置密码', 7, 'F', 'system:user:resetPwd', NULL, 1, NOW(), NOW());
-- 角色管理按钮权限
INSERT INTO sys_menu (id, parent_id, menu_name, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(121, 12, '角色查询', 1, 'F', 'system:role:query', NULL, 1, NOW(), NOW()),
(122, 12, '角色新增', 2, 'F', 'system:role:add', NULL, 1, NOW(), NOW()),
(123, 12, '角色修改', 3, 'F', 'system:role:edit', NULL, 1, NOW(), NOW()),
(124, 12, '角色删除', 4, 'F', 'system:role:remove', NULL, 1, NOW(), NOW()),
(125, 12, '角色导出', 5, 'F', 'system:role:export', NULL, 1, NOW(), NOW());
-- 菜单管理按钮权限
INSERT INTO sys_menu (id, parent_id, menu_name, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(131, 13, '菜单查询', 1, 'F', 'system:menu:query', NULL, 1, NOW(), NOW()),
(132, 13, '菜单新增', 2, 'F', 'system:menu:add', NULL, 1, NOW(), NOW()),
(133, 13, '菜单修改', 3, 'F', 'system:menu:edit', NULL, 1, NOW(), NOW()),
(134, 13, '菜单删除', 4, 'F', 'system:menu:remove', NULL, 1, NOW(), NOW());
-- 审计日志子菜单
INSERT INTO sys_menu (id, parent_id, menu_name, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(21, 2, '操作日志', 1, 'C', 'audit:operation:list', 'audit/operation/index', 1, NOW(), NOW()),
(22, 2, '登录日志', 2, 'C', 'audit:login:list', 'audit/login/index', 1, NOW(), NOW()),
(23, 2, '异常日志', 3, 'C', 'audit:exception:list', 'audit/exception/index', 1, NOW(), NOW());
-- 操作日志按钮权限
INSERT INTO sys_menu (id, parent_id, menu_name, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(211, 21, '操作查询', 1, 'F', 'audit:operation:query', NULL, 1, NOW(), NOW()),
(212, 21, '操作删除', 2, 'F', 'audit:operation:remove', NULL, 1, NOW(), NOW()),
(213, 21, '操作导出', 3, 'F', 'audit:operation:export', NULL, 1, NOW(), NOW());
-- 登录日志按钮权限
INSERT INTO sys_menu (id, parent_id, menu_name, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(221, 22, '登录查询', 1, 'F', 'audit:login:query', NULL, 1, NOW(), NOW()),
(222, 22, '登录删除', 2, 'F', 'audit:login:remove', NULL, 1, NOW(), NOW()),
(223, 22, '登录导出', 3, 'F', 'audit:login:export', NULL, 1, NOW(), NOW());
-- 异常日志按钮权限
INSERT INTO sys_menu (id, parent_id, menu_name, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(231, 23, '异常查询', 1, 'F', 'audit:exception:query', NULL, 1, NOW(), NOW()),
(232, 23, '异常删除', 2, 'F', 'audit:exception:remove', NULL, 1, NOW(), NOW()),
(233, 23, '异常导出', 3, 'F', 'audit:exception:export', NULL, 1, NOW(), NOW());
-- 系统监控子菜单
INSERT INTO sys_menu (id, parent_id, menu_name, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(31, 3, '在线用户', 1, 'C', 'monitor:online:list', 'monitor/online/index', 1, NOW(), NOW()),
(32, 3, '定时任务', 2, 'C', 'monitor:job:list', 'monitor/job/index', 1, NOW(), NOW()),
(33, 3, '数据监控', 3, 'C', 'monitor:data:list', 'monitor/data/index', 1, NOW(), NOW()),
(34, 3, '服务监控', 4, 'C', 'monitor:server:list', 'monitor/server/index', 1, NOW(), NOW()),
(35, 3, '缓存监控', 5, 'C', 'monitor:cache:list', 'monitor/cache/index', 1, NOW(), NOW());
-- 在线用户按钮权限
INSERT INTO sys_menu (id, parent_id, menu_name, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(311, 31, '在线查询', 1, 'F', 'monitor:online:query', NULL, 1, NOW(), NOW()),
(312, 31, '在线强退', 2, 'F', 'monitor:online:forceLogout', NULL, 1, NOW(), NOW());
-- 定时任务按钮权限
INSERT INTO sys_menu (id, parent_id, menu_name, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(321, 32, '任务查询', 1, 'F', 'monitor:job:query', NULL, 1, NOW(), NOW()),
(322, 32, '任务新增', 2, 'F', 'monitor:job:add', NULL, 1, NOW(), NOW()),
(323, 32, '任务修改', 3, 'F', 'monitor:job:edit', NULL, 1, NOW(), NOW()),
(324, 32, '任务删除', 4, 'F', 'monitor:job:remove', NULL, 1, NOW(), NOW()),
(325, 32, '任务执行', 5, 'F', 'monitor:job:execute', NULL, 1, NOW(), NOW());
@@ -0,0 +1,91 @@
package cn.novalon.manage.db.config;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.stream.Collectors;
import static org.junit.jupiter.api.Assertions.*;
class FlywayMigrationScriptTest {
@Test
void testMigrationScriptsExist() throws IOException {
Path migrationDir = Paths.get("src/main/resources/db/migration");
assertTrue(Files.exists(migrationDir), "Migration directory should exist");
List<Path> sqlFiles = Files.list(migrationDir)
.filter(p -> p.toString().endsWith(".sql"))
.sorted()
.collect(Collectors.toList());
assertFalse(sqlFiles.isEmpty(), "Should have migration scripts");
System.out.println("Found migration scripts:");
sqlFiles.forEach(p -> System.out.println(" - " + p.getFileName()));
}
@Test
void testMigrationScriptNaming() throws IOException {
Path migrationDir = Paths.get("src/main/resources/db/migration");
List<Path> sqlFiles = Files.list(migrationDir)
.filter(p -> p.toString().endsWith(".sql"))
.collect(Collectors.toList());
for (Path file : sqlFiles) {
String filename = file.getFileName().toString();
assertTrue(filename.matches("V\\d+__.*\\.sql"),
"Migration script should follow Flyway naming convention: " + filename);
}
}
@Test
void testMigrationScriptContent() throws IOException {
Path migrationDir = Paths.get("src/main/resources/db/migration");
List<Path> sqlFiles = Files.list(migrationDir)
.filter(p -> p.toString().endsWith(".sql"))
.sorted()
.collect(Collectors.toList());
for (Path file : sqlFiles) {
String content = Files.readString(file);
assertNotNull(content, "Migration script should have content: " + file.getFileName());
assertFalse(content.trim().isEmpty(), "Migration script should not be empty: " + file.getFileName());
if (content.contains("CREATE TABLE")) {
assertTrue(content.contains("IF NOT EXISTS"),
"CREATE TABLE statements should use IF NOT EXISTS: " + file.getFileName());
}
}
}
@Test
void testMigrationScriptVersionOrder() throws IOException {
Path migrationDir = Paths.get("src/main/resources/db/migration");
List<Path> sqlFiles = Files.list(migrationDir)
.filter(p -> p.toString().endsWith(".sql"))
.sorted()
.collect(Collectors.toList());
List<Integer> versions = sqlFiles.stream()
.map(p -> {
String filename = p.getFileName().toString();
String versionStr = filename.substring(1, filename.indexOf("__"));
return Integer.parseInt(versionStr);
})
.collect(Collectors.toList());
for (int i = 1; i < versions.size(); i++) {
assertTrue(versions.get(i) > versions.get(i - 1),
"Migration versions should be in ascending order");
}
}
}
@@ -0,0 +1,13 @@
spring:
r2dbc:
url: r2dbc:h2:mem:testdb;MODE=PostgreSQL
username: sa
password:
flyway:
enabled: true
locations: classpath:db/migration
baseline-on-migrate: true
baseline-version: 0
table: flyway_schema_history
validate-on-migrate: true
out-of-order: false