Jelajahi Sumber

操作日志

xiongzhu 4 tahun lalu
induk
melakukan
57ead80b9b

+ 5 - 0
pom.xml

@@ -80,6 +80,11 @@
             <artifactId>spring-boot-starter-cache</artifactId>
             <artifactId>spring-boot-starter-cache</artifactId>
         </dependency>
         </dependency>
 
 
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-aop</artifactId>
+        </dependency>
+
         <dependency>
         <dependency>
             <groupId>com.github.ben-manes.caffeine</groupId>
             <groupId>com.github.ben-manes.caffeine</groupId>
             <artifactId>caffeine</artifactId>
             <artifactId>caffeine</artifactId>

+ 14 - 0
src/main/java/com/izouma/awesomeAdmin/annotations/OperLog.java

@@ -0,0 +1,14 @@
+package com.izouma.awesomeAdmin.annotations;
+
+import java.lang.annotation.*;
+
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface OperLog {
+    String value() default "";
+
+    String type() default "";
+
+    String desc() default "";
+}

+ 188 - 0
src/main/java/com/izouma/awesomeAdmin/aspect/OperLogAspect.java

@@ -0,0 +1,188 @@
+package com.izouma.awesomeAdmin.aspect;
+
+import com.alibaba.fastjson.JSON;
+import com.izouma.awesomeAdmin.annotations.OperLog;
+import com.izouma.awesomeAdmin.domain.ExceptionLog;
+import com.izouma.awesomeAdmin.domain.OperationLog;
+import com.izouma.awesomeAdmin.domain.User;
+import com.izouma.awesomeAdmin.repo.ExceptionLogRepo;
+import com.izouma.awesomeAdmin.repo.OperationLogRepo;
+import com.izouma.awesomeAdmin.utils.IPUtils;
+import com.izouma.awesomeAdmin.utils.SecurityUtils;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.annotation.AfterReturning;
+import org.aspectj.lang.annotation.AfterThrowing;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Pointcut;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import org.springframework.web.context.request.RequestAttributes;
+import org.springframework.web.context.request.RequestContextHolder;
+
+import javax.servlet.http.HttpServletRequest;
+import java.lang.reflect.Method;
+import java.time.LocalDateTime;
+import java.util.HashMap;
+import java.util.Map;
+
+@Aspect
+@Component
+public class OperLogAspect {
+
+    @Autowired
+    private OperationLogRepo operationLogRepo;
+
+    @Autowired
+    private ExceptionLogRepo exceptionLogRepo;
+
+    /**
+     * 设置操作日志切入点 记录操作日志 在注解的位置切入代码
+     */
+    @Pointcut("@annotation(com.izouma.awesomeAdmin.annotations.OperLog)")
+    public void operLogPointCut() {
+    }
+
+    /**
+     * 设置操作异常切入点记录异常日志 扫描所有controller包下操作
+     */
+    @Pointcut("execution(* com.izouma.awesomeAdmin.web..*.*(..))")
+    public void operExceptionLogPointCut() {
+    }
+
+    /**
+     * 正常返回通知,拦截用户操作日志,连接点正常执行完成后执行, 如果连接点抛出异常,则不会执行
+     *
+     * @param joinPoint 切入点
+     * @param keys      返回结果
+     */
+    @AfterReturning(value = "operLogPointCut()", returning = "keys")
+    public void saveOperLog(JoinPoint joinPoint, Object keys) {
+        // 获取RequestAttributes
+        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
+        // 从获取RequestAttributes中获取HttpServletRequest的信息
+        HttpServletRequest request = (HttpServletRequest) requestAttributes
+                .resolveReference(RequestAttributes.REFERENCE_REQUEST);
+
+        OperationLog operationLog = new OperationLog();
+        try {
+            // 从切面织入点处通过反射机制获取织入点处的方法
+            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
+            // 获取切入点所在的方法
+            Method method = signature.getMethod();
+            // 获取操作
+            OperLog operLog = method.getAnnotation(OperLog.class);
+            if (operLog != null) {
+                operationLog.setName(operLog.value()); // 操作模块
+                operationLog.setType(operLog.type()); // 操作类型
+                operationLog.setDesc(operLog.desc()); // 操作描述
+            }
+            // 获取请求的类名
+            String className = joinPoint.getTarget().getClass().getName();
+            // 获取请求的方法名
+            String methodName = method.getName();
+            methodName = className + "." + methodName;
+
+            operationLog.setReqMethod(methodName);
+
+            // 请求的参数
+            Map<String, String> rtnMap = null;
+            String params = null;
+            if (request != null) {
+                rtnMap = convertMap(request.getParameterMap());
+                params = JSON.toJSONString(rtnMap);
+            }
+
+            operationLog.setReqParams(params);
+            operationLog.setResp(JSON.toJSONString(keys));
+
+            User user = SecurityUtils.getAuthenticatedUser();
+            if (user != null) {
+                operationLog.setUserId(String.valueOf(user.getId()));
+                operationLog.setUsername(user.getUsername());
+            }
+            operationLog.setReqIp(IPUtils.getIpAddr(request));
+            operationLog.setReqUrl(request != null ? request.getRequestURI() : null);
+            operationLog.setTime(LocalDateTime.now());
+            operationLogRepo.save(operationLog);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * 异常返回通知,用于拦截异常日志信息 连接点抛出异常后执行
+     *
+     * @param joinPoint 切入点
+     * @param e         异常信息
+     */
+    @AfterThrowing(pointcut = "operExceptionLogPointCut()", throwing = "e")
+    public void saveExceptionLog(JoinPoint joinPoint, Throwable e) {
+        // 获取RequestAttributes
+        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
+        // 从获取RequestAttributes中获取HttpServletRequest的信息
+        HttpServletRequest request = (HttpServletRequest) requestAttributes
+                .resolveReference(RequestAttributes.REFERENCE_REQUEST);
+
+        ExceptionLog exceptionLog = new ExceptionLog();
+        try {
+            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
+            Method method = signature.getMethod();
+            String className = joinPoint.getTarget().getClass().getName();
+            String methodName = method.getName();
+            methodName = className + "." + methodName;
+            exceptionLog.setReqMethod(methodName);
+
+            Map<String, String> rtnMap = convertMap(request.getParameterMap());
+            String params = JSON.toJSONString(rtnMap);
+            exceptionLog.setReqParams(params);
+
+            exceptionLog.setName(e.getClass().getName());
+            exceptionLog.setMessage(stackTraceToString(e.getClass().getName(), e.getMessage(), e
+                    .getStackTrace()));
+            User user = SecurityUtils.getAuthenticatedUser();
+            if (user != null) {
+                exceptionLog.setUserId(String.valueOf(user.getId()));
+                exceptionLog.setUsername(user.getUsername());
+            }
+
+            exceptionLog.setReqUrl(request.getRequestURI());
+            exceptionLog.setReqIp(IPUtils.getIpAddr(request));
+            exceptionLog.setTime(LocalDateTime.now());
+
+            exceptionLogRepo.save(exceptionLog);
+
+        } catch (Exception e2) {
+            e2.printStackTrace();
+        }
+
+    }
+
+    /**
+     * 转换request 请求参数
+     *
+     * @param paramMap request获取的参数数组
+     */
+    public Map<String, String> convertMap(Map<String, String[]> paramMap) {
+        Map<String, String> rtnMap = new HashMap<>();
+        for (String key : paramMap.keySet()) {
+            rtnMap.put(key, paramMap.get(key)[0]);
+        }
+        return rtnMap;
+    }
+
+    /**
+     * 转换异常信息为字符串
+     *
+     * @param exceptionName    异常名称
+     * @param exceptionMessage 异常信息
+     * @param elements         堆栈信息
+     */
+    public String stackTraceToString(String exceptionName, String exceptionMessage, StackTraceElement[] elements) {
+        StringBuilder strBuff = new StringBuilder();
+        for (StackTraceElement stet : elements) {
+            strBuff.append(stet).append("\n");
+        }
+        return exceptionName + ":" + exceptionMessage + "\n\t" + strBuff;
+    }
+}

+ 71 - 0
src/main/java/com/izouma/awesomeAdmin/domain/ExceptionLog.java

@@ -0,0 +1,71 @@
+package com.izouma.awesomeAdmin.domain;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import javax.persistence.*;
+import java.time.LocalDateTime;
+
+@Data
+@Entity
+@AllArgsConstructor
+@NoArgsConstructor
+@Builder
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonIgnoreProperties(value = {"hibernateLazyInitializer"}, ignoreUnknown = true)
+@ApiModel("异常日志")
+public class ExceptionLog {
+    @Id
+    @GeneratedValue(strategy = GenerationType.AUTO)
+    private Long id;
+
+    @ApiModelProperty("异常名称")
+    private String name;
+
+    @ApiModelProperty("操作类型")
+    private String type;
+
+    @ApiModelProperty("用户ID")
+    private String userId;
+
+    @ApiModelProperty("用户名")
+    private String username;
+
+    @ApiModelProperty("描述")
+    @Column(name = "description")
+    private String desc;
+
+    @ApiModelProperty("调用方法")
+    private String reqMethod;
+
+    @ApiModelProperty("请求地址")
+    private String reqUrl;
+
+    @ApiModelProperty("请求参数")
+    @Lob
+    private String reqParams;
+
+    @ApiModelProperty("请求ip")
+    private String reqIp;
+
+    @ApiModelProperty("返回结果")
+    @Lob
+    private String resp;
+
+    @ApiModelProperty("操作时间")
+    private LocalDateTime time;
+
+    @ApiModelProperty("异常消息")
+    @Lob
+    private String message;
+
+    @ApiModelProperty("错误追踪")
+    @Lob
+    private String stackTrace;
+}

+ 63 - 0
src/main/java/com/izouma/awesomeAdmin/domain/OperationLog.java

@@ -0,0 +1,63 @@
+package com.izouma.awesomeAdmin.domain;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import javax.persistence.*;
+import java.time.LocalDateTime;
+
+@Data
+@Entity
+@AllArgsConstructor
+@NoArgsConstructor
+@Builder
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonIgnoreProperties(value = {"hibernateLazyInitializer"}, ignoreUnknown = true)
+@ApiModel("操作日志")
+public class OperationLog {
+    @Id
+    @GeneratedValue(strategy = GenerationType.AUTO)
+    private Long id;
+
+    @ApiModelProperty("操作名称")
+    private String name;
+
+    @ApiModelProperty("操作类型")
+    private String type;
+
+    @ApiModelProperty("用户ID")
+    private String userId;
+
+    @ApiModelProperty("用户名")
+    private String username;
+
+    @ApiModelProperty("描述")
+    @Column(name = "description")
+    private String desc;
+
+    @ApiModelProperty("调用方法")
+    private String reqMethod;
+
+    @ApiModelProperty("请求地址")
+    private String reqUrl;
+
+    @ApiModelProperty("请求参数")
+    @Lob
+    private String reqParams;
+
+    @ApiModelProperty("请求ip")
+    private String reqIp;
+
+    @ApiModelProperty("返回结果")
+    @Lob
+    private String resp;
+
+    @ApiModelProperty("操作时间")
+    private LocalDateTime time;
+}

+ 8 - 0
src/main/java/com/izouma/awesomeAdmin/repo/ExceptionLogRepo.java

@@ -0,0 +1,8 @@
+package com.izouma.awesomeAdmin.repo;
+
+import com.izouma.awesomeAdmin.domain.ExceptionLog;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+
+public interface ExceptionLogRepo extends JpaRepository<ExceptionLog, Long>, JpaSpecificationExecutor<ExceptionLog> {
+}

+ 8 - 0
src/main/java/com/izouma/awesomeAdmin/repo/OperationLogRepo.java

@@ -0,0 +1,8 @@
+package com.izouma.awesomeAdmin.repo;
+
+import com.izouma.awesomeAdmin.domain.OperationLog;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+
+public interface OperationLogRepo extends JpaRepository<OperationLog, Long>, JpaSpecificationExecutor<OperationLog> {
+}

+ 13 - 0
src/main/java/com/izouma/awesomeAdmin/service/ExceptionLogService.java

@@ -0,0 +1,13 @@
+package com.izouma.awesomeAdmin.service;
+
+import com.izouma.awesomeAdmin.repo.ExceptionLogRepo;
+import lombok.AllArgsConstructor;
+import org.springframework.stereotype.Service;
+
+@Service
+@AllArgsConstructor
+public class ExceptionLogService {
+
+    private ExceptionLogRepo exceptionLogRepo;
+
+}

+ 13 - 0
src/main/java/com/izouma/awesomeAdmin/service/OperationLogService.java

@@ -0,0 +1,13 @@
+package com.izouma.awesomeAdmin.service;
+
+import com.izouma.awesomeAdmin.repo.OperationLogRepo;
+import lombok.AllArgsConstructor;
+import org.springframework.stereotype.Service;
+
+@Service
+@AllArgsConstructor
+public class OperationLogService {
+
+    private OperationLogRepo operationLogRepo;
+
+}

+ 71 - 0
src/main/java/com/izouma/awesomeAdmin/utils/IPUtils.java

@@ -0,0 +1,71 @@
+package com.izouma.awesomeAdmin.utils;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.util.StringUtils;
+
+import javax.servlet.http.HttpServletRequest;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+@Slf4j
+public class IPUtils {
+    private static final String IP_UTILS_FLAG = ",";
+    private static final String UNKNOWN       = "unknown";
+    private static final String LOCALHOST_IP  = "0:0:0:0:0:0:0:1";
+    private static final String LOCALHOST_IP1 = "127.0.0.1";
+
+    /**
+     * 获取IP地址
+     * <p>
+     * 使用Nginx等反向代理软件, 则不能通过request.getRemoteAddr()获取IP地址
+     * 如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,X-Forwarded-For中第一个非unknown的有效IP字符串,则为真实IP地址
+     */
+    public static String getIpAddr(HttpServletRequest request) {
+        String ip = null;
+        try {
+            //以下两个获取在k8s中,将真实的客户端IP,放到了x-Original-Forwarded-For。而将WAF的回源地址放到了 x-Forwarded-For了。
+            ip = request.getHeader("X-Original-Forwarded-For");
+            if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
+                ip = request.getHeader("X-Forwarded-For");
+            }
+            //获取nginx等代理的ip
+            if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
+                ip = request.getHeader("x-forwarded-for");
+            }
+            if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
+                ip = request.getHeader("Proxy-Client-IP");
+            }
+            if (StringUtils.isEmpty(ip) || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
+                ip = request.getHeader("WL-Proxy-Client-IP");
+            }
+            if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
+                ip = request.getHeader("HTTP_CLIENT_IP");
+            }
+            if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
+                ip = request.getHeader("HTTP_X_FORWARDED_FOR");
+            }
+            //兼容k8s集群获取ip
+            if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
+                ip = request.getRemoteAddr();
+                if (LOCALHOST_IP1.equalsIgnoreCase(ip) || LOCALHOST_IP.equalsIgnoreCase(ip)) {
+                    //根据网卡取本机配置的IP
+                    InetAddress iNet = null;
+                    try {
+                        iNet = InetAddress.getLocalHost();
+                        ip = iNet.getHostAddress();
+                    } catch (UnknownHostException e) {
+                        log.error("getClientIp error", e);
+                    }
+                }
+            }
+        } catch (Exception e) {
+            log.error("IPUtils ERROR ", e);
+        }
+        //使用代理,则获取第一个IP地址
+        if (!StringUtils.isEmpty(ip) && ip.indexOf(IP_UTILS_FLAG) > 0) {
+            ip = ip.substring(0, ip.indexOf(IP_UTILS_FLAG));
+        }
+
+        return ip;
+    }
+}

+ 63 - 0
src/main/java/com/izouma/awesomeAdmin/web/ExceptionLogController.java

@@ -0,0 +1,63 @@
+package com.izouma.awesomeAdmin.web;
+
+import com.izouma.awesomeAdmin.domain.ExceptionLog;
+import com.izouma.awesomeAdmin.dto.PageQuery;
+import com.izouma.awesomeAdmin.exception.BusinessException;
+import com.izouma.awesomeAdmin.repo.ExceptionLogRepo;
+import com.izouma.awesomeAdmin.service.ExceptionLogService;
+import com.izouma.awesomeAdmin.utils.ObjUtils;
+import com.izouma.awesomeAdmin.utils.excel.ExcelUtils;
+import lombok.AllArgsConstructor;
+import org.springframework.data.domain.Page;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.List;
+
+import static com.izouma.awesomeAdmin.utils.JpaUtils.toPageRequest;
+import static com.izouma.awesomeAdmin.utils.JpaUtils.toSpecification;
+
+@RestController
+@RequestMapping("/exceptionLog")
+@AllArgsConstructor
+public class ExceptionLogController extends BaseController {
+    private ExceptionLogService exceptionLogService;
+    private ExceptionLogRepo    exceptionLogRepo;
+
+    //@PreAuthorize("hasRole('ADMIN')")
+    @PostMapping("/save")
+    public ExceptionLog save(@RequestBody ExceptionLog record) {
+        if (record.getId() != null) {
+            ExceptionLog orig = exceptionLogRepo.findById(record.getId()).orElseThrow(new BusinessException("无记录"));
+            ObjUtils.merge(orig, record);
+            return exceptionLogRepo.save(orig);
+        }
+        return exceptionLogRepo.save(record);
+    }
+
+
+    //@PreAuthorize("hasRole('ADMIN')")
+    @PostMapping("/all")
+    public Page<ExceptionLog> all(@RequestBody PageQuery pageQuery) {
+        return exceptionLogRepo.findAll(toSpecification(pageQuery, ExceptionLog.class), toPageRequest(pageQuery));
+    }
+
+    @GetMapping("/get/{id}")
+    public ExceptionLog get(@PathVariable Long id) {
+        return exceptionLogRepo.findById(id).orElseThrow(new BusinessException("无记录"));
+    }
+
+    @PostMapping("/del/{id}")
+    public void del(@PathVariable Long id) {
+        exceptionLogRepo.deleteById(id);
+    }
+
+    @GetMapping("/excel")
+    @ResponseBody
+    public void excel(HttpServletResponse response, PageQuery pageQuery) throws IOException {
+        List<ExceptionLog> data = all(pageQuery).getContent();
+        ExcelUtils.export(response, data);
+    }
+}
+

+ 63 - 0
src/main/java/com/izouma/awesomeAdmin/web/OperationLogController.java

@@ -0,0 +1,63 @@
+package com.izouma.awesomeAdmin.web;
+
+import com.izouma.awesomeAdmin.domain.OperationLog;
+import com.izouma.awesomeAdmin.dto.PageQuery;
+import com.izouma.awesomeAdmin.exception.BusinessException;
+import com.izouma.awesomeAdmin.repo.OperationLogRepo;
+import com.izouma.awesomeAdmin.service.OperationLogService;
+import com.izouma.awesomeAdmin.utils.ObjUtils;
+import com.izouma.awesomeAdmin.utils.excel.ExcelUtils;
+import lombok.AllArgsConstructor;
+import org.springframework.data.domain.Page;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.List;
+
+import static com.izouma.awesomeAdmin.utils.JpaUtils.toPageRequest;
+import static com.izouma.awesomeAdmin.utils.JpaUtils.toSpecification;
+
+@RestController
+@RequestMapping("/operationLog")
+@AllArgsConstructor
+public class OperationLogController extends BaseController {
+    private OperationLogService operationLogService;
+    private OperationLogRepo operationLogRepo;
+
+    //@PreAuthorize("hasRole('ADMIN')")
+    @PostMapping("/save")
+    public OperationLog save(@RequestBody OperationLog record) {
+        if (record.getId() != null) {
+            OperationLog orig = operationLogRepo.findById(record.getId()).orElseThrow(new BusinessException("无记录"));
+            ObjUtils.merge(orig, record);
+            return operationLogRepo.save(orig);
+        }
+        return operationLogRepo.save(record);
+    }
+
+
+    //@PreAuthorize("hasRole('ADMIN')")
+    @PostMapping("/all")
+    public Page<OperationLog> all(@RequestBody PageQuery pageQuery) {
+        return operationLogRepo.findAll(toSpecification(pageQuery,OperationLog.class), toPageRequest(pageQuery));
+    }
+
+    @GetMapping("/get/{id}")
+    public OperationLog get(@PathVariable Long id) {
+        return operationLogRepo.findById(id).orElseThrow(new BusinessException("无记录"));
+    }
+
+    @PostMapping("/del/{id}")
+    public void del(@PathVariable Long id) {
+        operationLogRepo.deleteById(id);
+    }
+
+    @GetMapping("/excel")
+    @ResponseBody
+    public void excel(HttpServletResponse response, PageQuery pageQuery) throws IOException {
+        List<OperationLog> data = all(pageQuery).getContent();
+        ExcelUtils.export(response, data);
+    }
+}
+

+ 4 - 1
src/main/java/com/izouma/awesomeAdmin/web/TestClassController.java

@@ -1,4 +1,6 @@
 package com.izouma.awesomeAdmin.web;
 package com.izouma.awesomeAdmin.web;
+
+import com.izouma.awesomeAdmin.annotations.OperLog;
 import com.izouma.awesomeAdmin.domain.TestClass;
 import com.izouma.awesomeAdmin.domain.TestClass;
 import com.izouma.awesomeAdmin.service.TestClassService;
 import com.izouma.awesomeAdmin.service.TestClassService;
 import com.izouma.awesomeAdmin.dto.PageQuery;
 import com.izouma.awesomeAdmin.dto.PageQuery;
@@ -20,10 +22,11 @@ import java.util.List;
 @AllArgsConstructor
 @AllArgsConstructor
 public class TestClassController extends BaseController {
 public class TestClassController extends BaseController {
     private TestClassService testClassService;
     private TestClassService testClassService;
-    private TestClassRepo testClassRepo;
+    private TestClassRepo    testClassRepo;
 
 
     //@PreAuthorize("hasRole('ADMIN')")
     //@PreAuthorize("hasRole('ADMIN')")
     @PostMapping("/save")
     @PostMapping("/save")
+    @OperLog("测试操作")
     public TestClass save(@RequestBody TestClass record) {
     public TestClass save(@RequestBody TestClass record) {
         if (record.getId() != null) {
         if (record.getId() != null) {
             TestClass orig = testClassRepo.findById(record.getId()).orElseThrow(new BusinessException("无记录"));
             TestClass orig = testClassRepo.findById(record.getId()).orElseThrow(new BusinessException("无记录"));

+ 4 - 5
src/main/resources/templates/EditViewTemplate.ftl

@@ -2,7 +2,7 @@
     <div class="edit-view">
     <div class="edit-view">
         <page-title>
         <page-title>
             <el-button @click="$router.go(-1)">取消</el-button>
             <el-button @click="$router.go(-1)">取消</el-button>
-            <el-button @click="del" :loading="$store.state.fetchingData" type="danger" v-if="formData.id">
+            <el-button @click="onDelete" :loading="$store.state.fetchingData" type="danger" v-if="formData.id">
                 删除
                 删除
             </el-button>
             </el-button>
             <el-button @click="onSave" :loading="$store.state.fetchingData" type="primary">保存</el-button>
             <el-button @click="onSave" :loading="$store.state.fetchingData" type="primary">保存</el-button>
@@ -98,10 +98,10 @@
                         <el-button @click="onSave" :loading="saving" type="primary">
                         <el-button @click="onSave" :loading="saving" type="primary">
                             保存
                             保存
                         </el-button>
                         </el-button>
-                        <el-button @click="onDelete" :loading="saving" type="danger" v-if="formData.id">
+                        <el-button @click="onDelete" :disabled="saving" type="danger" v-if="formData.id">
                             删除
                             删除
                         </el-button>
                         </el-button>
-                        <el-button @click="$router.go(-1)">取消</el-button>
+                        <el-button @click="$router.go(-1)" :disabled="saving">取消</el-button>
                     </el-form-item>
                     </el-form-item>
                 </el-form>
                 </el-form>
             </div>
             </div>
@@ -301,5 +301,4 @@
         }
         }
     }
     }
 </script>
 </script>
-<style lang="less" scoped>
-</style>
+<style lang="less" scoped></style>

+ 16 - 0
src/main/vue/src/router.js

@@ -118,6 +118,22 @@ const router = new Router({
                     meta: {
                     meta: {
                         title: '测试'
                         title: '测试'
                     }
                     }
+                },
+                {
+                    path: '/operationLogList',
+                    name: 'OperationLogList',
+                    component: () => import(/* webpackChunkName: "operationLogList" */ '@/views/OperationLogList.vue'),
+                    meta: {
+                        title: '操作日志'
+                    }
+                },
+                {
+                    path: '/exceptionLogList',
+                    name: 'ExceptionLogList',
+                    component: () => import(/* webpackChunkName: "exceptionLogList" */ '@/views/ExceptionLogList.vue'),
+                    meta: {
+                        title: '异常日志'
+                    }
                 }
                 }
                 /**INSERT_LOCATION**/
                 /**INSERT_LOCATION**/
             ]
             ]

+ 189 - 0
src/main/vue/src/views/ExceptionLogList.vue

@@ -0,0 +1,189 @@
+<template>
+    <div class="list-view">
+        <page-title></page-title>
+        <div class="filters-container">
+            <el-input placeholder="输入关键字" v-model="search" clearable class="filter-item"></el-input>
+            <el-button @click="getData" type="primary" icon="el-icon-search" class="filter-item">搜索 </el-button>
+            <el-button @click="addRow" type="primary" icon="el-icon-plus" class="filter-item">添加 </el-button>
+            <el-button
+                @click="download"
+                type="primary"
+                icon="el-icon-download"
+                :loading="downloading"
+                class="filter-item"
+                >导出EXCEL
+            </el-button>
+        </div>
+        <el-table
+            :data="tableData"
+            row-key="id"
+            ref="table"
+            header-row-class-name="table-header-row"
+            header-cell-class-name="table-header-cell"
+            row-class-name="table-row"
+            cell-class-name="table-cell"
+            :height="tableHeight"
+        >
+            <el-table-column v-if="multipleMode" align="center" type="selection" width="50"> </el-table-column>
+            <el-table-column prop="id" label="ID" width="100"> </el-table-column>
+            <el-table-column prop="name" label="异常名称"> </el-table-column>
+            <el-table-column prop="type" label="操作类型"> </el-table-column>
+            <el-table-column prop="userId" label="用户ID"> </el-table-column>
+            <el-table-column prop="username" label="用户名"> </el-table-column>
+            <el-table-column prop="desc" label="描述"> </el-table-column>
+            <el-table-column prop="reqMethod" label="调用方法"> </el-table-column>
+            <el-table-column prop="reqUrl" label="请求地址"> </el-table-column>
+            <el-table-column prop="reqParams" label="请求参数"> </el-table-column>
+            <el-table-column prop="reqIp" label="请求IP"> </el-table-column>
+            <el-table-column prop="resp" label="返回结果"> </el-table-column>
+            <el-table-column prop="time" label="操作时间"> </el-table-column>
+            <el-table-column prop="message" label="异常消息"> </el-table-column>
+            <el-table-column prop="stackTrace" label="错误追踪"> </el-table-column>
+            <el-table-column label="操作" align="center" fixed="right" min-width="150">
+                <template slot-scope="{ row }">
+                    <el-button @click="editRow(row)" type="primary" size="mini" plain>编辑</el-button>
+                    <el-button @click="deleteRow(row)" type="danger" size="mini" plain>删除</el-button>
+                </template>
+            </el-table-column>
+        </el-table>
+        <div class="pagination-wrapper">
+            <!-- <div class="multiple-mode-wrapper">
+                <el-button v-if="!multipleMode" @click="toggleMultipleMode(true)">批量编辑</el-button>
+                <el-button-group v-else>
+                    <el-button @click="operation1">批量操作1</el-button>
+                    <el-button @click="operation2">批量操作2</el-button>
+                    <el-button @click="toggleMultipleMode(false)">取消</el-button>
+                </el-button-group>
+            </div> -->
+            <el-pagination
+                background
+                @size-change="onSizeChange"
+                @current-change="onCurrentChange"
+                :current-page="page"
+                :page-sizes="[10, 20, 30, 40, 50]"
+                :page-size="pageSize"
+                layout="total, sizes, prev, pager, next, jumper"
+                :total="totalElements"
+            >
+            </el-pagination>
+        </div>
+        <el-dialog :visible.sync="showResp" title="返回结果">
+            <pre
+                >{{ resp }}
+           </pre
+            >
+        </el-dialog>
+    </div>
+</template>
+<script>
+import { mapState } from 'vuex';
+import pageableTable from '@/mixins/pageableTable';
+
+export default {
+    name: 'ExceptionLogList',
+    mixins: [pageableTable],
+    data() {
+        return {
+            multipleMode: false,
+            search: '',
+            url: '/exceptionLog/all',
+            downloading: false,
+            sortStr: '',
+            resp: '',
+            showResp: false
+        };
+    },
+    computed: {
+        selection() {
+            return this.$refs.table.selection.map(i => i.id);
+        }
+    },
+    methods: {
+        beforeGetData() {
+            if (this.search) {
+                return { search: this.search };
+            }
+        },
+        toggleMultipleMode(multipleMode) {
+            this.multipleMode = multipleMode;
+            if (!multipleMode) {
+                this.$refs.table.clearSelection();
+            }
+        },
+        addRow() {
+            this.$router.push({
+                path: '/exceptionLogEdit',
+                query: {
+                    ...this.$route.query
+                }
+            });
+        },
+        editRow(row) {
+            this.$router.push({
+                path: '/exceptionLogEdit',
+                query: {
+                    id: row.id
+                }
+            });
+        },
+        download() {
+            this.downloading = true;
+            this.$axios
+                .get('/exceptionLog/excel', {
+                    responseType: 'blob',
+                    params: { size: 10000 }
+                })
+                .then(res => {
+                    console.log(res);
+                    this.downloading = false;
+                    const downloadUrl = window.URL.createObjectURL(new Blob([res.data]));
+                    const link = document.createElement('a');
+                    link.href = downloadUrl;
+                    link.setAttribute('download', res.headers['content-disposition'].split('filename=')[1]);
+                    document.body.appendChild(link);
+                    link.click();
+                    link.remove();
+                })
+                .catch(e => {
+                    console.log(e);
+                    this.downloading = false;
+                    this.$message.error(e.error);
+                });
+        },
+        operation1() {
+            this.$notify({
+                title: '提示',
+                message: this.selection
+            });
+        },
+        operation2() {
+            this.$message('操作2');
+        },
+        deleteRow(row) {
+            this.$alert('删除将无法恢复,确认要删除么?', '警告', { type: 'error' })
+                .then(() => {
+                    return this.$http.post(`/exceptionLog/del/${row.id}`);
+                })
+                .then(() => {
+                    this.$message.success('删除成功');
+                    this.getData();
+                })
+                .catch(e => {
+                    if (e !== 'cancel') {
+                        this.$message.error(e.error);
+                    }
+                });
+        },
+        viewResp(row) {
+            try {
+                this.resp = JSON.stringify(JSON.parse(row.resp), null, 4);
+            } catch (e) {
+                console.log(e);
+                this.resp = row.resp;
+            }
+            this.showResp = true;
+        }
+    }
+};
+</script>
+<style lang="less" scoped></style>

+ 132 - 0
src/main/vue/src/views/OperationLogList.vue

@@ -0,0 +1,132 @@
+<template>
+    <div class="list-view">
+        <page-title></page-title>
+        <div class="filters-container">
+            <el-input placeholder="输入关键字" v-model="search" clearable class="filter-item"></el-input>
+            <el-button @click="getData" type="primary" icon="el-icon-search" class="filter-item">搜索 </el-button>
+            <el-button
+                @click="download"
+                type="primary"
+                icon="el-icon-download"
+                :loading="downloading"
+                class="filter-item"
+                >导出EXCEL
+            </el-button>
+        </div>
+        <el-table
+            :data="tableData"
+            row-key="id"
+            ref="table"
+            header-row-class-name="table-header-row"
+            header-cell-class-name="table-header-cell"
+            row-class-name="table-row"
+            cell-class-name="table-cell"
+            :height="tableHeight"
+        >
+            <el-table-column prop="id" label="ID" width="100"> </el-table-column>
+            <el-table-column prop="name" label="操作名称" show-overflow-tooltip> </el-table-column>
+            <el-table-column prop="type" label="操作类型" show-overflow-tooltip> </el-table-column>
+            <el-table-column prop="userId" label="用户ID" show-overflow-tooltip> </el-table-column>
+            <el-table-column prop="username" label="用户名" show-overflow-tooltip> </el-table-column>
+            <el-table-column prop="desc" label="描述" show-overflow-tooltip> </el-table-column>
+            <el-table-column prop="reqMethod" label="调用方法" show-overflow-tooltip> </el-table-column>
+            <el-table-column prop="reqUrl" label="请求地址" show-overflow-tooltip> </el-table-column>
+            <el-table-column prop="reqParams" label="请求参数" show-overflow-tooltip> </el-table-column>
+            <el-table-column prop="reqIp" label="请求IP" show-overflow-tooltip> </el-table-column>
+            <el-table-column prop="resp" label="返回结果" align="center" width="80">
+                <template v-slot="{ row }">
+                    <el-button @click="viewResp(row)" type="text">查看</el-button>
+                </template>
+            </el-table-column>
+            <el-table-column prop="time" label="操作时间" width="150"> </el-table-column>
+        </el-table>
+        <div class="pagination-wrapper">
+            <!-- <div class="multiple-mode-wrapper">
+                <el-button v-if="!multipleMode" @click="toggleMultipleMode(true)">批量编辑</el-button>
+                <el-button-group v-else>
+                    <el-button @click="operation1">批量操作1</el-button>
+                    <el-button @click="operation2">批量操作2</el-button>
+                    <el-button @click="toggleMultipleMode(false)">取消</el-button>
+                </el-button-group>
+            </div> -->
+            <el-pagination
+                background
+                @size-change="onSizeChange"
+                @current-change="onCurrentChange"
+                :current-page="page"
+                :page-sizes="[10, 20, 30, 40, 50]"
+                :page-size="pageSize"
+                layout="total, sizes, prev, pager, next, jumper"
+                :total="totalElements"
+            >
+            </el-pagination>
+        </div>
+        <el-dialog :visible.sync="showResp" title="返回结果">
+            <pre
+                >{{ resp }}
+           </pre
+            >
+        </el-dialog>
+    </div>
+</template>
+<script>
+import { mapState } from 'vuex';
+import pageableTable from '@/mixins/pageableTable';
+
+export default {
+    name: 'OperationLogList',
+    mixins: [pageableTable],
+    data() {
+        return {
+            search: '',
+            url: '/operationLog/all',
+            downloading: false,
+            sortStr: 'time,desc',
+            resp: '',
+            showResp: false
+        };
+    },
+    computed: {},
+    methods: {
+        beforeGetData() {
+            if (this.search) {
+                return { search: this.search };
+            }
+        },
+        download() {
+            this.downloading = true;
+            this.$axios
+                .get('/operationLog/excel', {
+                    responseType: 'blob',
+                    params: { size: 10000 }
+                })
+                .then(res => {
+                    console.log(res);
+                    this.downloading = false;
+                    const downloadUrl = window.URL.createObjectURL(new Blob([res.data]));
+                    const link = document.createElement('a');
+                    link.href = downloadUrl;
+                    link.setAttribute('download', res.headers['content-disposition'].split('filename=')[1]);
+                    document.body.appendChild(link);
+                    link.click();
+                    link.remove();
+                })
+                .catch(e => {
+                    console.log(e);
+                    this.downloading = false;
+                    this.$message.error(e.error);
+                });
+        },
+        viewResp(row) {
+            try {
+                this.resp = JSON.stringify(JSON.parse(row.resp), null, 4);
+            } catch (e) {
+                console.log(e);
+                this.resp = row.resp;
+            }
+            this.showResp = true;
+        }
+    }
+};
+</script>
+<style lang="less" scoped></style>

+ 2 - 2
src/main/vue/src/views/TestClassEdit.vue

@@ -2,7 +2,7 @@
     <div class="edit-view">
     <div class="edit-view">
         <page-title>
         <page-title>
             <el-button @click="$router.go(-1)">取消</el-button>
             <el-button @click="$router.go(-1)">取消</el-button>
-            <el-button @click="del" :loading="$store.state.fetchingData" type="danger" v-if="formData.id">
+            <el-button @click="onDelete" :loading="$store.state.fetchingData" type="danger" v-if="formData.id">
                 删除
                 删除
             </el-button>
             </el-button>
             <el-button @click="onSave" :loading="$store.state.fetchingData" type="primary">保存</el-button>
             <el-button @click="onSave" :loading="$store.state.fetchingData" type="primary">保存</el-button>
@@ -26,7 +26,7 @@
                         <el-button @click="onDelete" :disabled="saving" size="small" type="danger" v-if="formData.id">
                         <el-button @click="onDelete" :disabled="saving" size="small" type="danger" v-if="formData.id">
                             删除
                             删除
                         </el-button>
                         </el-button>
-                        <el-button @click="$router.go(-1)" :disabled="disabled" size="small">取消</el-button>
+                        <el-button @click="$router.go(-1)" :disabled="saving" size="small">取消</el-button>
                     </el-form-item>
                     </el-form-item>
                 </el-form>
                 </el-form>
             </div>
             </div>