refactor(common): 操作日志采集与落库解耦为端口-适配器

common 层日志切面改为只依赖 framework 层定义的 OperationLogPort 端口,
日志采集逻辑从 common.aspect.LogAspect 迁移至 framework.web.log.OperationLogAspect,
由 system.adapter.log.OperationLogAdapter 实现端口落库 sys_log 表,
通过 LogConverter 完成载荷到实体的转换,消除 common 对 system 的反向依赖。

同步将 system.security.adapter 下的适配器迁移至 system.adapter.security,
保持 system.adapter.<业务域> 的目录组织一致。

版本 4.6.0 -> 4.7.0
This commit is contained in:
Ray.Hao
2026-08-14 00:09:42 +08:00
parent 132f9caa57
commit 5085d4c7e0
10 changed files with 145 additions and 40 deletions

View File

@@ -6,7 +6,7 @@
<groupId>com.youlai</groupId> <groupId>com.youlai</groupId>
<artifactId>youlai-boot</artifactId> <artifactId>youlai-boot</artifactId>
<version>4.6.0</version> <version>4.7.0</version>
<description>基于 Java 17 + SpringBoot 4 + Spring Security 构建的权限管理系统。</description> <description>基于 Java 17 + SpringBoot 4 + Spring Security 构建的权限管理系统。</description>
<parent> <parent>

View File

@@ -8,7 +8,7 @@ import java.util.Set;
* 由 system 模块提供适配器实现,framework 层通过此接口获取角色权限集合, * 由 system 模块提供适配器实现,framework 层通过此接口获取角色权限集合,
* 不直接依赖 system 模块的 {@code RoleMenuService}。 * 不直接依赖 system 模块的 {@code RoleMenuService}。
* *
* @see com.youlai.boot.system.security.adapter.PermissionAdapter * @see com.youlai.boot.system.adapter.security.PermissionAdapter
*/ */
public interface PermissionPort { public interface PermissionPort {

View File

@@ -9,7 +9,7 @@ import com.youlai.boot.framework.security.model.SecurityUser;
* 由 system 模块提供适配器实现,framework 层通过此接口获取认证数据, * 由 system 模块提供适配器实现,framework 层通过此接口获取认证数据,
* 不直接依赖 system 模块的 {@code UserService} / {@code UserSocialService}。 * 不直接依赖 system 模块的 {@code UserService} / {@code UserSocialService}。
* *
* @see com.youlai.boot.system.security.adapter.UserAuthenticationAdapter * @see com.youlai.boot.system.adapter.security.UserAuthenticationAdapter
*/ */
public interface UserAuthenticationPort { public interface UserAuthenticationPort {

View File

@@ -0,0 +1,35 @@
package com.youlai.boot.framework.web.log;
import com.youlai.boot.common.enums.ActionTypeEnum;
import com.youlai.boot.common.enums.LogModuleEnum;
import java.time.LocalDateTime;
/**
* 操作日志通用载荷
* <p>由操作日志切面采集,经 {@link OperationLogPort} 交给各业务模块落库</p>
*
* @author Ray.Hao
* @since 0.0.1
*/
public record OperationLog(
LogModuleEnum module,
ActionTypeEnum actionType,
String title,
String content,
Long operatorId,
String operatorName,
String requestUri,
String requestMethod,
String ip,
String province,
String city,
String device,
String os,
String browser,
Integer status,
String errorMsg,
Integer executionTime,
LocalDateTime createTime
) {
}

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.common.aspect; package com.youlai.boot.framework.web.log;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.hutool.http.useragent.UserAgent; import cn.hutool.http.useragent.UserAgent;
@@ -8,8 +8,6 @@ import com.youlai.boot.common.enums.ActionTypeEnum;
import com.youlai.boot.common.enums.LogModuleEnum; import com.youlai.boot.common.enums.LogModuleEnum;
import com.youlai.boot.common.util.IPUtils; import com.youlai.boot.common.util.IPUtils;
import com.youlai.boot.framework.security.util.SecurityUtils; import com.youlai.boot.framework.security.util.SecurityUtils;
import com.youlai.boot.system.model.entity.SysLog;
import com.youlai.boot.system.service.LogService;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.ProceedingJoinPoint;
@@ -27,7 +25,7 @@ import java.util.concurrent.Executor;
/** /**
* 操作日志切面 * 操作日志切面
* <p>@Log 标注的方法执行后异步写入 sys_log 表</p> * <p>@Log 标注的方法执行后异步采集操作日志,通过 {@link OperationLogPort} 落库</p>
* *
* @author Ray.Hao * @author Ray.Hao
* @since 0.0.1 * @since 0.0.1
@@ -35,18 +33,18 @@ import java.util.concurrent.Executor;
@Aspect @Aspect
@Component @Component
@Slf4j @Slf4j
public class LogAspect { public class OperationLogAspect {
private static final int MAX_ERROR_MSG_LENGTH = 1000; private static final int MAX_ERROR_MSG_LENGTH = 1000;
private final LogService logService; private final OperationLogPort operationLogPort;
private final Executor operationLogExecutor; private final Executor operationLogExecutor;
public LogAspect( public OperationLogAspect(
LogService logService, OperationLogPort operationLogPort,
@Qualifier("operationLogExecutor") Executor operationLogExecutor @Qualifier("operationLogExecutor") Executor operationLogExecutor
) { ) {
this.logService = logService; this.operationLogPort = operationLogPort;
this.operationLogExecutor = operationLogExecutor; this.operationLogExecutor = operationLogExecutor;
} }
@@ -69,13 +67,13 @@ public class LogAspect {
} finally { } finally {
long elapsed = System.currentTimeMillis() - start; long elapsed = System.currentTimeMillis() - start;
try { try {
SysLog entity = buildEntity(logAnnotation, elapsed, failure, userId, username); OperationLog operationLog = buildOperationLog(logAnnotation, elapsed, failure, userId, username);
if (entity != null) { if (operationLog != null) {
operationLogExecutor.execute(() -> { operationLogExecutor.execute(() -> {
try { try {
logService.save(entity); operationLogPort.record(operationLog);
} catch (Exception e) { } catch (Exception e) {
log.error("保存操作日志失败 title={} uri={}", entity.getTitle(), entity.getRequestUri(), e); log.error("保存操作日志失败 title={} uri={}", operationLog.title(), operationLog.requestUri(), e);
} }
}); });
} }
@@ -85,7 +83,7 @@ public class LogAspect {
} }
} }
private SysLog buildEntity(Log ann, long elapsed, Throwable failure, Long userId, String username) { private OperationLog buildOperationLog(Log ann, long elapsed, Throwable failure, Long userId, String username) {
ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attrs == null) return null; if (attrs == null) return null;
@@ -96,29 +94,28 @@ public class LogAspect {
LogModuleEnum module = ann.module(); LogModuleEnum module = ann.module();
ActionTypeEnum action = ann.value(); ActionTypeEnum action = ann.value();
SysLog entity = new SysLog();
entity.setModule(module);
entity.setActionType(action);
entity.setTitle(StrUtil.blankToDefault(ann.title(), module.getLabel() + "-" + action.getLabel()));
entity.setContent(ann.content());
entity.setOperatorId(userId);
entity.setOperatorName(username);
entity.setRequestUri(req.getRequestURI());
entity.setRequestMethod(req.getMethod());
entity.setIp(ip);
entity.setDevice(getDevice(ua));
entity.setOs(getOs(ua));
entity.setBrowser(getBrowser(ua));
entity.setStatus(failure == null ? 1 : 0);
entity.setErrorMsg(failure == null ? null : truncate(failure.getMessage(), MAX_ERROR_MSG_LENGTH));
entity.setExecutionTime((int) Math.min(elapsed, Integer.MAX_VALUE));
entity.setCreateTime(LocalDateTime.now());
IpRegion region = parseRegion(ip); IpRegion region = parseRegion(ip);
entity.setProvince(region.province);
entity.setCity(region.city);
return entity; return new OperationLog(
module,
action,
StrUtil.blankToDefault(ann.title(), module.getLabel() + "-" + action.getLabel()),
ann.content(),
userId,
username,
req.getRequestURI(),
req.getMethod(),
ip,
region.province(),
region.city(),
getDevice(ua),
getOs(ua),
getBrowser(ua),
failure == null ? 1 : 0,
failure == null ? null : truncate(failure.getMessage(), MAX_ERROR_MSG_LENGTH),
(int) Math.min(elapsed, Integer.MAX_VALUE),
LocalDateTime.now()
);
} }
private IpRegion parseRegion(String ip) { private IpRegion parseRegion(String ip) {

View File

@@ -0,0 +1,20 @@
package com.youlai.boot.framework.web.log;
/**
* 操作日志落库端口
* <p>由各业务模块实现,framework 层通过此接口写入操作日志,不依赖具体落库实现。
* 如 system 模块的 {@code OperationLogAdapter} 落库 sys_log 表。</p>
*
* @see com.youlai.boot.system.adapter.log.OperationLogAdapter
* @author Ray.Hao
* @since 0.0.1
*/
public interface OperationLogPort {
/**
* 记录操作日志
*
* @param operationLog 操作日志载荷
*/
void record(OperationLog operationLog);
}

View File

@@ -0,0 +1,36 @@
package com.youlai.boot.system.adapter.log;
import com.youlai.boot.framework.web.log.OperationLog;
import com.youlai.boot.framework.web.log.OperationLogPort;
import com.youlai.boot.system.converter.LogConverter;
import com.youlai.boot.system.model.entity.SysLog;
import com.youlai.boot.system.service.LogService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
/**
* 操作日志适配器
* <p>实现 framework 层的 {@link OperationLogPort},将切面采集的操作日志落库 sys_log 表</p>
*
* @author Ray.Hao
* @since 0.0.1
* @see OperationLogPort
*/
@Component
@RequiredArgsConstructor
public class OperationLogAdapter implements OperationLogPort {
private final LogService logService;
private final LogConverter logConverter;
/**
* 记录操作日志
* <p>通过 {@link LogConverter} 将操作日志载荷转换为 {@link SysLog} 后落库</p>
*
* @param operationLog 操作日志载荷
*/
@Override
public void record(OperationLog operationLog) {
logService.save(logConverter.toEntity(operationLog));
}
}

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.system.security.adapter; package com.youlai.boot.system.adapter.security;
import com.youlai.boot.framework.security.port.PermissionPort; import com.youlai.boot.framework.security.port.PermissionPort;
import com.youlai.boot.system.service.RoleMenuService; import com.youlai.boot.system.service.RoleMenuService;

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.system.security.adapter; package com.youlai.boot.system.adapter.security;
import com.youlai.boot.common.enums.SocialPlatformEnum; import com.youlai.boot.common.enums.SocialPlatformEnum;
import com.youlai.boot.framework.security.model.SecurityUser; import com.youlai.boot.framework.security.model.SecurityUser;

View File

@@ -0,0 +1,17 @@
package com.youlai.boot.system.converter;
import com.youlai.boot.framework.web.log.OperationLog;
import com.youlai.boot.system.model.entity.SysLog;
import org.mapstruct.Mapper;
/**
* 系统日志对象转换器
*
* @author Ray.Hao
* @since 0.0.1
*/
@Mapper(componentModel = "spring")
public interface LogConverter {
SysLog toEntity(OperationLog operationLog);
}