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>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-webflux</artifactId>
|
<artifactId>spring-boot-starter-webflux</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-aop</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||||
|
|||||||
+35
-4
@@ -1,16 +1,24 @@
|
|||||||
package cn.novalon.gym.manage.app;
|
package cn.novalon.gym.manage.app;
|
||||||
|
|
||||||
|
import cn.novalon.gym.manage.sys.core.service.IOperationLogService;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.boot.CommandLineRunner;
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
import org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration;
|
import org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration;
|
||||||
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
|
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.ComponentScan;
|
import org.springframework.context.annotation.ComponentScan;
|
||||||
import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories;
|
import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories;
|
||||||
|
import org.springframework.web.server.WebFilter;
|
||||||
|
|
||||||
@SpringBootApplication(scanBasePackages = "cn.novalon.gym.manage", exclude = {ReactiveUserDetailsServiceAutoConfiguration.class})
|
import java.util.List;
|
||||||
@EnableR2dbcRepositories(basePackages = {"cn.novalon.gym.manage.db.dao", "cn.novalon.gym.manage.sys.audit.repository"})
|
|
||||||
|
@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 {
|
public class ManageApplication {
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(ManageApplication.class);
|
private static final Logger logger = LoggerFactory.getLogger(ManageApplication.class);
|
||||||
@@ -18,9 +26,32 @@ public class ManageApplication {
|
|||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
logger.info("应用程序启动中...");
|
logger.info("应用程序启动中...");
|
||||||
logger.info("包扫描路径: cn.novalon.gym.manage");
|
logger.info("包扫描路径: cn.novalon.gym.manage");
|
||||||
|
|
||||||
// 使用简单的启动方式,避免自动配置问题
|
|
||||||
SpringApplication.run(ManageApplication.class, args);
|
SpringApplication.run(ManageApplication.class, args);
|
||||||
logger.info("应用程序启动完成");
|
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:
|
logging:
|
||||||
level:
|
level:
|
||||||
cn.novalon.manage: DEBUG
|
cn.novalon.manage: DEBUG
|
||||||
|
cn.novalon.gym.manage: DEBUG
|
||||||
|
cn.novalon.gym.manage.sys.audit: DEBUG
|
||||||
org.springframework.r2dbc: DEBUG
|
org.springframework.r2dbc: DEBUG
|
||||||
cn.novalon.manage.db: 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.sys.audit.domain.AuditLog;
|
||||||
import cn.novalon.gym.manage.db.entity.AuditLogEntity;
|
import cn.novalon.gym.manage.db.entity.AuditLogEntity;
|
||||||
|
import io.r2dbc.postgresql.codec.Json;
|
||||||
|
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@@ -28,8 +29,8 @@ public class AuditLogConverter {
|
|||||||
domain.setOperationType(entity.getOperationType());
|
domain.setOperationType(entity.getOperationType());
|
||||||
domain.setOperator(entity.getOperator());
|
domain.setOperator(entity.getOperator());
|
||||||
domain.setOperationTime(entity.getOperationTime());
|
domain.setOperationTime(entity.getOperationTime());
|
||||||
domain.setBeforeData(entity.getBeforeData());
|
domain.setBeforeData(entity.getBeforeData() != null ? entity.getBeforeData().asString() : null);
|
||||||
domain.setAfterData(entity.getAfterData());
|
domain.setAfterData(entity.getAfterData() != null ? entity.getAfterData().asString() : null);
|
||||||
domain.setChangedFields(entity.getChangedFields());
|
domain.setChangedFields(entity.getChangedFields());
|
||||||
domain.setIpAddress(entity.getIpAddress());
|
domain.setIpAddress(entity.getIpAddress());
|
||||||
domain.setUserAgent(entity.getUserAgent());
|
domain.setUserAgent(entity.getUserAgent());
|
||||||
@@ -53,8 +54,8 @@ public class AuditLogConverter {
|
|||||||
entity.setOperationType(domain.getOperationType());
|
entity.setOperationType(domain.getOperationType());
|
||||||
entity.setOperator(domain.getOperator());
|
entity.setOperator(domain.getOperator());
|
||||||
entity.setOperationTime(domain.getOperationTime());
|
entity.setOperationTime(domain.getOperationTime());
|
||||||
entity.setBeforeData(domain.getBeforeData());
|
entity.setBeforeData(domain.getBeforeData() != null ? Json.of(domain.getBeforeData()) : null);
|
||||||
entity.setAfterData(domain.getAfterData());
|
entity.setAfterData(domain.getAfterData() != null ? Json.of(domain.getAfterData()) : null);
|
||||||
entity.setChangedFields(domain.getChangedFields());
|
entity.setChangedFields(domain.getChangedFields());
|
||||||
entity.setIpAddress(domain.getIpAddress());
|
entity.setIpAddress(domain.getIpAddress());
|
||||||
entity.setUserAgent(domain.getUserAgent());
|
entity.setUserAgent(domain.getUserAgent());
|
||||||
|
|||||||
+7
-6
@@ -1,5 +1,6 @@
|
|||||||
package cn.novalon.gym.manage.db.entity;
|
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.Column;
|
||||||
import org.springframework.data.relational.core.mapping.Table;
|
import org.springframework.data.relational.core.mapping.Table;
|
||||||
|
|
||||||
@@ -28,10 +29,10 @@ public class AuditLogEntity extends BaseEntity {
|
|||||||
private java.time.LocalDateTime operationTime;
|
private java.time.LocalDateTime operationTime;
|
||||||
|
|
||||||
@Column("before_data")
|
@Column("before_data")
|
||||||
private String beforeData;
|
private Json beforeData;
|
||||||
|
|
||||||
@Column("after_data")
|
@Column("after_data")
|
||||||
private String afterData;
|
private Json afterData;
|
||||||
|
|
||||||
@Column("changed_fields")
|
@Column("changed_fields")
|
||||||
private String[] changedFields;
|
private String[] changedFields;
|
||||||
@@ -85,19 +86,19 @@ public class AuditLogEntity extends BaseEntity {
|
|||||||
this.operationTime = operationTime;
|
this.operationTime = operationTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getBeforeData() {
|
public Json getBeforeData() {
|
||||||
return beforeData;
|
return beforeData;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setBeforeData(String beforeData) {
|
public void setBeforeData(Json beforeData) {
|
||||||
this.beforeData = beforeData;
|
this.beforeData = beforeData;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getAfterData() {
|
public Json getAfterData() {
|
||||||
return afterData;
|
return afterData;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setAfterData(String afterData) {
|
public void setAfterData(Json afterData) {
|
||||||
this.afterData = afterData;
|
this.afterData = afterData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ CREATE TABLE IF NOT EXISTS sys_user (
|
|||||||
phone VARCHAR(20),
|
phone VARCHAR(20),
|
||||||
nickname VARCHAR(100),
|
nickname VARCHAR(100),
|
||||||
status INTEGER DEFAULT 1,
|
status INTEGER DEFAULT 1,
|
||||||
|
role_id BIGINT,
|
||||||
create_by VARCHAR(50),
|
create_by VARCHAR(50),
|
||||||
update_by VARCHAR(50),
|
update_by VARCHAR(50),
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
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
|
@Override
|
||||||
@Transactional
|
@Transactional(transactionManager = "connectionFactoryTransactionManager")
|
||||||
public Mono<Long> archiveOldLogs(int daysToKeep) {
|
public Mono<Long> archiveOldLogs(int daysToKeep) {
|
||||||
LocalDateTime archiveBefore = LocalDateTime.now().minusDays(daysToKeep);
|
LocalDateTime archiveBefore = LocalDateTime.now().minusDays(daysToKeep);
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ public class AuditLogArchiveService implements IAuditLogArchiveService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional(transactionManager = "connectionFactoryTransactionManager")
|
||||||
public Mono<AuditLogArchive> archiveLog(AuditLog auditLog) {
|
public Mono<AuditLogArchive> archiveLog(AuditLog auditLog) {
|
||||||
AuditLogArchive archive = convertToArchive(auditLog);
|
AuditLogArchive archive = convertToArchive(auditLog);
|
||||||
|
|
||||||
@@ -99,7 +99,7 @@ public class AuditLogArchiveService implements IAuditLogArchiveService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional(transactionManager = "connectionFactoryTransactionManager")
|
||||||
public Mono<Void> deleteArchivedLogsOlderThan(LocalDateTime date) {
|
public Mono<Void> deleteArchivedLogsOlderThan(LocalDateTime date) {
|
||||||
return auditLogArchiveRepository.findByOperationTimeBetween(LocalDateTime.MIN, date)
|
return auditLogArchiveRepository.findByOperationTimeBetween(LocalDateTime.MIN, date)
|
||||||
.flatMap(archive -> auditLogArchiveRepository.deleteById(archive.getId()))
|
.flatMap(archive -> auditLogArchiveRepository.deleteById(archive.getId()))
|
||||||
|
|||||||
+5
-5
@@ -160,13 +160,13 @@ public class AuditLogService implements IAuditLogService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional(transactionManager = "connectionFactoryTransactionManager")
|
||||||
public Mono<Void> deleteById(Long id) {
|
public Mono<Void> deleteById(Long id) {
|
||||||
return auditLogRepository.deleteById(id);
|
return auditLogRepository.deleteById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional(transactionManager = "connectionFactoryTransactionManager")
|
||||||
public Mono<Void> logicalDeleteById(Long id) {
|
public Mono<Void> logicalDeleteById(Long id) {
|
||||||
return auditLogRepository.findById(id)
|
return auditLogRepository.findById(id)
|
||||||
.flatMap(auditLog -> {
|
.flatMap(auditLog -> {
|
||||||
@@ -177,7 +177,7 @@ public class AuditLogService implements IAuditLogService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional(transactionManager = "connectionFactoryTransactionManager")
|
||||||
public Mono<Void> logicalDeleteByIds(List<Long> ids) {
|
public Mono<Void> logicalDeleteByIds(List<Long> ids) {
|
||||||
return Flux.fromIterable(ids)
|
return Flux.fromIterable(ids)
|
||||||
.flatMap(this::logicalDeleteById)
|
.flatMap(this::logicalDeleteById)
|
||||||
@@ -185,7 +185,7 @@ public class AuditLogService implements IAuditLogService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional(transactionManager = "connectionFactoryTransactionManager")
|
||||||
public Mono<Void> restoreById(Long id) {
|
public Mono<Void> restoreById(Long id) {
|
||||||
return auditLogRepository.findById(id)
|
return auditLogRepository.findById(id)
|
||||||
.flatMap(auditLog -> {
|
.flatMap(auditLog -> {
|
||||||
@@ -196,7 +196,7 @@ public class AuditLogService implements IAuditLogService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional(transactionManager = "connectionFactoryTransactionManager")
|
||||||
public Mono<Void> restoreByIds(List<Long> ids) {
|
public Mono<Void> restoreByIds(List<Long> ids) {
|
||||||
return Flux.fromIterable(ids)
|
return Flux.fromIterable(ids)
|
||||||
.flatMap(this::restoreById)
|
.flatMap(this::restoreById)
|
||||||
|
|||||||
+7
-7
@@ -1,5 +1,6 @@
|
|||||||
package cn.novalon.gym.manage.sys.config;
|
package cn.novalon.gym.manage.sys.config;
|
||||||
|
|
||||||
|
import cn.novalon.gym.manage.sys.audit.OperationLogWebFilter;
|
||||||
import cn.novalon.gym.manage.sys.security.JwtAuthenticationFilter;
|
import cn.novalon.gym.manage.sys.security.JwtAuthenticationFilter;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
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.config.web.server.ServerHttpSecurity;
|
||||||
import org.springframework.security.web.server.SecurityWebFilterChain;
|
import org.springframework.security.web.server.SecurityWebFilterChain;
|
||||||
|
|
||||||
/**
|
|
||||||
* 安全配置类
|
|
||||||
*
|
|
||||||
* @author 张翔
|
|
||||||
* @date 2026-03-13
|
|
||||||
*/
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableWebFluxSecurity
|
@EnableWebFluxSecurity
|
||||||
public class SecurityConfig {
|
public class SecurityConfig {
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(SecurityConfig.class);
|
private static final Logger logger = LoggerFactory.getLogger(SecurityConfig.class);
|
||||||
private final JwtAuthenticationFilter jwtAuthenticationFilter;
|
private final JwtAuthenticationFilter jwtAuthenticationFilter;
|
||||||
|
private final OperationLogWebFilter operationLogWebFilter;
|
||||||
private final Environment environment;
|
private final Environment environment;
|
||||||
|
|
||||||
public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter, Environment environment) {
|
public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter,
|
||||||
|
OperationLogWebFilter operationLogWebFilter,
|
||||||
|
Environment environment) {
|
||||||
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
|
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
|
||||||
|
this.operationLogWebFilter = operationLogWebFilter;
|
||||||
this.environment = environment;
|
this.environment = environment;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,6 +45,7 @@ public class SecurityConfig {
|
|||||||
.httpBasic(ServerHttpSecurity.HttpBasicSpec::disable)
|
.httpBasic(ServerHttpSecurity.HttpBasicSpec::disable)
|
||||||
.formLogin(ServerHttpSecurity.FormLoginSpec::disable)
|
.formLogin(ServerHttpSecurity.FormLoginSpec::disable)
|
||||||
.addFilterBefore(jwtAuthenticationFilter, SecurityWebFiltersOrder.AUTHENTICATION)
|
.addFilterBefore(jwtAuthenticationFilter, SecurityWebFiltersOrder.AUTHENTICATION)
|
||||||
|
.addFilterAfter(operationLogWebFilter, SecurityWebFiltersOrder.AUTHORIZATION)
|
||||||
.authorizeExchange(spec -> {
|
.authorizeExchange(spec -> {
|
||||||
spec.pathMatchers("/api/auth/**").permitAll()
|
spec.pathMatchers("/api/auth/**").permitAll()
|
||||||
.pathMatchers("/api/public/**").permitAll()
|
.pathMatchers("/api/public/**").permitAll()
|
||||||
|
|||||||
-1
@@ -29,7 +29,6 @@ public class OperationLogService implements IOperationLogService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Mono<OperationLog> save(OperationLog log) {
|
public Mono<OperationLog> save(OperationLog log) {
|
||||||
log.setCreatedAt(LocalDateTime.now());
|
|
||||||
return logRepository.save(log);
|
return logRepository.save(log);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -99,7 +99,7 @@ public class SysPermissionService implements ISysPermissionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional(transactionManager = "connectionFactoryTransactionManager")
|
||||||
public Mono<Void> assignPermissionsToRole(Long roleId, List<Long> permissionIds) {
|
public Mono<Void> assignPermissionsToRole(Long roleId, List<Long> permissionIds) {
|
||||||
return rolePermissionRepository.deleteByRoleId(roleId)
|
return rolePermissionRepository.deleteByRoleId(roleId)
|
||||||
.then(Flux.fromIterable(permissionIds)
|
.then(Flux.fromIterable(permissionIds)
|
||||||
|
|||||||
+1
-1
@@ -120,7 +120,7 @@ public class SysRoleService implements ISysRoleService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional(transactionManager = "connectionFactoryTransactionManager")
|
||||||
public Mono<Void> deleteRole(Long id) {
|
public Mono<Void> deleteRole(Long id) {
|
||||||
logger.debug("开始删除角色,ID: {}", id);
|
logger.debug("开始删除角色,ID: {}", id);
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -164,7 +164,7 @@ public class SysUserService implements ISysUserService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional(transactionManager = "connectionFactoryTransactionManager")
|
||||||
public Mono<Void> deleteUser(Long id) {
|
public Mono<Void> deleteUser(Long id) {
|
||||||
logger.debug("开始删除用户,ID: {}", id);
|
logger.debug("开始删除用户,ID: {}", id);
|
||||||
|
|
||||||
@@ -244,7 +244,7 @@ public class SysUserService implements ISysUserService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional(transactionManager = "connectionFactoryTransactionManager")
|
||||||
public Mono<Void> assignRolesToUser(Long userId, List<Long> roleIds) {
|
public Mono<Void> assignRolesToUser(Long userId, List<Long> roleIds) {
|
||||||
logger.debug("开始为用户分配角色,用户ID: {}, 角色IDs: {}", userId, roleIds);
|
logger.debug("开始为用户分配角色,用户ID: {}, 角色IDs: {}", userId, roleIds);
|
||||||
|
|
||||||
|
|||||||
+76
-1
@@ -1,12 +1,13 @@
|
|||||||
package cn.novalon.gym.manage.sys.util;
|
package cn.novalon.gym.manage.sys.util;
|
||||||
|
|
||||||
|
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* IP地址工具类
|
* IP地址工具类
|
||||||
* 用于从ServerRequest中获取客户端真实IP地址
|
* 用于从ServerRequest或ServerHttpRequest中获取客户端真实IP地址
|
||||||
* 支持代理服务器场景(X-Forwarded-For, X-Real-IP)
|
* 支持代理服务器场景(X-Forwarded-For, X-Real-IP)
|
||||||
*
|
*
|
||||||
* @author 张翔
|
* @author 张翔
|
||||||
@@ -48,6 +49,36 @@ public class IpUtils {
|
|||||||
return UNKNOWN;
|
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头获取IP地址
|
||||||
* X-Forwarded-For格式: client, proxy1, proxy2
|
* X-Forwarded-For格式: client, proxy1, proxy2
|
||||||
@@ -98,4 +129,48 @@ public class IpUtils {
|
|||||||
private static boolean isValidIp(String ip) {
|
private static boolean isValidIp(String ip) {
|
||||||
return ip != null && ip.length() > 0 && !UNKNOWN.equalsIgnoreCase(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;
|
package cn.novalon.gym.manage.sys.config;
|
||||||
|
|
||||||
|
import cn.novalon.gym.manage.sys.audit.OperationLogWebFilter;
|
||||||
import cn.novalon.gym.manage.sys.security.JwtAuthenticationFilter;
|
import cn.novalon.gym.manage.sys.security.JwtAuthenticationFilter;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
@@ -16,6 +17,9 @@ class SecurityConfigTest {
|
|||||||
@Mock
|
@Mock
|
||||||
private JwtAuthenticationFilter jwtAuthenticationFilter;
|
private JwtAuthenticationFilter jwtAuthenticationFilter;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private OperationLogWebFilter operationLogWebFilter;
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private Environment environment;
|
private Environment environment;
|
||||||
|
|
||||||
@@ -23,7 +27,7 @@ class SecurityConfigTest {
|
|||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
void setUp() {
|
||||||
securityConfig = new SecurityConfig(jwtAuthenticationFilter, environment);
|
securityConfig = new SecurityConfig(jwtAuthenticationFilter, operationLogWebFilter, environment);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
+1
-1
@@ -22,7 +22,7 @@ class IpUtilsTest {
|
|||||||
@Test
|
@Test
|
||||||
@DisplayName("当request为null时,应返回unknown")
|
@DisplayName("当request为null时,应返回unknown")
|
||||||
void getClientIp_whenRequestIsNull_shouldReturnUnknown() {
|
void getClientIp_whenRequestIsNull_shouldReturnUnknown() {
|
||||||
String ip = IpUtils.getClientIp(null);
|
String ip = IpUtils.getClientIp((ServerRequest) null);
|
||||||
assertEquals("unknown", ip);
|
assertEquals("unknown", ip);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user