feat(sys): 迁移 manage-sys 模块主代码(任务 T2.1)

- 删除 novalon manage-sys 现有 Java 源代码
- 从 gym-manage 复制所有 Java 文件并替换包名 cn.novalon.gym.manage → cn.novalon.manage
- 替换 AutoConfiguration.imports
- 编译验证通过
This commit is contained in:
张翔
2026-04-27 14:06:17 +08:00
parent 956ba6a505
commit cf067dccc3
26 changed files with 703 additions and 363 deletions
@@ -2,171 +2,84 @@ package cn.novalon.manage.sys.audit;
import cn.novalon.manage.sys.audit.domain.AuditLog;
import cn.novalon.manage.sys.audit.service.IAuditLogService;
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.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
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
@Deprecated
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) {
public AuditLogAspect(IAuditLogService auditLogService) {
this.auditLogService = auditLogService;
this.objectMapper = objectMapper;
logger.info("=== AuditLogAspect 初始化完成 ===");
}
@Around("execution(* cn.novalon.manage.db.repository.*Repository.save(..)) || " +
"execution(* cn.novalon.manage.db.repository.*Repository.delete(..)) || " +
"execution(* cn.novalon.manage.db.repository.*Repository.deleteById(..))")
public Object logAuditEvent(ProceedingJoinPoint joinPoint) throws Throwable {
String methodName = joinPoint.getSignature().getName();
@Before("execution(* cn.novalon.manage.sys.core.service.impl.SysUserService.createUser(..))")
public void testAopWorking() {
logger.info("=== AuditLogAspect @Before 测试: SysUserService.createUser 被调用 ===");
}
@Around("@annotation(auditable)")
public Object logAuditEvent(ProceedingJoinPoint joinPoint, Auditable auditable) throws Throwable {
String methodName = ((MethodSignature) joinPoint.getSignature()).getName();
String className = joinPoint.getTarget().getClass().getSimpleName();
Object[] args = joinPoint.getArgs();
String entityType = auditable.entityType();
String operationType = auditable.operationType();
String operationType = determineOperationType(methodName);
String entityType = extractEntityType(className);
logger.debug("拦截审计操作: {}.{}, 操作类型: {}, 实体类型: {}",
className, methodName, operationType, entityType);
logger.debug("审计切面拦截: {}.{}(), entityType={}, operationType={}", className, methodName, entityType, operationType);
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 {
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 ((Mono<Object>) result).flatMap(retValue -> {
Long entityId = extractIdFromResult(retValue);
String afterData = serializeEntity(retValue);
return createAndSaveAuditLog(
entityType, finalEntityId, finalOperationType,
finalBeforeData, afterData, savedEntity
).thenReturn(savedEntity);
entityType, entityId, operationType,
null, afterData
).thenReturn(retValue);
});
} else if (result instanceof Flux) {
return ((Flux<Object>) result).collectList()
.flatMapMany(list -> {
String afterData = serializeEntity(list);
return createAndSaveAuditLog(
entityType, null, operationType,
null, afterData
).thenMany(Flux.fromIterable(list));
});
}
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);
logger.error("审计日志记录失败: {}.{}()", className, methodName, error);
throw error;
}
}
private Mono<Void> createAndSaveAuditLog(String entityType, Long entityId,
String operationType, String beforeData,
String afterData, Object entity) {
String afterData) {
logger.debug("创建审计日志: entityType={}, entityId={}, operationType={}", entityType, entityId, operationType);
return ReactiveSecurityContextHolder.getContext()
.map(ctx -> ctx.getAuthentication().getPrincipal())
.defaultIfEmpty("system")
@@ -178,121 +91,65 @@ public class AuditLogAspect {
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()))
return auditLogService.saveAsync(auditLog)
.doOnSuccess(saved -> logger.debug("审计日志保存成功: {} - {}, ID={}",
entityType, operationType, saved.getId()))
.doOnError(error -> logger.error("审计日志保存失败: {}", error.getMessage()))
.then();
})
.onErrorResume(error -> {
logger.error("创建审计日志失败,但不影响主流程: {}", error.getMessage());
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";
private Long extractIdFromResult(Object result) {
if (result == null) {
return null;
}
return "UNKNOWN";
try {
var getIdMethod = result.getClass().getMethod("getId");
Object id = getIdMethod.invoke(result);
if (id instanceof Number) {
return ((Number) id).longValue();
}
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";
if (id instanceof String) {
try {
return Long.parseLong((String) id);
} catch (NumberFormatException e) {
return null;
}
return className.replace("Repository", "").replace("Impl", "");
}
private String fetchEntityBeforeData(String entityType, Long entityId) {
} catch (NoSuchMethodException e) {
logger.debug("结果对象没有getId方法: {}", result.getClass().getSimpleName());
} catch (Exception e) {
logger.debug("提取结果ID失败: {}", e.getMessage());
}
return null;
}
private String serializeEntity(Object entity) {
try {
return objectMapper.writeValueAsString(entity);
ObjectMapper mapper = new ObjectMapper()
.registerModule(new JavaTimeModule())
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.disable(SerializationFeature.FAIL_ON_SELF_REFERENCES);
return mapper.writeValueAsString(entity);
} catch (Exception e) {
logger.error("序列化实体失败: {}", e.getMessage());
return null;
}
}
private Long extractEntityId(Object entity) {
logger.debug("提取实体ID: entity class={}", entity.getClass().getName());
if (entity instanceof Persistable) {
Persistable<?> persistable = (Persistable<?>) entity;
Object id = persistable.getId();
logger.debug("Persistable实体ID: id={}, isNew={}", id, persistable.isNew());
return id != null ? ((Number) id).longValue() : null;
}
logger.debug("实体不是Persistable类型");
return null;
}
private String[] extractChangedFields(String beforeData, String afterData) {
try {
JsonNode beforeNode = objectMapper.readTree(beforeData);
JsonNode afterNode = objectMapper.readTree(afterData);
List<String> changedFields = new ArrayList<>();
beforeNode.fieldNames().forEachRemaining(fieldName -> {
JsonNode beforeValue = beforeNode.get(fieldName);
JsonNode afterValue = afterNode.get(fieldName);
if (afterValue == null || !beforeValue.equals(afterValue)) {
changedFields.add(fieldName);
}
});
afterNode.fieldNames().forEachRemaining(fieldName -> {
if (!beforeNode.has(fieldName)) {
changedFields.add(fieldName);
}
});
return changedFields.toArray(new String[0]);
} catch (Exception e) {
logger.error("提取变更字段失败: {}", e.getMessage());
return new String[0];
}
}
private String generateDescription(String entityType, String operationType, Long entityId) {
String operation = "";
switch (operationType) {
case "CREATE":
operation = "创建";
break;
case "UPDATE":
operation = "更新";
break;
case "DELETE":
operation = "删除";
break;
default:
operation = "操作";
}
String operation = switch (operationType) {
case "CREATE" -> "创建";
case "UPDATE" -> "更新";
case "DELETE" -> "删除";
default -> "操作";
};
return String.format("%s%s (ID: %s)", operation, entityType,
entityId != null ? entityId : "未知");
@@ -0,0 +1,80 @@
package cn.novalon.manage.sys.audit;
import cn.novalon.manage.sys.audit.domain.AuditLog;
import cn.novalon.manage.sys.audit.service.IAuditLogService;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import reactor.core.publisher.Mono;
public final class AuditLogHelper {
private static final Logger logger = LoggerFactory.getLogger(AuditLogHelper.class);
private static final ObjectMapper objectMapper = new ObjectMapper()
.registerModule(new JavaTimeModule())
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.disable(SerializationFeature.FAIL_ON_SELF_REFERENCES);
private AuditLogHelper() {}
public static Mono<Void> record(IAuditLogService auditLogService,
String entityType, Long entityId,
String operationType, Object afterEntity) {
return record(auditLogService, entityType, entityId, operationType, null, afterEntity);
}
public static Mono<Void> record(IAuditLogService auditLogService,
String entityType, Long entityId,
String operationType, Object beforeEntity, Object afterEntity) {
return ReactiveSecurityContextHolder.getContext()
.map(ctx -> ctx.getAuthentication().getPrincipal())
.defaultIfEmpty("system")
.flatMap(principal -> {
AuditLog auditLog = new AuditLog();
auditLog.generateId();
auditLog.setEntityType(entityType);
auditLog.setEntityId(entityId != null ? entityId : 0L);
auditLog.setOperationType(operationType);
auditLog.setOperator(principal instanceof String ? (String) principal : "system");
auditLog.setBeforeData(serializeEntity(beforeEntity));
auditLog.setAfterData(serializeEntity(afterEntity));
auditLog.setDescription(generateDescription(entityType, operationType, entityId));
logger.info("记录审计日志: {} {} ID={}, operator={}", operationType, entityType, entityId, auditLog.getOperator());
return auditLogService.saveAsync(auditLog)
.doOnSuccess(saved -> logger.info("审计日志保存成功: {} - {}, ID={}",
entityType, operationType, saved.getId()))
.doOnError(error -> logger.error("审计日志保存失败: {}", error.getMessage()))
.then();
})
.onErrorResume(error -> {
logger.error("记录审计日志失败,但不影响主流程: {}", error.getMessage(), error);
return Mono.empty();
});
}
private static String serializeEntity(Object entity) {
try {
if (entity == null) return null;
return objectMapper.writeValueAsString(entity);
} catch (Exception e) {
logger.error("序列化实体失败: {}", e.getMessage());
return null;
}
}
private static String generateDescription(String entityType, String operationType, Long entityId) {
String operation = switch (operationType) {
case "CREATE" -> "创建";
case "UPDATE" -> "更新";
case "DELETE" -> "删除";
default -> "操作";
};
return String.format("%s%s (ID: %s)", operation, entityType,
entityId != null ? entityId : "未知");
}
}
@@ -0,0 +1,15 @@
package cn.novalon.manage.sys.audit;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Auditable {
String entityType();
String operationType() default "CREATE";
String description() default "";
}
@@ -0,0 +1,181 @@
package cn.novalon.manage.sys.audit;
import cn.novalon.manage.sys.core.domain.OperationLog;
import cn.novalon.manage.sys.core.service.IOperationLogService;
import cn.novalon.manage.sys.util.IpUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.PostConstruct;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpMethod;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.HandlerStrategies;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Component
@Order(Ordered.LOWEST_PRECEDENCE)
public class OperationLogWebFilter implements WebFilter {
private static final Logger logger = LoggerFactory.getLogger(OperationLogWebFilter.class);
private final IOperationLogService operationLogService;
private final ObjectMapper objectMapper;
private static final Map<String, OperationInfo> OPERATION_MAPPING = new ConcurrentHashMap<>();
static {
OPERATION_MAPPING.put("POST:/api/roles", new OperationInfo("角色管理", "创建角色"));
OPERATION_MAPPING.put("PUT:/api/roles/", new OperationInfo("角色管理", "更新角色"));
OPERATION_MAPPING.put("DELETE:/api/roles/", new OperationInfo("角色管理", "删除角色"));
OPERATION_MAPPING.put("POST:/api/users", new OperationInfo("用户管理", "创建用户"));
OPERATION_MAPPING.put("PUT:/api/users/", new OperationInfo("用户管理", "更新用户"));
OPERATION_MAPPING.put("DELETE:/api/users/", new OperationInfo("用户管理", "删除用户"));
OPERATION_MAPPING.put("POST:/api/users/", new OperationInfo("用户管理", "用户操作"));
OPERATION_MAPPING.put("POST:/api/menus", new OperationInfo("菜单管理", "创建菜单"));
OPERATION_MAPPING.put("PUT:/api/menus/", new OperationInfo("菜单管理", "更新菜单"));
OPERATION_MAPPING.put("DELETE:/api/menus/", new OperationInfo("菜单管理", "删除菜单"));
}
public OperationLogWebFilter(IOperationLogService operationLogService, ObjectMapper objectMapper) {
logger.info("=== OperationLogWebFilter 构造函数被调用 ===");
this.operationLogService = operationLogService;
this.objectMapper = objectMapper;
}
@PostConstruct
public void init() {
logger.info("=== OperationLogWebFilter 初始化 ===");
logger.info("操作日志映射配置数量: {}", OPERATION_MAPPING.size());
OPERATION_MAPPING.forEach((key, value) -> {
logger.info(" {} -> {}:{}", key, value.module, value.operation);
});
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String method = request.getMethod().name();
String path = request.getPath().value();
logger.info("WebFilter 拦截请求: {} {}", method, path);
OperationInfo operationInfo = findOperationInfo(method, path);
if (operationInfo == null) {
logger.info("未匹配到操作日志配置,跳过: {} {}", method, path);
return chain.filter(exchange);
}
logger.info("匹配到操作日志配置: {} {} -> {}:{}", method, path, operationInfo.module, operationInfo.operation);
long startTime = System.currentTimeMillis();
String ip = IpUtils.getClientIp(request);
return Mono.deferContextual(contextView -> {
return chain.filter(exchange)
.then(Mono.defer(() -> {
long duration = System.currentTimeMillis() - startTime;
logger.info("请求处理完成,准备保存操作日志: {} {}, 耗时: {}ms", method, path, duration);
return ReactiveSecurityContextHolder.getContext()
.flatMap(securityContext -> {
Object principal = securityContext.getAuthentication().getPrincipal();
String username = principal instanceof String ? (String) principal : "system";
logger.info("获取到用户名: {}", username);
return Mono.just(username);
})
.defaultIfEmpty("system")
.flatMap(username -> {
logger.info("开始保存操作日志: 用户={}, 操作={}", username,
operationInfo.module + " - " + operationInfo.operation);
OperationLog log = new OperationLog();
log.setUsername(username);
log.setOperation(operationInfo.module + " - " + operationInfo.operation);
log.setMethod(method + " " + path);
log.setParams(null);
log.setIp(ip);
log.setDuration(duration);
log.setStatus("0");
return operationLogService.save(log)
.doOnSuccess(saved -> logger.info("操作日志保存成功: {} - {}",
operationInfo.module, operationInfo.operation))
.doOnError(e -> logger.error("操作日志保存失败: {}", e.getMessage(), e))
.onErrorResume(e -> Mono.empty());
})
.then();
}))
.onErrorResume(error -> {
long duration = System.currentTimeMillis() - startTime;
logger.error("请求处理失败: {} {}, 错误: {}", method, path, error.getMessage());
return ReactiveSecurityContextHolder.getContext()
.flatMap(securityContext -> {
Object principal = securityContext.getAuthentication().getPrincipal();
String username = principal instanceof String ? (String) principal : "system";
return Mono.just(username);
})
.defaultIfEmpty("system")
.flatMap(username -> {
OperationLog log = new OperationLog();
log.setUsername(username);
log.setOperation(operationInfo.module + " - " + operationInfo.operation);
log.setMethod(method + " " + path);
log.setParams(null);
log.setIp(ip);
log.setDuration(duration);
log.setStatus("1");
log.setErrorMsg(error.getMessage());
return operationLogService.save(log)
.doOnError(e -> logger.error("错误日志保存失败: {}", e.getMessage()))
.onErrorResume(e -> Mono.empty());
})
.then(Mono.error(error));
});
});
}
private OperationInfo findOperationInfo(String method, String path) {
String key = method + ":" + path;
if (OPERATION_MAPPING.containsKey(key)) {
return OPERATION_MAPPING.get(key);
}
for (Map.Entry<String, OperationInfo> entry : OPERATION_MAPPING.entrySet()) {
String mappingKey = entry.getKey();
if (key.startsWith(mappingKey)) {
return entry.getValue();
}
}
return null;
}
private static class OperationInfo {
final String module;
final String operation;
OperationInfo(String module, String operation) {
this.module = module;
this.operation = operation;
}
}
}
@@ -139,6 +139,29 @@ public class AuditLog extends BaseDomain {
this.description = description;
}
@Override
public String toString() {
return "AuditLog{" +
"id=" + id +
", entityType='" + entityType + '\'' +
", entityId=" + entityId +
", operationType='" + operationType + '\'' +
", operator='" + operator + '\'' +
", operationTime=" + operationTime +
", beforeData='" + beforeData + '\'' +
", afterData='" + afterData + '\'' +
", changedFields=" + java.util.Arrays.toString(changedFields) +
", ipAddress='" + ipAddress + '\'' +
", userAgent='" + userAgent + '\'' +
", description='" + description + '\'' +
", createBy='" + createBy + '\'' +
", updateBy='" + updateBy + '\'' +
", createdAt=" + createdAt +
", updatedAt=" + updatedAt +
", deletedAt=" + deletedAt +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
@@ -39,7 +39,7 @@ public class AuditLogArchiveService implements IAuditLogArchiveService {
}
@Override
@Transactional
@Transactional(transactionManager = "connectionFactoryTransactionManager")
public Mono<Long> archiveOldLogs(int daysToKeep) {
LocalDateTime archiveBefore = LocalDateTime.now().minusDays(daysToKeep);
@@ -53,7 +53,7 @@ public class AuditLogArchiveService implements IAuditLogArchiveService {
}
@Override
@Transactional
@Transactional(transactionManager = "connectionFactoryTransactionManager")
public Mono<AuditLogArchive> archiveLog(AuditLog auditLog) {
AuditLogArchive archive = convertToArchive(auditLog);
@@ -99,7 +99,7 @@ public class AuditLogArchiveService implements IAuditLogArchiveService {
}
@Override
@Transactional
@Transactional(transactionManager = "connectionFactoryTransactionManager")
public Mono<Void> deleteArchivedLogsOlderThan(LocalDateTime date) {
return auditLogArchiveRepository.findByOperationTimeBetween(LocalDateTime.MIN, date)
.flatMap(archive -> auditLogArchiveRepository.deleteById(archive.getId()))
@@ -150,7 +150,6 @@ public class AuditLogService implements IAuditLogService {
}
@Override
@Async("auditLogExecutor")
public Mono<AuditLog> saveAsync(AuditLog auditLog) {
logger.debug("异步保存审计日志: {} - {}", auditLog.getEntityType(), auditLog.getOperationType());
@@ -161,13 +160,13 @@ public class AuditLogService implements IAuditLogService {
}
@Override
@Transactional
@Transactional(transactionManager = "connectionFactoryTransactionManager")
public Mono<Void> deleteById(Long id) {
return auditLogRepository.deleteById(id);
}
@Override
@Transactional
@Transactional(transactionManager = "connectionFactoryTransactionManager")
public Mono<Void> logicalDeleteById(Long id) {
return auditLogRepository.findById(id)
.flatMap(auditLog -> {
@@ -178,7 +177,7 @@ public class AuditLogService implements IAuditLogService {
}
@Override
@Transactional
@Transactional(transactionManager = "connectionFactoryTransactionManager")
public Mono<Void> logicalDeleteByIds(List<Long> ids) {
return Flux.fromIterable(ids)
.flatMap(this::logicalDeleteById)
@@ -186,7 +185,7 @@ public class AuditLogService implements IAuditLogService {
}
@Override
@Transactional
@Transactional(transactionManager = "connectionFactoryTransactionManager")
public Mono<Void> restoreById(Long id) {
return auditLogRepository.findById(id)
.flatMap(auditLog -> {
@@ -197,7 +196,7 @@ public class AuditLogService implements IAuditLogService {
}
@Override
@Transactional
@Transactional(transactionManager = "connectionFactoryTransactionManager")
public Mono<Void> restoreByIds(List<Long> ids) {
return Flux.fromIterable(ids)
.flatMap(this::restoreById)
@@ -1,5 +1,6 @@
package cn.novalon.manage.sys.config;
import cn.novalon.manage.sys.audit.OperationLogWebFilter;
import cn.novalon.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()
@@ -76,10 +76,29 @@ public abstract class BaseDomain {
return this.id;
}
/**
* 删除(幂等操作)
* 已删除的对象不会更新删除时间
*/
public void delete() {
if (this.deletedAt == null) {
this.deletedAt = LocalDateTime.now();
}
}
/**
* 恢复已删除的对象
*/
public void restore() {
this.deletedAt = null;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
BaseDomain that = (BaseDomain) o;
return id != null && id.equals(that.id);
}
@@ -77,18 +77,4 @@ public class SysPermission extends BaseDomain {
public void setStatus(Integer status) {
this.status = status;
}
/**
* 删除权限
*/
public void delete() {
this.deletedAt = java.time.LocalDateTime.now();
}
/**
* 恢复权限
*/
public void restore() {
this.deletedAt = null;
}
}
@@ -57,18 +57,4 @@ public class SysRole extends BaseDomain {
public void setStatus(Integer status) {
this.status = status;
}
/**
* 删除角色
*/
public void delete() {
this.deletedAt = LocalDateTime.now();
}
/**
* 恢复角色
*/
public void restore() {
this.deletedAt = null;
}
}
@@ -100,11 +100,4 @@ public class SysUser extends BaseDomain {
public void setStatus(Integer status) {
this.status = status;
}
/**
* 删除用户
*/
public void delete() {
this.deletedAt = LocalDateTime.now();
}
}
@@ -24,6 +24,8 @@ public interface ISysMenuRepository {
Mono<SysMenu> save(SysMenu sysMenu);
Mono<SysMenu> update(SysMenu sysMenu);
Mono<Void> deleteById(Long id);
Flux<SysMenu> findAll();
@@ -28,6 +28,8 @@ public interface ISysUserRepository {
Mono<SysUser> save(SysUser sysUser);
Mono<SysUser> update(SysUser sysUser);
Mono<Void> deleteById(Long id);
Flux<SysUser> findAll();
@@ -48,13 +48,11 @@ public class DictionaryService implements IDictionaryService {
@Override
public Mono<Dictionary> save(Dictionary dictionary) {
if (dictionary.getId() == null) {
dictionary.setCreatedAt(LocalDateTime.now());
return checkTypeAndCodeExists(dictionary.getType(), dictionary.getCode())
.flatMap(exists -> {
if (exists) {
return Mono.error(new DictionaryAlreadyExistsException(dictionary.getType(), dictionary.getCode()));
}
dictionary.setUpdatedAt(LocalDateTime.now());
return repository.save(dictionary);
});
}
@@ -29,7 +29,6 @@ public class OperationLogService implements IOperationLogService {
@Override
public Mono<OperationLog> save(OperationLog log) {
log.setCreatedAt(LocalDateTime.now());
return logRepository.save(log);
}
@@ -1,27 +1,23 @@
package cn.novalon.manage.sys.core.service.impl;
import cn.novalon.manage.sys.audit.AuditLogHelper;
import cn.novalon.manage.sys.audit.service.IAuditLogService;
import cn.novalon.manage.sys.core.domain.SysConfig;
import cn.novalon.manage.sys.core.repository.ISysConfigRepository;
import cn.novalon.manage.sys.core.service.ISysConfigService;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* 系统配置服务实现类
*
* @author 张翔
* @date 2026-03-14
*/
@Service
public class SysConfigService implements ISysConfigService {
private final ISysConfigRepository repository;
private final IAuditLogService auditLogService;
public SysConfigService(ISysConfigRepository repository) {
public SysConfigService(ISysConfigRepository repository, IAuditLogService auditLogService) {
this.repository = repository;
this.auditLogService = auditLogService;
}
@Override
@@ -30,27 +26,28 @@ public class SysConfigService implements ISysConfigService {
}
@Override
@Cacheable(value = "sysConfig", key = "#id")
public Mono<SysConfig> findById(Long id) {
return repository.findById(id);
}
@Override
@Cacheable(value = "sysConfig", key = "#configKey")
public Mono<SysConfig> findByConfigKey(String configKey) {
return repository.findByConfigKeyAndDeletedAtIsNull(configKey);
}
@Override
@CacheEvict(value = "sysConfig", allEntries = true)
public Mono<SysConfig> save(SysConfig config) {
return repository.save(config);
return repository.save(config)
.flatMap(saved -> AuditLogHelper.record(auditLogService, "Config", saved.getId(), "CREATE", saved)
.thenReturn(saved));
}
@Override
@CacheEvict(value = "sysConfig", key = "#id")
public Mono<Void> deleteById(Long id) {
return repository.deleteByIdAndDeletedAtIsNull(id);
return repository.findById(id)
.flatMap(config -> repository.deleteByIdAndDeletedAtIsNull(id)
.then(AuditLogHelper.record(auditLogService, "Config", id, "DELETE", config, null)))
.then();
}
@Override
@@ -1,5 +1,7 @@
package cn.novalon.manage.sys.core.service.impl;
import cn.novalon.manage.sys.audit.AuditLogHelper;
import cn.novalon.manage.sys.audit.service.IAuditLogService;
import cn.novalon.manage.sys.core.domain.SysDictType;
import cn.novalon.manage.sys.core.repository.ISysDictTypeRepository;
import cn.novalon.manage.sys.core.service.ISysDictTypeService;
@@ -7,19 +9,15 @@ import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* 字典类型服务实现类
*
* @author 张翔
* @date 2026-03-14
*/
@Service
public class SysDictTypeService implements ISysDictTypeService {
private final ISysDictTypeRepository repository;
private final IAuditLogService auditLogService;
public SysDictTypeService(ISysDictTypeRepository repository) {
public SysDictTypeService(ISysDictTypeRepository repository, IAuditLogService auditLogService) {
this.repository = repository;
this.auditLogService = auditLogService;
}
@Override
@@ -39,11 +37,16 @@ public class SysDictTypeService implements ISysDictTypeService {
@Override
public Mono<SysDictType> save(SysDictType dictType) {
return repository.save(dictType);
return repository.save(dictType)
.flatMap(saved -> AuditLogHelper.record(auditLogService, "Dict", saved.getId(), "CREATE", saved)
.thenReturn(saved));
}
@Override
public Mono<Void> deleteById(Long id) {
return repository.deleteByIdAndDeletedAtIsNull(id);
return repository.findById(id)
.flatMap(dict -> repository.deleteByIdAndDeletedAtIsNull(id)
.then(AuditLogHelper.record(auditLogService, "Dict", id, "DELETE", dict, null)))
.then();
}
}
@@ -6,6 +6,8 @@ import cn.novalon.manage.sys.core.service.ISysMenuService;
import cn.novalon.manage.sys.core.command.CreateMenuCommand;
import cn.novalon.manage.sys.core.command.UpdateMenuCommand;
import cn.novalon.manage.common.util.StatusConstants;
import cn.novalon.manage.sys.audit.AuditLogHelper;
import cn.novalon.manage.sys.audit.service.IAuditLogService;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@@ -24,9 +26,11 @@ import java.util.stream.Collectors;
public class SysMenuService implements ISysMenuService {
private final ISysMenuRepository menuRepository;
private final IAuditLogService auditLogService;
public SysMenuService(ISysMenuRepository menuRepository) {
public SysMenuService(ISysMenuRepository menuRepository, IAuditLogService auditLogService) {
this.menuRepository = menuRepository;
this.auditLogService = auditLogService;
}
@Override
@@ -46,8 +50,9 @@ public class SysMenuService implements ISysMenuService {
@Override
public Mono<SysMenu> createMenu(SysMenu menu) {
menu.setCreatedAt(LocalDateTime.now());
return menuRepository.save(menu);
return menuRepository.save(menu)
.flatMap(saved -> AuditLogHelper.record(auditLogService, "Menu", saved.getId(), "CREATE", saved)
.thenReturn(saved));
}
@Override
@@ -60,14 +65,18 @@ public class SysMenuService implements ISysMenuService {
menu.setComponent(command.component());
menu.setPerms(command.perms());
menu.setStatus(command.status() != null ? command.status() : StatusConstants.ENABLED);
menu.setCreatedAt(LocalDateTime.now());
return menuRepository.save(menu);
return menuRepository.save(menu)
.flatMap(saved -> AuditLogHelper.record(auditLogService, "Menu", saved.getId(), "CREATE", saved)
.thenReturn(saved));
}
@Override
public Mono<SysMenu> updateMenu(SysMenu menu) {
menu.setUpdatedAt(LocalDateTime.now());
return menuRepository.save(menu);
return menuRepository.findById(menu.getId())
.flatMap(before -> menuRepository.update(menu)
.flatMap(saved -> AuditLogHelper.record(auditLogService, "Menu", saved.getId(), "UPDATE", before, saved)
.thenReturn(saved)));
}
@Override
@@ -75,6 +84,15 @@ public class SysMenuService implements ISysMenuService {
return menuRepository.findById(command.id())
.switchIfEmpty(Mono.error(new RuntimeException("Menu not found")))
.flatMap(menu -> {
SysMenu before = new SysMenu();
before.setId(menu.getId());
before.setParentId(menu.getParentId());
before.setMenuName(menu.getMenuName());
before.setMenuType(menu.getMenuType());
before.setOrderNum(menu.getOrderNum());
before.setComponent(menu.getComponent());
before.setPerms(menu.getPerms());
before.setStatus(menu.getStatus());
if (command.parentId() != null) {
menu.setParentId(command.parentId());
}
@@ -97,13 +115,18 @@ public class SysMenuService implements ISysMenuService {
menu.setStatus(command.status());
}
menu.setUpdatedAt(LocalDateTime.now());
return menuRepository.save(menu);
return menuRepository.update(menu)
.flatMap(saved -> AuditLogHelper.record(auditLogService, "Menu", saved.getId(), "UPDATE", before, saved)
.thenReturn(saved));
});
}
@Override
public Mono<Void> deleteMenu(Long id) {
return menuRepository.deleteById(id);
return menuRepository.findById(id)
.flatMap(menu -> menuRepository.deleteById(id)
.then(AuditLogHelper.record(auditLogService, "Menu", id, "DELETE", menu, null)))
.then();
}
@Override
@@ -1,11 +1,15 @@
package cn.novalon.manage.sys.core.service.impl;
import cn.novalon.manage.common.util.StatusConstants;
import cn.novalon.manage.sys.audit.AuditLogHelper;
import cn.novalon.manage.sys.audit.service.IAuditLogService;
import cn.novalon.manage.sys.core.domain.SysPermission;
import cn.novalon.manage.sys.core.domain.SysRolePermission;
import cn.novalon.manage.sys.core.repository.ISysPermissionRepository;
import cn.novalon.manage.sys.core.repository.ISysRolePermissionRepository;
import cn.novalon.manage.sys.core.service.ISysPermissionService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -24,13 +28,18 @@ import java.util.List;
@Service
public class SysPermissionService implements ISysPermissionService {
private static final Logger logger = LoggerFactory.getLogger(SysPermissionService.class);
private final ISysPermissionRepository permissionRepository;
private final ISysRolePermissionRepository rolePermissionRepository;
private final IAuditLogService auditLogService;
public SysPermissionService(ISysPermissionRepository permissionRepository,
ISysRolePermissionRepository rolePermissionRepository) {
ISysRolePermissionRepository rolePermissionRepository,
IAuditLogService auditLogService) {
this.permissionRepository = permissionRepository;
this.rolePermissionRepository = rolePermissionRepository;
this.auditLogService = auditLogService;
}
@Override
@@ -60,25 +69,41 @@ public class SysPermissionService implements ISysPermissionService {
@Override
public Mono<SysPermission> createPermission(SysPermission permission) {
permission.setCreatedAt(LocalDateTime.now());
if (permission.getStatus() == null) {
permission.setStatus(StatusConstants.ENABLED);
}
return permissionRepository.save(permission);
return permissionRepository.save(permission)
.flatMap(saved -> AuditLogHelper.record(auditLogService, "Permission", saved.getId(), "CREATE", saved)
.doOnError(e -> logger.error("Audit log failed for Permission CREATE id={}: {}", saved.getId(), e.getMessage()))
.thenReturn(saved));
}
@Override
public Mono<SysPermission> updatePermission(SysPermission permission) {
permission.setUpdatedAt(LocalDateTime.now());
return permissionRepository.updatePermission(permission);
return permissionRepository.findById(permission.getId())
.flatMap(before -> permissionRepository.updatePermission(permission)
.flatMap(saved -> AuditLogHelper.record(auditLogService, "Permission", saved.getId(), "UPDATE", before, saved)
.thenReturn(saved)));
}
@Override
public Mono<Void> deletePermission(Long id) {
return permissionRepository.findById(id)
.flatMap(permission -> {
SysPermission before = new SysPermission();
before.setId(permission.getId());
before.setPermissionName(permission.getPermissionName());
before.setPermissionCode(permission.getPermissionCode());
before.setResource(permission.getResource());
before.setAction(permission.getAction());
before.setStatus(permission.getStatus());
before.setCreatedAt(permission.getCreatedAt());
before.setUpdatedAt(permission.getUpdatedAt());
before.setDeletedAt(permission.getDeletedAt());
permission.delete();
return permissionRepository.updatePermission(permission)
.flatMap(saved -> AuditLogHelper.record(auditLogService, "Permission", id, "DELETE", before, saved))
.then(rolePermissionRepository.deleteByPermissionId(id));
});
}
@@ -99,7 +124,7 @@ public class SysPermissionService implements ISysPermissionService {
}
@Override
@Transactional
@Transactional(transactionManager = "connectionFactoryTransactionManager")
public Mono<Void> assignPermissionsToRole(Long roleId, List<Long> permissionIds) {
return rolePermissionRepository.deleteByRoleId(roleId)
.then(Flux.fromIterable(permissionIds)
@@ -107,7 +132,6 @@ public class SysPermissionService implements ISysPermissionService {
SysRolePermission rolePermission = new SysRolePermission();
rolePermission.setRoleId(roleId);
rolePermission.setPermissionId(permissionId);
rolePermission.setCreatedAt(LocalDateTime.now());
return rolePermissionRepository.save(rolePermission);
})
.then());
@@ -1,6 +1,8 @@
package cn.novalon.manage.sys.core.service.impl;
import cn.novalon.manage.common.util.StatusConstants;
import cn.novalon.manage.sys.audit.AuditLogHelper;
import cn.novalon.manage.sys.audit.service.IAuditLogService;
import cn.novalon.manage.sys.core.domain.SysRole;
import cn.novalon.manage.sys.core.query.SysRoleQuery;
import cn.novalon.manage.sys.core.repository.ISysRoleRepository;
@@ -21,12 +23,6 @@ import reactor.core.publisher.Mono;
import java.time.LocalDateTime;
/**
* 系统角色服务实现类
*
* @author 张翔
* @date 2026-03-14
*/
@Service
public class SysRoleService implements ISysRoleService {
@@ -35,13 +31,16 @@ public class SysRoleService implements ISysRoleService {
private final ISysUserService userService;
private final IUserRoleRepository userRoleRepository;
private final ISysRolePermissionRepository rolePermissionRepository;
private final IAuditLogService auditLogService;
public SysRoleService(ISysRoleRepository roleRepository, ISysUserService userService,
IUserRoleRepository userRoleRepository, ISysRolePermissionRepository rolePermissionRepository) {
IUserRoleRepository userRoleRepository, ISysRolePermissionRepository rolePermissionRepository,
IAuditLogService auditLogService) {
this.roleRepository = roleRepository;
this.userService = userService;
this.userRoleRepository = userRoleRepository;
this.rolePermissionRepository = rolePermissionRepository;
this.auditLogService = auditLogService;
}
@Override
@@ -76,7 +75,9 @@ public class SysRoleService implements ISysRoleService {
if (role.getStatus() == null) {
role.setStatus(StatusConstants.ENABLED);
}
return roleRepository.save(role);
return roleRepository.save(role)
.flatMap(saved -> AuditLogHelper.record(auditLogService, "Role", saved.getId(), "CREATE", saved)
.thenReturn(saved));
}
@Override
@@ -88,13 +89,18 @@ public class SysRoleService implements ISysRoleService {
role.setRoleSort(command.roleSort());
role.setStatus(command.status() != null ? command.status() : StatusConstants.ENABLED);
role.setCreatedAt(LocalDateTime.now());
return roleRepository.save(role);
return roleRepository.save(role)
.flatMap(saved -> AuditLogHelper.record(auditLogService, "Role", saved.getId(), "CREATE", saved)
.thenReturn(saved));
}
@Override
public Mono<SysRole> updateRole(SysRole role) {
role.setUpdatedAt(LocalDateTime.now());
return roleRepository.save(role);
return roleRepository.findById(role.getId())
.flatMap(before -> roleRepository.updateRole(role)
.flatMap(saved -> AuditLogHelper.record(auditLogService, "Role", saved.getId(), "UPDATE", before, saved)
.thenReturn(saved)));
}
@Override
@@ -102,6 +108,15 @@ public class SysRoleService implements ISysRoleService {
return roleRepository.findById(command.id())
.switchIfEmpty(Mono.error(new RuntimeException("Role not found")))
.flatMap(role -> {
SysRole before = new SysRole();
before.setId(role.getId());
before.setRoleName(role.getRoleName());
before.setRoleKey(role.getRoleKey());
before.setRoleSort(role.getRoleSort());
before.setStatus(role.getStatus());
before.setCreatedAt(role.getCreatedAt());
before.setUpdatedAt(role.getUpdatedAt());
before.setDeletedAt(role.getDeletedAt());
if (command.roleName() != null) {
role.setRoleName(command.roleName());
}
@@ -115,12 +130,14 @@ public class SysRoleService implements ISysRoleService {
role.setStatus(command.status());
}
role.setUpdatedAt(LocalDateTime.now());
return roleRepository.save(role);
return roleRepository.updateRole(role)
.flatMap(saved -> AuditLogHelper.record(auditLogService, "Role", saved.getId(), "UPDATE", before, saved)
.thenReturn(saved));
});
}
@Override
@Transactional
@Transactional(transactionManager = "connectionFactoryTransactionManager")
public Mono<Void> deleteRole(Long id) {
logger.debug("开始删除角色,ID: {}", id);
@@ -138,7 +155,8 @@ public class SysRoleService implements ISysRoleService {
.doOnError(e -> logger.error("更新用户角色ID失败", e))
.then(roleRepository.deleteById(id))
.doOnSuccess(v -> logger.debug("成功删除角色"))
.doOnError(e -> logger.error("删除角色失败", e));
.doOnError(e -> logger.error("删除角色失败", e))
.then(AuditLogHelper.record(auditLogService, "Role", id, "DELETE", role, null));
});
}
@@ -156,8 +174,19 @@ public class SysRoleService implements ISysRoleService {
public Mono<SysRole> logicalDeleteRole(Long id) {
return roleRepository.findByIdIncludingDeleted(id)
.flatMap(role -> {
SysRole before = new SysRole();
before.setId(role.getId());
before.setRoleName(role.getRoleName());
before.setRoleKey(role.getRoleKey());
before.setRoleSort(role.getRoleSort());
before.setStatus(role.getStatus());
before.setCreatedAt(role.getCreatedAt());
before.setUpdatedAt(role.getUpdatedAt());
before.setDeletedAt(role.getDeletedAt());
role.delete();
return roleRepository.updateRole(role);
return roleRepository.updateRole(role)
.flatMap(saved -> AuditLogHelper.record(auditLogService, "Role", saved.getId(), "DELETE", before, saved)
.thenReturn(saved));
});
}
@@ -1,9 +1,12 @@
package cn.novalon.manage.sys.core.service.impl;
import cn.novalon.manage.common.util.StatusConstants;
import cn.novalon.manage.sys.audit.AuditLogHelper;
import cn.novalon.manage.sys.audit.service.IAuditLogService;
import cn.novalon.manage.sys.core.domain.SysUser;
import cn.novalon.manage.sys.core.domain.SysRole;
import cn.novalon.manage.sys.core.domain.UserRole;
import cn.novalon.manage.sys.core.query.SysUserQuery;
import cn.novalon.manage.common.dto.PageRequest;
import cn.novalon.manage.common.dto.PageResponse;
import cn.novalon.manage.sys.core.repository.ISysUserRepository;
@@ -25,16 +28,6 @@ import reactor.core.publisher.Mono;
import java.time.LocalDateTime;
import java.util.List;
/**
* 用户服务实现类
*
* 文件定义:实现用户管理的核心业务逻辑
* 涉及业务:用户注册、登录、信息修改、删除、密码修改、逻辑删除等用户生命周期管理
* 算法:使用R2DBC进行响应式数据库操作,支持分页查询、条件查询、批量操作
*
* @author 张翔
* @date 2026-03-13
*/
@Service
public class SysUserService implements ISysUserService {
@@ -43,15 +36,18 @@ public class SysUserService implements ISysUserService {
private final ISysRoleRepository roleRepository;
private final IUserRoleRepository userRoleRepository;
private final PasswordEncoder passwordEncoder;
private final IAuditLogService auditLogService;
public SysUserService(ISysUserRepository userRepository,
ISysRoleRepository roleRepository,
IUserRoleRepository userRoleRepository,
@Qualifier("passwordEncoder") PasswordEncoder passwordEncoder) {
@Qualifier("passwordEncoder") PasswordEncoder passwordEncoder,
IAuditLogService auditLogService) {
this.userRepository = userRepository;
this.roleRepository = roleRepository;
this.userRoleRepository = userRoleRepository;
this.passwordEncoder = passwordEncoder;
this.auditLogService = auditLogService;
logger.info("使用的密码编码器类型: {}", passwordEncoder.getClass().getName());
}
@@ -80,7 +76,9 @@ public class SysUserService implements ISysUserService {
@Override
public Mono<PageResponse<SysUser>> findUsersByPage(PageRequest pageRequest) {
return userRepository.findByQueryWithPagination(null, pageRequest);
SysUserQuery query = new SysUserQuery();
query.setKeyword(pageRequest.getKeyword());
return userRepository.findByQueryWithPagination(query, pageRequest);
}
@Override
@@ -110,7 +108,9 @@ public class SysUserService implements ISysUserService {
if (user.getStatus() == null) {
user.setStatus(StatusConstants.ENABLED);
}
return userRepository.save(user);
return userRepository.save(user)
.flatMap(saved -> AuditLogHelper.record(auditLogService, "User", saved.getId(), "CREATE", saved)
.thenReturn(saved));
}
@Override
@@ -124,13 +124,18 @@ public class SysUserService implements ISysUserService {
user.setPhone(command.phone());
user.setRoleId(command.roleId());
user.setStatus(command.status() != null ? command.status() : StatusConstants.ENABLED);
return userRepository.save(user);
return userRepository.save(user)
.flatMap(saved -> AuditLogHelper.record(auditLogService, "User", saved.getId(), "CREATE", saved)
.thenReturn(saved));
}
@Override
public Mono<SysUser> updateUser(SysUser user) {
user.setUpdatedAt(LocalDateTime.now());
return userRepository.save(user);
return userRepository.findById(user.getId())
.flatMap(before -> userRepository.update(user)
.flatMap(saved -> AuditLogHelper.record(auditLogService, "User", saved.getId(), "UPDATE", before, saved)
.thenReturn(saved)));
}
@Override
@@ -138,6 +143,17 @@ public class SysUserService implements ISysUserService {
return userRepository.findById(command.id())
.switchIfEmpty(Mono.error(new RuntimeException("User not found")))
.flatMap(user -> {
SysUser before = new SysUser();
before.setId(user.getId());
before.setUsername(user.getUsername());
before.setEmail(user.getEmail());
before.setNickname(user.getNickname());
before.setPhone(user.getPhone());
before.setRoleId(user.getRoleId());
before.setStatus(user.getStatus());
before.setCreatedAt(user.getCreatedAt());
before.setUpdatedAt(user.getUpdatedAt());
before.setDeletedAt(user.getDeletedAt());
if (command.username() != null) {
user.setUsername(command.username());
}
@@ -156,12 +172,15 @@ public class SysUserService implements ISysUserService {
user.setStatus(command.status());
}
user.setUpdatedAt(LocalDateTime.now());
return userRepository.save(user);
return userRepository.update(user)
.flatMap(saved -> AuditLogHelper
.record(auditLogService, "User", saved.getId(), "UPDATE", before, saved)
.thenReturn(saved));
});
}
@Override
@Transactional
@Transactional(transactionManager = "connectionFactoryTransactionManager")
public Mono<Void> deleteUser(Long id) {
logger.debug("开始删除用户,ID: {}", id);
@@ -174,7 +193,8 @@ public class SysUserService implements ISysUserService {
.doOnError(e -> logger.error("删除用户角色关联记录失败", e))
.then(userRepository.deleteById(id))
.doOnSuccess(v -> logger.debug("成功删除用户"))
.doOnError(e -> logger.error("删除用户失败", e));
.doOnError(e -> logger.error("删除用户失败", e))
.then(AuditLogHelper.record(auditLogService, "User", id, "DELETE", user, null));
});
}
@@ -192,7 +212,10 @@ public class SysUserService implements ISysUserService {
}
user.setPassword(passwordEncoder.encode(newPassword));
user.setUpdatedAt(LocalDateTime.now());
return userRepository.save(user);
return userRepository.update(user)
.flatMap(saved -> AuditLogHelper
.record(auditLogService, "User", saved.getId(), "UPDATE", saved)
.thenReturn(saved));
});
}
@@ -214,8 +237,20 @@ public class SysUserService implements ISysUserService {
public Mono<Void> logicalDeleteUser(Long id) {
return userRepository.findByIdIncludingDeleted(id)
.flatMap(user -> {
SysUser before = new SysUser();
before.setId(user.getId());
before.setUsername(user.getUsername());
before.setEmail(user.getEmail());
before.setNickname(user.getNickname());
before.setPhone(user.getPhone());
before.setRoleId(user.getRoleId());
before.setStatus(user.getStatus());
before.setCreatedAt(user.getCreatedAt());
before.setUpdatedAt(user.getUpdatedAt());
before.setDeletedAt(user.getDeletedAt());
user.setDeletedAt(LocalDateTime.now());
return userRepository.save(user);
return userRepository.save(user)
.flatMap(saved -> AuditLogHelper.record(auditLogService, "User", saved.getId(), "DELETE", before, saved));
})
.then();
}
@@ -241,7 +276,7 @@ public class SysUserService implements ISysUserService {
}
@Override
@Transactional
@Transactional(transactionManager = "connectionFactoryTransactionManager")
public Mono<Void> assignRolesToUser(Long userId, List<Long> roleIds) {
logger.debug("开始为用户分配角色,用户ID: {}, 角色IDs: {}", userId, roleIds);
@@ -249,7 +284,8 @@ public class SysUserService implements ISysUserService {
logger.debug("角色列表为空,删除用户的所有角色关联");
return userRoleRepository.deleteByUserId(userId)
.doOnSuccess(v -> logger.debug("成功删除用户的所有角色关联"))
.doOnError(e -> logger.error("删除用户角色关联失败", e));
.doOnError(e -> logger.error("删除用户角色关联失败", e))
.then(AuditLogHelper.record(auditLogService, "User", userId, "UPDATE", null));
}
return userRoleRepository.deleteByUserId(userId)
@@ -262,12 +298,12 @@ public class SysUserService implements ISysUserService {
UserRole userRole = new UserRole();
userRole.setUserId(userId);
userRole.setRoleId(roleId);
userRole.setCreatedAt(LocalDateTime.now());
return userRoleRepository.save(userRole)
.doOnSuccess(v -> logger.debug("成功保存用户角色关联"))
.doOnError(e -> logger.error("保存用户角色关联失败", e));
})
.then());
.then())
.then(AuditLogHelper.record(auditLogService, "User", userId, "UPDATE", null));
}
@Override
@@ -1,15 +1,25 @@
package cn.novalon.manage.sys.dto.request;
import java.util.List;
import java.util.stream.Collectors;
public class AssignRolesRequest {
private List<Long> roleIds;
private List<String> roleIds;
public List<Long> getRoleIds() {
public List<String> getRoleIds() {
return roleIds;
}
public void setRoleIds(List<Long> roleIds) {
public void setRoleIds(List<String> roleIds) {
this.roleIds = roleIds;
}
public List<Long> getRoleIdsAsLong() {
if (roleIds == null) {
return null;
}
return roleIds.stream()
.map(Long::valueOf)
.collect(Collectors.toList());
}
}
@@ -63,6 +63,10 @@ public class SysUserHandler {
String order = request.queryParam("order").orElse("asc");
String keyword = request.queryParam("keyword").orElse(null);
System.out.println("=== SysUserHandler.getUsersByPage ===");
System.out.println("page: " + page + ", size: " + size + ", sort: " + sort + ", order: " + order);
System.out.println("keyword: " + keyword);
PageRequest pageRequest = new PageRequest();
pageRequest.setPage(page);
pageRequest.setSize(size);
@@ -259,7 +263,7 @@ public class SysUserHandler {
public Mono<ServerResponse> assignRoles(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return request.bodyToMono(AssignRolesRequest.class)
.flatMap(req -> userService.assignRolesToUser(id, req.getRoleIds()))
.flatMap(req -> userService.assignRolesToUser(id, req.getRoleIdsAsLong()))
.then(ServerResponse.ok().build())
.onErrorResume(error -> {
logger.error("分配角色失败", error);
@@ -1,12 +1,13 @@
package cn.novalon.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;
}
}
@@ -1,2 +1 @@
cn.novalon.manage.sys.config.ExceptionLogConfig
cn.novalon.manage.sys.config.SystemRouter