首页 > 其他分享 >springboot+vue前后端分离项目-项目搭建17-集成AOP系统日志

springboot+vue前后端分离项目-项目搭建17-集成AOP系统日志

时间:2024-08-04 12:17:23浏览次数:15  
标签:type springboot 17 ip demo import com example 系统日志

后端

1. 新增logs表和实体类,新增com/example/demo/mapper/LogsMapper.java,新增com/example/demo/controller/LogsController.java

package com.example.demo.controller;

import cn.hutool.core.util.StrUtil;
import cn.hutool.poi.excel.ExcelReader;
import cn.hutool.poi.excel.ExcelUtil;
import cn.hutool.poi.excel.ExcelWriter;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.example.demo.common.AuthAccess;
import com.example.demo.common.Result;
import com.example.demo.entity.Logs;
import com.example.demo.mapper.LogsMapper;
import jakarta.annotation.Resource;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.bind.annotation.*;

import java.io.IOException;
import java.net.URLEncoder;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

@RestController
@RequestMapping("/logs")
public class LogsController {

    //正常Mapper是在Service里引用,Controllerl里引用Service,本案例是为了方便调用,非正规操作
    @Resource
    LogsMapper logsMapper;

    @PutMapping
    public Result<?> update(@RequestBody Logs logs){
        logsMapper.updateById(logs);
        return Result.success();
    }

    @DeleteMapping("/{id}")
    public Result<?> delete(@PathVariable Long id){
        logsMapper.deleteById(id);
        return Result.success();
    }

    @PostMapping("/deleteBatch") //  批量删除
    public Result<?> deleteBatch(@RequestBody List<Integer> ids){
        logsMapper.deleteBatchIds(ids);
        return Result.success();
    }

    @GetMapping
    public Result<?> findPage(@RequestParam(defaultValue = "1") Integer pageNum,
                              @RequestParam(defaultValue = "10") Integer pageSize,
                              @RequestParam(defaultValue = "") String search,
                              @RequestParam(defaultValue = "") String type){
        LambdaQueryWrapper<Logs> wrapper = Wrappers.<Logs>lambdaQuery();
        if(StrUtil.isNotBlank(search)){
            wrapper.like(Logs::getOperation, search);
        }
        if(StrUtil.isNotBlank(type)){
            wrapper.like(Logs::getType, type);
        }
        Page<Logs> logsPage = logsMapper.selectPage(new Page<>(pageNum, pageSize), wrapper);
        return Result.success(logsPage);
    }

    //批量导出
    @AuthAccess
    @GetMapping("/export")
    public void exportData(@RequestParam(required = false) String search,
                           @RequestParam(required = false) String ids,  // 1,2,3,4
                           HttpServletResponse response) throws IOException {
        List<Logs> list;
        QueryWrapper<Logs> queryWrapper = new QueryWrapper<>();
        if (StrUtil.isNotBlank(ids)) {  //  第二种按选择的行导出
            List<Integer> idsArr = Arrays.stream(ids.split(",")).map(Integer::valueOf).collect(Collectors.toList());
            queryWrapper.in("id", idsArr);
        } else {  // 第一种全部导出或条件导出
            queryWrapper.like(StrUtil.isNotBlank(search),"operation", search);
        }
        list = logsMapper.selectList(queryWrapper);
        ExcelWriter writer = ExcelUtil.getWriter(true);
        writer.write(list, true);
        //设置响应文件类型,设置响应文件名称
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8");
        response.setHeader("Content-Disposition","attachment;filename=" + URLEncoder.encode("操作日志","utf-8") + ".xlsx");
        //导出数据写到响应的输出流里,关闭流
        ServletOutputStream outputStream = response.getOutputStream();
        writer.flush(outputStream, true);
        writer.close();
        outputStream.flush();
        outputStream.close();
    }
}

 

2. 新增 com/example/demo/common/HoneyLogs.java 注解,controller的方法上增加这个注解,让它被aop识别,进行日志记录

package com.example.demo.common;

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface HoneyLogs {
    // 操作的模块
    String operation();
    // 操作类型
    LogType type();
}

3. 新增 com/example/demo/common/LogType.java 枚举类 ,系统日志的操作类型

package com.example.demo.common;

/**
 * 系统日志的操作类型枚举
 */
public enum LogType {

    ADD("新增"),UPDATE("修改"),DELETE("删除"),BATCH_DELETE("批量删除"),
    LOGIN("登录"),REGISTER("注册");

    private String value;

    public String getValue() {
        return value;
    }

    LogType(String value) {
        this.value = value;
    }
}

4. 新增 com/example/demo/utils/IpUtils.java ,获取用户的ip地址

package com.example.demo.utils;

import jakarta.servlet.http.HttpServletRequest;

public class IpUtils {

    /**
     * 获取IP地址
     * @param request
     * @return
     */
    public static String getIpAddr(HttpServletRequest request) {
        String ip = request.getHeader("x-forwarded-for");
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("X-Forwarded-For");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("X-Real-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip;
    }
}

5. 新增 com/example/demo/Service/aop/LogsAspect.java ,切面拦截所有添加了 HoneyLogs 注解的方法,记录日志到数据库日志表

package com.example.demo.Service.aop;

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.ArrayUtil;
import com.example.demo.common.HoneyLogs;
import com.example.demo.entity.Logs;
import com.example.demo.entity.User;
import com.example.demo.mapper.LogsMapper;
import com.example.demo.utils.IpUtils;
import com.example.demo.utils.TokenUtils;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

@Component
@Aspect
@Slf4j
public class LogsAspect {

    @Resource
    LogsMapper logsMapper;

    @AfterReturning(pointcut = "@annotation(honeyLogs)", returning = "jsonResult")
    public void recordLog(JoinPoint joinPoint, HoneyLogs honeyLogs, Object jsonResult) {
        User loginUser = TokenUtils.getCurrentUser();//获取当前登陆的用户信息
        if (loginUser == null) { // 用户未登录时,从参数里获取操作人信息,使用joinPoint可以获取参数
            // 登录、注册
            Object[] args = joinPoint.getArgs();
            if (ArrayUtil.isNotEmpty(args)) {
                if (args[0] instanceof User) {
                    loginUser = (User) args[0];
                }
            }
        }
        if (loginUser == null) {
            log.error("记录日志信息错误,未获取到当前操作用户信息");
            return;
        }
        // 获取 HttpServletRequest 对象
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = requestAttributes.getRequest();
        // 获取 ip 地址
        String ipAddr = IpUtils.getIpAddr(request);
        // 组装日志的实体对象
        Logs logs = Logs.builder()
                .operation(honeyLogs.operation())
                .type(honeyLogs.type().getValue())
                .ip(ipAddr)
                .user(loginUser.getUsername())
                .time(DateUtil.now())
                .build();
        // 插入到数据库, 通过 ThreadUtil 工具类异步插入,这步失败不影响业务进程
        ThreadUtil.execAsync(() -> {
            logsMapper.insert(logs);
        });
    }
}

6. 在 com/example/demo/controller/BookController.java的增删改方法上增加HoneyLogs注解的使用,并定义操作模块和类型

在 com/example/demo/controller/UserController.java的登录注册方法上增加HoneyLogs注解的使用,并定义操作模块和类型

 

 

前端

1. 新增 vue/src/views/Logs.vue 

<template>
  <div style="width: 100%; padding: 10px">
<!--    功能区-->
    <div style="margin: 10px 0">
      <el-popconfirm title="确定删除吗" @confirm="deleteBatch">
        <template #reference>
          <el-button type="danger">批量删除</el-button>
        </template>
      </el-popconfirm>
      <el-button type="info" plain @click="exportData">批量导出</el-button>
    </div>
<!--    搜索区-->
    <div style="display: flex; margin: 10px 0">
      <el-input v-model="search" placeholder="查询模块" style="width: 20%" clearable></el-input>
      <el-select style="width: 20%; margin: 0 10px;" v-model="type" clearable>
        <el-option v-for="item in ['新增','修改','删除']" :key="item" :value="item" :label="item"></el-option>
      </el-select>
      <el-button type="primary" style="margin-left: 10px" @click="load">查询</el-button>
    </div>
    <el-table :data="tableData" border stripe @selection-change="handleSelectionChange">
      <el-table-column type="selection" width="55"></el-table-column>
      <el-table-column prop="id" label="ID"
        sortable
      />
      <el-table-column prop="operation" label="操作模块" />
      <el-table-column prop="type" label="操作类型" >
        <template v-slot="scope">
          <el-tag type="primary" v-if="scope.row.type === '新增'">{{ scope.row.type }}</el-tag>
          <el-tag type="info" v-if="scope.row.type === '修改'">{{ scope.row.type }}</el-tag>
          <el-tag type="danger" v-if="scope.row.type === '删除'">{{ scope.row.type }}</el-tag>
          <el-tag type="danger" v-if="scope.row.type === '批量删除'">{{ scope.row.type }}</el-tag>
          <el-tag type="success" v-if="scope.row.type === '登录'">{{ scope.row.type }}</el-tag>
          <el-tag type="success" v-if="scope.row.type === '注册'">{{ scope.row.type }}</el-tag>
        </template>
      </el-table-column>
      <el-table-column prop="ip" label="ip地址" />
      <el-table-column prop="user" label="操作人" />
      <el-table-column prop="time" label="操作时间" />
      <el-table-column fixed="right" label="操作" min-width="120">
        <template #default="scope">
          <el-button link type="primary" size="small" @click="handleEdit(scope.row)">
            编辑
          </el-button>
          <el-popconfirm title="确认删除吗?" @confirm="handleDelete(scope.row.id)">
            <template #reference>
              <el-button link type="primary" size="small">删除</el-button>
            </template>
          </el-popconfirm>
        </template>
      </el-table-column>
    </el-table>
    <div style="margin: 10px 0">
      <el-pagination
          v-model:current-page="currentPage"
          v-model:page-size="pageSize"
          :page-sizes="[10, 20, 50, 100]"
          layout="total, sizes, prev, pager, next, jumper"
          :total="total"
          @size-change="handleSizeChange"
          @current-change="handleCurrentChange"
      />
    </div>
    <el-dialog v-model="dialogVisible" title="操作日志" width="30%">
      <el-form :label-position="labelPosition"  label-width="auto" :model="form" style="width: 600px">
        <el-form-item label="操作模块">
          <el-date-picker v-model="form.operation" type="date" style="width: 80%" clearable></el-date-picker>
        </el-form-item>
        <el-form-item label="操作类型">
          <el-input v-model="form.type" style="width: 80%"></el-input>
        </el-form-item>
        <el-form-item label="操作人ip">
          <el-input v-model="form.ip" style="width: 80%"></el-input>
        </el-form-item>
        <el-form-item label="操作人">
          <el-input v-model="form.user" style="width: 80%"></el-input>
        </el-form-item>
        <el-form-item label="操作时间">
          <el-input v-model="form.time" style="width: 80%"></el-input>
        </el-form-item>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="dialogVisible = false">取 消</el-button>
          <el-button type="primary" @click="save()">
            确 定
          </el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>

<script>

import request from "@/utils/request";

export default {
  name: 'Logs',
  components: {

  },
  data() {
    return {
      user: {},
      form: {},
      dialogVisible: false,
      search: '',
      currentPage: 1,
      pageSize: 10,
      total: 0,
      tableData: [],
      ids: [],
      type: ''
    }
  },
  created() {
    this.load()
    let userStr = localStorage.getItem("user") || {}
    this.user = JSON.parse(userStr)

    request.get("/user/" + this.user.id).then(res => {
      if (res.data === '0'){
        this.user = res.data
      }
    })
  },
  methods: {
    exportData(){  //批量导出
      if(!this.ids.length){  //没选择行时导出全部 或 根据我的搜索条件导出
        window.open('http://localhost:9090/logs/export?search=' + this.search)
      } else {
        let idStr = this.ids.join(',')  //  [1,2,3] => '1,2,3'
        window.open('http://localhost:9090/logs/export?ids=' + idStr)
      }
    },
    deleteBatch(){  //批量删除
      if(!this.ids.length){
        this.$message.warning("请选择数据!")
        return
      }
      request.post("/logs/deleteBatch", this.ids).then(res => {
        if(res.code === '200'){
          this.$message.success("批量删除成功")
          this.load()
        } else {
          this.$message.error(res.msg)
        }
      })
    },
    handleSelectionChange(val){  //多选后将选择的id存到ids数组中
      this.ids = val.map(v => v.id)   //[{id,name},{id,name}] => [id,id]
    },
    load() {
      request.get("/logs", {
        params:{
          pageNum: this.currentPage,
          pageSize: this.pageSize,
          search: this.search,
          type: this.type
        }
      }).then(res=>{
        console.log(res)
        this.tableData = res.data.records
        this.total = res.data.total
      })
    },
    save(){
      request.put("/logs", this.form).then(res => {
        console.log(res)
        if (res.code === '200') {
          this.$message({
            type: "success",
            message: "更新成功"
          })
        } else {
          this.$message({
            type: "error",
            message: "res.msg"
          })
        }
        this.load()      //更新后刷新表格数据
        this.dialogVisible = false   //关闭弹窗
      })
    },
    handleEdit(row) {
      this.form = JSON.parse(JSON.stringify(row))
      this.dialogVisible = true
    },
    handleDelete(id) {
      console.log(id)
      request.delete("/logs/" + id).then(res => {
        if(res.code === '200'){
          this.$message({
            type: "success",
            message: "删除成功"
          })
        }else {
          this.$message({
            type: "error",
            message: "res.msg"
          })
        }
        this.load()      //删除后刷新表格数据
      })
    },
    handleSizeChange() {     //改变当前每页个数触发
      this.load()
    },
    handleCurrentChange() {  //改变当前页码触发
      this.load()
    }
  }
}

</script>

2. 改造 vue/src/router/index.js 和 vue/src/components/Aside.vue

 3. 测试效果,登录、书籍管理中的修改、删除等操作都被记录

 

标签:type,springboot,17,ip,demo,import,com,example,系统日志
From: https://www.cnblogs.com/xiexieyc/p/18341607

相关文章