- 新增兜底操作人ID常量(99L)和X-Device-SN头解析 - 无登录用户时使用兜底ID和设备SN标识操作来源 - 调整文件上传临时目录为/tmp/ttstd/uploads
170 lines
6.4 KiB
Java
170 lines
6.4 KiB
Java
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);
|
||
}
|
||
}
|