From f68d18fbfcbeebbdae97811f14ecc28b5c974340 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E7=BF=94?= Date: Thu, 23 Apr 2026 16:35:14 +0800 Subject: [PATCH] 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 --- .../db/migration/V1__Create_all_tables.sql | 407 ++++++++++++++++++ gym-manage-api/manage-app/pom.xml | 4 + .../gym/manage/app/ManageApplication.java | 39 +- .../app/config/TransactionManagerConfig.java | 25 ++ .../src/main/resources/application-local.yml | 6 +- .../db/converter/AuditLogConverter.java | 9 +- .../gym/manage/db/entity/AuditLogEntity.java | 13 +- .../db/migration/V1__Create_all_tables.sql | 1 + .../V5__Create_operation_log_table.sql | 41 ++ .../gym/manage/sys/audit/AuditLogAspect.java | 315 -------------- .../sys/audit/OperationLogWebFilter.java | 181 ++++++++ .../service/impl/AuditLogArchiveService.java | 6 +- .../audit/service/impl/AuditLogService.java | 10 +- .../gym/manage/sys/config/SecurityConfig.java | 14 +- .../service/impl/OperationLogService.java | 1 - .../service/impl/SysPermissionService.java | 2 +- .../sys/core/service/impl/SysRoleService.java | 2 +- .../sys/core/service/impl/SysUserService.java | 4 +- .../novalon/gym/manage/sys/util/IpUtils.java | 77 +++- .../manage/sys/config/SecurityConfigTest.java | 6 +- .../gym/manage/sys/util/IpUtilsTest.java | 2 +- 21 files changed, 812 insertions(+), 353 deletions(-) create mode 100644 gym-manage-api/db/migration/V1__Create_all_tables.sql create mode 100644 gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/config/TransactionManagerConfig.java create mode 100644 gym-manage-api/manage-db/src/main/resources/db/migration/V5__Create_operation_log_table.sql delete mode 100644 gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/audit/AuditLogAspect.java create mode 100644 gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/audit/OperationLogWebFilter.java 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); }