浏览代码

feat(log): 添加 WAF 日志管理功能

- 新增 WAF 日志列表和详情页面
- 实现日志查询、筛选和分页功能
- 优化日志详情展示,支持 JSON 数据解析
- 调整后端 API 接口以支持日志管理
fusu 1 周之前
父节点
当前提交
90c2381d31

+ 1 - 1
api/v1/admin/wafLog.go

@@ -8,7 +8,7 @@ type SearchWafLogParams struct {
 	ExtraData string `form:"extraData" json:"extraData"`
 	Current  int	`form:"current" json:"current" default:"1"`
 	PageSize int	`form:"pageSize" json:"pageSize" default:"10"`
-	Column   string `form:"column" json:"column" default:"id"`
+	Column   string `form:"column" json:"column" default:"createTime"`
 	Order    string `form:"order" json:"order" default:"desc"`
 }
 

+ 1 - 1
internal/handler/admin/waflog.go

@@ -27,7 +27,7 @@ func NewWafLogHandler(
 
 func (h *WafLogHandler) GetWafLog(ctx *gin.Context) {
 	var req admin.LogId
-	if err := ctx.ShouldBind(req); err != nil {
+	if err := ctx.ShouldBind(&req); err != nil {
 		v1.HandleError(ctx, http.StatusBadRequest, v1.ErrBadRequest, err.Error())
 		return
 	}

+ 1 - 3
internal/repository/admin/gatewayipadmin.go

@@ -74,9 +74,7 @@ func (r *gatewayIpAdminRepository) GetGatewayGroupIpList(ctx context.Context,req
 	}
 
 	if req.Column != "" {
-		if req.Column == "createTime" {
-			query = query.Order("created_at" + " " + req.Order)
-		}
+		query = query.Order(req.Column + " " + req.Order)
 	}
 
 	if err := query.Count(&total).Error; err != nil {

+ 3 - 5
internal/repository/admin/waflog.go

@@ -29,14 +29,14 @@ type wafLogRepository struct {
 
 func (r *wafLogRepository) GetWafLog(ctx context.Context, id int64) (*model.Log, error) {
 	var res model.Log
-	return &res, r.DB(ctx).Where("id = ?", id).First(&res).Error
+	return &res, r.DBWithName(ctx,"admin").Where("id = ?", id).First(&res).Error
 }
 
 func (r *wafLogRepository) GetWafLogList(ctx context.Context, req admin.SearchWafLogParams) (*v1.PaginatedResponse[model.Log], error) {
 	var res []model.Log
 	var total int64
 
-	query := r.Db.WithContext(ctx).Model(&model.Gatewayip{})
+	query := r.DBWithName(ctx,"admin").Model(&model.Log{})
 	if  req.RequestIp != "" {
 		trimmedName := strings.TrimSpace(req.RequestIp)
 		// 使用 LIKE 进行模糊匹配
@@ -68,9 +68,7 @@ func (r *wafLogRepository) GetWafLogList(ctx context.Context, req admin.SearchWa
 	}
 
 	if req.Column != "" {
-		if req.Column == "createTime" {
-			query = query.Order("created_at" + " " + req.Order)
-		}
+		query = query.Order(req.Column + " " + req.Order)
 	}
 
 	if err := query.Count(&total).Error; err != nil {

+ 17 - 0
web/src/api/log/log.js

@@ -0,0 +1,17 @@
+import request from '~/utils/request.js'
+
+export function getWafLogList(params) {
+  return request({
+    url: '/v1/wafLog/getList',
+    method: 'get',
+    params
+  })
+}
+
+export function getWafLogInfo(id) {
+  return request({
+    url: '/v1/wafLog/get',
+    method: 'get',
+    params: { id }
+  })
+}

+ 144 - 0
web/src/pages/log/components/log-detail-modal.vue

@@ -0,0 +1,144 @@
+<template>
+  <a-drawer
+    v-model:visible="visible"
+    title="WAF日志详情"
+    placement="right"
+    :width="'60%'"
+    @close="handleCancel"
+  >
+    <a-spin :spinning="loading">
+      <a-descriptions bordered :column="{ xxl: 2, xl: 2, lg: 2, md: 2, sm: 1, xs: 1 }">
+        <a-descriptions-item label="ID">{{ info.Id }}</a-descriptions-item>
+        <a-descriptions-item label="Trace ID">{{ info.TraceId }}</a-descriptions-item>
+        <a-descriptions-item label="用户ID">{{ info.Uid }}</a-descriptions-item>
+        <a-descriptions-item label="请求IP">{{ info.RequestIp }}</a-descriptions-item>
+        <a-descriptions-item label="状态码">{{ info.StatusCode }}</a-descriptions-item>
+        <a-descriptions-item label="API">{{ info.Api }}</a-descriptions-item>
+        <a-descriptions-item label="消息">{{ info.Message }}</a-descriptions-item>
+        <a-descriptions-item label="创建时间">{{ info.CreatedAt }}</a-descriptions-item>
+        <a-descriptions-item label="更新时间">{{ info.UpdatedAt }}</a-descriptions-item>
+      </a-descriptions>
+      
+      <a-divider style="margin-top: 24px; margin-bottom: 24px" />
+      
+      <h3>详细信息</h3>
+      
+      <div class="detail-section">
+        <h4>User Agent</h4>
+        <div class="code-block">
+          <pre>{{ info.UserAgent || '无数据' }}</pre>
+        </div>
+      </div>
+      
+      <div class="detail-section">
+        <h4>原始数据</h4>
+        <div class="code-block">
+          <pre>{{ info.ExtraData || '无数据' }}</pre>
+        </div>
+      </div>
+    </a-spin>
+  </a-drawer>
+</template>
+
+<script setup>
+import { ref, defineExpose } from 'vue';
+import { getWafLogInfo } from '~/api/log/log.js';
+
+const visible = ref(false);
+const loading = ref(false);
+const info = ref({});
+
+const fetchInfo = async (id) => {
+  try {
+    loading.value = true;
+    console.log('获取日志ID:', id);
+    
+    const response = await getWafLogInfo(id);
+    console.log('日志详情响应:', response);
+    
+    // 处理不同的返回格式
+    if (response && response.code === 0 && response.data) {
+      info.value = response.data;
+    } else if (response && response.data) {
+      info.value = response.data;
+    } else {
+      info.value = response || {};
+    }
+    
+    // 处理特殊字段
+    if (info.value.ExtraData && typeof info.value.ExtraData === 'string') {
+      try {
+        // 尝试解析JSON字符串为对象,便于美化显示
+        const parsedData = JSON.parse(info.value.ExtraData);
+        info.value.ExtraData = JSON.stringify(parsedData, null, 2);
+      } catch (e) {
+        // 不是JSON格式,保持原样
+      }
+    }
+    
+  } catch (error) {
+    console.error('获取日志详情出错:', error);
+  } finally {
+    loading.value = false;
+  }
+};
+
+const open = async (id) => {
+  visible.value = true;
+  info.value = {}; // 清空上一次的数据
+  await fetchInfo(id);
+};
+
+const handleCancel = () => {
+  visible.value = false;
+};
+
+defineExpose({
+  open,
+});
+</script>
+
+<style scoped>
+.detail-section {
+  margin-bottom: 24px;
+}
+
+.detail-section h4 {
+  font-size: 16px;
+  font-weight: 500;
+  margin-bottom: 12px;
+  color: rgba(0, 0, 0, 0.85);
+}
+
+.code-block {
+  background-color: #f5f5f5;
+  border-radius: 4px;
+  padding: 0;
+}
+
+pre {
+  padding: 16px;
+  white-space: pre-wrap;       
+  word-wrap: break-word;
+  max-height: 300px;
+  overflow-y: auto;
+  font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
+  font-size: 14px;
+  line-height: 1.5;
+  margin: 0;
+}
+
+:deep(.ant-descriptions-item-label),
+:deep(.ant-descriptions-item-content) {
+  font-size: 14px;
+}
+
+:deep(.ant-descriptions-item-label) {
+  font-weight: bold;
+  min-width: 120px;
+}
+
+:deep(.ant-drawer-body) {
+  padding: 24px;
+}
+</style>

+ 112 - 0
web/src/pages/log/waf-log-info.vue

@@ -0,0 +1,112 @@
+<template>
+  <page-header-wrapper>
+    <a-card :bordered="false" :loading="loading">
+      <a-descriptions title="WAF日志详情" :column="{ xs: 1, sm: 2, md: 3 }">
+        <a-descriptions-item label="ID">{{ info.Id }}</a-descriptions-item>
+        <a-descriptions-item label="Trace ID">{{ info.TraceId }}</a-descriptions-item>
+        <a-descriptions-item label="用户ID">{{ info.Uid }}</a-descriptions-item>
+        <a-descriptions-item label="请求IP">{{ info.RequestIp }}</a-descriptions-item>
+        <a-descriptions-item label="状态码">{{ info.StatusCode }}</a-descriptions-item>
+        <a-descriptions-item label="API">{{ info.Api }}</a-descriptions-item>
+        <a-descriptions-item label="消息">{{ info.Message }}</a-descriptions-item>
+        <a-descriptions-item label="创建时间">{{ info.CreatedAt }}</a-descriptions-item>
+        <a-descriptions-item label="更新时间">{{ info.UpdatedAt }}</a-descriptions-item>
+      </a-descriptions>
+      <a-divider style="margin-bottom: 32px"/>
+      
+      <a-descriptions title="User Agent" :column="1">
+        <a-descriptions-item>
+          <pre>{{ info.UserAgent || '无数据' }}</pre>
+        </a-descriptions-item>
+      </a-descriptions>
+      <a-divider style="margin-bottom: 32px"/>
+      
+      <a-descriptions title="原始数据" :column="1">
+        <a-descriptions-item>
+          <pre>{{ info.ExtraData || '无数据' }}</pre>
+        </a-descriptions-item>
+      </a-descriptions>
+      
+      <a-divider style="margin-bottom: 32px"/>
+      <a-button type="primary" @click="handleBack">返回</a-button>
+    </a-card>
+  </page-header-wrapper>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue';
+import { useRouter, useRoute } from 'vue-router';
+import { getWafLogInfo } from '~/api/log/log.js';
+
+const router = useRouter();
+const route = useRoute();
+const info = ref({});
+const loading = ref(true);
+
+const fetchInfo = async () => {
+  try {
+    loading.value = true;
+    const id = route.params.id;
+    console.log('获取日志ID:', id);
+    
+    const response = await getWafLogInfo(id);
+    console.log('日志详情响应:', response);
+    
+    // 处理不同的返回格式
+    if (response && response.code === 0 && response.data) {
+      info.value = response.data;
+    } else if (response && response.data) {
+      info.value = response.data;
+    } else {
+      info.value = response || {};
+    }
+    
+    // 可以处理特殊字段
+    if (info.value.ExtraData && typeof info.value.ExtraData === 'string') {
+      try {
+        // 尝试解析JSON字符串为对象,便于美化显示
+        const parsedData = JSON.parse(info.value.ExtraData);
+        info.value.ExtraData = JSON.stringify(parsedData, null, 2);
+      } catch (e) {
+        // 不是JSON格式,保持原样
+      }
+    }
+    
+  } catch (error) {
+    console.error('获取日志详情出错:', error);
+  } finally {
+    loading.value = false;
+  }
+};
+
+const handleBack = () => {
+  router.push('/log/waf-log');
+};
+
+onMounted(() => {
+  fetchInfo();
+});
+</script>
+<style scoped>
+pre {
+  background-color: #f5f5f5;
+  padding: 15px;
+  border-radius: 5px;
+  white-space: pre-wrap;       
+  word-wrap: break-word;
+  max-height: 400px;
+  overflow-y: auto;
+  font-family: 'Courier New', Courier, monospace;
+  font-size: 14px;
+  line-height: 1.5;
+}
+
+:deep(.ant-descriptions-item-label) {
+  font-weight: bold;
+  min-width: 120px;
+}
+
+:deep(.ant-descriptions-row > th, .ant-descriptions-row > td) {
+  padding-bottom: 16px;
+}
+</style>

+ 214 - 0
web/src/pages/log/waf-log.vue

@@ -0,0 +1,214 @@
+<template>
+  <div>
+    <a-card>
+      <div class="table-page-search-wrapper">
+        <a-form layout="inline" @submit.prevent="handleSearch">
+          <a-row :gutter="48">
+            <a-col :md="6" :sm="24">
+              <a-form-item label="请求IP">
+                <a-input v-model="queryParam.requestIp" placeholder="请输入请求IP" />
+              </a-form-item>
+            </a-col>
+            <a-col :md="6" :sm="24">
+              <a-form-item label="用户ID">
+                <a-input v-model="queryParam.uid" placeholder="请输入用户ID" />
+              </a-form-item>
+            </a-col>
+            <a-col :md="6" :sm="24">
+              <a-form-item label="API">
+                <a-input v-model="queryParam.api" placeholder="请输入API路径" />
+              </a-form-item>
+            </a-col>
+            <a-col :md="6" :sm="24">
+              <a-form-item label="消息">
+                <a-input v-model="queryParam.message" placeholder="请输入日志信息" />
+              </a-form-item>
+            </a-col>
+            <a-col :md="24" :sm="24">
+              <span class="table-page-search-submitButtons">
+                <a-button type="primary" @click="handleSearch">查询</a-button>
+                <a-button style="margin-left: 8px" @click="resetSearch">重置</a-button>
+              </span>
+            </a-col>
+          </a-row>
+        </a-form>
+      </div>
+
+      <a-table
+        :columns="columns"
+        :row-key="record => record.Id"
+        :data-source="dataSource"
+        :loading="loading"
+        :pagination="pagination"
+        @change="handleTableChange"
+        :scroll="{ x: 'max-content' }"
+        bordered
+        size="middle"
+      >
+        <template #bodyCell="{ column, record }">
+          <template v-if="column.dataIndex === 'action'">
+            <a-button type="link" @click="goToInfo(record.Id)">详情</a-button>
+          </template>
+        </template>
+      </a-table>
+    </a-card>
+    
+    <!-- 日志详情模态框 -->
+    <log-detail-modal ref="detailModalRef" />
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue';
+import { useRouter } from 'vue-router';
+import { getWafLogList } from '~/api/log/log.js';
+import LogDetailModal from './components/log-detail-modal.vue';
+
+const router = useRouter();
+const loading = ref(false);
+const dataSource = ref([]);
+const pagination = ref({
+  current: 1,
+  pageSize: 10,
+  total: 0,
+  showTotal: (total) => `共 ${total} 条`,
+});
+
+const queryParam = ref({
+  requestIp: '',
+  uid: '',
+  api: '',
+  message: '',
+  extraData: ''
+});
+
+const columns = [
+  { title: 'ID', dataIndex: 'Id', fixed: 'left', width: 80 },
+  { title: 'Trace ID', dataIndex: 'TraceId', width: 160 },
+  { title: '用户ID', dataIndex: 'Uid', width: 100 },
+  { title: '请求IP', dataIndex: 'RequestIp', width: 140 },
+  { title: 'API', dataIndex: 'Api', width: 180, ellipsis: true },
+  { title: '状态码', dataIndex: 'StatusCode', width: 100 },
+  { title: 'User Agent', dataIndex: 'UserAgent', width: 250, ellipsis: true },
+  { title: '消息', dataIndex: 'Message', width: 250, ellipsis: true },
+  { title: '附加数据', dataIndex: 'ExtraData', width: 200, ellipsis: true },
+  { title: '创建时间', dataIndex: 'CreatedAt', width: 180 },
+  { title: '更新时间', dataIndex: 'UpdatedAt', width: 180 },
+  { title: '操作', dataIndex: 'action', fixed: 'right', width: 100 },
+];
+
+const fetchData = () => {
+  loading.value = true;
+  const params = {
+    requestIp: queryParam.value.requestIp || '',
+    uid: queryParam.value.uid ? parseInt(queryParam.value.uid) : 0,
+    api: queryParam.value.api || '',
+    message: queryParam.value.message || '',
+    extraData: queryParam.value.extraData || '',
+    current: pagination.value.current,
+    pageSize: pagination.value.pageSize,
+    column: 'id',
+    order: 'desc'
+  };
+  
+  console.log('发送查询参数:', params);
+  
+  getWafLogList(params).then(response => {
+    console.log('API响应数据:', response);
+    
+    // 根据实际返回格式处理数据
+    // 格式: {code: 0, message: "ok", data: {records: [...], page: 1, pageSize: 10, total: 1963, totalPages: 197}}
+    if (response && response.code === 0 && response.data) {
+      const apiData = response.data;
+      if (apiData.records && Array.isArray(apiData.records)) {
+        dataSource.value = apiData.records;
+        pagination.value.total = apiData.total || 0;
+        pagination.value.current = apiData.page || 1;
+        pagination.value.pageSize = apiData.pageSize || 10;
+        console.log('成功加载数据,共', apiData.total, '条记录');
+      } else {
+        dataSource.value = [];
+        pagination.value.total = 0;
+        console.warn('响应中无数据记录');
+      }
+    } else {
+      // 未识别的格式或错误响应
+      dataSource.value = [];
+      pagination.value.total = 0;
+      console.error('无效的响应格式或响应错误', response);
+    }
+    loading.value = false;
+  }).catch((error) => {
+    console.error('请求失败:', error);
+    dataSource.value = [];
+    loading.value = false;
+  });
+};
+
+const handleTableChange = (pager) => {
+  pagination.value.current = pager.current;
+  pagination.value.pageSize = pager.pageSize;
+  fetchData();
+};
+
+const handleSearch = () => {
+  pagination.value.current = 1;
+  fetchData();
+};
+
+const resetSearch = () => {
+  queryParam.value.requestIp = '';
+  queryParam.value.uid = '';
+  queryParam.value.api = '';
+  queryParam.value.message = '';
+  queryParam.value.extraData = '';
+  handleSearch();
+};
+
+const detailModalRef = ref(null);
+
+const goToInfo = (id) => {
+  // 打开模态框而不是跳转页面
+  detailModalRef.value?.open(id);
+};
+
+onMounted(() => {
+  fetchData();
+});
+</script>
+
+<style scoped>
+.table-page-search-wrapper .ant-form-inline .ant-form-item {
+  margin-bottom: 24px;
+}
+
+/* 表格样式优化 */
+:deep(.ant-table-body) {
+  overflow-x: auto !important;
+}
+
+:deep(.ant-table-fixed-header .ant-table-scroll .ant-table-header) {
+  margin-bottom: 0 !important;
+  padding-bottom: 0 !important;
+  overflow: hidden !important;
+}
+
+:deep(.ant-table-body::-webkit-scrollbar) {
+  width: 8px;
+  height: 8px;
+}
+
+:deep(.ant-table-body::-webkit-scrollbar-track) {
+  background: #f1f1f1;
+  border-radius: 4px;
+}
+
+:deep(.ant-table-body::-webkit-scrollbar-thumb) {
+  background: #c1c1c1;
+  border-radius: 4px;
+}
+
+:deep(.ant-table-body::-webkit-scrollbar-thumb:hover) {
+  background: #a8a8a8;
+}
+</style>

+ 30 - 0
web/src/router/dynamic-routes.js

@@ -77,6 +77,36 @@ export default [
       },
     ],
   },
+  {
+    path: '/log',
+    redirect: '/log/waf-log',
+    name: 'Log',
+    meta: {
+      title: '日志管理',
+      icon: 'ProfileOutlined',
+    },
+    component: basicRouteMap.RouteView,
+    children: [
+      {
+        path: '/log/waf-log',
+        name: 'LogWafLog',
+        component: () => import('~/pages/log/waf-log.vue'),
+        meta: {
+          title: 'WAF日志',
+        },
+      },
+      {
+        path: '/log/waf-log-info/:id',
+        name: 'LogWafLogInfo',
+        component: () => import('~/pages/log/waf-log-info.vue'),
+        meta: {
+          title: 'WAF日志详情',
+          hidden: true, // 不在菜单中显示
+          activeMenu: '/log/waf-log' // 高亮父菜单
+        },
+      },
+    ],
+  },
   {
     path: '/link',
     redirect: '/link/iframe',