添加操作日志

This commit is contained in:
2026-06-03 10:45:33 +08:00
parent 009275590d
commit b1cc92f026
23 changed files with 1087 additions and 8 deletions

View File

@@ -157,6 +157,10 @@
<groupId>org.xujun</groupId>
<artifactId>xtools-app-sys-risk</artifactId>
</dependency>
<dependency>
<groupId>org.xujun</groupId>
<artifactId>xtools-app-sys-tag</artifactId>
</dependency>
<!-- 项目模块 end -->
<!-- mapstruct begin -->

View File

@@ -4,6 +4,9 @@ import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import java.util.ArrayList;
import java.util.List;
/**
* <p>Title : SysConfig</p>
* <p>Description : SysConfig</p>
@@ -57,6 +60,11 @@ public class SysConfig {
* 最大保存天数
*/
private int maxDays = 2;
/**
* 忽略操作日志
*/
private List<String> ignoreOptLog = new ArrayList<>();
}
}

View File

@@ -0,0 +1,64 @@
package xtools.app.sys.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import xtools.app.sys.model.dto.excel.SysOptLogExcel;
import xtools.app.sys.model.dto.req.SysOptLogPageReq;
import xtools.app.sys.model.dto.resp.SysOptLogResp;
import xtools.app.sys.service.SysOptLogService;
import xtools.boot.api.exection.BizError;
import xtools.boot.api.model.dto.Result;
import xtools.boot.api.model.dto.page.PageReq;
import xtools.boot.api.model.dto.page.PageResp;
import xtools.extend.office.FesodUtils;
import xtools.web.HttpServletUtils;
import java.io.IOException;
import java.util.List;
/**
* <p>Title : SysOptLogController</p>
* <p>Description : 系统操作日志 Controller</p>
* <p>Company : org.xujun</p>
*
* @author : xujun
* @version : 1.0.0
* @date : 2026-06-02 16:44:47
*/
@RequiredArgsConstructor
@Tag(name = "系统操作日志")
@RestController
@RequestMapping("/sys/opt-log")
public class SysOptLogController {
private final SysOptLogService sysOptLogService;
@Operation(summary = "分页请求")
@PostMapping("page")
public Result<PageResp<SysOptLogResp>> page(@RequestBody @Valid PageReq<SysOptLogPageReq> pageReq) {
return sysOptLogService.page(pageReq);
}
@Operation(summary = "导出Excel")
@PostMapping("export")
public void exportExcel(@RequestBody @Valid SysOptLogPageReq req, HttpServletResponse response) {
String sheetName = "系统操作日志";
String filename = sheetName + ".xlsx";
List<SysOptLogExcel> dataList = sysOptLogService.exportExcel(req);
try {
FesodUtils.write(response.getOutputStream(), SysOptLogExcel.class, sheetName, dataList);
} catch (IOException e) {
throw new BizError("导出Excel失败");
}
// 设置 header 和 contentType.写在最后的原因是,避免报错时,响应 contentType 已经被修改
HttpServletUtils.addDownloadHeader(response, filename);
}
}

View File

@@ -0,0 +1,55 @@
package xtools.app.sys.convert;
import org.mapstruct.Mapper;
import xtools.app.sys.auth.model.dto.OptLogDto;
import xtools.app.sys.model.dto.excel.SysOptLogExcel;
import xtools.app.sys.model.dto.resp.SysOptLogResp;
import xtools.app.sys.model.entity.SysOptLog;
import java.util.List;
/**
* <p>Title : SysOptLogConvert</p>
* <p>Description : 系统操作日志 Convert</p>
* <p>Company : org.xujun</p>
*
* @author : xujun
* @version : 1.0.0
* @date : 2026-06-02 16:44:47
*/
@Mapper(componentModel = "spring")
public interface SysOptLogConvert {
/**
* 实体转响应
*
* @param data 实体
* @return 响应
*/
SysOptLogResp entityToResp(SysOptLog data);
/**
* 批量实体转响应
*
* @param dataList 批量实体
* @return 响应
*/
List<SysOptLogResp> entityToRespList(List<SysOptLog> dataList);
/**
* 实体转Excel
*
* @param data 实体
* @return Excel
*/
SysOptLogExcel entityToExcel(SysOptLog data);
/**
* DTO转实体
*
* @param data DTO
* @return 实体
*/
SysOptLog dtoToEntity(OptLogDto data);
}

View File

@@ -0,0 +1,19 @@
package xtools.app.sys.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import xtools.app.sys.model.entity.SysOptLog;
/**
* <p>Title : SysOptLogMapper</p>
* <p>Description : 系统操作日志 Mapper 接口</p>
* <p>Company : org.xujun</p>
*
* @author : xujun
* @version : 1.0.0
* @date : 2026-06-02 16:44:47
*/
@Mapper
public interface SysOptLogMapper extends BaseMapper<SysOptLog> {
}

View File

@@ -0,0 +1,81 @@
package xtools.app.sys.model.dto.excel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.fesod.sheet.annotation.ExcelProperty;
import org.apache.fesod.sheet.annotation.format.DateTimeFormat;
import java.io.Serializable;
import java.util.Date;
/**
* <p>Title : SysOptLogExcel</p>
* <p>Description : 系统操作日志Excel对象</p>
* <p>Company : org.xujun</p>
*
* @author : xujun
* @version : 1.0.0
* @date : 2026-06-02 16:44:47
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SysOptLogExcel implements Serializable {
/**
* 日志追踪id
*/
@ExcelProperty("日志追踪id")
private String traceId;
/**
* 标题
*/
@ExcelProperty("标题")
private String title;
/**
* 账号ID
*/
@ExcelProperty("账号ID")
private Long accountId;
/**
* 账号
*/
@ExcelProperty("账号")
private String account;
/**
* IP
*/
@ExcelProperty("IP")
private String ip;
/**
* 地址
*/
@ExcelProperty("地址")
private String addr;
/**
* 操作URI
*/
@ExcelProperty("操作URI")
private String uri;
/**
* 备注
*/
@ExcelProperty("备注")
private String memo;
/**
* 创建时间
*/
@ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date gmtCreate;
}

View File

@@ -0,0 +1,91 @@
package xtools.app.sys.model.dto.req;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.time.Instant;
/**
* <p>Title : SysOptLogPageReq</p>
* <p>Description : 系统操作日志分页请求对象</p>
* <p>Company : org.xujun</p>
*
* @author : xujun
* @version : 1.0.0
* @date : 2026-06-02 16:44:47
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SysOptLogPageReq implements Serializable {
/**
* ID
*/
@Schema(description = "ID")
private Long id;
/**
* 日志追踪id
*/
@Schema(description = "日志追踪id")
private String traceId;
/**
* 标题
*/
@Schema(description = "标题")
private String title;
/**
* 账号ID
*/
@Schema(description = "账号ID")
private Long accountId;
/**
* 账号
*/
@Schema(description = "账号")
private String account;
/**
* IP
*/
@Schema(description = "IP")
private String ip;
/**
* 地址
*/
@Schema(description = "地址")
private String addr;
/**
* 地址code
*/
@Schema(description = "地址code")
private String addrCode;
/**
* 操作URI
*/
@Schema(description = "操作URI")
private String uri;
/**
* 备注
*/
@Schema(description = "备注")
private String memo;
/**
* 创建时间(范围)
*/
@Schema(description = "创建时间(范围)", example = "['2026-01-01 00:00:00', '2026-01-01 12:00:00']")
private Instant[] gmtCreateRange;
}

View File

@@ -0,0 +1,85 @@
package xtools.app.sys.model.dto.resp;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import xtools.boot.api.model.entity.BaseEntity;
/**
* <p>Title : SysOptLogResp</p>
* <p>Description : 系统操作日志响应对象</p>
* <p>Company : org.xujun</p>
*
* @author : xujun
* @version : 1.0.0
* @date : 2026-06-02 16:44:47
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class SysOptLogResp extends BaseEntity {
/**
* ID
*/
@Schema(description = "ID")
private Long id;
/**
* 日志追踪id
*/
@Schema(description = "日志追踪id")
private String traceId;
/**
* 标题
*/
@Schema(description = "标题")
private String title;
/**
* 账号ID
*/
@Schema(description = "账号ID")
private Long accountId;
/**
* 账号
*/
@Schema(description = "账号")
private String account;
/**
* IP
*/
@Schema(description = "IP")
private String ip;
/**
* 地址
*/
@Schema(description = "地址")
private String addr;
/**
* 地址code
*/
@Schema(description = "地址code")
private String addrCode;
/**
* 操作URI
*/
@Schema(description = "操作URI")
private String uri;
/**
* 备注
*/
@Schema(description = "备注")
private String memo;
}

View File

@@ -0,0 +1,107 @@
package xtools.app.sys.model.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.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.time.Instant;
/**
* <p>Title : SysOptLog</p>
* <p>Description : 系统操作日志实体对象</p>
* <p>Company : org.xujun</p>
*
* @author : xujun
* @version : 1.0.0
* @date : 2026-06-02 16:44:47
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("sys_opt_log")
public class SysOptLog implements Serializable {
/**
* ID
*/
@Schema(description = "ID")
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
/**
* 日志追踪id
*/
@Schema(description = "日志追踪id")
@TableField(value = "trace_id")
private String traceId;
/**
* 标题
*/
@Schema(description = "标题")
@TableField(value = "title")
private String title;
/**
* 账号ID
*/
@Schema(description = "账号ID")
@TableField(value = "account_id")
private Long accountId;
/**
* 账号
*/
@Schema(description = "账号")
@TableField(value = "account")
private String account;
/**
* IP
*/
@Schema(description = "IP")
@TableField(value = "ip")
private String ip;
/**
* 地址
*/
@Schema(description = "地址")
@TableField(value = "addr")
private String addr;
/**
* 地址code
*/
@Schema(description = "地址code")
@TableField(value = "addr_code")
private String addrCode;
/**
* 操作URI
*/
@Schema(description = "操作URI")
@TableField(value = "uri")
private String uri;
/**
* 备注
*/
@Schema(description = "备注")
@TableField(value = "memo")
private String memo;
/**
* 创建时间
*/
@TableField(value = "gmt_create")
@Schema(description = "创建时间", example = "2026-01-05 10:32:00")
private Instant gmtCreate;
}

View File

@@ -0,0 +1,48 @@
package xtools.app.sys.service;
import com.alibaba.fastjson2.JSONObject;
import xtools.app.sys.model.dto.excel.SysOptLogExcel;
import xtools.app.sys.model.dto.req.SysOptLogPageReq;
import xtools.app.sys.model.dto.resp.SysOptLogResp;
import xtools.boot.api.model.dto.Result;
import xtools.boot.api.model.dto.page.PageReq;
import xtools.boot.api.model.dto.page.PageResp;
import java.util.List;
/**
* <p>Title : SysOptLogService</p>
* <p>Description : 系统操作日志 Service</p>
* <p>Company : org.xujun</p>
*
* @author : xujun
* @version : 1.0.0
* @date : 2026-06-02 16:44:47
*/
public interface SysOptLogService {
/**
* 分页查询
*
* @param pageReq 分页请求
* @return 分页结果
*/
Result<PageResp<SysOptLogResp>> page(PageReq<SysOptLogPageReq> pageReq);
/**
* 保存日志
*
* @param traceId 日志追踪 ID
* @param logData 日志
*/
void save(String traceId, JSONObject logData);
/**
* 导出 Excel
*
* @param req 请求参数
* @return Excel 数据
*/
List<SysOptLogExcel> exportExcel(SysOptLogPageReq req);
}

View File

@@ -0,0 +1,20 @@
package xtools.app.sys.service.base;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Component;
import xtools.app.sys.mapper.SysOptLogMapper;
import xtools.app.sys.model.entity.SysOptLog;
/**
* <p>Title : SysOptLogBaseService</p>
* <p>Description : 系统操作日志 BaseService</p>
* <p>Company : org.xujun</p>
*
* @author : xujun
* @version : 1.0.0
* @date : 2026-06-02 16:44:47
*/
@Component
public class SysOptLogBaseService extends ServiceImpl<SysOptLogMapper, SysOptLog> {
}

View File

@@ -9,7 +9,9 @@ import org.springframework.stereotype.Service;
import xtools.app.common.log.bus.service.LogBusService;
import xtools.app.sys.model.entity.SysLog;
import xtools.app.sys.service.SysLogService;
import xtools.app.sys.service.SysOptLogService;
import xtools.boot.api.model.dto.log.LogTrack;
import xtools.boot.log.enums.LogBusBaseType;
import xtools.boot.log.model.dto.LogBody;
import xtools.boot.log.model.dto.RunInfo;
import xtools.core.CollectionUtils;
@@ -37,6 +39,8 @@ public class LogBusServiceImpl implements LogBusService {
private final SysLogService sysLogService;
private final SysOptLogService sysOptLogService;
/**
* 保存日志
*
@@ -44,18 +48,25 @@ public class LogBusServiceImpl implements LogBusService {
*/
@Override
public void saveLog(LogBody logBody) {
LogTrack logTrack = logBody.getLogTrack();
RunInfo runInfo = logBody.getRunInfo();
// 获取日志类型
String logType = logBody.getType();
LogTrack logTrack = logBody.getLogTrack();
JSONObject logData = logBody.getLogData();
if (CollectionUtils.isEmpty(logData)) {
logData = new JSONObject();
}
// 扩展日志处理
if (extLog(logType, logTrack.traceId(), logData)) {
return;
}
RunInfo runInfo = logBody.getRunInfo();
JSONArray stackTrace = logBody.getStackTrace();
// 处理数据
String title = logBody.getTitle();
String logType = logBody.getType();
title = StringUtils.isBlank(title) ? logType : title;
// 获取请求ip
String ip = logData.getString("ip");
@@ -90,4 +101,20 @@ public class LogBusServiceImpl implements LogBusService {
log.error("保存日志异常,logBody:{}", JsonUtils.toStrPretty(logBody), e);
}
}
/**
* 扩展日志
*
* @param logType 日志类型
* @param traceId 日志追踪 ID
* @param logData 日志数据
* @return 保存结果
*/
private boolean extLog(String logType, String traceId, JSONObject logData) {
if (LogBusBaseType.OPT_LOG.desc().equals(logType)) {
sysOptLogService.save(traceId, logData);
return true;
}
return false;
}
}

View File

@@ -0,0 +1,187 @@
package xtools.app.sys.service.impl;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Service;
import xtools.app.sys.auth.model.dto.OptLogDto;
import xtools.app.sys.config.SysConfig;
import xtools.app.sys.convert.SysOptLogConvert;
import xtools.app.sys.model.dto.excel.SysOptLogExcel;
import xtools.app.sys.model.dto.req.SysOptLogPageReq;
import xtools.app.sys.model.dto.resp.SysOptLogResp;
import xtools.app.sys.model.entity.SysOptLog;
import xtools.app.sys.service.SysOptLogService;
import xtools.app.sys.service.base.SysOptLogBaseService;
import xtools.boot.api.model.dto.Result;
import xtools.boot.api.model.dto.page.PageReq;
import xtools.boot.api.model.dto.page.PageResp;
import xtools.boot.core.utils.PathPatternUtils;
import xtools.boot.db.mybatisplus.utils.QueryUtils;
import xtools.boot.ip.utils.IpUtils;
import xtools.core.StringUtils;
import xtools.extend.dto.IpAddrDto;
import java.util.List;
import java.util.Objects;
/**
* <p>Title : SysOptLogServiceImpl</p>
* <p>Description : 系统操作日志 ServiceImpl</p>
* <p>Company : org.xujun</p>
*
* @author : xujun
* @version : 1.0.0
* @date : 2026-06-02 16:44:47
*/
@Slf4j
@Primary
@Service
@RequiredArgsConstructor
public class SysOptLogServiceImpl implements SysOptLogService {
private final SysConfig sysConfig;
private final SysOptLogBaseService sysOptLogBaseService;
private final SysOptLogConvert sysOptLogConvert;
/**
* 分页查询
*
* @param pageReq 分页请求
* @return 分页结果
*/
@Override
public Result<PageResp<SysOptLogResp>> page(PageReq<SysOptLogPageReq> pageReq) {
// 分页查询
Page<SysOptLog> page = getPageData(pageReq.getCurrentPage(), pageReq.getPageSize(), pageReq.getQuery());
// 分装结果
PageResp<SysOptLogResp> pageResp = new PageResp<>(pageReq, page.getTotal(), sysOptLogConvert.entityToRespList(page.getRecords()));
return Result.ok(pageResp);
}
/**
* 保存日志
*
* @param traceId 日志追踪 ID
* @param logData 日志
*/
@Override
public void save(String traceId, JSONObject logData) {
// 忽略操作日志
List<String> ignoreOptLog = sysConfig.getLog().getIgnoreOptLog();
OptLogDto dto = logData.toJavaObject(OptLogDto.class);
String uri = dto.getUri();
// 判断忽略操作日志
if (PathPatternUtils.match(ignoreOptLog, uri)) {
return;
}
dto.setTraceId(traceId);
SysOptLog optLog = sysOptLogConvert.dtoToEntity(dto);
String ip = optLog.getIp();
if (StringUtils.isNotBlank(ip)) {
try {
IpAddrDto ipAddr = IpUtils.search(ip);
optLog.setAddr(IpUtils.searchAddr(ipAddr));
optLog.setAddrCode(IpUtils.getCode(ipAddr));
} catch (Exception e) {
log.warn("获取 IP 地址信息失败,IP = {}", ip);
}
}
sysOptLogBaseService.save(optLog);
}
/**
* 导出 Excel
*
* @param req 请求参数
* @return Excel 数据
*/
@Override
public List<SysOptLogExcel> exportExcel(SysOptLogPageReq req) {
// 创建查询条件
LambdaQueryWrapper<SysOptLog> query = new LambdaQueryWrapper<>();
// 查询字段
query.select(
SysOptLog::getTraceId
, SysOptLog::getTitle
, SysOptLog::getAccountId
, SysOptLog::getAccount
, SysOptLog::getIp
, SysOptLog::getAddr
, SysOptLog::getUri
, SysOptLog::getMemo
, SysOptLog::getGmtCreate
);
// 设置查询条件
setQueryWrapper(query, req);
// 排序
query.orderByDesc(SysOptLog::getGmtCreate);
List<SysOptLog> dataList = sysOptLogBaseService.list(query);
return dataList.stream().map(sysOptLogConvert::entityToExcel).toList();
}
/**
* 获取分页数据
*
* @param currentPage 当前页
* @param pageSize 每页数量
* @param req 请求参数
* @return 分页数据
*/
private Page<SysOptLog> getPageData(Integer currentPage, Integer pageSize, SysOptLogPageReq req) {
// 创建查询条件
LambdaQueryWrapper<SysOptLog> query = new LambdaQueryWrapper<>();
// 查询字段
query.select(
SysOptLog::getId
, SysOptLog::getTraceId
, SysOptLog::getTitle
, SysOptLog::getAccountId
, SysOptLog::getAccount
, SysOptLog::getIp
, SysOptLog::getAddr
, SysOptLog::getAddrCode
, SysOptLog::getUri
, SysOptLog::getMemo
, SysOptLog::getGmtCreate
);
// 设置查询条件
setQueryWrapper(query, req);
// 排序
query.orderByDesc(SysOptLog::getGmtCreate);
return sysOptLogBaseService.page(QueryUtils.getPage(currentPage, pageSize), query);
}
/**
* 设置查询条件
*
* @param query 查询条件
* @param req 请求参数
*/
private void setQueryWrapper(LambdaQueryWrapper<SysOptLog> query, SysOptLogPageReq req) {
if (Objects.isNull(req)) {
return;
}
// 查询条件
query.eq(Objects.nonNull(req.getId()), SysOptLog::getId, req.getId());
query.like(StringUtils.isNotBlank(req.getTraceId()), SysOptLog::getTraceId, req.getTraceId());
query.like(StringUtils.isNotBlank(req.getTitle()), SysOptLog::getTitle, req.getTitle());
query.eq(Objects.nonNull(req.getAccountId()), SysOptLog::getAccountId, req.getAccountId());
query.like(StringUtils.isNotBlank(req.getAccount()), SysOptLog::getAccount, req.getAccount());
query.like(StringUtils.isNotBlank(req.getIp()), SysOptLog::getIp, req.getIp());
query.like(StringUtils.isNotBlank(req.getAddr()), SysOptLog::getAddr, req.getAddr());
query.like(StringUtils.isNotBlank(req.getAddrCode()), SysOptLog::getAddrCode, req.getAddrCode());
query.like(StringUtils.isNotBlank(req.getUri()), SysOptLog::getUri, req.getUri());
query.like(StringUtils.isNotBlank(req.getMemo()), SysOptLog::getMemo, req.getMemo());
QueryUtils.addTimeRange(query, req.getGmtCreateRange(), SysOptLog::getGmtCreate);
}
}