diff --git a/pom.xml b/pom.xml
index a6a2eda..ca200b4 100644
--- a/pom.xml
+++ b/pom.xml
@@ -158,6 +158,11 @@
xtools-app-sys-scheduled
${project.version}
+
+ org.xujun
+ xtools-app-sys-tag
+ ${project.version}
+
diff --git a/xtools-app-gen/xtools-app-gen-biz/pom.xml b/xtools-app-gen/xtools-app-gen-biz/pom.xml
index 8f96c75..6ddd2ea 100644
--- a/xtools-app-gen/xtools-app-gen-biz/pom.xml
+++ b/xtools-app-gen/xtools-app-gen-biz/pom.xml
@@ -90,6 +90,10 @@
org.xujun
xtools-app-sys-auth
+
+ org.xujun
+ xtools-app-sys-tag
+
diff --git a/xtools-app-gen/xtools-app-gen-biz/src/main/resources/templates/gen/vue/index.vue.vm b/xtools-app-gen/xtools-app-gen-biz/src/main/resources/templates/gen/vue/index.vue.vm
index 5325b30..ccc494d 100644
--- a/xtools-app-gen/xtools-app-gen-biz/src/main/resources/templates/gen/vue/index.vue.vm
+++ b/xtools-app-gen/xtools-app-gen-biz/src/main/resources/templates/gen/vue/index.vue.vm
@@ -367,10 +367,10 @@ import {useSettingsStore} from "@/store";
import {DictItem, PageReq, PageResult} from "@/types/global";
import SysCommonAPI from "@/api/sys/sys-common-api";
#else
-import {PageReq} from "@/types/global";
+import {PageReq, PageResult} from "@/types/global";
#end
#if($exportExcel)
-import {FileUtils, Format} from "@/utils/utils";
+import {FileUtils, Format} from "@/utils";
#end
import ${table.entityName}API, {
EditForm as ${table.entityName}EditForm,
diff --git a/xtools-app-standalone/src/main/resources/application-app-sys.yaml b/xtools-app-standalone/src/main/resources/application-app-sys.yaml
index 24f9fbf..deca5b2 100644
--- a/xtools-app-standalone/src/main/resources/application-app-sys.yaml
+++ b/xtools-app-standalone/src/main/resources/application-app-sys.yaml
@@ -7,4 +7,10 @@ sys:
# 日志配置
log:
# 存储类型(elasticsearch|mysql)
- type: ${SYS_LOG_TYPE:elasticsearch}
\ No newline at end of file
+ type: ${SYS_LOG_TYPE:elasticsearch}
+ # 最大保存天数
+ max-days: 2
+ # 忽略操作日志
+ ignore-opt-log:
+ - /sys/dict-item/get-by-code/*
+ - /**/page
diff --git a/xtools-app-sys/pom.xml b/xtools-app-sys/pom.xml
index 27a7332..0220cfd 100644
--- a/xtools-app-sys/pom.xml
+++ b/xtools-app-sys/pom.xml
@@ -24,6 +24,7 @@
xtools-app-sys-scheduled
xtools-app-sys-risk
xtools-app-sys-file-web
+ xtools-app-sys-tag
\ No newline at end of file
diff --git a/xtools-app-sys/xtools-app-sys-auth/src/main/java/xtools/app/sys/auth/filter/AuthFilter.java b/xtools-app-sys/xtools-app-sys-auth/src/main/java/xtools/app/sys/auth/filter/AuthFilter.java
index 46a88ce..f3dbbae 100644
--- a/xtools-app-sys/xtools-app-sys-auth/src/main/java/xtools/app/sys/auth/filter/AuthFilter.java
+++ b/xtools-app-sys/xtools-app-sys-auth/src/main/java/xtools/app/sys/auth/filter/AuthFilter.java
@@ -11,6 +11,7 @@ import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import xtools.app.common.cache.enums.AppCache;
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.PremUtils;
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.utils.PathPatternUtils;
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.web.filter.base.BaseFilter;
import xtools.core.CollectionUtils;
import xtools.core.StringUtils;
+import xtools.core.enums.LogLevel;
import xtools.web.HeaderUtils;
import xtools.web.HttpServletUtils;
import java.io.IOException;
+import java.time.Instant;
import java.util.Collection;
import java.util.HashSet;
import java.util.Objects;
@@ -47,7 +52,7 @@ import java.util.Set;
* @date : 2026/1/31 21:18
*/
@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 {
// 获取访问 uri
String uri = HeaderUtils.getUri(request);
+ String ip = HeaderUtils.getIp(request);
+ OptLogDto log = new OptLogDto();
+ log.setUri(uri);
+ log.setIp(ip);
// 忽略权限校验
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)));
return;
}
-
+ log.setAccountId(loginUser.getId());
+ log.setAccount(loginUser.getAccount());
// 校验 uri 访问权限
String method = request.getMethod();
if (!PremUtils.checkInterfacePerm(uri, method, loginUser.getRoleIds())) {
+ saveOptLog(log, "URI访问权限校验失败");
HttpServletUtils.respWriter(response, JSONObject.from(new Result<>(ResultType.FORBIDDEN, null)));
return;
}
@@ -199,7 +210,27 @@ public class AuthFilter extends BaseFilter implements Ordered, BaseParams {
// 校验忽略掩码
checkIgnoreMask();
+ CommonHolder.set(GET_SWAGGER_TAG, true);
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();
}
/**
diff --git a/xtools-app-sys/xtools-app-sys-auth/src/main/java/xtools/app/sys/auth/model/dto/OptLogDto.java b/xtools-app-sys/xtools-app-sys-auth/src/main/java/xtools/app/sys/auth/model/dto/OptLogDto.java
new file mode 100644
index 0000000..0fd84c8
--- /dev/null
+++ b/xtools-app-sys/xtools-app-sys-auth/src/main/java/xtools/app/sys/auth/model/dto/OptLogDto.java
@@ -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;
+
+
+/**
+ *
Title : OptLogDto
+ * Description : OptLogDto
+ * DevelopTools : Idea_x64_v2026.1
+ * DevelopSystem : macOS Sequoia 15.7.5
+ * Company : org.xujun
+ *
+ * @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;
+
+}
diff --git a/xtools-app-sys/xtools-app-sys-biz/pom.xml b/xtools-app-sys/xtools-app-sys-biz/pom.xml
index ad66818..18bb0ce 100644
--- a/xtools-app-sys/xtools-app-sys-biz/pom.xml
+++ b/xtools-app-sys/xtools-app-sys-biz/pom.xml
@@ -157,6 +157,10 @@
org.xujun
xtools-app-sys-risk
+
+ org.xujun
+ xtools-app-sys-tag
+
diff --git a/xtools-app-sys/xtools-app-sys-biz/src/main/java/xtools/app/sys/config/SysConfig.java b/xtools-app-sys/xtools-app-sys-biz/src/main/java/xtools/app/sys/config/SysConfig.java
index fa4bc87..8dcbf33 100644
--- a/xtools-app-sys/xtools-app-sys-biz/src/main/java/xtools/app/sys/config/SysConfig.java
+++ b/xtools-app-sys/xtools-app-sys-biz/src/main/java/xtools/app/sys/config/SysConfig.java
@@ -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;
+
/**
* Title : SysConfig
* Description : SysConfig
@@ -57,6 +60,11 @@ public class SysConfig {
* 最大保存天数
*/
private int maxDays = 2;
+
+ /**
+ * 忽略操作日志
+ */
+ private List ignoreOptLog = new ArrayList<>();
}
}
diff --git a/xtools-app-sys/xtools-app-sys-biz/src/main/java/xtools/app/sys/controller/SysOptLogController.java b/xtools-app-sys/xtools-app-sys-biz/src/main/java/xtools/app/sys/controller/SysOptLogController.java
new file mode 100644
index 0000000..1b0735e
--- /dev/null
+++ b/xtools-app-sys/xtools-app-sys-biz/src/main/java/xtools/app/sys/controller/SysOptLogController.java
@@ -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;
+
+/**
+ * Title : SysOptLogController
+ * Description : 系统操作日志 Controller
+ * Company : org.xujun
+ *
+ * @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> page(@RequestBody @Valid PageReq 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 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);
+ }
+
+}
diff --git a/xtools-app-sys/xtools-app-sys-biz/src/main/java/xtools/app/sys/convert/SysOptLogConvert.java b/xtools-app-sys/xtools-app-sys-biz/src/main/java/xtools/app/sys/convert/SysOptLogConvert.java
new file mode 100644
index 0000000..96cce05
--- /dev/null
+++ b/xtools-app-sys/xtools-app-sys-biz/src/main/java/xtools/app/sys/convert/SysOptLogConvert.java
@@ -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;
+
+/**
+ * Title : SysOptLogConvert
+ * Description : 系统操作日志 Convert
+ * Company : org.xujun
+ *
+ * @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 entityToRespList(List dataList);
+
+ /**
+ * 实体转Excel
+ *
+ * @param data 实体
+ * @return Excel
+ */
+ SysOptLogExcel entityToExcel(SysOptLog data);
+
+
+ /**
+ * DTO转实体
+ *
+ * @param data DTO
+ * @return 实体
+ */
+ SysOptLog dtoToEntity(OptLogDto data);
+}
\ No newline at end of file
diff --git a/xtools-app-sys/xtools-app-sys-biz/src/main/java/xtools/app/sys/mapper/SysOptLogMapper.java b/xtools-app-sys/xtools-app-sys-biz/src/main/java/xtools/app/sys/mapper/SysOptLogMapper.java
new file mode 100644
index 0000000..898d811
--- /dev/null
+++ b/xtools-app-sys/xtools-app-sys-biz/src/main/java/xtools/app/sys/mapper/SysOptLogMapper.java
@@ -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;
+
+/**
+ * Title : SysOptLogMapper
+ * Description : 系统操作日志 Mapper 接口
+ * Company : org.xujun
+ *
+ * @author : xujun
+ * @version : 1.0.0
+ * @date : 2026-06-02 16:44:47
+ */
+@Mapper
+public interface SysOptLogMapper extends BaseMapper {
+
+}
diff --git a/xtools-app-sys/xtools-app-sys-biz/src/main/java/xtools/app/sys/model/dto/excel/SysOptLogExcel.java b/xtools-app-sys/xtools-app-sys-biz/src/main/java/xtools/app/sys/model/dto/excel/SysOptLogExcel.java
new file mode 100644
index 0000000..9fa1248
--- /dev/null
+++ b/xtools-app-sys/xtools-app-sys-biz/src/main/java/xtools/app/sys/model/dto/excel/SysOptLogExcel.java
@@ -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;
+
+/**
+ * Title : SysOptLogExcel
+ * Description : 系统操作日志Excel对象
+ * Company : org.xujun
+ *
+ * @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;
+
+}
\ No newline at end of file
diff --git a/xtools-app-sys/xtools-app-sys-biz/src/main/java/xtools/app/sys/model/dto/req/SysOptLogPageReq.java b/xtools-app-sys/xtools-app-sys-biz/src/main/java/xtools/app/sys/model/dto/req/SysOptLogPageReq.java
new file mode 100644
index 0000000..2c7f163
--- /dev/null
+++ b/xtools-app-sys/xtools-app-sys-biz/src/main/java/xtools/app/sys/model/dto/req/SysOptLogPageReq.java
@@ -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;
+
+/**
+ * Title : SysOptLogPageReq
+ * Description : 系统操作日志分页请求对象
+ * Company : org.xujun
+ *
+ * @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;
+
+}
\ No newline at end of file
diff --git a/xtools-app-sys/xtools-app-sys-biz/src/main/java/xtools/app/sys/model/dto/resp/SysOptLogResp.java b/xtools-app-sys/xtools-app-sys-biz/src/main/java/xtools/app/sys/model/dto/resp/SysOptLogResp.java
new file mode 100644
index 0000000..1aba337
--- /dev/null
+++ b/xtools-app-sys/xtools-app-sys-biz/src/main/java/xtools/app/sys/model/dto/resp/SysOptLogResp.java
@@ -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;
+
+/**
+ * Title : SysOptLogResp
+ * Description : 系统操作日志响应对象
+ * Company : org.xujun
+ *
+ * @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;
+
+}
\ No newline at end of file
diff --git a/xtools-app-sys/xtools-app-sys-biz/src/main/java/xtools/app/sys/model/entity/SysOptLog.java b/xtools-app-sys/xtools-app-sys-biz/src/main/java/xtools/app/sys/model/entity/SysOptLog.java
new file mode 100644
index 0000000..e725b55
--- /dev/null
+++ b/xtools-app-sys/xtools-app-sys-biz/src/main/java/xtools/app/sys/model/entity/SysOptLog.java
@@ -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;
+
+/**
+ * Title : SysOptLog
+ * Description : 系统操作日志实体对象
+ * Company : org.xujun
+ *
+ * @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;
+
+}
\ No newline at end of file
diff --git a/xtools-app-sys/xtools-app-sys-biz/src/main/java/xtools/app/sys/service/SysOptLogService.java b/xtools-app-sys/xtools-app-sys-biz/src/main/java/xtools/app/sys/service/SysOptLogService.java
new file mode 100644
index 0000000..95c3e0e
--- /dev/null
+++ b/xtools-app-sys/xtools-app-sys-biz/src/main/java/xtools/app/sys/service/SysOptLogService.java
@@ -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;
+
+/**
+ * Title : SysOptLogService
+ * Description : 系统操作日志 Service
+ * Company : org.xujun
+ *
+ * @author : xujun
+ * @version : 1.0.0
+ * @date : 2026-06-02 16:44:47
+ */
+public interface SysOptLogService {
+
+ /**
+ * 分页查询
+ *
+ * @param pageReq 分页请求
+ * @return 分页结果
+ */
+ Result> page(PageReq pageReq);
+
+ /**
+ * 保存日志
+ *
+ * @param traceId 日志追踪 ID
+ * @param logData 日志
+ */
+ void save(String traceId, JSONObject logData);
+
+ /**
+ * 导出 Excel
+ *
+ * @param req 请求参数
+ * @return Excel 数据
+ */
+ List exportExcel(SysOptLogPageReq req);
+
+}
diff --git a/xtools-app-sys/xtools-app-sys-biz/src/main/java/xtools/app/sys/service/base/SysOptLogBaseService.java b/xtools-app-sys/xtools-app-sys-biz/src/main/java/xtools/app/sys/service/base/SysOptLogBaseService.java
new file mode 100644
index 0000000..de0f4c8
--- /dev/null
+++ b/xtools-app-sys/xtools-app-sys-biz/src/main/java/xtools/app/sys/service/base/SysOptLogBaseService.java
@@ -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;
+
+/**
+ * Title : SysOptLogBaseService
+ * Description : 系统操作日志 BaseService
+ * Company : org.xujun
+ *
+ * @author : xujun
+ * @version : 1.0.0
+ * @date : 2026-06-02 16:44:47
+ */
+@Component
+public class SysOptLogBaseService extends ServiceImpl {
+}
diff --git a/xtools-app-sys/xtools-app-sys-biz/src/main/java/xtools/app/sys/service/impl/LogBusServiceImpl.java b/xtools-app-sys/xtools-app-sys-biz/src/main/java/xtools/app/sys/service/impl/LogBusServiceImpl.java
index dd9e545..0b1a3d1 100644
--- a/xtools-app-sys/xtools-app-sys-biz/src/main/java/xtools/app/sys/service/impl/LogBusServiceImpl.java
+++ b/xtools-app-sys/xtools-app-sys-biz/src/main/java/xtools/app/sys/service/impl/LogBusServiceImpl.java
@@ -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;
+ }
}
diff --git a/xtools-app-sys/xtools-app-sys-biz/src/main/java/xtools/app/sys/service/impl/SysOptLogServiceImpl.java b/xtools-app-sys/xtools-app-sys-biz/src/main/java/xtools/app/sys/service/impl/SysOptLogServiceImpl.java
new file mode 100644
index 0000000..d261049
--- /dev/null
+++ b/xtools-app-sys/xtools-app-sys-biz/src/main/java/xtools/app/sys/service/impl/SysOptLogServiceImpl.java
@@ -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;
+
+/**
+ * Title : SysOptLogServiceImpl
+ * Description : 系统操作日志 ServiceImpl
+ * Company : org.xujun
+ *
+ * @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> page(PageReq pageReq) {
+ // 分页查询
+ Page page = getPageData(pageReq.getCurrentPage(), pageReq.getPageSize(), pageReq.getQuery());
+ // 分装结果
+ PageResp 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 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 exportExcel(SysOptLogPageReq req) {
+ // 创建查询条件
+ LambdaQueryWrapper 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 dataList = sysOptLogBaseService.list(query);
+ return dataList.stream().map(sysOptLogConvert::entityToExcel).toList();
+ }
+
+ /**
+ * 获取分页数据
+ *
+ * @param currentPage 当前页
+ * @param pageSize 每页数量
+ * @param req 请求参数
+ * @return 分页数据
+ */
+ private Page getPageData(Integer currentPage, Integer pageSize, SysOptLogPageReq req) {
+ // 创建查询条件
+ LambdaQueryWrapper 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 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);
+ }
+
+}
diff --git a/xtools-app-sys/xtools-app-sys-tag/pom.xml b/xtools-app-sys/xtools-app-sys-tag/pom.xml
new file mode 100644
index 0000000..fa84b87
--- /dev/null
+++ b/xtools-app-sys/xtools-app-sys-tag/pom.xml
@@ -0,0 +1,42 @@
+
+
+ 4.0.0
+
+ org.xujun
+ xtools-app-sys
+ 1.0.0
+
+ xtools-app-sys-tag
+
+
+
+
+
+
+ org.xujun
+ xtools-boot-core
+
+
+
+
+
+ org.springframework
+ spring-webmvc
+
+
+
+
+
+ jakarta.annotation
+ jakarta.annotation-api
+
+
+ jakarta.servlet
+ jakarta.servlet-api
+ compile
+
+
+
+
\ No newline at end of file
diff --git a/xtools-app-sys/xtools-app-sys-tag/src/main/java/xtools/app/sys/tag/config/WebMvcSwaggerConfig.java b/xtools-app-sys/xtools-app-sys-tag/src/main/java/xtools/app/sys/tag/config/WebMvcSwaggerConfig.java
new file mode 100644
index 0000000..c26bc62
--- /dev/null
+++ b/xtools-app-sys/xtools-app-sys-tag/src/main/java/xtools/app/sys/tag/config/WebMvcSwaggerConfig.java
@@ -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;
+
+/**
+ * Title : WebMvcSwaggerConfig
+ * Description : WebMvcSwaggerConfig
+ * DevelopTools : Idea_x64_v2026.1
+ * DevelopSystem : macOS Sequoia 15.7.5
+ * Company : org.xujun
+ *
+ * @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);
+ }
+
+}
diff --git a/xtools-app-sys/xtools-app-sys-tag/src/main/java/xtools/app/sys/tag/interceptor/SwaggerTagInterceptor.java b/xtools-app-sys/xtools-app-sys-tag/src/main/java/xtools/app/sys/tag/interceptor/SwaggerTagInterceptor.java
new file mode 100644
index 0000000..3192118
--- /dev/null
+++ b/xtools-app-sys/xtools-app-sys-tag/src/main/java/xtools/app/sys/tag/interceptor/SwaggerTagInterceptor.java
@@ -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;
+
+/**
+ * Title : SwaggerTagInterceptor
+ * Description : SwaggerTagInterceptor
+ * DevelopTools : Idea_x64_v2026.1
+ * DevelopSystem : macOS Sequoia 15.7.5
+ * Company : org.xujun
+ *
+ * @author : XuJun
+ * Version : 1.0.0
+ * @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;
+ }
+
+}