up.
This commit is contained in:
parent
e60702340b
commit
9c2e5a7b0b
@ -58,7 +58,7 @@ class MenuController extends BaseController
|
||||
'path' => $data['path'],
|
||||
'component' => $data['component'],
|
||||
'authority' => $data['authority'],
|
||||
'sortNumber' => $data['sortNumber'],
|
||||
'sort_number' => $data['sortNumber'],
|
||||
'hide' => $data['hide'],
|
||||
'meta' => $data['meta'],
|
||||
]);
|
||||
|
||||
@ -5,7 +5,6 @@ namespace app\controller\admin\system;
|
||||
use app\BaseController;
|
||||
use app\entity\SysRequestRecord;
|
||||
use app\service\CurdService;
|
||||
use think\db\exception\DbException;
|
||||
use think\response\Json;
|
||||
|
||||
class RequestRecordController extends BaseController
|
||||
@ -13,19 +12,32 @@ class RequestRecordController extends BaseController
|
||||
/**
|
||||
* 分页查询请求日志记录
|
||||
* @return Json
|
||||
* @throws DbException
|
||||
*/
|
||||
public function page(): Json
|
||||
{
|
||||
$model = SysRequestRecord::withSearch(['createTime'], [
|
||||
'createTime' => [
|
||||
$this->request->get('createTimeStart/s', ''),
|
||||
$this->request->get('createTimeEnd/s', '')
|
||||
],
|
||||
]);
|
||||
|
||||
$paginate = CurdService::getPaginate($this->request, $model);
|
||||
|
||||
ini_set('memory_limit', '512M');
|
||||
try {
|
||||
/*
|
||||
* 这里有两个字段数据太大,提到详情里面做查询
|
||||
*/
|
||||
$model = SysRequestRecord::withoutField('request_headers,response_data')->withSearch(['createTime'], [
|
||||
'createTime' => [
|
||||
$this->request->get('createTimeStart/s', ''),
|
||||
$this->request->get('createTimeEnd/s', '')
|
||||
],
|
||||
]);
|
||||
$paginate = CurdService::getPaginate($this->request, $model);
|
||||
} catch (\Exception $e) {
|
||||
return $this->writeError($e->getMessage());
|
||||
}
|
||||
return $this->writeSuccess('ok', $paginate);
|
||||
}
|
||||
|
||||
public function info(): Json
|
||||
{
|
||||
ini_set('memory_limit', '512M');
|
||||
$id = $this->request->get('id');
|
||||
$data = SysRequestRecord::find($id);
|
||||
return $this->writeSuccess('ok', $data);
|
||||
}
|
||||
}
|
||||
@ -25,6 +25,10 @@ class ContextMiddleware extends middleware
|
||||
return $response;
|
||||
}
|
||||
|
||||
protected $noHttpLog = [
|
||||
'GET@/adminapi/system/request-record'
|
||||
];
|
||||
|
||||
/**
|
||||
* 结束
|
||||
* @param Response $response
|
||||
@ -36,8 +40,8 @@ class ContextMiddleware extends middleware
|
||||
$log = (array)$this->app->config->get('admin.httpLog');
|
||||
|
||||
$logOpen = $log['open'] ?? false;
|
||||
|
||||
if (!$logOpen) {
|
||||
$logOpen = false;
|
||||
if (!$logOpen || in_array($request->method() . '@' . $request->pathinfo(),$this->noHttpLog)) {
|
||||
return;
|
||||
}
|
||||
$request_start_time = $request->time(true);
|
||||
|
||||
@ -55,6 +55,7 @@ Route::group("adminapi", function () {
|
||||
Route::get('login-record/page', [LoginRecordController::class, "page"])->name("system.pageLoginRecords");
|
||||
Route::get('operate-record/page', [OperateRecordController::class, "page"])->name("system.pageOperateRecords");
|
||||
Route::get('request-record/page', [RequestRecordController::class, "page"])->name("system.pageRequestRecords");
|
||||
Route::get('request-record/info', [RequestRecordController::class, "info"])->name("system.pageRequestInfo");
|
||||
/*
|
||||
* 缓存管理
|
||||
*/
|
||||
|
||||
30
z_ele/src/api/system/request-record/index.ts
Normal file
30
z_ele/src/api/system/request-record/index.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult, PageResult } from '@/api';
|
||||
import type { RequestRecord, RequestRecordParam } from './model';
|
||||
|
||||
/**
|
||||
* 分页查询请求日志
|
||||
*/
|
||||
export async function pageRequestRecords(params: RequestRecordParam) {
|
||||
const res = await request.get<ApiResult<PageResult<RequestRecord>>>(
|
||||
'/system/request-record/page',
|
||||
{ params }
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
return res.data.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
/**
|
||||
* 获取请求日志详情
|
||||
*/
|
||||
export async function getRequestRecordInfo(id: number) {
|
||||
const res = await request.get<ApiResult<RequestRecord>>(
|
||||
'/system/request-record/info?id=' + id
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
return res.data.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
|
||||
57
z_ele/src/api/system/request-record/model/index.ts
Normal file
57
z_ele/src/api/system/request-record/model/index.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import { PageParam } from '@/api';
|
||||
|
||||
/**
|
||||
* 操作日志
|
||||
*/
|
||||
export interface RequestRecord {
|
||||
/** 操作日志id */
|
||||
id?: number;
|
||||
/** 用户id */
|
||||
userId?: number;
|
||||
/** 操作模块 */
|
||||
module: string;
|
||||
/** 操作功能 */
|
||||
description: string;
|
||||
/** 请求地址 */
|
||||
url: string;
|
||||
/** 请求方式 */
|
||||
requestMethod: string;
|
||||
/** 调用方法 */
|
||||
method: string;
|
||||
/** 请求参数 */
|
||||
params: string;
|
||||
/** 返回结果 */
|
||||
result: string;
|
||||
/** 异常信息 */
|
||||
error: string;
|
||||
/** 消耗时间, 单位毫秒 */
|
||||
spendTime: number;
|
||||
/** 操作系统 */
|
||||
os: string;
|
||||
/** 设备名称 */
|
||||
device: string;
|
||||
/** 浏览器类型 */
|
||||
browser: string;
|
||||
/** ip地址 */
|
||||
ip: string;
|
||||
/** 状态, 0成功, 1异常 */
|
||||
status: number;
|
||||
/** 操作时间 */
|
||||
createTime: string;
|
||||
/** 用户昵称 */
|
||||
nickname: string;
|
||||
/** 用户账号 */
|
||||
username: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 操作日志搜索条件
|
||||
*/
|
||||
export interface RequestRecordParam extends PageParam {
|
||||
/** contextId */
|
||||
contextId?: string;
|
||||
/** 开始时间 */
|
||||
createTimeStart?: string;
|
||||
/** 截至时间 */
|
||||
createTimeEnd?: string;
|
||||
}
|
||||
@ -219,3 +219,34 @@ export function doWithTransition(
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 将字节数转换为易读的存储单位(GB, MB, KB, B)
|
||||
* @param bytes 字节数量
|
||||
* @param precision 可选参数,指定保留的小数位数(默认2位)
|
||||
* @returns 格式化的字符串结果(如 '1.23GB', '500B')
|
||||
*/
|
||||
export function formatBytes(bytes: number, precision: number = 2): string {
|
||||
// 定义单位常量
|
||||
const UNITS = ['B', 'KB', 'MB', 'GB'];
|
||||
const UNIT_SCALE = 1024;
|
||||
|
||||
// 处理无效输入
|
||||
if (isNaN(bytes) || !isFinite(bytes)) return '0B';
|
||||
if (bytes === 0) return '0B';
|
||||
|
||||
// 计算单位级别
|
||||
const level = Math.floor(Math.log(bytes) / Math.log(UNIT_SCALE));
|
||||
|
||||
// 获取转换后的值和对应单位
|
||||
const unit = UNITS[Math.min(level, UNITS.length - 1)];
|
||||
const value = bytes / Math.pow(UNIT_SCALE, level);
|
||||
|
||||
// 智能处理小数部分:
|
||||
// 1. 当结果为整数时省略小数部分
|
||||
// 2. 保留指定精度但去除末尾多余的0
|
||||
const formattedValue = value.toFixed(precision).replace(/(\.0+|0+)$/, '');
|
||||
|
||||
return `${formattedValue}${unit}`;
|
||||
}
|
||||
|
||||
@ -0,0 +1,121 @@
|
||||
<!-- 详情弹窗 -->
|
||||
<template>
|
||||
<ele-modal title="请求详情" :width="960" v-model="visible">
|
||||
<el-descriptions
|
||||
v-if="data"
|
||||
:border="true"
|
||||
:column="mobile ? 1 : 2"
|
||||
class="detail-table"
|
||||
>
|
||||
<el-descriptions-item label="ContextId">
|
||||
<div>{{ data.contextId }}</div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="IP地址">
|
||||
<div>{{ data.requestIp }}</div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="请求时间">
|
||||
<div>{{ data.requestTime }}<span style="color:#9e9e9e;"> - {{ data.requestEndTime }}</span></div>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="接口耗时">
|
||||
<div v-if="!isNaN(data.runningTime)">{{ data.runningTime / 1000 }}s</div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="请求方式">
|
||||
<div>{{ data.requestMethod }}</div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="状态码">
|
||||
<el-tag
|
||||
size="small"
|
||||
:type="data.responseCode === 200 ? 'success' : 'danger'"
|
||||
:disable-transitions="true"
|
||||
>
|
||||
{{ data.responseCode }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="请求地址" :span="2">
|
||||
<div style="word-break: break-all">{{ data.requestDomain }}{{ data.requestPath }}</div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="规则名称" :span="2">
|
||||
<div style="word-break: break-all">{{ data.ruleName }}</div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="路由地址" :span="2">
|
||||
<div style="word-break: break-all">{{ data.ruleRoute }}</div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="请求头信息" :span="2">
|
||||
<ele-ellipsis :max-line="4" :tooltip="ellipsisTooltipProps">
|
||||
{{ data.requestHeaders }}
|
||||
</ele-ellipsis>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="请求参数" :span="2">
|
||||
<ele-ellipsis :max-line="4" :tooltip="ellipsisTooltipProps">
|
||||
{{ data.requestBody }}
|
||||
</ele-ellipsis>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="返回类型" :span="1">
|
||||
<ele-ellipsis :max-line="4" :tooltip="ellipsisTooltipProps">
|
||||
{{ data.responseType }}
|
||||
</ele-ellipsis>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="运行消耗" :span="1">
|
||||
<ele-ellipsis :max-line="4" :tooltip="ellipsisTooltipProps">
|
||||
{{ formatBytes(data.runningMemoryUsage) }} / {{ data.runningMemoryLimit }}
|
||||
</ele-ellipsis>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="返回结果" :span="2">
|
||||
<ele-ellipsis :max-line="4" :tooltip="ellipsisTooltipProps">
|
||||
{{ data.responseData }}
|
||||
</ele-ellipsis>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</ele-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive } from 'vue';
|
||||
import type { EleTooltipProps } from 'ele-admin-plus/es/ele-app/plus';
|
||||
import type { OperationRecord } from '@/api/system/operation-record/model';
|
||||
import { useMobile } from '@/utils/use-mobile';
|
||||
import {formatBytes} from "@/utils/common";
|
||||
|
||||
defineProps<{
|
||||
/** 修改回显的数据 */
|
||||
data: OperationRecord;
|
||||
}>();
|
||||
|
||||
/** 弹窗是否打开 */
|
||||
const visible = defineModel({ type: Boolean });
|
||||
|
||||
/** 文字省略组件的提示组件的属性 */
|
||||
const ellipsisTooltipProps = reactive<EleTooltipProps>({
|
||||
popperStyle: {
|
||||
width: '580px',
|
||||
maxWidth: '90%',
|
||||
wordBreak: 'break-all'
|
||||
},
|
||||
bodyStyle: {
|
||||
maxWidth: 'calc(100vw - 32px)',
|
||||
maxHeight: '252px',
|
||||
overflowY: 'auto',
|
||||
'--ele-scrollbar-color': '#5e5e5e',
|
||||
'--ele-scrollbar-hover-color': '#707070',
|
||||
'--ele-scrollbar-size': '8px'
|
||||
},
|
||||
offset: 4,
|
||||
placement: 'top'
|
||||
});
|
||||
|
||||
/** 是否是移动端 */
|
||||
const { mobile } = useMobile();
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.detail-table :deep(td.el-descriptions__label.el-descriptions__cell) {
|
||||
width: 88px;
|
||||
text-align: right;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.detail-table :deep(.el-descriptions__content > div) {
|
||||
max-height: 100%;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,65 @@
|
||||
<!-- 搜索表单 -->
|
||||
<template>
|
||||
<ele-card :body-style="{ paddingBottom: '2px' }">
|
||||
<el-form label-width="72px" @keyup.enter="search" @submit.prevent="">
|
||||
<el-row :gutter="8">
|
||||
<el-col :lg="6" :md="18" :sm="17" :xs="24">
|
||||
<el-form-item label="上下文ID">
|
||||
<el-input v-model="form.contextId" maxlength="32" placeholder=""></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="请求时间">
|
||||
<el-date-picker
|
||||
unlink-panels
|
||||
type="datetimerange"
|
||||
v-model="dateRange"
|
||||
range-separator="-"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
class="ele-fluid"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :lg="4" :md="6" :sm="7" :xs="24">
|
||||
<el-form-item label-width="6px">
|
||||
<div style="white-space: nowrap">
|
||||
<el-button type="primary" @click="search">查询</el-button>
|
||||
<el-button @click="reset">重置</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</ele-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { useFormData } from '@/utils/use-form-data';
|
||||
import type { RequestRecordParam } from '@/api/system/request-record/model';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'search', where?: RequestRecordParam): void;
|
||||
}>();
|
||||
|
||||
/** 表单数据 */
|
||||
const [form, resetFields] = useFormData<RequestRecordParam>({
|
||||
contextId: ""
|
||||
});
|
||||
|
||||
/** 日期范围 */
|
||||
const dateRange = ref<[string, string]>(['', '']);
|
||||
|
||||
/** 搜索 */
|
||||
const search = () => {
|
||||
const [createTimeStart, createTimeEnd] = dateRange.value || [];
|
||||
emit('search', { ...form, createTimeStart, createTimeEnd });
|
||||
};
|
||||
|
||||
/** 重置 */
|
||||
const reset = () => {
|
||||
resetFields();
|
||||
dateRange.value = ['', ''];
|
||||
search();
|
||||
};
|
||||
</script>
|
||||
199
z_ele/src/views/system/request-record/index.vue
Normal file
199
z_ele/src/views/system/request-record/index.vue
Normal file
@ -0,0 +1,199 @@
|
||||
<template>
|
||||
<ele-page>
|
||||
<request-record-search @search="reload" />
|
||||
<ele-card :body-style="{ paddingTop: '8px' }">
|
||||
<ele-pro-table
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:columns="columns"
|
||||
:datasource="datasource"
|
||||
:show-overflow-tooltip="true"
|
||||
:highlight-current-row="true"
|
||||
:export-config="{ fileName: '请求日志数据' }"
|
||||
cache-key="systemOperationRecordTable"
|
||||
>
|
||||
|
||||
<template #requestPath="{ row }">
|
||||
<el-tag size="small">{{ row.requestMethod }}</el-tag>
|
||||
<span style="margin-left: 10px;">{{ row.requestPath }}</span>
|
||||
</template>
|
||||
<template #responseCode="{ row }">
|
||||
<el-tag
|
||||
size="small"
|
||||
:type="row.responseCode === 200 ? 'success' : 'danger'"
|
||||
:disable-transitions="true"
|
||||
>
|
||||
{{ row.responseCode }}
|
||||
</el-tag>
|
||||
</template>
|
||||
<template #action="{ row }">
|
||||
<el-link type="primary" underline="never" @click="openDetail(row)">
|
||||
详情
|
||||
</el-link>
|
||||
</template>
|
||||
</ele-pro-table>
|
||||
</ele-card>
|
||||
<!-- 详情弹窗 -->
|
||||
<request-record-detail v-model="showInfo" :data="current" />
|
||||
</ele-page>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import type { EleProTable } from 'ele-admin-plus';
|
||||
import type {
|
||||
DatasourceFunction,
|
||||
Columns
|
||||
} from 'ele-admin-plus/es/ele-pro-table/types';
|
||||
import RequestRecordSearch from './components/request-record-search.vue';
|
||||
import RequestRecordDetail from './components/request-record-detail.vue';
|
||||
import {
|
||||
getRequestRecordInfo,
|
||||
pageRequestRecords
|
||||
} from '@/api/system/request-record';
|
||||
import type {
|
||||
RequestRecord,
|
||||
RequestRecordParam
|
||||
} from '@/api/system/request-record/model';
|
||||
import {formatBytes} from "@/utils/common";
|
||||
|
||||
defineOptions({ name: 'SystemRequestRecord' });
|
||||
|
||||
/** 表格实例 */
|
||||
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||
|
||||
/** 表格列配置 */
|
||||
const columns = ref<Columns>([
|
||||
{
|
||||
type: 'index',
|
||||
columnKey: 'index',
|
||||
width: 50,
|
||||
align: 'center' /* ,
|
||||
fixed: 'left' */
|
||||
},
|
||||
{
|
||||
prop: 'contextId',
|
||||
label: 'ContextId',
|
||||
width: 280
|
||||
},
|
||||
{
|
||||
prop: 'requestPath',
|
||||
label: 'Request',
|
||||
slot: 'requestPath',
|
||||
minWidth: 220
|
||||
},
|
||||
{
|
||||
prop: 'ruleName',
|
||||
label: '规则名称',
|
||||
minWidth: 110
|
||||
},
|
||||
{
|
||||
prop: 'ruleRoute',
|
||||
label: '路由地址',
|
||||
minWidth: 210
|
||||
},
|
||||
{
|
||||
prop: 'responseCode',
|
||||
label: '状态码',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
slot: 'responseCode',
|
||||
filters: [
|
||||
{
|
||||
text: '正常',
|
||||
value: '1'
|
||||
},
|
||||
{
|
||||
text: '异常',
|
||||
value: '2'
|
||||
}
|
||||
],
|
||||
filterMultiple: false,
|
||||
formatter: (row) => (row.responseCode == 200 ? '正常' : '异常')
|
||||
},
|
||||
{
|
||||
prop: 'runningTime',
|
||||
label: '耗时',
|
||||
sortable: 'custom',
|
||||
width: 100,
|
||||
formatter: (row) => `${row.runningTime / 1000}s`,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
prop: 'runningMemoryUsage',
|
||||
label: '内存消耗',
|
||||
width: 110,
|
||||
sortable: 'custom',
|
||||
align: 'center',
|
||||
formatter: (row) => formatBytes(row.runningMemoryUsage),
|
||||
|
||||
},
|
||||
{
|
||||
prop: 'requestTime',
|
||||
label: '请求时间',
|
||||
align: 'center',
|
||||
width: 180
|
||||
},
|
||||
{
|
||||
columnKey: 'action',
|
||||
label: '操作',
|
||||
width: 90,
|
||||
align: 'center',
|
||||
slot: 'action' /* ,
|
||||
fixed: 'right' */,
|
||||
hideInPrint: true,
|
||||
hideInExport: true
|
||||
}
|
||||
]);
|
||||
|
||||
/** 当前选中数据 */
|
||||
const current = ref<RequestRecord>({
|
||||
module: '',
|
||||
description: '',
|
||||
url: '',
|
||||
requestMethod: '',
|
||||
method: '',
|
||||
params: '',
|
||||
result: '',
|
||||
error: '',
|
||||
spendTime: 0,
|
||||
os: '',
|
||||
device: '',
|
||||
browser: '',
|
||||
ip: '',
|
||||
status: 0,
|
||||
createTime: '',
|
||||
nickname: '',
|
||||
username: ''
|
||||
});
|
||||
|
||||
/** 是否显示查看弹窗 */
|
||||
const showInfo = ref(false);
|
||||
|
||||
/** 表格数据源 */
|
||||
const datasource: DatasourceFunction = ({
|
||||
pages,
|
||||
where,
|
||||
orders,
|
||||
filters
|
||||
}) => {
|
||||
return pageRequestRecords({
|
||||
...where,
|
||||
...orders,
|
||||
...filters,
|
||||
...pages
|
||||
});
|
||||
};
|
||||
|
||||
/** 刷新表格 */
|
||||
const reload = (where?: RequestRecordParam) => {
|
||||
tableRef.value?.reload?.({ page: 1, where });
|
||||
};
|
||||
|
||||
/** 详情 */
|
||||
const openDetail = async (row: RequestRecord) => {
|
||||
current.value = await getRequestRecordInfo(row.id);
|
||||
showInfo.value = true;
|
||||
};
|
||||
|
||||
</script>
|
||||
Loading…
Reference in New Issue
Block a user