后端
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