diff --git a/gym-manage-api/db/migration/V1__Create_all_tables.sql b/gym-manage-api/db/migration/V1__Create_all_tables.sql
new file mode 100644
index 0000000..ab538c2
--- /dev/null
+++ b/gym-manage-api/db/migration/V1__Create_all_tables.sql
@@ -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 '归档时间';
diff --git a/gym-manage-api/manage-app/pom.xml b/gym-manage-api/manage-app/pom.xml
index 9351954..45fcc60 100644
--- a/gym-manage-api/manage-app/pom.xml
+++ b/gym-manage-api/manage-app/pom.xml
@@ -42,6 +42,10 @@
org.springframework.boot
spring-boot-starter-webflux
+
+ org.springframework.boot
+ spring-boot-starter-aop
+
org.springframework.boot
spring-boot-starter-actuator
diff --git a/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/ManageApplication.java b/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/ManageApplication.java
index be201b8..f74c2f7 100644
--- a/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/ManageApplication.java
+++ b/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/ManageApplication.java
@@ -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 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());
+ };
+ }
}
diff --git a/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/config/TransactionManagerConfig.java b/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/config/TransactionManagerConfig.java
new file mode 100644
index 0000000..1039476
--- /dev/null
+++ b/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/config/TransactionManagerConfig.java
@@ -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);
+ }
+}
diff --git a/gym-manage-api/manage-app/src/main/resources/application-local.yml b/gym-manage-api/manage-app/src/main/resources/application-local.yml
index 96ae438..2e9c4f6 100644
--- a/gym-manage-api/manage-app/src/main/resources/application-local.yml
+++ b/gym-manage-api/manage-app/src/main/resources/application-local.yml
@@ -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
\ No newline at end of file
+ org.flywaydb: DEBUG
+
+debug: true
\ No newline at end of file
diff --git a/gym-manage-api/manage-db/src/main/java/cn/novalon/gym/manage/db/converter/AuditLogConverter.java b/gym-manage-api/manage-db/src/main/java/cn/novalon/gym/manage/db/converter/AuditLogConverter.java
index c92cfa6..f7db8c3 100644
--- a/gym-manage-api/manage-db/src/main/java/cn/novalon/gym/manage/db/converter/AuditLogConverter.java
+++ b/gym-manage-api/manage-db/src/main/java/cn/novalon/gym/manage/db/converter/AuditLogConverter.java
@@ -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());
diff --git a/gym-manage-api/manage-db/src/main/java/cn/novalon/gym/manage/db/entity/AuditLogEntity.java b/gym-manage-api/manage-db/src/main/java/cn/novalon/gym/manage/db/entity/AuditLogEntity.java
index c1638b3..5fcfda0 100644
--- a/gym-manage-api/manage-db/src/main/java/cn/novalon/gym/manage/db/entity/AuditLogEntity.java
+++ b/gym-manage-api/manage-db/src/main/java/cn/novalon/gym/manage/db/entity/AuditLogEntity.java
@@ -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;
}
diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V1__Create_all_tables.sql b/gym-manage-api/manage-db/src/main/resources/db/migration/V1__Create_all_tables.sql
index 88b0986..ab538c2 100644
--- a/gym-manage-api/manage-db/src/main/resources/db/migration/V1__Create_all_tables.sql
+++ b/gym-manage-api/manage-db/src/main/resources/db/migration/V1__Create_all_tables.sql
@@ -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,
diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V5__Create_operation_log_table.sql b/gym-manage-api/manage-db/src/main/resources/db/migration/V5__Create_operation_log_table.sql
new file mode 100644
index 0000000..0794081
--- /dev/null
+++ b/gym-manage-api/manage-db/src/main/resources/db/migration/V5__Create_operation_log_table.sql
@@ -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 '删除时间';
diff --git a/gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/audit/AuditLogAspect.java b/gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/audit/AuditLogAspect.java
deleted file mode 100644
index 8a60aa8..0000000
--- a/gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/audit/AuditLogAspect.java
+++ /dev/null
@@ -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 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 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 : "未知");
- }
-}
diff --git a/gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/audit/OperationLogWebFilter.java b/gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/audit/OperationLogWebFilter.java
new file mode 100644
index 0000000..0d36934
--- /dev/null
+++ b/gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/audit/OperationLogWebFilter.java
@@ -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 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 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 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;
+ }
+ }
+}
diff --git a/gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/audit/service/impl/AuditLogArchiveService.java b/gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/audit/service/impl/AuditLogArchiveService.java
index 957ddbb..b7ae151 100644
--- a/gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/audit/service/impl/AuditLogArchiveService.java
+++ b/gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/audit/service/impl/AuditLogArchiveService.java
@@ -39,7 +39,7 @@ public class AuditLogArchiveService implements IAuditLogArchiveService {
}
@Override
- @Transactional
+ @Transactional(transactionManager = "connectionFactoryTransactionManager")
public Mono 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 archiveLog(AuditLog auditLog) {
AuditLogArchive archive = convertToArchive(auditLog);
@@ -99,7 +99,7 @@ public class AuditLogArchiveService implements IAuditLogArchiveService {
}
@Override
- @Transactional
+ @Transactional(transactionManager = "connectionFactoryTransactionManager")
public Mono deleteArchivedLogsOlderThan(LocalDateTime date) {
return auditLogArchiveRepository.findByOperationTimeBetween(LocalDateTime.MIN, date)
.flatMap(archive -> auditLogArchiveRepository.deleteById(archive.getId()))
diff --git a/gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/audit/service/impl/AuditLogService.java b/gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/audit/service/impl/AuditLogService.java
index 2890ba0..d942c61 100644
--- a/gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/audit/service/impl/AuditLogService.java
+++ b/gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/audit/service/impl/AuditLogService.java
@@ -160,13 +160,13 @@ public class AuditLogService implements IAuditLogService {
}
@Override
- @Transactional
+ @Transactional(transactionManager = "connectionFactoryTransactionManager")
public Mono deleteById(Long id) {
return auditLogRepository.deleteById(id);
}
@Override
- @Transactional
+ @Transactional(transactionManager = "connectionFactoryTransactionManager")
public Mono 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 logicalDeleteByIds(List ids) {
return Flux.fromIterable(ids)
.flatMap(this::logicalDeleteById)
@@ -185,7 +185,7 @@ public class AuditLogService implements IAuditLogService {
}
@Override
- @Transactional
+ @Transactional(transactionManager = "connectionFactoryTransactionManager")
public Mono 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 restoreByIds(List ids) {
return Flux.fromIterable(ids)
.flatMap(this::restoreById)
diff --git a/gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/config/SecurityConfig.java b/gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/config/SecurityConfig.java
index fa91f16..4df2a4b 100644
--- a/gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/config/SecurityConfig.java
+++ b/gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/config/SecurityConfig.java
@@ -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()
diff --git a/gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/core/service/impl/OperationLogService.java b/gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/core/service/impl/OperationLogService.java
index a198725..4a7d157 100644
--- a/gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/core/service/impl/OperationLogService.java
+++ b/gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/core/service/impl/OperationLogService.java
@@ -29,7 +29,6 @@ public class OperationLogService implements IOperationLogService {
@Override
public Mono save(OperationLog log) {
- log.setCreatedAt(LocalDateTime.now());
return logRepository.save(log);
}
diff --git a/gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/core/service/impl/SysPermissionService.java b/gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/core/service/impl/SysPermissionService.java
index 0b8b4bb..78eb28c 100644
--- a/gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/core/service/impl/SysPermissionService.java
+++ b/gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/core/service/impl/SysPermissionService.java
@@ -99,7 +99,7 @@ public class SysPermissionService implements ISysPermissionService {
}
@Override
- @Transactional
+ @Transactional(transactionManager = "connectionFactoryTransactionManager")
public Mono assignPermissionsToRole(Long roleId, List permissionIds) {
return rolePermissionRepository.deleteByRoleId(roleId)
.then(Flux.fromIterable(permissionIds)
diff --git a/gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/core/service/impl/SysRoleService.java b/gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/core/service/impl/SysRoleService.java
index fc13896..334b658 100644
--- a/gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/core/service/impl/SysRoleService.java
+++ b/gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/core/service/impl/SysRoleService.java
@@ -120,7 +120,7 @@ public class SysRoleService implements ISysRoleService {
}
@Override
- @Transactional
+ @Transactional(transactionManager = "connectionFactoryTransactionManager")
public Mono deleteRole(Long id) {
logger.debug("开始删除角色,ID: {}", id);
diff --git a/gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/core/service/impl/SysUserService.java b/gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/core/service/impl/SysUserService.java
index e606099..168a4c5 100644
--- a/gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/core/service/impl/SysUserService.java
+++ b/gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/core/service/impl/SysUserService.java
@@ -164,7 +164,7 @@ public class SysUserService implements ISysUserService {
}
@Override
- @Transactional
+ @Transactional(transactionManager = "connectionFactoryTransactionManager")
public Mono deleteUser(Long id) {
logger.debug("开始删除用户,ID: {}", id);
@@ -244,7 +244,7 @@ public class SysUserService implements ISysUserService {
}
@Override
- @Transactional
+ @Transactional(transactionManager = "connectionFactoryTransactionManager")
public Mono assignRolesToUser(Long userId, List roleIds) {
logger.debug("开始为用户分配角色,用户ID: {}, 角色IDs: {}", userId, roleIds);
diff --git a/gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/util/IpUtils.java b/gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/util/IpUtils.java
index 0e6518a..fbc3b49 100644
--- a/gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/util/IpUtils.java
+++ b/gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/util/IpUtils.java
@@ -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;
+ }
}
diff --git a/gym-manage-api/manage-sys/src/test/java/cn/novalon/gym/manage/sys/config/SecurityConfigTest.java b/gym-manage-api/manage-sys/src/test/java/cn/novalon/gym/manage/sys/config/SecurityConfigTest.java
index 95669fa..1e2e8ff 100644
--- a/gym-manage-api/manage-sys/src/test/java/cn/novalon/gym/manage/sys/config/SecurityConfigTest.java
+++ b/gym-manage-api/manage-sys/src/test/java/cn/novalon/gym/manage/sys/config/SecurityConfigTest.java
@@ -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
diff --git a/gym-manage-api/manage-sys/src/test/java/cn/novalon/gym/manage/sys/util/IpUtilsTest.java b/gym-manage-api/manage-sys/src/test/java/cn/novalon/gym/manage/sys/util/IpUtilsTest.java
index 8b3c811..4e82a6f 100644
--- a/gym-manage-api/manage-sys/src/test/java/cn/novalon/gym/manage/sys/util/IpUtilsTest.java
+++ b/gym-manage-api/manage-sys/src/test/java/cn/novalon/gym/manage/sys/util/IpUtilsTest.java
@@ -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);
}