refactor(backend): optimize service layer and add transaction support
- Add TransactionManagerConfig for reactive transaction management - Add OperationLogWebFilter for operation logging - Remove deprecated AuditLogAspect in favor of WebFilter approach - Optimize service implementations (SysUserService, SysRoleService, etc.) - Enhance audit log functionality with better error handling - Update security configuration and tests - Add operation_log table migration script - Improve IP utility with better validation
This commit is contained in:
@@ -0,0 +1,407 @@
|
||||
-- Novalon管理系统数据库初始化脚本
|
||||
-- 版本: V1
|
||||
-- 描述: 创建所有核心表结构(合并版)
|
||||
|
||||
-- ============================================
|
||||
-- 用户与角色相关表
|
||||
-- ============================================
|
||||
|
||||
-- 用户表
|
||||
CREATE TABLE IF NOT EXISTS sys_user (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
username VARCHAR(50) NOT NULL UNIQUE,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(100),
|
||||
phone VARCHAR(20),
|
||||
nickname VARCHAR(100),
|
||||
status INTEGER DEFAULT 1,
|
||||
role_id BIGINT,
|
||||
create_by VARCHAR(50),
|
||||
update_by VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- 角色表
|
||||
CREATE TABLE IF NOT EXISTS sys_role (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
role_name VARCHAR(100) NOT NULL,
|
||||
role_key VARCHAR(100) NOT NULL UNIQUE,
|
||||
role_sort INTEGER DEFAULT 0,
|
||||
status INTEGER DEFAULT 1,
|
||||
create_by VARCHAR(50),
|
||||
update_by VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- 用户角色关联表(支持多对多关系)
|
||||
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 sys_user(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_user_role_role FOREIGN KEY (role_id) REFERENCES sys_role(id) ON DELETE CASCADE,
|
||||
CONSTRAINT uk_user_role UNIQUE (user_id, role_id)
|
||||
);
|
||||
|
||||
-- ============================================
|
||||
-- 权限相关表
|
||||
-- ============================================
|
||||
|
||||
-- 权限表
|
||||
CREATE TABLE IF NOT EXISTS sys_permission (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
permission_name VARCHAR(100) NOT NULL,
|
||||
permission_code VARCHAR(100) NOT NULL UNIQUE,
|
||||
resource VARCHAR(200) NOT NULL,
|
||||
action VARCHAR(50) NOT NULL,
|
||||
description VARCHAR(500),
|
||||
status INTEGER DEFAULT 1,
|
||||
create_by VARCHAR(50),
|
||||
update_by VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- 角色权限关联表
|
||||
CREATE TABLE IF NOT EXISTS sys_role_permission (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
role_id BIGINT NOT NULL,
|
||||
permission_id BIGINT NOT NULL,
|
||||
create_by VARCHAR(50),
|
||||
update_by VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (role_id) REFERENCES sys_role(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (permission_id) REFERENCES sys_permission(id) ON DELETE CASCADE,
|
||||
UNIQUE (role_id, permission_id)
|
||||
);
|
||||
|
||||
-- ============================================
|
||||
-- 菜单相关表
|
||||
-- ============================================
|
||||
|
||||
-- 菜单表
|
||||
CREATE TABLE IF NOT EXISTS sys_menu (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
menu_name VARCHAR(50) NOT NULL,
|
||||
parent_id BIGINT DEFAULT 0,
|
||||
order_num INTEGER DEFAULT 0,
|
||||
menu_type VARCHAR(1) DEFAULT 'C',
|
||||
perms VARCHAR(100),
|
||||
component VARCHAR(200),
|
||||
status INTEGER DEFAULT 1,
|
||||
create_by VARCHAR(50),
|
||||
update_by VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- ============================================
|
||||
-- 字典相关表
|
||||
-- ============================================
|
||||
|
||||
-- 字典类型表
|
||||
CREATE TABLE IF NOT EXISTS sys_dict_type (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
dict_name VARCHAR(100) NOT NULL,
|
||||
dict_type VARCHAR(100) NOT NULL UNIQUE,
|
||||
status VARCHAR(1) DEFAULT '0',
|
||||
remark VARCHAR(500),
|
||||
create_by VARCHAR(50),
|
||||
update_by VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- 字典数据表
|
||||
CREATE TABLE IF NOT EXISTS sys_dict_data (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
dict_sort INTEGER DEFAULT 0,
|
||||
dict_label VARCHAR(100) NOT NULL,
|
||||
dict_value VARCHAR(100) NOT NULL,
|
||||
dict_type VARCHAR(100) NOT NULL,
|
||||
css_class VARCHAR(100),
|
||||
list_class VARCHAR(100),
|
||||
is_default VARCHAR(1) DEFAULT 'N',
|
||||
status VARCHAR(1) DEFAULT '0',
|
||||
create_by VARCHAR(50),
|
||||
update_by VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- 字典表(通用字典)
|
||||
CREATE TABLE IF NOT EXISTS sys_dictionary (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
type VARCHAR(100) NOT NULL,
|
||||
code VARCHAR(100) NOT NULL,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
value VARCHAR(500),
|
||||
remark VARCHAR(500),
|
||||
sort INTEGER DEFAULT 0,
|
||||
create_by VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- ============================================
|
||||
-- 系统配置表
|
||||
-- ============================================
|
||||
|
||||
-- 系统配置表
|
||||
CREATE TABLE IF NOT EXISTS sys_config (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
config_name VARCHAR(100) NOT NULL,
|
||||
config_key VARCHAR(100) NOT NULL UNIQUE,
|
||||
config_value VARCHAR(500) NOT NULL,
|
||||
config_type VARCHAR(1) DEFAULT 'N',
|
||||
create_by VARCHAR(50),
|
||||
update_by VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- ============================================
|
||||
-- 日志相关表
|
||||
-- ============================================
|
||||
|
||||
-- 登录日志表
|
||||
CREATE TABLE IF NOT EXISTS sys_login_log (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
username VARCHAR(50),
|
||||
ip VARCHAR(50),
|
||||
location VARCHAR(255),
|
||||
browser VARCHAR(50),
|
||||
os VARCHAR(50),
|
||||
status VARCHAR(1),
|
||||
message VARCHAR(255),
|
||||
login_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- 异常日志表
|
||||
CREATE TABLE IF NOT EXISTS sys_exception_log (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
username VARCHAR(50),
|
||||
title VARCHAR(100),
|
||||
exception_name VARCHAR(100),
|
||||
method_name VARCHAR(255),
|
||||
method_params TEXT,
|
||||
exception_msg TEXT,
|
||||
exception_stack TEXT,
|
||||
ip VARCHAR(50),
|
||||
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- 操作日志表
|
||||
CREATE TABLE IF NOT EXISTS operation_log (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
username VARCHAR(50),
|
||||
operation VARCHAR(100),
|
||||
method VARCHAR(200),
|
||||
params TEXT,
|
||||
result TEXT,
|
||||
ip VARCHAR(50),
|
||||
duration BIGINT,
|
||||
status VARCHAR(1) DEFAULT '0',
|
||||
error_msg TEXT,
|
||||
create_by VARCHAR(50),
|
||||
update_by VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- 审计日志表
|
||||
CREATE TABLE IF NOT EXISTS audit_log (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
entity_type VARCHAR(100) NOT NULL,
|
||||
entity_id BIGINT,
|
||||
operation_type VARCHAR(20) NOT NULL,
|
||||
operator VARCHAR(100),
|
||||
operation_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
before_data JSONB,
|
||||
after_data JSONB,
|
||||
changed_fields TEXT[],
|
||||
ip_address VARCHAR(50),
|
||||
user_agent TEXT,
|
||||
description TEXT,
|
||||
create_by VARCHAR(50),
|
||||
update_by VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- 审计日志归档表
|
||||
CREATE TABLE IF NOT EXISTS audit_log_archive (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
entity_type VARCHAR(100) NOT NULL,
|
||||
entity_id BIGINT,
|
||||
operation_type VARCHAR(20) NOT NULL,
|
||||
operator VARCHAR(100),
|
||||
operation_time TIMESTAMP,
|
||||
before_data JSONB,
|
||||
after_data JSONB,
|
||||
changed_fields TEXT[],
|
||||
ip_address VARCHAR(50),
|
||||
user_agent TEXT,
|
||||
description TEXT,
|
||||
created_at TIMESTAMP,
|
||||
archived_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- ============================================
|
||||
-- 通知与消息表
|
||||
-- ============================================
|
||||
|
||||
-- 系统公告表
|
||||
CREATE TABLE IF NOT EXISTS sys_notice (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
notice_title VARCHAR(50) NOT NULL,
|
||||
notice_type VARCHAR(1) NOT NULL,
|
||||
notice_content TEXT,
|
||||
status VARCHAR(1) DEFAULT '0',
|
||||
create_by VARCHAR(50),
|
||||
update_by VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- 用户消息表
|
||||
CREATE TABLE IF NOT EXISTS sys_user_message (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL,
|
||||
notice_id BIGINT,
|
||||
message_title VARCHAR(255),
|
||||
message_content TEXT,
|
||||
is_read VARCHAR(1) DEFAULT '0',
|
||||
read_time TIMESTAMP,
|
||||
create_by VARCHAR(50),
|
||||
update_by VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- ============================================
|
||||
-- 文件管理表
|
||||
-- ============================================
|
||||
|
||||
-- 文件管理表
|
||||
CREATE TABLE IF NOT EXISTS sys_file (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
file_name VARCHAR(255) NOT NULL,
|
||||
file_path VARCHAR(500) NOT NULL,
|
||||
file_size BIGINT,
|
||||
file_type VARCHAR(100),
|
||||
file_extension VARCHAR(10),
|
||||
storage_type VARCHAR(50),
|
||||
create_by VARCHAR(50),
|
||||
update_by VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- ============================================
|
||||
-- OAuth2相关表
|
||||
-- ============================================
|
||||
|
||||
-- OAuth2客户端表
|
||||
CREATE TABLE IF NOT EXISTS oauth2_client (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
client_id VARCHAR(100) NOT NULL UNIQUE,
|
||||
client_secret VARCHAR(255) NOT NULL,
|
||||
client_name VARCHAR(100),
|
||||
web_server_redirect_uri VARCHAR(500),
|
||||
scope VARCHAR(500),
|
||||
authorized_grant_types VARCHAR(500),
|
||||
access_token_validity_seconds INTEGER,
|
||||
refresh_token_validity_seconds INTEGER,
|
||||
auto_approve VARCHAR(1) DEFAULT 'false',
|
||||
enabled VARCHAR(1) DEFAULT 'true',
|
||||
create_by VARCHAR(50),
|
||||
update_by VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- ============================================
|
||||
-- 表注释
|
||||
-- ============================================
|
||||
|
||||
COMMENT ON TABLE sys_user IS '系统用户表';
|
||||
COMMENT ON TABLE sys_role IS '系统角色表';
|
||||
COMMENT ON TABLE user_role IS '用户角色关联表';
|
||||
COMMENT ON TABLE sys_permission IS '系统权限表';
|
||||
COMMENT ON TABLE sys_role_permission IS '角色权限关联表';
|
||||
COMMENT ON TABLE sys_menu IS '系统菜单表';
|
||||
COMMENT ON TABLE sys_dict_type IS '字典类型表';
|
||||
COMMENT ON TABLE sys_dict_data IS '字典数据表';
|
||||
COMMENT ON TABLE sys_dictionary IS '通用字典表';
|
||||
COMMENT ON TABLE sys_config IS '系统配置表';
|
||||
COMMENT ON TABLE sys_login_log IS '登录日志表';
|
||||
COMMENT ON TABLE sys_exception_log IS '异常日志表';
|
||||
COMMENT ON TABLE operation_log IS '操作日志表';
|
||||
COMMENT ON TABLE audit_log IS '审计日志表';
|
||||
COMMENT ON TABLE audit_log_archive IS '审计日志归档表';
|
||||
COMMENT ON TABLE sys_notice IS '系统公告表';
|
||||
COMMENT ON TABLE sys_user_message IS '用户消息表';
|
||||
COMMENT ON TABLE sys_file IS '文件管理表';
|
||||
COMMENT ON TABLE oauth2_client IS 'OAuth2客户端表';
|
||||
|
||||
COMMENT ON TABLE sys_exception_log IS '异常日志表';
|
||||
COMMENT ON COLUMN sys_exception_log.id IS '主键ID';
|
||||
COMMENT ON COLUMN sys_exception_log.username IS '操作用户';
|
||||
COMMENT ON COLUMN sys_exception_log.title IS '异常标题';
|
||||
COMMENT ON COLUMN sys_exception_log.exception_name IS '异常名称';
|
||||
COMMENT ON COLUMN sys_exception_log.method_name IS '方法名称';
|
||||
COMMENT ON COLUMN sys_exception_log.method_params IS '方法参数';
|
||||
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 audit_log IS '审计日志表';
|
||||
COMMENT ON COLUMN audit_log.id IS '主键ID';
|
||||
COMMENT ON COLUMN audit_log.entity_type IS '实体类型(如User, Role等)';
|
||||
COMMENT ON COLUMN audit_log.entity_id IS '实体ID';
|
||||
COMMENT ON COLUMN audit_log.operation_type IS '操作类型(CREATE, UPDATE, DELETE)';
|
||||
COMMENT ON COLUMN audit_log.operator IS '操作人';
|
||||
COMMENT ON COLUMN audit_log.operation_time IS '操作时间';
|
||||
COMMENT ON COLUMN audit_log.before_data IS '变更前数据(JSON格式)';
|
||||
COMMENT ON COLUMN audit_log.after_data IS '变更后数据(JSON格式)';
|
||||
COMMENT ON COLUMN audit_log.changed_fields IS '变更字段列表';
|
||||
COMMENT ON COLUMN audit_log.ip_address IS 'IP地址';
|
||||
COMMENT ON COLUMN audit_log.description IS '操作描述';
|
||||
COMMENT ON COLUMN audit_log.created_at IS '记录创建时间';
|
||||
|
||||
COMMENT ON TABLE audit_log_archive IS '审计日志归档表';
|
||||
COMMENT ON COLUMN audit_log_archive.id IS '主键ID';
|
||||
COMMENT ON COLUMN audit_log_archive.entity_type IS '实体类型(如User, Role等)';
|
||||
COMMENT ON COLUMN audit_log_archive.entity_id IS '实体ID';
|
||||
COMMENT ON COLUMN audit_log_archive.operation_type IS '操作类型(CREATE, UPDATE, DELETE)';
|
||||
COMMENT ON COLUMN audit_log_archive.operator IS '操作人';
|
||||
COMMENT ON COLUMN audit_log_archive.operation_time IS '操作时间';
|
||||
COMMENT ON COLUMN audit_log_archive.before_data IS '变更前数据(JSON格式)';
|
||||
COMMENT ON COLUMN audit_log_archive.after_data IS '变更后数据(JSON格式)';
|
||||
COMMENT ON COLUMN audit_log_archive.changed_fields IS '变更字段列表';
|
||||
COMMENT ON COLUMN audit_log_archive.ip_address IS 'IP地址';
|
||||
COMMENT ON COLUMN audit_log_archive.user_agent IS '用户代理';
|
||||
COMMENT ON COLUMN audit_log_archive.description IS '操作描述';
|
||||
COMMENT ON COLUMN audit_log_archive.created_at IS '记录创建时间';
|
||||
COMMENT ON COLUMN audit_log_archive.archived_at IS '归档时间';
|
||||
@@ -42,6 +42,10 @@
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-webflux</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-aop</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
|
||||
+35
-4
@@ -1,16 +1,24 @@
|
||||
package cn.novalon.gym.manage.app;
|
||||
|
||||
import cn.novalon.gym.manage.sys.core.service.IOperationLogService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration;
|
||||
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories;
|
||||
import org.springframework.web.server.WebFilter;
|
||||
|
||||
@SpringBootApplication(scanBasePackages = "cn.novalon.gym.manage", exclude = {ReactiveUserDetailsServiceAutoConfiguration.class})
|
||||
@EnableR2dbcRepositories(basePackages = {"cn.novalon.gym.manage.db.dao", "cn.novalon.gym.manage.sys.audit.repository"})
|
||||
import java.util.List;
|
||||
|
||||
@SpringBootApplication(scanBasePackages = "cn.novalon.gym.manage", exclude = {
|
||||
ReactiveUserDetailsServiceAutoConfiguration.class })
|
||||
@EnableR2dbcRepositories(basePackages = { "cn.novalon.gym.manage.db.dao",
|
||||
"cn.novalon.gym.manage.sys.audit.repository" })
|
||||
public class ManageApplication {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ManageApplication.class);
|
||||
@@ -18,9 +26,32 @@ public class ManageApplication {
|
||||
public static void main(String[] args) {
|
||||
logger.info("应用程序启动中...");
|
||||
logger.info("包扫描路径: cn.novalon.gym.manage");
|
||||
|
||||
// 使用简单的启动方式,避免自动配置问题
|
||||
|
||||
SpringApplication.run(ManageApplication.class, args);
|
||||
logger.info("应用程序启动完成");
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CommandLineRunner checkWebFilters(List<WebFilter> webFilters) {
|
||||
return args -> {
|
||||
logger.info("=== 检查已注册的 WebFilter ===");
|
||||
logger.info("WebFilter 总数: {}", webFilters.size());
|
||||
for (WebFilter filter : webFilters) {
|
||||
logger.info(" - {} (Order: {})",
|
||||
filter.getClass().getName(),
|
||||
filter.getClass().getAnnotation(org.springframework.core.annotation.Order.class) != null
|
||||
? filter.getClass().getAnnotation(org.springframework.core.annotation.Order.class)
|
||||
.value()
|
||||
: "无");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CommandLineRunner checkOperationLogService(IOperationLogService service) {
|
||||
return args -> {
|
||||
logger.info("=== 检查 IOperationLogService ===");
|
||||
logger.info("IOperationLogService 实现: {}", service.getClass().getName());
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package cn.novalon.gym.manage.app.config;
|
||||
|
||||
import io.r2dbc.spi.ConnectionFactory;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.transaction.ReactiveTransactionManager;
|
||||
import org.springframework.transaction.reactive.TransactionalOperator;
|
||||
import org.springframework.r2dbc.connection.R2dbcTransactionManager;
|
||||
|
||||
@Configuration
|
||||
public class TransactionManagerConfig {
|
||||
|
||||
@Bean(name = "connectionFactoryTransactionManager")
|
||||
@Primary
|
||||
public ReactiveTransactionManager reactiveTransactionManager(ConnectionFactory connectionFactory) {
|
||||
return new R2dbcTransactionManager(connectionFactory);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
public TransactionalOperator transactionalOperator(ReactiveTransactionManager reactiveTransactionManager) {
|
||||
return TransactionalOperator.create(reactiveTransactionManager);
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,10 @@ spring:
|
||||
logging:
|
||||
level:
|
||||
cn.novalon.manage: DEBUG
|
||||
cn.novalon.gym.manage: DEBUG
|
||||
cn.novalon.gym.manage.sys.audit: DEBUG
|
||||
org.springframework.r2dbc: DEBUG
|
||||
cn.novalon.manage.db: DEBUG
|
||||
org.flywaydb: DEBUG
|
||||
org.flywaydb: DEBUG
|
||||
|
||||
debug: true
|
||||
+5
-4
@@ -2,6 +2,7 @@ package cn.novalon.gym.manage.db.converter;
|
||||
|
||||
import cn.novalon.gym.manage.sys.audit.domain.AuditLog;
|
||||
import cn.novalon.gym.manage.db.entity.AuditLogEntity;
|
||||
import io.r2dbc.postgresql.codec.Json;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@@ -28,8 +29,8 @@ public class AuditLogConverter {
|
||||
domain.setOperationType(entity.getOperationType());
|
||||
domain.setOperator(entity.getOperator());
|
||||
domain.setOperationTime(entity.getOperationTime());
|
||||
domain.setBeforeData(entity.getBeforeData());
|
||||
domain.setAfterData(entity.getAfterData());
|
||||
domain.setBeforeData(entity.getBeforeData() != null ? entity.getBeforeData().asString() : null);
|
||||
domain.setAfterData(entity.getAfterData() != null ? entity.getAfterData().asString() : null);
|
||||
domain.setChangedFields(entity.getChangedFields());
|
||||
domain.setIpAddress(entity.getIpAddress());
|
||||
domain.setUserAgent(entity.getUserAgent());
|
||||
@@ -53,8 +54,8 @@ public class AuditLogConverter {
|
||||
entity.setOperationType(domain.getOperationType());
|
||||
entity.setOperator(domain.getOperator());
|
||||
entity.setOperationTime(domain.getOperationTime());
|
||||
entity.setBeforeData(domain.getBeforeData());
|
||||
entity.setAfterData(domain.getAfterData());
|
||||
entity.setBeforeData(domain.getBeforeData() != null ? Json.of(domain.getBeforeData()) : null);
|
||||
entity.setAfterData(domain.getAfterData() != null ? Json.of(domain.getAfterData()) : null);
|
||||
entity.setChangedFields(domain.getChangedFields());
|
||||
entity.setIpAddress(domain.getIpAddress());
|
||||
entity.setUserAgent(domain.getUserAgent());
|
||||
|
||||
+7
-6
@@ -1,5 +1,6 @@
|
||||
package cn.novalon.gym.manage.db.entity;
|
||||
|
||||
import io.r2dbc.postgresql.codec.Json;
|
||||
import org.springframework.data.relational.core.mapping.Column;
|
||||
import org.springframework.data.relational.core.mapping.Table;
|
||||
|
||||
@@ -28,10 +29,10 @@ public class AuditLogEntity extends BaseEntity {
|
||||
private java.time.LocalDateTime operationTime;
|
||||
|
||||
@Column("before_data")
|
||||
private String beforeData;
|
||||
private Json beforeData;
|
||||
|
||||
@Column("after_data")
|
||||
private String afterData;
|
||||
private Json afterData;
|
||||
|
||||
@Column("changed_fields")
|
||||
private String[] changedFields;
|
||||
@@ -85,19 +86,19 @@ public class AuditLogEntity extends BaseEntity {
|
||||
this.operationTime = operationTime;
|
||||
}
|
||||
|
||||
public String getBeforeData() {
|
||||
public Json getBeforeData() {
|
||||
return beforeData;
|
||||
}
|
||||
|
||||
public void setBeforeData(String beforeData) {
|
||||
public void setBeforeData(Json beforeData) {
|
||||
this.beforeData = beforeData;
|
||||
}
|
||||
|
||||
public String getAfterData() {
|
||||
public Json getAfterData() {
|
||||
return afterData;
|
||||
}
|
||||
|
||||
public void setAfterData(String afterData) {
|
||||
public void setAfterData(Json afterData) {
|
||||
this.afterData = afterData;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ CREATE TABLE IF NOT EXISTS sys_user (
|
||||
phone VARCHAR(20),
|
||||
nickname VARCHAR(100),
|
||||
status INTEGER DEFAULT 1,
|
||||
role_id BIGINT,
|
||||
create_by VARCHAR(50),
|
||||
update_by VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
-- 创建操作日志表
|
||||
CREATE TABLE IF NOT EXISTS sys_operation_log (
|
||||
id BIGINT PRIMARY KEY,
|
||||
username VARCHAR(50),
|
||||
operation VARCHAR(100),
|
||||
method VARCHAR(200),
|
||||
params TEXT,
|
||||
result TEXT,
|
||||
ip VARCHAR(50),
|
||||
duration BIGINT,
|
||||
status VARCHAR(1) DEFAULT '0',
|
||||
error_msg TEXT,
|
||||
create_by VARCHAR(50),
|
||||
update_by VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- 创建索引
|
||||
CREATE INDEX IF NOT EXISTS idx_operation_log_username ON sys_operation_log(username);
|
||||
CREATE INDEX IF NOT EXISTS idx_operation_log_created_at ON sys_operation_log(created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_operation_log_status ON sys_operation_log(status);
|
||||
|
||||
-- 添加注释
|
||||
COMMENT ON TABLE sys_operation_log IS '操作日志表';
|
||||
COMMENT ON COLUMN sys_operation_log.id IS '主键ID';
|
||||
COMMENT ON COLUMN sys_operation_log.username IS '操作用户';
|
||||
COMMENT ON COLUMN sys_operation_log.operation IS '操作描述';
|
||||
COMMENT ON COLUMN sys_operation_log.method IS '请求方法';
|
||||
COMMENT ON COLUMN sys_operation_log.params IS '请求参数';
|
||||
COMMENT ON COLUMN sys_operation_log.result IS '操作结果';
|
||||
COMMENT ON COLUMN sys_operation_log.ip IS 'IP地址';
|
||||
COMMENT ON COLUMN sys_operation_log.duration IS '执行时长(毫秒)';
|
||||
COMMENT ON COLUMN sys_operation_log.status IS '操作状态(0成功 1失败)';
|
||||
COMMENT ON COLUMN sys_operation_log.error_msg IS '错误消息';
|
||||
COMMENT ON COLUMN sys_operation_log.create_by IS '创建人';
|
||||
COMMENT ON COLUMN sys_operation_log.update_by IS '更新人';
|
||||
COMMENT ON COLUMN sys_operation_log.created_at IS '创建时间';
|
||||
COMMENT ON COLUMN sys_operation_log.updated_at IS '更新时间';
|
||||
COMMENT ON COLUMN sys_operation_log.deleted_at IS '删除时间';
|
||||
-315
@@ -1,315 +0,0 @@
|
||||
package cn.novalon.gym.manage.sys.audit;
|
||||
|
||||
import cn.novalon.gym.manage.sys.audit.domain.AuditLog;
|
||||
import cn.novalon.gym.manage.sys.audit.service.IAuditLogService;
|
||||
import com.fasterxml.jackson.annotation.JsonIdentityInfo;
|
||||
import com.fasterxml.jackson.annotation.ObjectIdGenerators;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.data.domain.Persistable;
|
||||
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
|
||||
import org.springframework.stereotype.Component;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 审计日志切面
|
||||
*
|
||||
* 文件定义:使用AOP自动拦截Repository操作,记录审计日志
|
||||
* 涉及业务:自动记录所有数据变更操作,包括变更前后对比
|
||||
* 算法:使用异步方式记录日志,不阻塞主流程
|
||||
*
|
||||
* @author 张翔
|
||||
* @date 2026-04-01
|
||||
*/
|
||||
@Aspect
|
||||
@Component
|
||||
public class AuditLogAspect {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(AuditLogAspect.class);
|
||||
|
||||
private final IAuditLogService auditLogService;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public AuditLogAspect(IAuditLogService auditLogService, ObjectMapper objectMapper) {
|
||||
this.auditLogService = auditLogService;
|
||||
this.objectMapper = new ObjectMapper()
|
||||
.registerModule(new JavaTimeModule())
|
||||
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
|
||||
.disable(SerializationFeature.FAIL_ON_SELF_REFERENCES);
|
||||
}
|
||||
|
||||
@Around("(execution(* cn.novalon.gym.manage.db.repository.*Repository.save(..)) || " +
|
||||
"execution(* cn.novalon.gym.manage.db.repository.*Repository.delete(..)) || " +
|
||||
"execution(* cn.novalon.gym.manage.db.repository.*Repository.deleteById(..))) && " +
|
||||
"!execution(* cn.novalon.gym.manage.db.repository.AuditLogRepository.*(..)) && " +
|
||||
"!execution(* cn.novalon.gym.manage.db.dao.AuditLogDao.*(..))")
|
||||
public Object logAuditEvent(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||
String methodName = joinPoint.getSignature().getName();
|
||||
String className = joinPoint.getTarget().getClass().getSimpleName();
|
||||
Object[] args = joinPoint.getArgs();
|
||||
|
||||
String operationType = determineOperationType(methodName);
|
||||
String entityType = extractEntityType(className);
|
||||
|
||||
logger.debug("拦截审计操作: {}.{}, 操作类型: {}, 实体类型: {}",
|
||||
className, methodName, operationType, entityType);
|
||||
|
||||
try {
|
||||
if ("save".equals(methodName) && args.length > 0) {
|
||||
return handleSaveOperation(joinPoint, args[0], entityType, operationType);
|
||||
} else if ("delete".equals(methodName) || "deleteById".equals(methodName)) {
|
||||
return handleDeleteOperation(joinPoint, args, entityType, operationType);
|
||||
}
|
||||
|
||||
return joinPoint.proceed();
|
||||
} catch (Throwable error) {
|
||||
logger.error("审计日志记录失败: {}", error.getMessage(), error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private Object handleSaveOperation(ProceedingJoinPoint joinPoint, Object entity,
|
||||
String entityType, String operationType) throws Throwable {
|
||||
String entityClassName = entity.getClass().getSimpleName();
|
||||
if (entityClassName.contains("AuditLog") || entityClassName.contains("AuditLogEntity")) {
|
||||
logger.debug("跳过审计日志实体的审计记录: {}", entityClassName);
|
||||
return joinPoint.proceed();
|
||||
}
|
||||
|
||||
try {
|
||||
final String[] beforeDataHolder = {null};
|
||||
final Long[] entityIdHolder = {null};
|
||||
final String[] operationTypeHolder = {operationType};
|
||||
|
||||
if (entity instanceof Persistable) {
|
||||
Persistable<?> persistable = (Persistable<?>) entity;
|
||||
entityIdHolder[0] = persistable.getId() != null ?
|
||||
((Number) persistable.getId()).longValue() : null;
|
||||
|
||||
if (entityIdHolder[0] != null) {
|
||||
beforeDataHolder[0] = fetchEntityBeforeData(entityType, entityIdHolder[0]);
|
||||
operationTypeHolder[0] = "UPDATE";
|
||||
} else {
|
||||
operationTypeHolder[0] = "CREATE";
|
||||
}
|
||||
}
|
||||
|
||||
Object result = joinPoint.proceed();
|
||||
|
||||
if (result instanceof Mono) {
|
||||
return ((Mono<?>) result).flatMap(savedEntity -> {
|
||||
String afterData = serializeEntity(savedEntity);
|
||||
Long finalEntityId = entityIdHolder[0] != null ? entityIdHolder[0] : extractEntityId(savedEntity);
|
||||
String finalOperationType = operationTypeHolder[0];
|
||||
String finalBeforeData = beforeDataHolder[0];
|
||||
|
||||
logger.debug("保存操作审计日志: entityType={}, entityIdHolder={}, extractedEntityId={}, finalEntityId={}",
|
||||
entityType, entityIdHolder[0], extractEntityId(savedEntity), finalEntityId);
|
||||
|
||||
return createAndSaveAuditLog(
|
||||
entityType, finalEntityId, finalOperationType,
|
||||
finalBeforeData, afterData, savedEntity
|
||||
).thenReturn(savedEntity);
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (Throwable error) {
|
||||
logger.error("保存操作审计日志记录失败", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private Object handleDeleteOperation(ProceedingJoinPoint joinPoint, Object[] args,
|
||||
String entityType, String operationType) throws Throwable {
|
||||
try {
|
||||
Long entityId = null;
|
||||
String beforeData = null;
|
||||
|
||||
if (args.length > 0) {
|
||||
if (args[0] instanceof Number) {
|
||||
entityId = ((Number) args[0]).longValue();
|
||||
beforeData = fetchEntityBeforeData(entityType, entityId);
|
||||
} else if (args[0] instanceof Persistable) {
|
||||
Persistable<?> persistable = (Persistable<?>) args[0];
|
||||
entityId = persistable.getId() != null ?
|
||||
((Number) persistable.getId()).longValue() : null;
|
||||
beforeData = serializeEntity(args[0]);
|
||||
}
|
||||
}
|
||||
|
||||
Object result = joinPoint.proceed();
|
||||
|
||||
if (result instanceof Mono) {
|
||||
Long finalEntityId = entityId;
|
||||
String finalBeforeData = beforeData;
|
||||
return ((Mono<?>) result).flatMap(deleted ->
|
||||
createAndSaveAuditLog(
|
||||
entityType, finalEntityId, "DELETE",
|
||||
finalBeforeData, null, null
|
||||
).thenReturn(deleted)
|
||||
);
|
||||
} else if (result instanceof Flux) {
|
||||
Long finalEntityId = entityId;
|
||||
String finalBeforeData = beforeData;
|
||||
return ((Flux<?>) result).flatMap(deleted ->
|
||||
createAndSaveAuditLog(
|
||||
entityType, finalEntityId, "DELETE",
|
||||
finalBeforeData, null, null
|
||||
).thenReturn(deleted)
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (Throwable error) {
|
||||
logger.error("删除操作审计日志记录失败", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private Mono<Void> createAndSaveAuditLog(String entityType, Long entityId,
|
||||
String operationType, String beforeData,
|
||||
String afterData, Object entity) {
|
||||
logger.debug("创建审计日志: entityType={}, entityId={}, operationType={}", entityType, entityId, operationType);
|
||||
return ReactiveSecurityContextHolder.getContext()
|
||||
.map(ctx -> ctx.getAuthentication().getPrincipal())
|
||||
.defaultIfEmpty("system")
|
||||
.flatMap(principal -> {
|
||||
AuditLog auditLog = new AuditLog();
|
||||
auditLog.setEntityType(entityType);
|
||||
auditLog.setEntityId(entityId != null ? entityId : 0L);
|
||||
auditLog.setOperationType(operationType);
|
||||
auditLog.setOperator(principal instanceof String ? (String) principal : "system");
|
||||
auditLog.setBeforeData(beforeData);
|
||||
auditLog.setAfterData(afterData);
|
||||
|
||||
logger.debug("审计日志对象: entityId={}, entityType={}, operationType={}",
|
||||
auditLog.getEntityId(), auditLog.getEntityType(), auditLog.getOperationType());
|
||||
|
||||
if (beforeData != null && afterData != null) {
|
||||
String[] changedFields = extractChangedFields(beforeData, afterData);
|
||||
auditLog.setChangedFields(changedFields);
|
||||
}
|
||||
|
||||
auditLog.setDescription(generateDescription(entityType, operationType, entityId));
|
||||
|
||||
return auditLogService.save(auditLog)
|
||||
.doOnSuccess(saved -> logger.debug("审计日志保存成功: {} - {}",
|
||||
entityType, operationType))
|
||||
.doOnError(error -> logger.error("审计日志保存失败: {}",
|
||||
error.getMessage()))
|
||||
.then();
|
||||
})
|
||||
.onErrorResume(error -> {
|
||||
logger.error("创建审计日志失败,但不影响主流程: {}", error.getMessage(), error);
|
||||
return Mono.empty();
|
||||
});
|
||||
}
|
||||
|
||||
private String determineOperationType(String methodName) {
|
||||
if (methodName.startsWith("save")) {
|
||||
return "SAVE";
|
||||
} else if (methodName.startsWith("delete")) {
|
||||
return "DELETE";
|
||||
}
|
||||
return "UNKNOWN";
|
||||
}
|
||||
|
||||
private String extractEntityType(String className) {
|
||||
if (className.contains("User")) {
|
||||
return "User";
|
||||
} else if (className.contains("Role")) {
|
||||
return "Role";
|
||||
} else if (className.contains("Menu")) {
|
||||
return "Menu";
|
||||
} else if (className.contains("Permission")) {
|
||||
return "Permission";
|
||||
}
|
||||
return className.replace("Repository", "").replace("Impl", "");
|
||||
}
|
||||
|
||||
private String fetchEntityBeforeData(String entityType, Long entityId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
private String serializeEntity(Object entity) {
|
||||
try {
|
||||
return objectMapper.writeValueAsString(entity);
|
||||
} catch (Exception e) {
|
||||
logger.error("序列化实体失败: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private Long extractEntityId(Object entity) {
|
||||
logger.debug("提取实体ID: entity class={}", entity.getClass().getName());
|
||||
if (entity instanceof Persistable) {
|
||||
Persistable<?> persistable = (Persistable<?>) entity;
|
||||
Object id = persistable.getId();
|
||||
logger.debug("Persistable实体ID: id={}, isNew={}", id, persistable.isNew());
|
||||
return id != null ? ((Number) id).longValue() : null;
|
||||
}
|
||||
logger.debug("实体不是Persistable类型");
|
||||
return null;
|
||||
}
|
||||
|
||||
private String[] extractChangedFields(String beforeData, String afterData) {
|
||||
try {
|
||||
JsonNode beforeNode = objectMapper.readTree(beforeData);
|
||||
JsonNode afterNode = objectMapper.readTree(afterData);
|
||||
|
||||
List<String> changedFields = new ArrayList<>();
|
||||
|
||||
beforeNode.fieldNames().forEachRemaining(fieldName -> {
|
||||
JsonNode beforeValue = beforeNode.get(fieldName);
|
||||
JsonNode afterValue = afterNode.get(fieldName);
|
||||
|
||||
if (afterValue == null || !beforeValue.equals(afterValue)) {
|
||||
changedFields.add(fieldName);
|
||||
}
|
||||
});
|
||||
|
||||
afterNode.fieldNames().forEachRemaining(fieldName -> {
|
||||
if (!beforeNode.has(fieldName)) {
|
||||
changedFields.add(fieldName);
|
||||
}
|
||||
});
|
||||
|
||||
return changedFields.toArray(new String[0]);
|
||||
} catch (Exception e) {
|
||||
logger.error("提取变更字段失败: {}", e.getMessage());
|
||||
return new String[0];
|
||||
}
|
||||
}
|
||||
|
||||
private String generateDescription(String entityType, String operationType, Long entityId) {
|
||||
String operation = "";
|
||||
switch (operationType) {
|
||||
case "CREATE":
|
||||
operation = "创建";
|
||||
break;
|
||||
case "UPDATE":
|
||||
operation = "更新";
|
||||
break;
|
||||
case "DELETE":
|
||||
operation = "删除";
|
||||
break;
|
||||
default:
|
||||
operation = "操作";
|
||||
}
|
||||
|
||||
return String.format("%s%s (ID: %s)", operation, entityType,
|
||||
entityId != null ? entityId : "未知");
|
||||
}
|
||||
}
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
package cn.novalon.gym.manage.sys.audit;
|
||||
|
||||
import cn.novalon.gym.manage.sys.core.domain.OperationLog;
|
||||
import cn.novalon.gym.manage.sys.core.service.IOperationLogService;
|
||||
import cn.novalon.gym.manage.sys.util.IpUtils;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.core.io.buffer.DataBufferUtils;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.server.HandlerStrategies;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebFilter;
|
||||
import org.springframework.web.server.WebFilterChain;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@Component
|
||||
@Order(Ordered.LOWEST_PRECEDENCE)
|
||||
public class OperationLogWebFilter implements WebFilter {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(OperationLogWebFilter.class);
|
||||
|
||||
private final IOperationLogService operationLogService;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
private static final Map<String, OperationInfo> OPERATION_MAPPING = new ConcurrentHashMap<>();
|
||||
|
||||
static {
|
||||
OPERATION_MAPPING.put("POST:/api/roles", new OperationInfo("角色管理", "创建角色"));
|
||||
OPERATION_MAPPING.put("PUT:/api/roles/", new OperationInfo("角色管理", "更新角色"));
|
||||
OPERATION_MAPPING.put("DELETE:/api/roles/", new OperationInfo("角色管理", "删除角色"));
|
||||
OPERATION_MAPPING.put("POST:/api/users", new OperationInfo("用户管理", "创建用户"));
|
||||
OPERATION_MAPPING.put("PUT:/api/users/", new OperationInfo("用户管理", "更新用户"));
|
||||
OPERATION_MAPPING.put("DELETE:/api/users/", new OperationInfo("用户管理", "删除用户"));
|
||||
OPERATION_MAPPING.put("POST:/api/users/", new OperationInfo("用户管理", "用户操作"));
|
||||
OPERATION_MAPPING.put("POST:/api/menus", new OperationInfo("菜单管理", "创建菜单"));
|
||||
OPERATION_MAPPING.put("PUT:/api/menus/", new OperationInfo("菜单管理", "更新菜单"));
|
||||
OPERATION_MAPPING.put("DELETE:/api/menus/", new OperationInfo("菜单管理", "删除菜单"));
|
||||
}
|
||||
|
||||
public OperationLogWebFilter(IOperationLogService operationLogService, ObjectMapper objectMapper) {
|
||||
logger.info("=== OperationLogWebFilter 构造函数被调用 ===");
|
||||
this.operationLogService = operationLogService;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
logger.info("=== OperationLogWebFilter 初始化 ===");
|
||||
logger.info("操作日志映射配置数量: {}", OPERATION_MAPPING.size());
|
||||
OPERATION_MAPPING.forEach((key, value) -> {
|
||||
logger.info(" {} -> {}:{}", key, value.module, value.operation);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
|
||||
ServerHttpRequest request = exchange.getRequest();
|
||||
String method = request.getMethod().name();
|
||||
String path = request.getPath().value();
|
||||
|
||||
logger.info("WebFilter 拦截请求: {} {}", method, path);
|
||||
|
||||
OperationInfo operationInfo = findOperationInfo(method, path);
|
||||
|
||||
if (operationInfo == null) {
|
||||
logger.info("未匹配到操作日志配置,跳过: {} {}", method, path);
|
||||
return chain.filter(exchange);
|
||||
}
|
||||
|
||||
logger.info("匹配到操作日志配置: {} {} -> {}:{}", method, path, operationInfo.module, operationInfo.operation);
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
String ip = IpUtils.getClientIp(request);
|
||||
|
||||
return Mono.deferContextual(contextView -> {
|
||||
return chain.filter(exchange)
|
||||
.then(Mono.defer(() -> {
|
||||
long duration = System.currentTimeMillis() - startTime;
|
||||
logger.info("请求处理完成,准备保存操作日志: {} {}, 耗时: {}ms", method, path, duration);
|
||||
|
||||
return ReactiveSecurityContextHolder.getContext()
|
||||
.flatMap(securityContext -> {
|
||||
Object principal = securityContext.getAuthentication().getPrincipal();
|
||||
String username = principal instanceof String ? (String) principal : "system";
|
||||
logger.info("获取到用户名: {}", username);
|
||||
return Mono.just(username);
|
||||
})
|
||||
.defaultIfEmpty("system")
|
||||
.flatMap(username -> {
|
||||
logger.info("开始保存操作日志: 用户={}, 操作={}", username,
|
||||
operationInfo.module + " - " + operationInfo.operation);
|
||||
|
||||
OperationLog log = new OperationLog();
|
||||
log.setUsername(username);
|
||||
log.setOperation(operationInfo.module + " - " + operationInfo.operation);
|
||||
log.setMethod(method + " " + path);
|
||||
log.setParams(null);
|
||||
log.setIp(ip);
|
||||
log.setDuration(duration);
|
||||
log.setStatus("0");
|
||||
|
||||
return operationLogService.save(log)
|
||||
.doOnSuccess(saved -> logger.info("操作日志保存成功: {} - {}",
|
||||
operationInfo.module, operationInfo.operation))
|
||||
.doOnError(e -> logger.error("操作日志保存失败: {}", e.getMessage(), e))
|
||||
.onErrorResume(e -> Mono.empty());
|
||||
})
|
||||
.then();
|
||||
}))
|
||||
.onErrorResume(error -> {
|
||||
long duration = System.currentTimeMillis() - startTime;
|
||||
logger.error("请求处理失败: {} {}, 错误: {}", method, path, error.getMessage());
|
||||
|
||||
return ReactiveSecurityContextHolder.getContext()
|
||||
.flatMap(securityContext -> {
|
||||
Object principal = securityContext.getAuthentication().getPrincipal();
|
||||
String username = principal instanceof String ? (String) principal : "system";
|
||||
return Mono.just(username);
|
||||
})
|
||||
.defaultIfEmpty("system")
|
||||
.flatMap(username -> {
|
||||
OperationLog log = new OperationLog();
|
||||
log.setUsername(username);
|
||||
log.setOperation(operationInfo.module + " - " + operationInfo.operation);
|
||||
log.setMethod(method + " " + path);
|
||||
log.setParams(null);
|
||||
log.setIp(ip);
|
||||
log.setDuration(duration);
|
||||
log.setStatus("1");
|
||||
log.setErrorMsg(error.getMessage());
|
||||
|
||||
return operationLogService.save(log)
|
||||
.doOnError(e -> logger.error("错误日志保存失败: {}", e.getMessage()))
|
||||
.onErrorResume(e -> Mono.empty());
|
||||
})
|
||||
.then(Mono.error(error));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private OperationInfo findOperationInfo(String method, String path) {
|
||||
String key = method + ":" + path;
|
||||
if (OPERATION_MAPPING.containsKey(key)) {
|
||||
return OPERATION_MAPPING.get(key);
|
||||
}
|
||||
|
||||
for (Map.Entry<String, OperationInfo> entry : OPERATION_MAPPING.entrySet()) {
|
||||
String mappingKey = entry.getKey();
|
||||
if (key.startsWith(mappingKey)) {
|
||||
return entry.getValue();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static class OperationInfo {
|
||||
final String module;
|
||||
final String operation;
|
||||
|
||||
OperationInfo(String module, String operation) {
|
||||
this.module = module;
|
||||
this.operation = operation;
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -39,7 +39,7 @@ public class AuditLogArchiveService implements IAuditLogArchiveService {
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Transactional(transactionManager = "connectionFactoryTransactionManager")
|
||||
public Mono<Long> archiveOldLogs(int daysToKeep) {
|
||||
LocalDateTime archiveBefore = LocalDateTime.now().minusDays(daysToKeep);
|
||||
|
||||
@@ -53,7 +53,7 @@ public class AuditLogArchiveService implements IAuditLogArchiveService {
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Transactional(transactionManager = "connectionFactoryTransactionManager")
|
||||
public Mono<AuditLogArchive> archiveLog(AuditLog auditLog) {
|
||||
AuditLogArchive archive = convertToArchive(auditLog);
|
||||
|
||||
@@ -99,7 +99,7 @@ public class AuditLogArchiveService implements IAuditLogArchiveService {
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Transactional(transactionManager = "connectionFactoryTransactionManager")
|
||||
public Mono<Void> deleteArchivedLogsOlderThan(LocalDateTime date) {
|
||||
return auditLogArchiveRepository.findByOperationTimeBetween(LocalDateTime.MIN, date)
|
||||
.flatMap(archive -> auditLogArchiveRepository.deleteById(archive.getId()))
|
||||
|
||||
+5
-5
@@ -160,13 +160,13 @@ public class AuditLogService implements IAuditLogService {
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Transactional(transactionManager = "connectionFactoryTransactionManager")
|
||||
public Mono<Void> deleteById(Long id) {
|
||||
return auditLogRepository.deleteById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Transactional(transactionManager = "connectionFactoryTransactionManager")
|
||||
public Mono<Void> logicalDeleteById(Long id) {
|
||||
return auditLogRepository.findById(id)
|
||||
.flatMap(auditLog -> {
|
||||
@@ -177,7 +177,7 @@ public class AuditLogService implements IAuditLogService {
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Transactional(transactionManager = "connectionFactoryTransactionManager")
|
||||
public Mono<Void> logicalDeleteByIds(List<Long> ids) {
|
||||
return Flux.fromIterable(ids)
|
||||
.flatMap(this::logicalDeleteById)
|
||||
@@ -185,7 +185,7 @@ public class AuditLogService implements IAuditLogService {
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Transactional(transactionManager = "connectionFactoryTransactionManager")
|
||||
public Mono<Void> restoreById(Long id) {
|
||||
return auditLogRepository.findById(id)
|
||||
.flatMap(auditLog -> {
|
||||
@@ -196,7 +196,7 @@ public class AuditLogService implements IAuditLogService {
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Transactional(transactionManager = "connectionFactoryTransactionManager")
|
||||
public Mono<Void> restoreByIds(List<Long> ids) {
|
||||
return Flux.fromIterable(ids)
|
||||
.flatMap(this::restoreById)
|
||||
|
||||
+7
-7
@@ -1,5 +1,6 @@
|
||||
package cn.novalon.gym.manage.sys.config;
|
||||
|
||||
import cn.novalon.gym.manage.sys.audit.OperationLogWebFilter;
|
||||
import cn.novalon.gym.manage.sys.security.JwtAuthenticationFilter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -11,22 +12,20 @@ import org.springframework.security.config.web.server.SecurityWebFiltersOrder;
|
||||
import org.springframework.security.config.web.server.ServerHttpSecurity;
|
||||
import org.springframework.security.web.server.SecurityWebFilterChain;
|
||||
|
||||
/**
|
||||
* 安全配置类
|
||||
*
|
||||
* @author 张翔
|
||||
* @date 2026-03-13
|
||||
*/
|
||||
@Configuration
|
||||
@EnableWebFluxSecurity
|
||||
public class SecurityConfig {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(SecurityConfig.class);
|
||||
private final JwtAuthenticationFilter jwtAuthenticationFilter;
|
||||
private final OperationLogWebFilter operationLogWebFilter;
|
||||
private final Environment environment;
|
||||
|
||||
public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter, Environment environment) {
|
||||
public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter,
|
||||
OperationLogWebFilter operationLogWebFilter,
|
||||
Environment environment) {
|
||||
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
|
||||
this.operationLogWebFilter = operationLogWebFilter;
|
||||
this.environment = environment;
|
||||
}
|
||||
|
||||
@@ -46,6 +45,7 @@ public class SecurityConfig {
|
||||
.httpBasic(ServerHttpSecurity.HttpBasicSpec::disable)
|
||||
.formLogin(ServerHttpSecurity.FormLoginSpec::disable)
|
||||
.addFilterBefore(jwtAuthenticationFilter, SecurityWebFiltersOrder.AUTHENTICATION)
|
||||
.addFilterAfter(operationLogWebFilter, SecurityWebFiltersOrder.AUTHORIZATION)
|
||||
.authorizeExchange(spec -> {
|
||||
spec.pathMatchers("/api/auth/**").permitAll()
|
||||
.pathMatchers("/api/public/**").permitAll()
|
||||
|
||||
-1
@@ -29,7 +29,6 @@ public class OperationLogService implements IOperationLogService {
|
||||
|
||||
@Override
|
||||
public Mono<OperationLog> save(OperationLog log) {
|
||||
log.setCreatedAt(LocalDateTime.now());
|
||||
return logRepository.save(log);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -99,7 +99,7 @@ public class SysPermissionService implements ISysPermissionService {
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Transactional(transactionManager = "connectionFactoryTransactionManager")
|
||||
public Mono<Void> assignPermissionsToRole(Long roleId, List<Long> permissionIds) {
|
||||
return rolePermissionRepository.deleteByRoleId(roleId)
|
||||
.then(Flux.fromIterable(permissionIds)
|
||||
|
||||
+1
-1
@@ -120,7 +120,7 @@ public class SysRoleService implements ISysRoleService {
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Transactional(transactionManager = "connectionFactoryTransactionManager")
|
||||
public Mono<Void> deleteRole(Long id) {
|
||||
logger.debug("开始删除角色,ID: {}", id);
|
||||
|
||||
|
||||
+2
-2
@@ -164,7 +164,7 @@ public class SysUserService implements ISysUserService {
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Transactional(transactionManager = "connectionFactoryTransactionManager")
|
||||
public Mono<Void> deleteUser(Long id) {
|
||||
logger.debug("开始删除用户,ID: {}", id);
|
||||
|
||||
@@ -244,7 +244,7 @@ public class SysUserService implements ISysUserService {
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Transactional(transactionManager = "connectionFactoryTransactionManager")
|
||||
public Mono<Void> assignRolesToUser(Long userId, List<Long> roleIds) {
|
||||
logger.debug("开始为用户分配角色,用户ID: {}, 角色IDs: {}", userId, roleIds);
|
||||
|
||||
|
||||
+76
-1
@@ -1,12 +1,13 @@
|
||||
package cn.novalon.gym.manage.sys.util;
|
||||
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* IP地址工具类
|
||||
* 用于从ServerRequest中获取客户端真实IP地址
|
||||
* 用于从ServerRequest或ServerHttpRequest中获取客户端真实IP地址
|
||||
* 支持代理服务器场景(X-Forwarded-For, X-Real-IP)
|
||||
*
|
||||
* @author 张翔
|
||||
@@ -48,6 +49,36 @@ public class IpUtils {
|
||||
return UNKNOWN;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从ServerHttpRequest中获取客户端真实IP地址
|
||||
* 支持代理服务器场景,优先级: X-Forwarded-For > X-Real-IP > RemoteAddress
|
||||
*
|
||||
* @param request ServerHttpRequest对象
|
||||
* @return 客户端IP地址,获取失败返回"unknown"
|
||||
*/
|
||||
public static String getClientIp(ServerHttpRequest request) {
|
||||
if (request == null) {
|
||||
return UNKNOWN;
|
||||
}
|
||||
|
||||
String ip = getXForwardedForIp(request);
|
||||
if (isValidIp(ip)) {
|
||||
return ip;
|
||||
}
|
||||
|
||||
ip = getXRealIp(request);
|
||||
if (isValidIp(ip)) {
|
||||
return ip;
|
||||
}
|
||||
|
||||
ip = getRemoteAddress(request);
|
||||
if (isValidIp(ip)) {
|
||||
return ip;
|
||||
}
|
||||
|
||||
return UNKNOWN;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从X-Forwarded-For头获取IP地址
|
||||
* X-Forwarded-For格式: client, proxy1, proxy2
|
||||
@@ -98,4 +129,48 @@ public class IpUtils {
|
||||
private static boolean isValidIp(String ip) {
|
||||
return ip != null && ip.length() > 0 && !UNKNOWN.equalsIgnoreCase(ip);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从X-Forwarded-For头获取IP地址(ServerHttpRequest版本)
|
||||
* X-Forwarded-For格式: client, proxy1, proxy2
|
||||
* 取第一个非unknown的有效IP
|
||||
*/
|
||||
private static String getXForwardedForIp(ServerHttpRequest request) {
|
||||
String ip = request.getHeaders().getFirst("X-Forwarded-For");
|
||||
if (ip != null && ip.length() > 0 && !UNKNOWN.equalsIgnoreCase(ip)) {
|
||||
int index = ip.indexOf(",");
|
||||
if (index != -1) {
|
||||
return ip.substring(0, index);
|
||||
}
|
||||
return ip;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从X-Real-IP头获取IP地址(ServerHttpRequest版本)
|
||||
*/
|
||||
private static String getXRealIp(ServerHttpRequest request) {
|
||||
String ip = request.getHeaders().getFirst("X-Real-IP");
|
||||
if (ip != null && ip.length() > 0 && !UNKNOWN.equalsIgnoreCase(ip)) {
|
||||
return ip;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从RemoteAddress获取IP地址(ServerHttpRequest版本)
|
||||
* 将IPv6本地地址转换为IPv4格式
|
||||
*/
|
||||
private static String getRemoteAddress(ServerHttpRequest request) {
|
||||
InetSocketAddress remoteAddress = request.getRemoteAddress();
|
||||
if (remoteAddress != null) {
|
||||
String ip = remoteAddress.getAddress().getHostAddress();
|
||||
if (LOCALHOST_IPV6.equals(ip)) {
|
||||
ip = LOCALHOST_IP;
|
||||
}
|
||||
return ip;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
+5
-1
@@ -1,5 +1,6 @@
|
||||
package cn.novalon.gym.manage.sys.config;
|
||||
|
||||
import cn.novalon.gym.manage.sys.audit.OperationLogWebFilter;
|
||||
import cn.novalon.gym.manage.sys.security.JwtAuthenticationFilter;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -16,6 +17,9 @@ class SecurityConfigTest {
|
||||
@Mock
|
||||
private JwtAuthenticationFilter jwtAuthenticationFilter;
|
||||
|
||||
@Mock
|
||||
private OperationLogWebFilter operationLogWebFilter;
|
||||
|
||||
@Mock
|
||||
private Environment environment;
|
||||
|
||||
@@ -23,7 +27,7 @@ class SecurityConfigTest {
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
securityConfig = new SecurityConfig(jwtAuthenticationFilter, environment);
|
||||
securityConfig = new SecurityConfig(jwtAuthenticationFilter, operationLogWebFilter, environment);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ class IpUtilsTest {
|
||||
@Test
|
||||
@DisplayName("当request为null时,应返回unknown")
|
||||
void getClientIp_whenRequestIsNull_shouldReturnUnknown() {
|
||||
String ip = IpUtils.getClientIp(null);
|
||||
String ip = IpUtils.getClientIp((ServerRequest) null);
|
||||
assertEquals("unknown", ip);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user