添加操作日志

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

@@ -158,6 +158,11 @@
<artifactId>xtools-app-sys-scheduled</artifactId> <artifactId>xtools-app-sys-scheduled</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.xujun</groupId>
<artifactId>xtools-app-sys-tag</artifactId>
<version>${project.version}</version>
</dependency>
<!-- Sys 模块 end --> <!-- Sys 模块 end -->
<!-- Gen 模块 begin --> <!-- Gen 模块 begin -->

View File

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

View File

@@ -367,10 +367,10 @@ import {useSettingsStore} from "@/store";
import {DictItem, PageReq, PageResult} from "@/types/global"; import {DictItem, PageReq, PageResult} from "@/types/global";
import SysCommonAPI from "@/api/sys/sys-common-api"; import SysCommonAPI from "@/api/sys/sys-common-api";
#else #else
import {PageReq} from "@/types/global"; import {PageReq, PageResult} from "@/types/global";
#end #end
#if($exportExcel) #if($exportExcel)
import {FileUtils, Format} from "@/utils/utils"; import {FileUtils, Format} from "@/utils";
#end #end
import ${table.entityName}API, { import ${table.entityName}API, {
EditForm as ${table.entityName}EditForm, EditForm as ${table.entityName}EditForm,

View File

@@ -8,3 +8,9 @@ sys:
log: log:
# 存储类型(elasticsearch|mysql) # 存储类型(elasticsearch|mysql)
type: ${SYS_LOG_TYPE:elasticsearch} type: ${SYS_LOG_TYPE:elasticsearch}
# 最大保存天数
max-days: 2
# 忽略操作日志
ignore-opt-log:
- /sys/dict-item/get-by-code/*
- /**/page

View File

@@ -24,6 +24,7 @@
<module>xtools-app-sys-scheduled</module> <module>xtools-app-sys-scheduled</module>
<module>xtools-app-sys-risk</module> <module>xtools-app-sys-risk</module>
<module>xtools-app-sys-file-web</module> <module>xtools-app-sys-file-web</module>
<module>xtools-app-sys-tag</module>
</modules> </modules>
</project> </project>

View File

@@ -11,6 +11,7 @@ import org.springframework.core.Ordered;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import xtools.app.common.cache.enums.AppCache; import xtools.app.common.cache.enums.AppCache;
import xtools.app.sys.auth.model.dto.LoginUserDto; import xtools.app.sys.auth.model.dto.LoginUserDto;
import xtools.app.sys.auth.model.dto.OptLogDto;
import xtools.app.sys.auth.utils.AuthUtils; import xtools.app.sys.auth.utils.AuthUtils;
import xtools.app.sys.auth.utils.PremUtils; import xtools.app.sys.auth.utils.PremUtils;
import xtools.app.sys.auth.whitelist.AuthWhitelist; import xtools.app.sys.auth.whitelist.AuthWhitelist;
@@ -22,14 +23,18 @@ import xtools.boot.cache.redis.base.RedisService;
import xtools.boot.core.holder.CommonHolder; import xtools.boot.core.holder.CommonHolder;
import xtools.boot.core.utils.PathPatternUtils; import xtools.boot.core.utils.PathPatternUtils;
import xtools.boot.core.utils.SpringContextUtils; import xtools.boot.core.utils.SpringContextUtils;
import xtools.boot.log.LogBus;
import xtools.boot.log.enums.LogBusBaseType;
import xtools.boot.mask.utils.MaskIgnoreUtils; import xtools.boot.mask.utils.MaskIgnoreUtils;
import xtools.boot.web.filter.base.BaseFilter; import xtools.boot.web.filter.base.BaseFilter;
import xtools.core.CollectionUtils; import xtools.core.CollectionUtils;
import xtools.core.StringUtils; import xtools.core.StringUtils;
import xtools.core.enums.LogLevel;
import xtools.web.HeaderUtils; import xtools.web.HeaderUtils;
import xtools.web.HttpServletUtils; import xtools.web.HttpServletUtils;
import java.io.IOException; import java.io.IOException;
import java.time.Instant;
import java.util.Collection; import java.util.Collection;
import java.util.HashSet; import java.util.HashSet;
import java.util.Objects; import java.util.Objects;
@@ -47,7 +52,7 @@ import java.util.Set;
* @date : 2026/1/31 21:18 * @date : 2026/1/31 21:18
*/ */
@Component @Component
public class AuthFilter extends BaseFilter implements Ordered, BaseParams { public class AuthFilter extends BaseFilter implements Ordered, BootCommonConstant, BaseParams {
/** /**
* 微服务标识 * 微服务标识
@@ -157,6 +162,10 @@ public class AuthFilter extends BaseFilter implements Ordered, BaseParams {
) throws ServletException, IOException { ) throws ServletException, IOException {
// 获取访问 uri // 获取访问 uri
String uri = HeaderUtils.getUri(request); String uri = HeaderUtils.getUri(request);
String ip = HeaderUtils.getIp(request);
OptLogDto log = new OptLogDto();
log.setUri(uri);
log.setIp(ip);
// 忽略权限校验 // 忽略权限校验
if (checkAuthWhiteList(uri)) { if (checkAuthWhiteList(uri)) {
@@ -184,11 +193,13 @@ public class AuthFilter extends BaseFilter implements Ordered, BaseParams {
HttpServletUtils.respWriter(response, JSONObject.from(new Result<>(ResultType.UNAUTHORIZED, null))); HttpServletUtils.respWriter(response, JSONObject.from(new Result<>(ResultType.UNAUTHORIZED, null)));
return; return;
} }
log.setAccountId(loginUser.getId());
log.setAccount(loginUser.getAccount());
// 校验 uri 访问权限 // 校验 uri 访问权限
String method = request.getMethod(); String method = request.getMethod();
if (!PremUtils.checkInterfacePerm(uri, method, loginUser.getRoleIds())) { if (!PremUtils.checkInterfacePerm(uri, method, loginUser.getRoleIds())) {
saveOptLog(log, "URI访问权限校验失败");
HttpServletUtils.respWriter(response, JSONObject.from(new Result<>(ResultType.FORBIDDEN, null))); HttpServletUtils.respWriter(response, JSONObject.from(new Result<>(ResultType.FORBIDDEN, null)));
return; return;
} }
@@ -199,7 +210,27 @@ public class AuthFilter extends BaseFilter implements Ordered, BaseParams {
// 校验忽略掩码 // 校验忽略掩码
checkIgnoreMask(); checkIgnoreMask();
CommonHolder.set(GET_SWAGGER_TAG, true);
filterChain.doFilter(request, response); filterChain.doFilter(request, response);
saveOptLog(log, null);
}
/**
* 保存操作日志
*
* @param log 日志
* @param memo 备注
*/
private void saveOptLog(OptLogDto log, String memo) {
Object tag = CommonHolder.get(SWAGGER_TAG);
if (Objects.nonNull(tag)) {
{
log.setTitle(tag.toString());
}
}
log.setMemo(memo);
log.setGmtCreate(Instant.now());
LogBus.init(LogLevel.INFO, LogBusBaseType.OPT_LOG).data(log).save();
} }
/** /**

View File

@@ -0,0 +1,76 @@
package xtools.app.sys.auth.model.dto;
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 : OptLogDto</p>
* <p>Description : OptLogDto</p>
* <p>DevelopTools : Idea_x64_v2026.1</p>
* <p>DevelopSystem : macOS Sequoia 15.7.5</p>
* <p>Company : org.xujun</p>
*
* @author : XuJun
* @version : 1.0.0
* @date : 2026/6/2 17:28
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class OptLogDto implements Serializable {
/**
* 日志追踪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;
/**
* 操作URI
*/
@Schema(description = "操作URI")
private String uri;
/**
* 备注
*/
@Schema(description = "备注")
private String memo;
/**
* 创建时间
*/
@Schema(description = "创建时间", example = "2026-01-05 10:32:00")
private Instant gmtCreate;
}

View File

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

View File

@@ -4,6 +4,9 @@ import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import java.util.ArrayList;
import java.util.List;
/** /**
* <p>Title : SysConfig</p> * <p>Title : SysConfig</p>
* <p>Description : SysConfig</p> * <p>Description : SysConfig</p>
@@ -57,6 +60,11 @@ public class SysConfig {
* 最大保存天数 * 最大保存天数
*/ */
private int maxDays = 2; 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.common.log.bus.service.LogBusService;
import xtools.app.sys.model.entity.SysLog; import xtools.app.sys.model.entity.SysLog;
import xtools.app.sys.service.SysLogService; import xtools.app.sys.service.SysLogService;
import xtools.app.sys.service.SysOptLogService;
import xtools.boot.api.model.dto.log.LogTrack; 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.LogBody;
import xtools.boot.log.model.dto.RunInfo; import xtools.boot.log.model.dto.RunInfo;
import xtools.core.CollectionUtils; import xtools.core.CollectionUtils;
@@ -37,6 +39,8 @@ public class LogBusServiceImpl implements LogBusService {
private final SysLogService sysLogService; private final SysLogService sysLogService;
private final SysOptLogService sysOptLogService;
/** /**
* 保存日志 * 保存日志
* *
@@ -44,18 +48,25 @@ public class LogBusServiceImpl implements LogBusService {
*/ */
@Override @Override
public void saveLog(LogBody logBody) { 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(); JSONObject logData = logBody.getLogData();
if (CollectionUtils.isEmpty(logData)) { if (CollectionUtils.isEmpty(logData)) {
logData = new JSONObject(); logData = new JSONObject();
} }
// 扩展日志处理
if (extLog(logType, logTrack.traceId(), logData)) {
return;
}
RunInfo runInfo = logBody.getRunInfo();
JSONArray stackTrace = logBody.getStackTrace(); JSONArray stackTrace = logBody.getStackTrace();
// 处理数据 // 处理数据
String title = logBody.getTitle(); String title = logBody.getTitle();
String logType = logBody.getType();
title = StringUtils.isBlank(title) ? logType : title; title = StringUtils.isBlank(title) ? logType : title;
// 获取请求ip // 获取请求ip
String ip = logData.getString("ip"); String ip = logData.getString("ip");
@@ -90,4 +101,20 @@ public class LogBusServiceImpl implements LogBusService {
log.error("保存日志异常,logBody:{}", JsonUtils.toStrPretty(logBody), e); 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);
}
}

View File

@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.xujun</groupId>
<artifactId>xtools-app-sys</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>xtools-app-sys-tag</artifactId>
<!-- 依赖 -->
<dependencies>
<!-- xtools begin -->
<!-- xtools-boot-base -->
<dependency>
<groupId>org.xujun</groupId>
<artifactId>xtools-boot-core</artifactId>
</dependency>
<!-- xtools end -->
<!-- spring begin -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
</dependency>
<!-- spring end -->
<!-- jakarta -->
<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
</dependency>
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<scope>compile</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,39 @@
package xtools.app.sys.tag.config;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import xtools.app.sys.tag.interceptor.SwaggerTagInterceptor;
import xtools.base.config.BaseParams;
/**
* <p>Title : WebMvcSwaggerConfig</p>
* <p>Description : WebMvcSwaggerConfig</p>
* <p>DevelopTools : Idea_x64_v2026.1</p>
* <p>DevelopSystem : macOS Sequoia 15.7.5</p>
* <p>Company : org.xujun</p>
*
* @author : XuJun
* @version : 1.0.0
* @date : 2026/6/2 16:44:47
*/
@Configuration
public class WebMvcSwaggerConfig implements WebMvcConfigurer, BaseParams {
@Resource
private SwaggerTagInterceptor swaggerTagInterceptor;
/**
* 添加拦截器
*
* @param registry 注册器
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(swaggerTagInterceptor)
.addPathPatterns("/**")
.order(CP_NUM0);
}
}

View File

@@ -0,0 +1,79 @@
package xtools.app.sys.tag.interceptor;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.jspecify.annotations.NonNull;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import xtools.base.config.BaseParams;
import xtools.boot.api.constant.BootCommonConstant;
import xtools.boot.core.holder.CommonHolder;
import xtools.core.StringUtils;
import java.util.Objects;
import java.util.StringJoiner;
/**
* <p>Title : SwaggerTagInterceptor</p>
* <p>Description : SwaggerTagInterceptor</p>
* <p>DevelopTools : Idea_x64_v2026.1</p>
* <p>DevelopSystem : macOS Sequoia 15.7.5</p>
* <p>Company : org.xujun</p>
*
* @author : XuJun
* <p>Version : 1.0.0</p>
* @date : 2026/6/3 17:28
*/
@Slf4j
@Component
public class SwaggerTagInterceptor implements HandlerInterceptor, BootCommonConstant, BaseParams {
/**
* 进入Handler方法之前执行
*
* @param request HttpServletRequest
* @param response HttpServletResponse
* @param handler handler
* @return true(放行) or false(拦截)
*/
@Override
public boolean preHandle(
@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull Object handler
) {
if (!(handler instanceof HandlerMethod handlerMethod)) {
return true;
}
Object get = CommonHolder.get(GET_SWAGGER_TAG);
if (Objects.isNull(get)) {
return true;
}
StringJoiner joiner = new StringJoiner(CP_LINE);
// 获取@Tag注解
Tag tag = handlerMethod.getBeanType().getAnnotation(Tag.class);
if (Objects.nonNull(tag)) {
String name = tag.name();
if (StringUtils.isNotBlank(name)) {
joiner.add(name);
}
}
// 获取@Operation注解
Operation operation = handlerMethod.getMethodAnnotation(Operation.class);
if (Objects.nonNull(operation)) {
String summary = operation.summary();
if (StringUtils.isNotBlank(summary)) {
joiner.add(summary);
}
}
CommonHolder.set(SWAGGER_TAG, joiner.toString());
return true;
}
}