diff --git a/src/main/java/com/biutag/supervision/aop/SystemLogAspect.java b/src/main/java/com/biutag/supervision/aop/SystemLogAspect.java new file mode 100644 index 0000000..8c83455 --- /dev/null +++ b/src/main/java/com/biutag/supervision/aop/SystemLogAspect.java @@ -0,0 +1,446 @@ +package com.biutag.supervision.aop; + +import com.biutag.supervision.common.UserContextHolder; +import com.biutag.supervision.constants.RedisKeyConstants; +import com.biutag.supervision.pojo.Result; +import com.biutag.supervision.pojo.domain.AppUser; +import com.biutag.supervision.pojo.entity.SystemLog; +import com.biutag.supervision.pojo.model.UserAuth; +import com.biutag.supervision.pojo.vo.TokenVo; +import com.biutag.supervision.service.SystemLogService; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +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.aspectj.lang.reflect.MethodSignature; +import org.springframework.core.annotation.Order; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; +import org.springframework.validation.BindingResult; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.multipart.MultipartFile; + +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Reader; +import java.io.Writer; +import java.lang.reflect.Array; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +/** + * 统一记录进入 Controller 调用链的接口访问日志。 + *

+ * 日志切面优先级高于现有参数检查切面,因此参数检查抛出的业务异常也可以被记录; + * 在 Spring MVC 调用 Controller 之前就被鉴权拦截的请求不会进入本切面。 + */ +@Slf4j +@Aspect +@Component +@Order(-100) +@RequiredArgsConstructor +public class SystemLogAspect { + + private static final int SUCCESS = 1; + private static final int FAILURE = 0; + private static final int MAX_PARAMS_LENGTH = 4000; + private static final int MAX_ERROR_LENGTH = 1000; + private static final String MASK_VALUE = "******"; + private static final String TRUNCATED_MARK = "...[内容已截断]"; + + private final SystemLogService systemLogService; + private final ObjectMapper objectMapper; + private final RedisTemplate redisTemplate; + + /** + * 只拦截项目 Controller 包中的方法,Service 和 Mapper 调用不会触发日志递归。 + */ + @Pointcut("execution(* com.biutag.supervision.controller..*(..))") + public void controllerPointcut() { + } + + /** + * 包裹 Controller 调用并在 finally 中保存日志,确保业务异常场景也能记录。 + */ + @Around("controllerPointcut()") + public Object recordSystemLog(ProceedingJoinPoint joinPoint) throws Throwable { + ServletRequestAttributes attributes = getRequestAttributes(); + HttpServletRequest request = attributes == null ? null : attributes.getRequest(); + HttpServletResponse response = attributes == null ? null : attributes.getResponse(); + LocalDateTime logTime = LocalDateTime.now(); + long startTime = System.nanoTime(); + + UserAuth currentUser = resolveCurrentUser(); + String requestParams = buildRequestParams(joinPoint, request); + Object result = null; + Throwable throwable = null; + + try { + result = joinPoint.proceed(); + return result; + } catch (Throwable ex) { + throwable = ex; + throw ex; + } finally { + long executionTime = (System.nanoTime() - startTime) / 1_000_000L; + UserAuth logUser = currentUser == null ? resolveLoginUser(result) : currentUser; + saveLogSafely(request, response, logUser, requestParams, result, throwable, executionTime, logTime); + } + } + + private ServletRequestAttributes getRequestAttributes() { + if (RequestContextHolder.getRequestAttributes() instanceof ServletRequestAttributes attributes) { + return attributes; + } + return null; + } + + /** + * 公开接口或外部接口可能没有后台登录态,此时用户字段保持为空,由前端显示为匿名调用。 + */ + private UserAuth resolveCurrentUser() { + try { + return UserContextHolder.getCurrentUser(); + } catch (RuntimeException ignored) { + return null; + } + } + + /** + * 登录请求进入 Controller 前尚未携带 Token。登录成功后,从返回值中临时提取 Token, + * 再读取登录流程刚写入的 Redis 会话,从而为账号登录和数字证书登录回填真实请求人。 + * App 登录已经直接返回 UserAuth,优先复用该对象,不保存任何响应内容。 + */ + private UserAuth resolveLoginUser(Object result) { + if (!(result instanceof Result apiResult) || apiResult.getCode() != HttpServletResponse.SC_OK) { + return null; + } + + Object data = apiResult.getData(); + if (data instanceof AppUser appUser && appUser.getUser() != null) { + return appUser.getUser(); + } + + String token = null; + if (data instanceof TokenVo tokenVo) { + token = tokenVo.getToken(); + } else if (data instanceof AppUser appUser) { + token = appUser.getToken(); + } + if (token == null || token.isBlank()) { + return null; + } + + try { + Object user = redisTemplate.opsForValue().get(String.format(RedisKeyConstants.LOGIN_USERINFO_KEY, token)); + return user instanceof UserAuth userAuth ? userAuth : null; + } catch (RuntimeException ex) { + log.warn("登录日志请求人解析失败:{}", ex.getMessage()); + return null; + } + } + + /** + * 同时采集原始 query/form 参数和 Controller 方法参数,以覆盖查询、路径变量及 JSON 请求体。 + */ + private String buildRequestParams(ProceedingJoinPoint joinPoint, HttpServletRequest request) { + try { + ObjectNode rootNode = objectMapper.createObjectNode(); + appendRequestParameters(rootNode, request); + appendMethodArguments(rootNode, joinPoint); + sanitizeNode(rootNode); + + if (rootNode.isEmpty()) { + return null; + } + return truncate(objectMapper.writeValueAsString(rootNode), MAX_PARAMS_LENGTH, TRUNCATED_MARK); + } catch (Exception ex) { + log.warn("系统日志请求参数解析失败:{}", ex.getMessage()); + return "[请求参数解析失败]"; + } + } + + private void appendRequestParameters(ObjectNode rootNode, HttpServletRequest request) { + if (request == null || request.getParameterMap().isEmpty()) { + return; + } + ObjectNode queryNode = rootNode.putObject("query"); + request.getParameterMap().forEach((key, values) -> { + if (values == null || values.length == 0) { + queryNode.putNull(key); + } else if (values.length == 1) { + queryNode.put(key, values[0]); + } else { + ArrayNode arrayNode = queryNode.putArray(key); + for (String value : values) { + arrayNode.add(value); + } + } + }); + } + + private void appendMethodArguments(ObjectNode rootNode, ProceedingJoinPoint joinPoint) { + Object[] arguments = joinPoint.getArgs(); + if (arguments == null || arguments.length == 0) { + return; + } + + String[] parameterNames = ((MethodSignature) joinPoint.getSignature()).getParameterNames(); + ObjectNode argumentNode = rootNode.putObject("arguments"); + for (int index = 0; index < arguments.length; index++) { + Object argument = arguments[index]; + if (shouldIgnoreArgument(argument)) { + continue; + } + String parameterName = parameterNames != null && index < parameterNames.length + ? parameterNames[index] + : "arg" + index; + argumentNode.set(parameterName, convertArgument(argument)); + } + + if (argumentNode.isEmpty()) { + rootNode.remove("arguments"); + } + } + + /** + * Servlet、校验结果和流对象不可序列化,也不属于业务请求参数,统一跳过。 + */ + private boolean shouldIgnoreArgument(Object argument) { + return argument instanceof ServletRequest + || argument instanceof ServletResponse + || argument instanceof BindingResult + || argument instanceof InputStream + || argument instanceof OutputStream + || argument instanceof Reader + || argument instanceof Writer; + } + + private JsonNode convertArgument(Object argument) { + if (argument == null) { + return objectMapper.getNodeFactory().nullNode(); + } + if (argument instanceof MultipartFile file) { + return buildFileNode(file); + } + if (argument instanceof byte[] bytes) { + return objectMapper.getNodeFactory().textNode("[二进制内容已忽略,大小:" + bytes.length + "字节]"); + } + if (argument.getClass().isArray()) { + ArrayNode arrayNode = objectMapper.createArrayNode(); + int length = Array.getLength(argument); + for (int index = 0; index < length; index++) { + arrayNode.add(convertArgument(Array.get(argument, index))); + } + return arrayNode; + } + if (argument instanceof Iterable iterable) { + ArrayNode arrayNode = objectMapper.createArrayNode(); + for (Object item : iterable) { + arrayNode.add(convertArgument(item)); + } + return arrayNode; + } + if (argument instanceof Map map) { + ObjectNode mapNode = objectMapper.createObjectNode(); + map.forEach((key, value) -> mapNode.set(String.valueOf(key), convertArgument(value))); + return mapNode; + } + return objectMapper.valueToTree(argument); + } + + /** + * 上传文件只记录元信息,禁止读取或序列化文件内容。 + */ + private ObjectNode buildFileNode(MultipartFile file) { + ObjectNode fileNode = objectMapper.createObjectNode(); + fileNode.put("fieldName", file.getName()); + fileNode.put("originalFilename", file.getOriginalFilename()); + fileNode.put("size", file.getSize()); + fileNode.put("contentType", file.getContentType()); + return fileNode; + } + + /** + * 递归处理对象、数组和嵌套 JSON 字符串,凭证字段统一替换为固定掩码。 + */ + private JsonNode sanitizeNode(JsonNode node) { + if (node == null) { + return objectMapper.getNodeFactory().nullNode(); + } + if (node.isObject()) { + ObjectNode objectNode = (ObjectNode) node; + List fieldNames = new ArrayList<>(); + objectNode.fieldNames().forEachRemaining(fieldNames::add); + for (String fieldName : fieldNames) { + if (isCredentialField(fieldName)) { + objectNode.put(fieldName, MASK_VALUE); + } else { + objectNode.set(fieldName, sanitizeNode(objectNode.get(fieldName))); + } + } + return objectNode; + } else if (node.isArray()) { + ArrayNode arrayNode = (ArrayNode) node; + for (int index = 0; index < arrayNode.size(); index++) { + arrayNode.set(index, sanitizeNode(arrayNode.get(index))); + } + return arrayNode; + } else if (node.isTextual()) { + return sanitizeJsonText(node); + } + return node; + } + + /** + * APP 转发参数中的 body 可能是 JSON 字符串,需要再次解析后脱敏,避免内部密码被遗漏。 + */ + private JsonNode sanitizeJsonText(JsonNode valueNode) { + String value = valueNode.textValue(); + if (value == null) { + return valueNode; + } + String trimmedValue = value.trim(); + if (!(trimmedValue.startsWith("{") || trimmedValue.startsWith("["))) { + return valueNode; + } + try { + JsonNode nestedNode = objectMapper.readTree(trimmedValue); + JsonNode sanitizedNode = sanitizeNode(nestedNode); + return objectMapper.getNodeFactory().textNode(objectMapper.writeValueAsString(sanitizedNode)); + } catch (Exception ignored) { + // 普通字符串可能恰好以大括号开头,解析失败时保留原值即可。 + return valueNode; + } + } + + private boolean isCredentialField(String fieldName) { + String normalizedName = fieldName.toLowerCase(Locale.ROOT).replace("_", "").replace("-", ""); + return normalizedName.contains("password") + || normalizedName.contains("passwd") + || normalizedName.contains("pwd") + || normalizedName.contains("token") + || normalizedName.contains("authorization") + || normalizedName.contains("cookie") + || normalizedName.contains("secret") + || normalizedName.contains("signeddata"); + } + + /** + * 日志写入属于附加能力,任何日志异常都不能改变原业务接口的返回或异常。 + */ + private void saveLogSafely(HttpServletRequest request, + HttpServletResponse response, + UserAuth currentUser, + String requestParams, + Object result, + Throwable throwable, + long executionTime, + LocalDateTime logTime) { + if (request == null) { + return; + } + try { + int status = resolveStatus(response, result, throwable); + SystemLog systemLog = new SystemLog(); + if (currentUser != null) { + systemLog.setUserId(currentUser.getUserId()); + systemLog.setUserName(currentUser.getUserName()); + systemLog.setNickName(currentUser.getNickName()); + } + systemLog.setRequestUri(request.getRequestURI()); + systemLog.setRequestMethod(request.getMethod()); + systemLog.setRequestParams(requestParams); + systemLog.setSourceIp(resolveSourceIp(request)); + systemLog.setExecutionTime(executionTime); + systemLog.setStatus(status); + systemLog.setErrorMessage(resolveErrorMessage(response, result, throwable, status)); + systemLog.setLogTime(logTime); + systemLogService.save(systemLog); + } catch (Exception ex) { + log.error("系统访问日志保存失败:{}", ex.getMessage(), ex); + } + } + + private int resolveStatus(HttpServletResponse response, Object result, Throwable throwable) { + if (throwable != null) { + return FAILURE; + } + if (response != null && response.getStatus() >= HttpServletResponse.SC_BAD_REQUEST) { + return FAILURE; + } + if (result instanceof Result apiResult && apiResult.getCode() != HttpServletResponse.SC_OK) { + return FAILURE; + } + return SUCCESS; + } + + private String resolveErrorMessage(HttpServletResponse response, + Object result, + Throwable throwable, + int status) { + if (status == SUCCESS) { + return null; + } + + String message = null; + if (throwable != null) { + message = throwable.getMessage(); + if (message == null || message.isBlank()) { + message = throwable.getClass().getSimpleName(); + } + } else if (result instanceof Result apiResult) { + message = apiResult.getMessage(); + } + if ((message == null || message.isBlank()) && response != null) { + message = "HTTP " + response.getStatus(); + } + return truncate(message, MAX_ERROR_LENGTH, TRUNCATED_MARK); + } + + /** + * 兼容常见反向代理,X-Forwarded-For 存在多级地址时取第一个有效值。 + */ + private String resolveSourceIp(HttpServletRequest request) { + String forwardedFor = request.getHeader("X-Forwarded-For"); + if (isValidIpHeader(forwardedFor)) { + for (String ip : forwardedFor.split(",")) { + if (isValidIpHeader(ip)) { + return ip.trim(); + } + } + } + + String realIp = request.getHeader("X-Real-IP"); + if (isValidIpHeader(realIp)) { + return realIp.trim(); + } + return request.getRemoteAddr(); + } + + private boolean isValidIpHeader(String value) { + return value != null && !value.isBlank() && !"unknown".equalsIgnoreCase(value.trim()); + } + + private String truncate(String value, int maxLength, String mark) { + if (value == null || value.length() <= maxLength) { + return value; + } + int contentLength = Math.max(0, maxLength - mark.length()); + return value.substring(0, contentLength) + mark; + } +} diff --git a/src/main/java/com/biutag/supervision/controller/system/SystemLogController.java b/src/main/java/com/biutag/supervision/controller/system/SystemLogController.java new file mode 100644 index 0000000..f5501c5 --- /dev/null +++ b/src/main/java/com/biutag/supervision/controller/system/SystemLogController.java @@ -0,0 +1,34 @@ +package com.biutag.supervision.controller.system; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.biutag.supervision.pojo.Result; +import com.biutag.supervision.pojo.entity.SystemLog; +import com.biutag.supervision.pojo.param.SystemLogQueryParam; +import com.biutag.supervision.service.SystemLogService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 系统访问日志查询接口。 + */ +@Tag(name = "系统日志") +@RequiredArgsConstructor +@RequestMapping("systemLog") +@RestController +public class SystemLogController { + + private final SystemLogService systemLogService; + + /** + * 分页查询系统访问日志。 + */ + @Operation(summary = "分页查询系统日志") + @GetMapping + public Result> list(SystemLogQueryParam queryParam) { + return Result.success(systemLogService.pageByQuery(queryParam)); + } +} diff --git a/src/main/java/com/biutag/supervision/job/SystemLogCleanJob.java b/src/main/java/com/biutag/supervision/job/SystemLogCleanJob.java new file mode 100644 index 0000000..9e4d907 --- /dev/null +++ b/src/main/java/com/biutag/supervision/job/SystemLogCleanJob.java @@ -0,0 +1,37 @@ +package com.biutag.supervision.job; + +import com.biutag.supervision.service.SystemLogService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; + +/** + * 系统访问日志定时清理任务,只保留最近四个月的数据。 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class SystemLogCleanJob { + + private static final int RETENTION_MONTHS = 4; + + private final SystemLogService systemLogService; + + /** + * 清理四个月以前的日志。 + * 当前处于上线观察期,未注册定时调度;确认启用后再增加每天凌晨 03:30 的 @Scheduled 注解。 + * 启用调度后,单次清理失败只记录错误信息,不影响后续执行。 + * @Scheduled(cron = "0 30 3 * * ?") + */ + public void cleanExpiredLogs() { + LocalDateTime expireTime = LocalDateTime.now().minusMonths(RETENTION_MONTHS); + try { + int removedCount = systemLogService.removeBefore(expireTime); + log.info("系统访问日志清理完成,截止时间:{},删除数量:{}", expireTime, removedCount); + } catch (RuntimeException ex) { + log.error("系统访问日志清理失败,截止时间:{},失败原因:{}", expireTime, ex.getMessage(), ex); + } + } +} diff --git a/src/main/java/com/biutag/supervision/mapper/SystemLogMapper.java b/src/main/java/com/biutag/supervision/mapper/SystemLogMapper.java new file mode 100644 index 0000000..65034ec --- /dev/null +++ b/src/main/java/com/biutag/supervision/mapper/SystemLogMapper.java @@ -0,0 +1,12 @@ +package com.biutag.supervision.mapper; + +import com.baomidou.dynamic.datasource.annotation.DS; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.biutag.supervision.pojo.entity.SystemLog; + +/** + * 系统访问日志数据访问接口。 + */ +@DS("master") +public interface SystemLogMapper extends BaseMapper { +} diff --git a/src/main/java/com/biutag/supervision/pojo/entity/SystemLog.java b/src/main/java/com/biutag/supervision/pojo/entity/SystemLog.java new file mode 100644 index 0000000..ead1c7d --- /dev/null +++ b/src/main/java/com/biutag/supervision/pojo/entity/SystemLog.java @@ -0,0 +1,69 @@ +package com.biutag.supervision.pojo.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDateTime; + +/** + * 系统接口访问日志。 + */ +@Schema(description = "系统接口访问日志") +@TableName("system_log") +@Getter +@Setter +public class SystemLog { + + @Schema(description = "日志主键") + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + @Schema(description = "请求人用户ID") + @TableField("user_id") + private String userId; + + @Schema(description = "请求人登录名") + @TableField("user_name") + private String userName; + + @Schema(description = "请求人姓名") + @TableField("nick_name") + private String nickName; + + @Schema(description = "请求接口地址") + @TableField("request_uri") + private String requestUri; + + @Schema(description = "HTTP访问方式", example = "GET") + @TableField("request_method") + private String requestMethod; + + @Schema(description = "脱敏并截断后的请求参数,最多4000字符") + @TableField("request_params") + private String requestParams; + + @Schema(description = "请求来源IP") + @TableField("source_ip") + private String sourceIp; + + @Schema(description = "Controller调用链执行耗时,单位:毫秒") + @TableField("execution_time") + private Long executionTime; + + @Schema(description = "执行状态:1-成功,0-失败", allowableValues = {"0", "1"}, example = "1") + @TableField("status") + private Integer status; + + @Schema(description = "异常或失败摘要,最多1000字符") + @TableField("error_message") + private String errorMessage; + + @Schema(description = "请求开始时间") + @TableField("log_time") + private LocalDateTime logTime; +} diff --git a/src/main/java/com/biutag/supervision/pojo/param/SystemLogQueryParam.java b/src/main/java/com/biutag/supervision/pojo/param/SystemLogQueryParam.java new file mode 100644 index 0000000..7a6e4d2 --- /dev/null +++ b/src/main/java/com/biutag/supervision/pojo/param/SystemLogQueryParam.java @@ -0,0 +1,43 @@ +package com.biutag.supervision.pojo.param; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +/** + * 系统日志分页查询参数。 + */ +@Schema(description = "系统日志分页查询参数") +@Getter +@Setter +public class SystemLogQueryParam extends BasePage { + + @Schema(description = "日志类型:login-登录日志,operation-操作日志", allowableValues = {"login", "operation"}) + private String logType; + + @Schema(description = "请求人登录名或姓名,支持模糊查询") + private String requester; + + @Schema(description = "接口地址,支持模糊查询") + private String requestUri; + + @Schema(description = "HTTP访问方式", example = "GET") + private String requestMethod; + + @Schema(description = "来源IP,支持模糊查询") + private String sourceIp; + + @Schema(description = "执行状态:1-成功,0-失败", allowableValues = {"0", "1"}) + private Integer status; + + @Schema(description = "日志开始时间,格式:yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime startTime; + + @Schema(description = "日志结束时间,格式:yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime endTime; +} diff --git a/src/main/java/com/biutag/supervision/service/SystemLogService.java b/src/main/java/com/biutag/supervision/service/SystemLogService.java new file mode 100644 index 0000000..6bcaa5c --- /dev/null +++ b/src/main/java/com/biutag/supervision/service/SystemLogService.java @@ -0,0 +1,78 @@ +package com.biutag.supervision.service; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.biutag.supervision.mapper.SystemLogMapper; +import com.biutag.supervision.pojo.entity.SystemLog; +import com.biutag.supervision.pojo.param.SystemLogQueryParam; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.Objects; +import java.util.Set; + +/** + * 系统访问日志服务。 + */ +@Service +public class SystemLogService extends ServiceImpl { + + private static final String LOG_TYPE_LOGIN = "login"; + private static final String LOG_TYPE_OPERATION = "operation"; + + /** 实际完成登录动作并创建登录态的接口。 */ + private static final Set LOGIN_REQUEST_URIS = Set.of( + "/login", + "/jit/p7certAuth", + "/app/login" + ); + + /** + * 按页面查询条件检索系统访问日志。 + * + * @param queryParam 分页及筛选条件 + * @return 日志分页数据 + */ + public Page pageByQuery(SystemLogQueryParam queryParam) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + + if (StrUtil.isNotBlank(queryParam.getRequester())) { + queryWrapper.and(wrapper -> wrapper + .like(SystemLog::getUserName, queryParam.getRequester()) + .or() + .like(SystemLog::getNickName, queryParam.getRequester())); + } + + // 登录日志和操作日志使用同一张表,通过登录接口白名单进行互斥分类。 + if (LOG_TYPE_LOGIN.equals(queryParam.getLogType())) { + queryWrapper.in(SystemLog::getRequestUri, LOGIN_REQUEST_URIS); + } else if (LOG_TYPE_OPERATION.equals(queryParam.getLogType())) { + queryWrapper.notIn(SystemLog::getRequestUri, LOGIN_REQUEST_URIS); + } + + queryWrapper + .like(StrUtil.isNotBlank(queryParam.getRequestUri()), SystemLog::getRequestUri, queryParam.getRequestUri()) + .eq(StrUtil.isNotBlank(queryParam.getRequestMethod()), SystemLog::getRequestMethod, queryParam.getRequestMethod()) + .like(StrUtil.isNotBlank(queryParam.getSourceIp()), SystemLog::getSourceIp, queryParam.getSourceIp()) + .eq(Objects.nonNull(queryParam.getStatus()), SystemLog::getStatus, queryParam.getStatus()) + .ge(Objects.nonNull(queryParam.getStartTime()), SystemLog::getLogTime, queryParam.getStartTime()) + .le(Objects.nonNull(queryParam.getEndTime()), SystemLog::getLogTime, queryParam.getEndTime()) + .orderByDesc(SystemLog::getLogTime) + .orderByDesc(SystemLog::getId); + + return page(Page.of(queryParam.getCurrent(), queryParam.getSize()), queryWrapper); + } + + /** + * 删除截止时间之前的系统访问日志。 + * + * @param expireTime 日志保留截止时间 + * @return 实际删除条数 + */ + public int removeBefore(LocalDateTime expireTime) { + return baseMapper.delete(new LambdaQueryWrapper() + .lt(SystemLog::getLogTime, expireTime)); + } +}