后端
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,本案例是为了方便调用,非正规操作 @ResourceLogsMapper logsMapper;@PutMappingpublic 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();}@GetMappingpublic 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 {@ResourceLogsMapper 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-paginationv-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.recordsthis.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. 测试效果,登录、书籍管理中的修改、删除等操作都被记录