feat(sysapi): 重构接口日志查询支持动态表名与报文检索

- 替换Java硬编码查询为MyBatis XML动态SQL,支持动态切换日志表
- 新增请求报文与响应报文关键字搜索条件
- 前端字段映射由驼峰调整为下划线,适配Map返回结构
- 日志详情新增JSON一键复制功能与归档遮罩动画
This commit is contained in:
yangyufu
2026-07-03 14:15:50 +08:00
parent 5ca480ed86
commit fc24f95299
8 changed files with 315 additions and 112 deletions

View File

@@ -40,67 +40,12 @@ public class SysApiLogController {
private final ISysApiLogHistoryService apiLogHistoryService;
/**
* 分页查询接口日志列表
* 分页查询接口日志列表走XML动态SQL支持动态表名
*/
@GetMapping
@Log("查询接口日志列表")
public ResponseEntity<Map<String, Object>> query(ApiLogQuery query) {
LambdaQueryWrapper<SysApiLog> wrapper = new LambdaQueryWrapper<>();
if (query.getDirection() != null) {
wrapper.eq(SysApiLog::getDirection, query.getDirection());
}
if (query.getSystemFlag() != null && !query.getSystemFlag().isEmpty()) {
wrapper.eq(SysApiLog::getSystemFlag, query.getSystemFlag());
}
if (query.getBizCode() != null && !query.getBizCode().isEmpty()) {
wrapper.eq(SysApiLog::getBizCode, query.getBizCode());
}
if (query.getTraceId() != null && !query.getTraceId().isEmpty()) {
wrapper.eq(SysApiLog::getTraceId, query.getTraceId());
}
if (query.getApiUrl() != null && !query.getApiUrl().isEmpty()) {
wrapper.like(SysApiLog::getApiUrl, query.getApiUrl());
}
if (query.getStatus() != null && !query.getStatus().isEmpty()) {
wrapper.eq(SysApiLog::getStatus, query.getStatus());
}
if (query.getBegin_time() != null && !query.getBegin_time().isEmpty()) {
String beginTime = query.getBegin_time() + " 00:00:00";
wrapper.ge(SysApiLog::getCreateTime, beginTime);
}
if (query.getEnd_time() != null && !query.getEnd_time().isEmpty()) {
String endTime = query.getEnd_time() + " 23:59:59";
wrapper.le(SysApiLog::getCreateTime, endTime);
}
if (query.getKeyword() != null && !query.getKeyword().isEmpty()) {
wrapper.and(w -> w.like(SysApiLog::getBizDesc, query.getKeyword())
.or().like(SysApiLog::getApiDesc, query.getKeyword())
.or().like(SysApiLog::getRequestParams, query.getKeyword())
.or().like(SysApiLog::getResponseBody, query.getKeyword()));
}
wrapper.orderByDesc(SysApiLog::getCreateTime);
Page<SysApiLog> page = new Page<>(query.getPage()+1, query.getSize());
IPage<SysApiLog> result = apiLogService.page(page, wrapper);
Map<String, Object> response = new HashMap<>();
response.put("content", result.getRecords());
response.put("totalElements", result.getTotal());
response.put("totalPages", result.getPages());
response.put("size", result.getSize());
response.put("number", result.getCurrent());
return new ResponseEntity<>(response, HttpStatus.OK);
public ResponseEntity<Object> query(ApiLogQuery query) {
return new ResponseEntity<>(apiLogService.queryApiLog(query), HttpStatus.OK);
}
/**

View File

@@ -1,4 +1,5 @@
package org.nl.system.service.sysapi.entity.dto;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.Data;
import org.nl.common.domain.query.PageQuery;
@@ -30,5 +31,13 @@ public class ApiLogQuery extends PageQuery {
private String keyword;
private String requestParams;
private String responseParams;
/**
* 动态表名从前台传入sys_api_log 或 sys_api_log_history
*/
private String tableName;
}

View File

@@ -1,7 +1,12 @@
package org.nl.system.service.sysapi.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.nl.system.service.sysapi.entity.SysApiLog;
import org.nl.system.service.sysapi.entity.dto.ApiLogQuery;
import java.util.List;
import java.util.Map;
/**
* @author ManMan.Yang
@@ -13,4 +18,11 @@ import org.nl.system.service.sysapi.entity.SysApiLog;
@Mapper
public interface SysApiLogMapper extends BaseMapper<SysApiLog> {
/**
* 分页查询接口日志(动态表名)
* @param query 查询条件
* @return List<Map>
*/
List<Map> queryApiLog(@Param("query") ApiLogQuery query);
}

View File

@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.nl.system.service.sysapi.mapper.SysApiLogMapper">
<select id="queryApiLog" resultType="java.util.Map">
SELECT
t.log_id,
t.direction,
t.system_flag,
t.biz_code,
t.biz_desc,
t.api_url,
t.api_desc,
t.trace_id,
t.request_method,
t.request_ip,
t.request_headers,
t.request_params,
t.response_body,
t.response_status,
t.cost_time,
t.status,
t.error_msg,
t.operator,
t.create_time
FROM
${query.tableName} t
<where>
1 = 1
<if test="query.direction != null">
and t.direction = #{query.direction}
</if>
<if test="query.systemFlag != null and query.systemFlag != ''">
and t.system_flag = #{query.systemFlag}
</if>
<if test="query.bizCode != null and query.bizCode != ''">
and t.biz_code = #{query.bizCode}
</if>
<if test="query.traceId != null and query.traceId != ''">
and t.trace_id = #{query.traceId}
</if>
<if test="query.status != null and query.status != ''">
and t.status = #{query.status}
</if>
<if test="query.begin_time != null and query.begin_time != ''">
and t.create_time <![CDATA[ >= ]]> CONCAT(#{query.begin_time}, ' 00:00:00')
</if>
<if test="query.end_time != null and query.end_time != ''">
and t.create_time <![CDATA[ <= ]]> CONCAT(#{query.end_time}, ' 23:59:59')
</if>
<if test="query.requestParams != null and query.requestParams != ''">
and t.request_params LIKE CONCAT('%', #{query.requestParams}, '%')
</if>
<if test="query.responseParams != null and query.responseParams != ''">
and t.response_body LIKE CONCAT('%', #{query.responseParams}, '%')
</if>
</where>
ORDER BY t.create_time DESC
</select>
</mapper>

View File

@@ -2,9 +2,12 @@ package org.nl.system.service.sysapi.service;
import com.baomidou.mybatisplus.extension.service.IService;
import org.apache.poi.ss.formula.functions.T;
import org.nl.system.service.sysapi.entity.SysApiLog;
import org.nl.system.service.sysapi.entity.dto.ApiLogQuery;
import org.springframework.http.ResponseEntity;
import org.springframework.scheduling.annotation.Async;
import java.util.Map;
/**
* @author ManMan.Yang
* @version V1.1
@@ -18,4 +21,11 @@ public interface ISysApiLogService extends IService<SysApiLog> {
void saveAsync(SysApiLog apiLog);
ResponseEntity<T> archiveLogs();
/**
* 分页查询接口日志走XML动态SQL
* @param query 查询条件
* @return Object
*/
Object queryApiLog(ApiLogQuery query);
}

View File

@@ -3,13 +3,17 @@ import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.formula.functions.T;
import org.nl.common.TableDataInfo;
import org.nl.common.utils.IdUtil;
import org.nl.modules.wql.util.SpringContextHolder;
import org.nl.system.service.param.impl.SysParamServiceImpl;
import org.nl.system.service.sysapi.entity.SysApiLog;
import org.nl.system.service.sysapi.entity.SysApiLogHistory;
import org.nl.system.service.sysapi.entity.dto.ApiLogQuery;
import org.nl.system.service.sysapi.mapper.SysApiLogHistoryMapper;
import org.nl.system.service.sysapi.mapper.SysApiLogMapper;
import org.springframework.beans.factory.annotation.Autowired;
@@ -20,6 +24,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
@@ -91,4 +96,18 @@ public class SysApiLogServiceImpl extends ServiceImpl<SysApiLogMapper, SysApiLog
throw new RuntimeException("日志归档失败: " + e.getMessage());
}
}
@Override
public Object queryApiLog(ApiLogQuery query) {
// 默认查主表 sys_api_log
if (StrUtil.isEmpty(query.getTableName())) {
query.setTableName("sys_api_log");
}
// 使用 PageHelper 分页(排序已在 XML 中定义)
Page<Object> page = PageHelper.startPage(query.getPage() + 1, query.getSize());
List<Map> list = this.baseMapper.queryApiLog(query);
TableDataInfo<Map> build = TableDataInfo.build(list);
build.setTotalElements(page.getTotal());
return build;
}
}

View File

@@ -1,6 +1,6 @@
import request from '@/utils/request'
export function getBizCodeList(systemFlag,direction) {
export function getBizCodeList(systemFlag, direction) {
return request({
url: 'api/sysApiLog/bizCodeList',
method: 'get',

View File

@@ -6,29 +6,31 @@
:inline="true"
class="demo-form-inline"
label-position="right"
label-width="90px"
label-width="120px"
label-suffix=":"
>
<el-form-item label="关键字">
<el-input
v-model="query.keyword"
clearable
<el-form-item label="查询日志表">
<el-select
v-model="query.tableName"
size="mini"
placeholder="请输入关键字查询"
class="filter-item"
style="width: 200px;"
/>
@change="handleTableNameChange"
>
<el-option label="sys_api_log" value="sys_api_log" />
<el-option label="sys_api_log_history" value="sys_api_log_history" />
</el-select>
</el-form-item>
<el-form-item label="接口走向">
<el-form-item label="接口走向类型">
<el-select
v-model="query.direction"
clearable
size="mini"
placeholder="请选择"
class="filter-item"
@change="directionChanged"
>
<el-option label="出站" :value="0" />
<el-option label="入站" :value="1" />
<el-option label="LMS请求外部系统" :value="0" />
<el-option label="外部系统请求LMS" :value="1" />
</el-select>
</el-form-item>
<el-form-item label="系统标识">
@@ -50,7 +52,6 @@
<el-select
v-model="query.bizCode"
clearable
filterable
size="mini"
placeholder="请选择"
class="filter-item"
@@ -85,6 +86,26 @@
value-format="yyyy-MM-dd"
/>
</el-form-item>
<el-form-item label="请求报文">
<el-input
v-model="query.requestParams"
clearable
size="mini"
placeholder="请输入请求报文关键字查询"
class="filter-item"
style="width: 200px;"
/>
</el-form-item>
<el-form-item label="响应报文">
<el-input
v-model="query.responseParams"
clearable
size="mini"
placeholder="请输入响应报文关键字查询"
class="filter-item"
style="width: 200px;"
/>
</el-form-item>
<rrOperation />
</el-form>
</div>
@@ -94,7 +115,6 @@
class="filter-item"
type="primary"
icon="el-icon-folder-checked"
:loading="archiveLoading"
@click="handleArchive"
>
日志归档
@@ -117,29 +137,31 @@
<el-tag v-else-if="scope.row.direction === 1" type="primary"><i class="el-icon-download"></i>入站</el-tag>
</template>
</el-table-column>
<el-table-column prop="systemFlag" label="系统标识" width="100" />
<el-table-column prop="bizCode" label="业务类型" width="260" />
<el-table-column prop="bizDesc" label="业务描述" show-overflow-tooltip width="500" />
<el-table-column prop="system_flag" label="系统标识" width="100" />
<el-table-column prop="biz_code" label="业务类型" width="260" />
<el-table-column prop="biz_desc" label="业务描述" show-overflow-tooltip width="500" />
<el-table-column prop="request_method" label="请求协议" show-overflow-tooltip min-width="200" />
<el-table-column prop="status" label="状态" width="100">
<template slot-scope="scope">
<el-tag v-if="scope.row.status === 'SUCCESS'" type="success">请求成功</el-tag>
<el-tag v-else-if="scope.row.status === 'FAIL'" type="danger">请求失败</el-tag>
</template>
</el-table-column>
<el-table-column prop="costTime" label="耗时(ms)" width="100" align="center">
<el-table-column prop="cost_time" label="耗时(ms)" width="100" align="center">
<template slot-scope="scope">
<el-tag v-if="scope.row.costTime <= 300" type="success">{{ scope.row.costTime }}ms</el-tag>
<el-tag v-else-if="scope.row.costTime <= 1000" type="warning">{{ scope.row.costTime }}ms</el-tag>
<el-tag v-else type="danger">{{ scope.row.costTime }}ms</el-tag>
<el-tag v-if="scope.row.cost_time <= 300" type="success">{{ scope.row.cost_time }}ms</el-tag>
<el-tag v-else-if="scope.row.cost_time <= 1000" type="warning">{{ scope.row.cost_time }}ms</el-tag>
<el-tag v-else type="danger">{{ scope.row.cost_time }}ms</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="请求时间" width="180">
<el-table-column prop="create_time" label="请求时间" width="180">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.createTime) }}</span>
<span>{{ parseTime(scope.row.create_time) }}</span>
</template>
</el-table-column>
<el-table-column prop="requestParams" label="请求参数" show-overflow-tooltip min-width="200" />
<el-table-column prop="responseBody" label="响应结果" show-overflow-tooltip min-width="200" />
<el-table-column prop="request_params" label="请求报文" show-overflow-tooltip min-width="200" />
<el-table-column prop="response_body" label="响应报文" show-overflow-tooltip min-width="200" />
<el-table-column label="详情" width="100" fixed="right">
<template slot-scope="scope">
<el-button size="mini" type="primary" plain @click="showDetail(scope.row)">查看详情</el-button>
@@ -159,40 +181,57 @@
>
<div class="drawer-content">
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="日志ID">{{ currentRow.logId }}</el-descriptions-item>
<el-descriptions-item label="链路追踪ID">{{ currentRow.traceId }}</el-descriptions-item>
<el-descriptions-item label="日志ID">{{ currentRow.log_id }}</el-descriptions-item>
<el-descriptions-item label="接口走向">
<el-tag v-if="currentRow.direction === 0" type="success" size="small"><i class="el-icon-upload2"></i>出站</el-tag>
<el-tag v-else-if="currentRow.direction === 1" type="primary" size="small"><i class="el-icon-download"></i>入站</el-tag>
</el-descriptions-item>
<el-descriptions-item label="系统标识">{{ currentRow.systemFlag }}</el-descriptions-item>
<el-descriptions-item label="业务类型">{{ currentRow.bizCode }}</el-descriptions-item>
<el-descriptions-item label="业务描述">{{ currentRow.bizDesc }}</el-descriptions-item>
<el-descriptions-item label="接口地址" :span="2">{{ currentRow.apiUrl }}</el-descriptions-item>
<el-descriptions-item label="请求方法">{{ currentRow.requestMethod }}</el-descriptions-item>
<el-descriptions-item label="请求IP">{{ currentRow.requestIp }}</el-descriptions-item>
<el-descriptions-item label="系统标识">{{ currentRow.system_flag }}</el-descriptions-item>
<el-descriptions-item label="业务类型">{{ currentRow.biz_code }}</el-descriptions-item>
<el-descriptions-item label="业务描述">{{ currentRow.biz_desc }}</el-descriptions-item>
<el-descriptions-item label="接口地址" :span="2">{{ currentRow.api_url }}</el-descriptions-item>
<el-descriptions-item label="请求方法">{{ currentRow.request_method }}</el-descriptions-item>
<el-descriptions-item label="请求IP">{{ currentRow.request_ip }}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag v-if="currentRow.status === 'SUCCESS'" type="success" size="small">SUCCESS</el-tag>
<el-tag v-else-if="currentRow.status === 'FAIL'" type="danger" size="small">FAIL</el-tag>
</el-descriptions-item>
<el-descriptions-item label="响应状态码">{{ currentRow.responseStatus }}</el-descriptions-item>
<el-descriptions-item label="耗时">{{ currentRow.costTime }}ms</el-descriptions-item>
<el-descriptions-item label="请求时间">{{ parseTime(currentRow.createTime) }}</el-descriptions-item>
<el-descriptions-item label="错误信息" :span="2" v-if="currentRow.errorMsg">
<span style="color: #f56c6c;">{{ currentRow.errorMsg }}</span>
<el-descriptions-item label="响应状态码">{{ currentRow.response_status }}</el-descriptions-item>
<el-descriptions-item label="耗时">{{ currentRow.cost_time }}ms</el-descriptions-item>
<el-descriptions-item label="请求时间">{{ parseTime(currentRow.create_time) }}</el-descriptions-item>
<el-descriptions-item label="错误信息" :span="2" v-if="currentRow.error_msg">
<span style="color: #f56c6c;">{{ currentRow.error_msg }}</span>
</el-descriptions-item>
</el-descriptions>
<el-divider content-position="left">请求头</el-divider>
<pre class="json-content">{{ formatJson(currentRow.requestHeaders) }}</pre>
<div class="json-wrapper">
<el-button class="copy-btn" type="text" icon="el-icon-document-copy" @click="copyJson(currentRow.request_headers)">复制</el-button>
<pre class="json-content">{{ formatJson(currentRow.request_headers) }}</pre>
</div>
<el-divider content-position="left">请求参数</el-divider>
<pre class="json-content">{{ formatJson(currentRow.requestParams) }}</pre>
<el-divider content-position="left">请求报文</el-divider>
<div class="json-wrapper">
<el-button class="copy-btn" type="text" icon="el-icon-document-copy" @click="copyJson(currentRow.request_params)">复制</el-button>
<pre class="json-content">{{ formatJson(currentRow.request_params) }}</pre>
</div>
<el-divider content-position="left">响应结果</el-divider>
<pre class="json-content">{{ formatJson(currentRow.responseBody) }}</pre>
<el-divider content-position="left">响应报文</el-divider>
<div class="json-wrapper">
<el-button class="copy-btn" type="text" icon="el-icon-document-copy" @click="copyJson(currentRow.response_body)">复制</el-button>
<pre class="json-content">{{ formatJson(currentRow.response_body) }}</pre>
</div>
</div>
</el-drawer>
<!-- 归档全局遮罩 -->
<div v-if="archiveLoading" class="archive-overlay">
<div class="archive-overlay__box">
<p>日志归档中请稍候...</p>
<div class="archive-overlay__bar">
<div class="archive-overlay__bar-inner"></div>
</div>
</div>
</div>
</div>
</template>
@@ -216,7 +255,13 @@ export default {
title: '接口日志',
url: 'api/sysApiLog',
query: {
createTime: [dateStr, dateStr]
createTime: [dateStr, dateStr],
tableName: 'sys_api_log',
direction: null,
systemFlag: null,
bizCode: null, // 关键提前声明Vue.observable 才能追踪
status: null,
keyword: null
}
})
},
@@ -238,15 +283,20 @@ export default {
}
},
methods: {
handleSystemFlagChange() {
this.query.bizCode = undefined
handleTableNameChange(){
this.query.direction = null;
this.query.systemFlag = null
this.query.bizCode = null
this.bizCodeList = []
if (this.query.systemFlag) {
},
handleSystemFlagChange() {
this.query.bizCode = null
this.bizCodeList = []
if (this.query.systemFlag != null && this.query.systemFlag !== '' && this.query.direction != null && this.query.direction !== '') {
apiLog.getBizCodeList(this.query.systemFlag, this.query.direction).then(res => {
this.bizCodeList = res
})
}
this.crud.toQuery()
},
showDetail(row) {
this.currentRow = row
@@ -256,6 +306,11 @@ export default {
this.drawerVisible = false
this.currentRow = {}
},
directionChanged() {
this.query.systemFlag = null
this.query.bizCode = null
this.bizCodeList = []
},
formatJson(json) {
if (!json) return ''
try {
@@ -264,6 +319,30 @@ export default {
return json
}
},
copyJson(raw) {
if (!raw) {
this.$message.warning('暂无内容可复制')
return
}
let text = raw
try {
text = JSON.stringify(JSON.parse(raw), null, 2)
} catch (e) {
// 非 JSON 格式,直接使用原始文本
}
navigator.clipboard.writeText(text).then(() => {
this.$message.success('已复制到剪贴板')
}).catch(() => {
// 降级方案
const textarea = document.createElement('textarea')
textarea.value = text
document.body.appendChild(textarea)
textarea.select()
document.execCommand('copy')
document.body.removeChild(textarea)
this.$message.success('已复制到剪贴板')
})
},
handleArchive() {
this.$confirm('确认将历史接口日志数据进行归档备份吗?此操作可能需要较长时间。', '提示', {
confirmButtonText: '确定',
@@ -328,6 +407,24 @@ export default {
margin: 10px 0;
}
.json-wrapper {
position: relative;
}
.copy-btn {
position: absolute;
top: 10px;
right: 10px;
z-index: 10;
color: #409eff;
font-size: 12px;
padding: 4px 8px;
}
.copy-btn:hover {
color: #66b1ff;
}
::v-deep .el-drawer__header {
margin-bottom: 10px;
padding: 15px 20px;
@@ -344,15 +441,56 @@ export default {
.biz-code-item .el-select {
width: 100%;
}
::v-deep .el-drawer__body {
padding: 0;
}
.biz-code-item {
min-width: 350px;
.archive-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.45);
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
}
.biz-code-item .el-select {
.archive-overlay__box {
text-align: center;
color: #fff;
width: 360px;
}
.archive-overlay__box p {
margin-bottom: 20px;
font-size: 18px;
letter-spacing: 4px;
color: aqua;
}
.archive-overlay__bar {
width: 100%;
height: 8px;
background-color: rgba(255, 255, 255, 0.55);
border-radius: 4px;
overflow: hidden;
}
.archive-overlay__bar-inner {
width: 30%;
height: 100%;
background-color: aqua;
border-radius: 4px;
animation: archive-bar-slide 1.2s ease-in-out infinite;
}
@keyframes archive-bar-slide {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(430%);
}
}
</style>