Files
laravel_swoole/docs/README_LOG.md
2026-02-08 22:38:13 +08:00

14 KiB
Raw Blame History

系统操作日志模块文档

概述

系统操作日志模块用于记录后台管理系统的所有操作请求包括用户操作、API 调用、错误信息等,方便管理员进行系统监控、审计和问题排查。

技术特性

  • 自动记录: 通过中间件自动记录所有请求,无需手动调用
  • 详细信息: 记录用户信息、请求参数、响应结果、执行时间等
  • 敏感信息保护: 自动过滤密码等敏感信息
  • 性能优化: 不影响业务响应速度
  • 多维度查询: 支持按用户、模块、操作、状态、时间等多维度筛选
  • 数据导出: 支持导出日志数据为 Excel 文件
  • 批量操作: 支持批量删除和定期清理

数据库表结构

system_logs 表

字段名 类型 说明
id bigint 主键 ID
user_id bigint 用户 ID
username varchar(100) 用户名
module varchar(50) 模块名称
action varchar(100) 操作名称
method varchar(10) 请求方法 (GET/POST/PUT/DELETE)
url text 请求 URL
ip varchar(45) 客户端 IP 地址
user_agent text 用户代理
params json 请求参数
result text 响应结果(仅错误时记录)
status_code int HTTP 状态码
status varchar(20) 状态 (success/error)
error_message text 错误信息
execution_time int 执行时间(毫秒)
created_at timestamp 创建时间
updated_at timestamp 更新时间

核心组件

1. 中间件 (Middleware)

LogRequestMiddleware

位置: app/Http/Middleware/LogRequestMiddleware.php

功能:

  • 自动拦截所有经过的请求
  • 记录请求和响应信息
  • 计算请求执行时间
  • 提取用户信息和操作详情
  • 过滤敏感参数
  • 处理异常情况

使用方式:

// 在路由中应用
Route::middleware(['log.request'])->group(function () {
    // 需要记录日志的路由
});

2. 服务层 (Service)

LogService

位置: app/Services/System/LogService.php

主要方法:

  • create(array $data): 创建日志记录
  • getList(array $params): 获取日志列表(分页)
  • getListQuery(array $params): 获取日志查询构建器
  • getById(int $id): 根据 ID 获取日志详情
  • delete(int $id): 删除单条日志
  • batchDelete(array $ids): 批量删除日志
  • clearLogs(string $days): 清理指定天数前的日志
  • getStatistics(array $params): 获取日志统计信息

3. 控制器 (Controller)

Log Controller

位置: app/Http/Controllers/System/Admin/Log.php

接口列表:

  • GET /admin/logs: 获取日志列表
  • GET /admin/logs/{id}: 获取日志详情
  • GET /admin/logs/statistics: 获取日志统计
  • POST /admin/logs/export: 导出日志
  • DELETE /admin/logs/{id}: 删除单条日志
  • POST /admin/logs/batch-delete: 批量删除日志
  • POST /admin/logs/clear: 清理历史日志

4. 请求验证 (Request Validation)

LogRequest

位置: app/Http/Requests/LogRequest.php

验证规则:

  • user_id: 用户 ID可选
  • username: 用户名(模糊查询,可选)
  • module: 模块名称(可选)
  • action: 操作名称(可选)
  • status: 状态success/error可选
  • start_date: 开始日期(可选)
  • end_date: 结束日期(可选)
  • ip: IP 地址(可选)
  • page: 页码(默认 1
  • page_size: 每页数量(默认 20最大 100

API 接口文档

1. 获取日志列表

接口: GET /admin/logs

请求参数:

{
  "user_id": 1,
  "username": "admin",
  "module": "users",
  "action": "创建 users",
  "status": "success",
  "start_date": "2024-01-01",
  "end_date": "2024-12-31",
  "ip": "192.168.1.1",
  "page": 1,
  "page_size": 20
}

响应示例:

{
  "code": 200,
  "message": "success",
  "data": {
    "list": [
      {
        "id": 1,
        "user_id": 1,
        "username": "admin",
        "module": "users",
        "action": "创建 users",
        "method": "POST",
        "url": "http://example.com/admin/users",
        "ip": "192.168.1.1",
        "user_agent": "Mozilla/5.0...",
        "params": {
          "name": "test",
          "email": "test@example.com"
        },
        "result": null,
        "status_code": 200,
        "status": "success",
        "error_message": null,
        "execution_time": 125,
        "created_at": "2024-01-01 12:00:00",
        "user": {
          "id": 1,
          "name": "管理员",
          "username": "admin"
        }
      }
    ],
    "total": 100,
    "page": 1,
    "page_size": 20
  }
}

2. 获取日志详情

接口: GET /admin/logs/{id}

响应示例:

{
  "code": 200,
  "message": "success",
  "data": {
    "id": 1,
    "user_id": 1,
    "username": "admin",
    "module": "users",
    "action": "创建 users",
    "method": "POST",
    "url": "http://example.com/admin/users",
    "ip": "192.168.1.1",
    "user_agent": "Mozilla/5.0...",
    "params": {
      "name": "test",
      "email": "test@example.com"
    },
    "result": null,
    "status_code": 200,
    "status": "success",
    "error_message": null,
    "execution_time": 125,
    "created_at": "2024-01-01 12:00:00",
    "user": {
      "id": 1,
      "name": "管理员",
      "username": "admin",
      "email": "admin@example.com",
      "created_at": "2024-01-01 10:00:00"
    }
  }
}

3. 获取日志统计

接口: GET /admin/logs/statistics

请求参数:

{
  "start_date": "2024-01-01",
  "end_date": "2024-12-31"
}

响应示例:

{
  "code": 200,
  "message": "success",
  "data": {
    "total": 1000,
    "success": 950,
    "error": 50
  }
}

4. 导出日志

接口: POST /admin/logs/export

请求参数: 与获取日志列表相同的查询参数

响应: Excel 文件下载

文件名格式: 系统操作日志_YYYYMMDDHHmmss.xlsx

包含字段:

  • ID
  • 用户名
  • 模块
  • 操作
  • 请求方法
  • URL
  • IP 地址
  • 状态码
  • 状态
  • 错误信息
  • 执行时间(ms)
  • 创建时间

5. 删除单条日志

接口: DELETE /admin/logs/{id}

响应示例:

{
  "code": 200,
  "message": "删除成功",
  "data": null
}

6. 批量删除日志

接口: POST /admin/logs/batch-delete

请求参数:

{
  "ids": [1, 2, 3, 4, 5]
}

响应示例:

{
  "code": 200,
  "message": "批量删除成功",
  "data": null
}

7. 清理历史日志

接口: POST /admin/logs/clear

请求参数:

{
  "days": 30
}

说明: 清理指定天数前的所有日志记录,默认清理 30 天前的数据。

响应示例:

{
  "code": 200,
  "message": "清理成功",
  "data": null
}

日志记录规则

1. 自动记录的请求

所有经过 log.request 中间件的请求都会被自动记录,包括:

  • 用户管理操作
  • 角色管理操作
  • 权限管理操作
  • 部门管理操作
  • 系统配置操作
  • 其他所有后台管理操作

2. 不记录的请求

  • 登录接口 (POST /admin/auth/login)
  • 健康检查接口 (GET /up)
  • 其他明确排除的路由

3. 敏感信息过滤

以下字段会被自动过滤,记录为 ******:

  • password
  • password_confirmation
  • token
  • secret
  • key

4. 错误日志处理

  • 成功请求 (HTTP 状态码 < 400): status = success
  • 失败请求 (HTTP 状态码 >= 400): status = error
  • 错误时记录响应内容和错误消息
  • 同时写入 Laravel 日志文件 (storage/logs/laravel.log)

模块和操作名称解析

模块名称

从 URL 路径中解析,例如:

  • /admin/users → 模块: users
  • /admin/roles → 模块: roles
  • /admin/configs → 模块: configs

操作名称

根据 HTTP 方法和资源名称生成:

  • GET /admin/users → 操作: 查询 users
  • POST /admin/users → 操作: 创建 users
  • PUT /admin/users/1 → 操作: 更新 users
  • DELETE /admin/users/1 → 操作: 删除 users

性能优化建议

1. 定期清理日志

建议使用 Laravel 任务调度器定期清理历史日志:

// app/Console/Kernel.php

protected function schedule(Schedule $schedule)
{
    // 每天凌晨 2 点清理 90 天前的日志
    $schedule->call(function () {
        app(LogService::class)->clearLogs(90);
    })->dailyAt('02:00');
}

2. 数据库索引

确保以下字段有索引:

  • user_id
  • username
  • module
  • status
  • created_at

3. 分页查询

列表查询必须使用分页,避免一次加载过多数据。

4. 异步记录

日志记录操作应放在请求处理后,不影响响应速度。

前端集成示例

Vue3 + Ant Design Vue

<template>
  <a-card title="操作日志">
    <!-- 搜索表单 -->
    <a-form layout="inline" :model="searchParams">
      <a-form-item label="用户名">
        <a-input v-model:value="searchParams.username" placeholder="请输入用户名" />
      </a-form-item>
      <a-form-item label="模块">
        <a-input v-model:value="searchParams.module" placeholder="请输入模块名" />
      </a-form-item>
      <a-form-item label="状态">
        <a-select v-model:value="searchParams.status" placeholder="请选择状态">
          <a-select-option value="success">成功</a-select-option>
          <a-select-option value="error">失败</a-select-option>
        </a-select>
      </a-form-item>
      <a-form-item>
        <a-button type="primary" @click="handleSearch">查询</a-button>
        <a-button @click="handleReset">重置</a-button>
        <a-button @click="handleExport">导出</a-button>
      </a-form-item>
    </a-form>

    <!-- 数据表格 -->
    <a-table
      :columns="columns"
      :data-source="logs"
      :loading="loading"
      :pagination="pagination"
      @change="handleTableChange"
    >
      <template #status="{ record }">
        <a-tag :color="record.status === 'success' ? 'green' : 'red'">
          {{ record.status === 'success' ? '成功' : '失败' }}
        </a-tag>
      </template>
      <template #action="{ record }">
        <a-button type="link" @click="handleView(record)">查看</a-button>
        <a-button type="link" danger @click="handleDelete(record.id)">删除</a-button>
      </template>
    </a-table>
  </a-card>
</template>

<script setup>
import { ref, reactive, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import request from '@/utils/request'

const logs = ref([])
const loading = ref(false)

const searchParams = reactive({
  username: '',
  module: '',
  status: null,
  page: 1,
  page_size: 20
})

const pagination = reactive({
  total: 0,
  current: 1,
  pageSize: 20
})

const columns = [
  { title: 'ID', dataIndex: 'id', width: 80 },
  { title: '用户名', dataIndex: 'username', width: 120 },
  { title: '模块', dataIndex: 'module', width: 100 },
  { title: '操作', dataIndex: 'action', width: 150 },
  { title: '请求方法', dataIndex: 'method', width: 100 },
  { title: 'IP 地址', dataIndex: 'ip', width: 150 },
  { title: '状态', dataIndex: 'status', slots: { customRender: 'status' }, width: 100 },
  { title: '执行时间', dataIndex: 'execution_time', width: 100 },
  { title: '创建时间', dataIndex: 'created_at', width: 180 },
  { title: '操作', slots: { customRender: 'action' }, width: 150, fixed: 'right' }
]

// 获取日志列表
const fetchLogs = async () => {
  loading.value = true
  try {
    const res = await request.get('/admin/logs', { params: searchParams })
    logs.value = res.data.list
    pagination.total = res.data.total
    pagination.current = res.data.page
    pagination.pageSize = res.data.page_size
  } catch (error) {
    message.error('获取日志失败')
  } finally {
    loading.value = false
  }
}

// 查询
const handleSearch = () => {
  searchParams.page = 1
  fetchLogs()
}

// 重置
const handleReset = () => {
  searchParams.username = ''
  searchParams.module = ''
  searchParams.status = null
  searchParams.page = 1
  fetchLogs()
}

// 导出
const handleExport = async () => {
  try {
    const res = await request.post('/admin/logs/export', searchParams, {
      responseType: 'blob'
    })
    const url = window.URL.createObjectURL(new Blob([res]))
    const link = document.createElement('a')
    link.href = url
    link.setAttribute('download', `操作日志_${new Date().getTime()}.xlsx`)
    document.body.appendChild(link)
    link.click()
    document.body.removeChild(link)
    message.success('导出成功')
  } catch (error) {
    message.error('导出失败')
  }
}

// 查看详情
const handleView = (record) => {
  // 打开详情对话框
  console.log('查看日志', record)
}

// 删除
const handleDelete = async (id) => {
  try {
    await request.delete(`/admin/logs/${id}`)
    message.success('删除成功')
    fetchLogs()
  } catch (error) {
    message.error('删除失败')
  }
}

// 表格分页变化
const handleTableChange = (pag) => {
  searchParams.page = pag.current
  searchParams.page_size = pag.pageSize
  fetchLogs()
}

onMounted(() => {
  fetchLogs()
})
</script>

注意事项

  1. 权限控制: 日志管理接口需要相应的权限才能访问
  2. 数据安全: 敏感信息已自动过滤,但仍需注意日志数据的安全存储
  3. 性能影响: 虽然日志记录不影响响应速度,但大量日志会增加数据库负载
  4. 定期备份: 重要日志数据建议定期备份
  5. 日志分析: 可结合 BI 工具对日志数据进行深度分析

常见问题

Q1: 为什么某些请求没有被记录?

A: 检查路由是否应用了 log.request 中间件,或者在中间件中是否被排除了。

Q2: 日志数据过多怎么办?

A: 使用 clearLogs 方法定期清理历史日志,或设置任务调度器自动清理。

Q3: 如何自定义日志记录规则?

A: 修改 LogRequestMiddleware 中的 parseModuleparseAction 方法。

Q4: 日志记录会影响性能吗?

A: 日志记录在请求处理后执行,不影响响应速度。但大量日志会增加数据库写入压力。

Q5: 如何查看完整的请求参数?

A: 在日志详情接口中,params 字段包含了完整的请求参数(敏感信息已过滤)。

更新日志

v1.0.0 (2024-01-01)

  • 初始版本
  • 实现基础日志记录功能
  • 支持多维度查询和筛选
  • 支持数据导出
  • 支持批量删除和清理