Files
youlai-boot/src/main/java/com/youlai/boot/common/aspect/LogAspect.java
TongTongStudio 5915da6517 fix(log): 修复设备类接口无用户上下文时操作日志报错
- 新增兜底操作人ID常量(99L)和X-Device-SN头解析
- 无登录用户时使用兜底ID和设备SN标识操作来源
- 调整文件上传临时目录为/tmp/ttstd/uploads
2026-08-04 21:56:45 +08:00

170 lines
6.4 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package com.youlai.boot.common.aspect;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.useragent.UserAgent;
import cn.hutool.http.useragent.UserAgentUtil;
import com.youlai.boot.common.annotation.Log;
import com.youlai.boot.common.enums.ActionTypeEnum;
import com.youlai.boot.common.enums.LogModuleEnum;
import com.youlai.boot.common.util.IPUtils;
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 lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.time.LocalDateTime;
import java.util.Optional;
import java.util.concurrent.Executor;
/**
* 操作日志切面
* <p>@Log 标注的方法执行后异步写入 sys_log 表</p>
*
* @author Ray.Hao
* @since 0.0.1
*/
@Aspect
@Component
@Slf4j
public class LogAspect {
private static final int MAX_ERROR_MSG_LENGTH = 1000;
private final LogService logService;
private final Executor operationLogExecutor;
public LogAspect(
LogService logService,
@Qualifier("operationLogExecutor") Executor operationLogExecutor
) {
this.logService = logService;
this.operationLogExecutor = operationLogExecutor;
}
@Pointcut("@annotation(logAnnotation)")
public void pointcut(Log logAnnotation) {
}
/**
* 无用户上下文(如设备通过 X-Device-SN 上报时的兜底操作人ID。
* 设备类接口没有登录会话operatorId 无法获取,用该常量保证日志可落库。
*/
private static final long UNKNOWN_OPERATOR_ID = 99L;
private static final String DEVICE_SN_HEADER = "X-Device-SN";
@Around(value = "pointcut(logAnnotation)", argNames = "jp,logAnnotation")
public Object around(ProceedingJoinPoint jp, Log logAnnotation) throws Throwable {
long start = System.currentTimeMillis();
Long userId = SecurityUtils.getUserId();
String username = SecurityUtils.getUsername();
Throwable failure = null;
try {
return jp.proceed();
} catch (Throwable e) {
failure = e;
throw e;
} finally {
long elapsed = System.currentTimeMillis() - start;
try {
SysLog entity = buildEntity(logAnnotation, elapsed, failure, userId, username);
if (entity != null) {
operationLogExecutor.execute(() -> {
try {
logService.save(entity);
} catch (Exception e) {
log.error("保存操作日志失败 title={} uri={}", entity.getTitle(), entity.getRequestUri(), e);
}
});
}
} catch (Exception ex) {
log.error("构建操作日志失败", ex);
}
}
}
private SysLog buildEntity(Log ann, long elapsed, Throwable failure, Long userId, String username) {
ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attrs == null) return null;
HttpServletRequest req = attrs.getRequest();
UserAgent ua = UserAgentUtil.parse(req.getHeader("User-Agent"));
String ip = IPUtils.getIpAddr(req);
LogModuleEnum module = ann.module();
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());
// 设备类接口无登录用户userId 为 null兜底为常量为保证日志可落库
entity.setOperatorId(userId != null ? userId : UNKNOWN_OPERATOR_ID);
// 无用户时尝试用设备SN标识操作来源
entity.setOperatorName(username != null ? username : resolveDeviceSn(req));
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);
entity.setProvince(region.province);
entity.setCity(region.city);
return entity;
}
private IpRegion parseRegion(String ip) {
String region = IPUtils.getRegion(ip);
if (StrUtil.isBlank(region)) return IpRegion.EMPTY;
String[] parts = region.split("\\|");
if (parts.length < 4) return IpRegion.EMPTY;
return new IpRegion(StrUtil.blankToDefault(parts[2], null), StrUtil.blankToDefault(parts[3], null));
}
private String truncate(String msg, int maxLen) {
if (msg == null) return null;
return msg.length() <= maxLen ? msg : msg.substring(0, maxLen);
}
private String getOs(UserAgent ua) {
return Optional.ofNullable(ua).map(UserAgent::getOs).map(os -> os.getName()).orElse(null);
}
private String getDevice(UserAgent ua) {
return Optional.ofNullable(ua).map(UserAgent::getPlatform).map(p -> p.getName()).orElse(null);
}
private String getBrowser(UserAgent ua) {
return Optional.ofNullable(ua).map(UserAgent::getBrowser).map(b -> b.getName()).orElse(null);
}
/**
* 从设备上报请求头解析设备SN用于无登录用户时标识操作来源。
*/
private String resolveDeviceSn(HttpServletRequest req) {
String sn = req.getHeader(DEVICE_SN_HEADER);
return StrUtil.isBlank(sn) ? null : "设备-" + sn;
}
private record IpRegion(String province, String city) {
static final IpRegion EMPTY = new IpRegion(null, null);
}
}