Преглед на файлове

重构操作日志记录实现

puhui999 преди 1 година
родител
ревизия
1fae341cae
променени са 22 файла, в които са добавени 491 реда и са изтрити 106 реда
  1. 9 14
      yudao-framework/yudao-spring-boot-starter-biz-operatelog/src/main/java/cn/iocoder/yudao/framework/operatelog/config/YudaoOperateLogAutoConfiguration.java
  2. 8 3
      yudao-framework/yudao-spring-boot-starter-biz-operatelog/src/main/java/cn/iocoder/yudao/framework/operatelogv2/config/YudaoOperateLogV2Configuration.java
  3. 325 0
      yudao-framework/yudao-spring-boot-starter-biz-operatelog/src/main/java/cn/iocoder/yudao/framework/operatelogv2/core/aop/OperateLogV2Aspect.java
  4. 34 0
      yudao-framework/yudao-spring-boot-starter-biz-operatelog/src/main/java/cn/iocoder/yudao/framework/operatelogv2/core/enums/OperateLogV2Constants.java
  5. 5 49
      yudao-framework/yudao-spring-boot-starter-biz-operatelog/src/main/java/cn/iocoder/yudao/framework/operatelogv2/core/service/ILogRecordServiceImpl.java
  6. 2 2
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java
  7. 0 1
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/bizlog/package-info.java
  8. 1 2
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/operatelog/function/CrmIndustryParseFunction.java
  9. 1 1
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/operatelog/function/CrmLevelParseFunction.java
  10. 1 1
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/operatelog/function/CrmSourceParseFunction.java
  11. 1 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/operatelog/package-info.java
  12. 38 1
      yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/logger/dto/OperateLogV2CreateReqDTO.java
  13. 4 2
      yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/logger/dto/OperateLogV2RespDTO.java
  14. 1 2
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/logger/OperateLogApiImpl.java
  15. 0 8
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/logger/OperateLogConvert.java
  16. 56 6
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/logger/OperateLogV2DO.java
  17. 0 11
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/logger/OperateLogV2Mapper.java
  18. 0 1
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/bizlog/package-info.java
  19. 1 1
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/operatelog/function/AdminUserParseFunction.java
  20. 1 1
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/operatelog/function/AreaParseFunction.java
  21. 1 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/operatelog/package-info.java
  22. 2 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/logger/OperateLogServiceImpl.java

+ 9 - 14
yudao-framework/yudao-spring-boot-starter-biz-operatelog/src/main/java/cn/iocoder/yudao/framework/operatelog/config/YudaoOperateLogAutoConfiguration.java

@@ -1,23 +1,18 @@
 package cn.iocoder.yudao.framework.operatelog.config;
 
-import cn.iocoder.yudao.framework.operatelog.core.aop.OperateLogAspect;
-import cn.iocoder.yudao.framework.operatelog.core.service.OperateLogFrameworkService;
-import cn.iocoder.yudao.framework.operatelog.core.service.OperateLogFrameworkServiceImpl;
-import cn.iocoder.yudao.module.system.api.logger.OperateLogApi;
 import org.springframework.boot.autoconfigure.AutoConfiguration;
-import org.springframework.context.annotation.Bean;
 
 @AutoConfiguration
 public class YudaoOperateLogAutoConfiguration {
 
-    @Bean
-    public OperateLogAspect operateLogAspect() {
-        return new OperateLogAspect();
-    }
-
-    @Bean
-    public OperateLogFrameworkService operateLogFrameworkService(OperateLogApi operateLogApi) {
-        return new OperateLogFrameworkServiceImpl(operateLogApi);
-    }
+    //@Bean
+    //public OperateLogAspect operateLogAspect() {
+    //    return new OperateLogAspect();
+    //}
+    //
+    //@Bean
+    //public OperateLogFrameworkService operateLogFrameworkService(OperateLogApi operateLogApi) {
+    //    return new OperateLogFrameworkServiceImpl(operateLogApi);
+    //}
 
 }

+ 8 - 3
yudao-framework/yudao-spring-boot-starter-biz-operatelog/src/main/java/cn/iocoder/yudao/framework/operatelogv2/config/YudaoOperateLogV2Configuration.java

@@ -1,7 +1,7 @@
 package cn.iocoder.yudao.framework.operatelogv2.config;
 
+import cn.iocoder.yudao.framework.operatelogv2.core.aop.OperateLogV2Aspect;
 import cn.iocoder.yudao.framework.operatelogv2.core.service.ILogRecordServiceImpl;
-import cn.iocoder.yudao.module.system.api.logger.OperateLogApi;
 import com.mzt.logapi.service.ILogRecordService;
 import com.mzt.logapi.starter.annotation.EnableLogRecord;
 import lombok.extern.slf4j.Slf4j;
@@ -21,8 +21,13 @@ public class YudaoOperateLogV2Configuration {
 
     @Bean
     @Primary
-    public ILogRecordService iLogRecordServiceImpl(OperateLogApi operateLogApi) {
-        return new ILogRecordServiceImpl(operateLogApi);
+    public ILogRecordService iLogRecordServiceImpl() {
+        return new ILogRecordServiceImpl();
+    }
+
+    @Bean
+    public OperateLogV2Aspect operateLogV2Aspect() {
+        return new OperateLogV2Aspect();
     }
 
 }

+ 325 - 0
yudao-framework/yudao-spring-boot-starter-biz-operatelog/src/main/java/cn/iocoder/yudao/framework/operatelogv2/core/aop/OperateLogV2Aspect.java

@@ -0,0 +1,325 @@
+package cn.iocoder.yudao.framework.operatelogv2.core.aop;
+
+import cn.hutool.core.date.LocalDateTimeUtil;
+import cn.hutool.core.exceptions.ExceptionUtil;
+import cn.hutool.core.util.ArrayUtil;
+import cn.hutool.core.util.ObjUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
+import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils;
+import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
+import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
+import cn.iocoder.yudao.module.system.api.logger.OperateLogApi;
+import cn.iocoder.yudao.module.system.api.logger.dto.OperateLogV2CreateReqDTO;
+import com.google.common.collect.Maps;
+import com.mzt.logapi.beans.LogRecord;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.core.annotation.AnnotationUtils;
+import org.springframework.validation.BindingResult;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Array;
+import java.time.LocalDateTime;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Predicate;
+import java.util.stream.IntStream;
+
+import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR;
+import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.SUCCESS;
+import static cn.iocoder.yudao.framework.operatelogv2.core.enums.OperateLogV2Constants.*;
+
+/**
+ * 拦截使用 @Operation 注解
+ *
+ * @author HUIHUI
+ */
+@Aspect
+@Slf4j
+public class OperateLogV2Aspect {
+
+    /**
+     * 用于记录操作内容的上下文
+     *
+     * @see OperateLogV2CreateReqDTO#getContent()
+     */
+    private static final ThreadLocal<LogRecord> CONTENT = new ThreadLocal<>();
+    /**
+     * 用于记录拓展字段的上下文
+     *
+     * @see OperateLogV2CreateReqDTO#getExtra()
+     */
+    private static final ThreadLocal<Map<String, Object>> EXTRA = new ThreadLocal<>();
+
+    @Resource
+    private OperateLogApi operateLogApi;
+
+    @Around("@annotation(operation)")
+    public Object around(ProceedingJoinPoint joinPoint, Operation operation) throws Throwable {
+        RequestMethod requestMethod = obtainFirstMatchRequestMethod(obtainRequestMethod(joinPoint));
+        if (requestMethod == RequestMethod.GET) { // 跳过 get 方法
+            return joinPoint.proceed();
+        }
+
+        // 目前,只有管理员,才记录操作日志!所以非管理员,直接调用,不进行记录
+        Integer userType = WebFrameworkUtils.getLoginUserType();
+        if (ObjUtil.notEqual(userType, UserTypeEnum.ADMIN.getValue())) {
+            return joinPoint.proceed();
+        }
+
+        // 记录开始时间
+        LocalDateTime startTime = LocalDateTime.now();
+        try {
+            // 执行原有方法
+            Object result = joinPoint.proceed();
+            // 记录正常执行时的操作日志
+            this.log(joinPoint, operation, startTime, result, null);
+            return result;
+        } catch (Throwable exception) {
+            this.log(joinPoint, operation, startTime, null, exception);
+            throw exception;
+        } finally {
+            clearThreadLocal();
+        }
+    }
+
+    public static void setContent(LogRecord content) {
+        CONTENT.set(content);
+    }
+
+    public static void addExtra(String key, Object value) {
+        if (EXTRA.get() == null) {
+            EXTRA.set(new HashMap<>());
+        }
+        EXTRA.get().put(key, value);
+    }
+
+    public static void addExtra(Map<String, Object> extra) {
+        if (EXTRA.get() == null) {
+            EXTRA.set(new HashMap<>());
+        }
+        EXTRA.get().putAll(extra);
+    }
+
+    private static void clearThreadLocal() {
+        CONTENT.remove();
+        EXTRA.remove();
+    }
+
+    private void log(ProceedingJoinPoint joinPoint, Operation operation,
+                     LocalDateTime startTime, Object result, Throwable exception) {
+        try {
+            // 判断不记录的情况(默认没有值就是记录)
+            if (EXTRA.get() != null && EXTRA.get().get(ENABLE) != null) {
+                return;
+            }
+            // 真正记录操作日志
+            this.log0(joinPoint, operation, startTime, result, exception);
+        } catch (Throwable ex) {
+            log.error("[log][记录操作日志时,发生异常,其中参数是 joinPoint({}) apiOperation({}) result({}) exception({}) ]",
+                    joinPoint, operation, result, exception, ex);
+        }
+    }
+
+    private void log0(ProceedingJoinPoint joinPoint, Operation operation,
+                      LocalDateTime startTime, Object result, Throwable exception) {
+        OperateLogV2CreateReqDTO reqDTO = new OperateLogV2CreateReqDTO();
+        // 补全通用字段
+        reqDTO.setTraceId(TracerUtils.getTraceId());
+        reqDTO.setStartTime(startTime);
+        // 补充用户信息
+        fillUserFields(reqDTO);
+        // 补全模块信息
+        fillModuleFields(reqDTO, joinPoint, operation);
+        // 补全请求信息
+        fillRequestFields(reqDTO);
+        // 补全方法信息
+        fillMethodFields(reqDTO, joinPoint, startTime, result, exception);
+
+        // 异步记录日志
+        operateLogApi.createOperateLogV2(reqDTO);
+    }
+
+    private static void fillUserFields(OperateLogV2CreateReqDTO reqDTO) {
+        reqDTO.setUserId(WebFrameworkUtils.getLoginUserId());
+        reqDTO.setUserType(WebFrameworkUtils.getLoginUserType());
+    }
+
+    private static void fillModuleFields(OperateLogV2CreateReqDTO reqDTO, ProceedingJoinPoint joinPoint, Operation operation) {
+        LogRecord logRecord = CONTENT.get();
+        reqDTO.setBizId(Long.parseLong(logRecord.getBizNo())); // 操作模块业务编号
+        reqDTO.setContent(logRecord.getAction());// 例如说,修改编号为 1 的用户信息,将性别从男改成女,将姓名从芋道改成源码。
+        if (EXTRA.get() != null) {
+            reqDTO.setExtra((Map<String, Object>) EXTRA.get().get(EXTRA_KEY)); // 拓展字段,有些复杂的业务,需要记录一些字段 ( JSON 格式 ),例如说,记录订单编号,{ orderId: "1"}
+        }
+
+        // type 属性
+        if (logRecord.getType() != null) {
+            reqDTO.setType(logRecord.getType()); // 大模块类型如 crm 客户
+        }
+        if (StrUtil.isEmpty(reqDTO.getType())) {
+            Tag tag = getClassAnnotation(joinPoint, Tag.class);
+            if (tag != null) {
+                // 优先读取 @Tag 的 name 属性
+                if (StrUtil.isNotEmpty(tag.name())) {
+                    reqDTO.setType(tag.name());
+                }
+                // 没有的话,读取 @API 的 description 属性
+                if (StrUtil.isEmpty(reqDTO.getType()) && ArrayUtil.isNotEmpty(tag.description())) {
+                    reqDTO.setType(tag.description());
+                }
+            }
+        }
+        // subType 属性
+        if (logRecord.getSubType() != null) {
+            reqDTO.setSubType(logRecord.getSubType());// 操作名称如 转移客户
+        }
+        if (StrUtil.isEmpty(reqDTO.getSubType()) && operation != null) {
+            reqDTO.setSubType(operation.summary());
+        }
+
+    }
+
+    private static void fillRequestFields(OperateLogV2CreateReqDTO reqDTO) {
+        // 获得 Request 对象
+        HttpServletRequest request = ServletUtils.getRequest();
+        if (request == null) {
+            return;
+        }
+        // 补全请求信息
+        reqDTO.setRequestMethod(request.getMethod());
+        reqDTO.setRequestUrl(request.getRequestURI());
+        reqDTO.setUserIp(ServletUtils.getClientIP(request));
+        reqDTO.setUserAgent(ServletUtils.getUserAgent(request));
+    }
+
+    private static void fillMethodFields(OperateLogV2CreateReqDTO reqDTO, ProceedingJoinPoint joinPoint, LocalDateTime startTime,
+                                         Object result, Throwable exception) {
+        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
+        reqDTO.setJavaMethod(methodSignature.toString());
+        if (EXTRA.get().get(IS_LOG_ARGS) == null) {
+            reqDTO.setJavaMethodArgs(obtainMethodArgs(joinPoint));
+        }
+        if (EXTRA.get().get(IS_LOG_RESULT_DATA) == null) {
+            reqDTO.setResultData(obtainResultData(result));
+        }
+        reqDTO.setDuration((int) (LocalDateTimeUtil.between(startTime, LocalDateTime.now()).toMillis()));
+        // (正常)处理 resultCode 和 resultMsg 字段
+        if (result instanceof CommonResult) {
+            CommonResult<?> commonResult = (CommonResult<?>) result;
+            reqDTO.setResultCode(commonResult.getCode());
+            reqDTO.setResultMsg(commonResult.getMsg());
+        } else {
+            reqDTO.setResultCode(SUCCESS.getCode());
+        }
+        // (异常)处理 resultCode 和 resultMsg 字段
+        if (exception != null) {
+            reqDTO.setResultCode(INTERNAL_SERVER_ERROR.getCode());
+            reqDTO.setResultMsg(ExceptionUtil.getRootCauseMessage(exception));
+        }
+    }
+
+    @SuppressWarnings("SameParameterValue")
+    private static <T extends Annotation> T getClassAnnotation(ProceedingJoinPoint joinPoint, Class<T> annotationClass) {
+        return ((MethodSignature) joinPoint.getSignature()).getMethod().getDeclaringClass().getAnnotation(annotationClass);
+    }
+
+    private static String obtainMethodArgs(ProceedingJoinPoint joinPoint) {
+        // TODO 提升:参数脱敏和忽略
+        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
+        String[] argNames = methodSignature.getParameterNames();
+        Object[] argValues = joinPoint.getArgs();
+        // 拼接参数
+        Map<String, Object> args = Maps.newHashMapWithExpectedSize(argValues.length);
+        for (int i = 0; i < argNames.length; i++) {
+            String argName = argNames[i];
+            Object argValue = argValues[i];
+            // 被忽略时,标记为 ignore 字符串,避免和 null 混在一起
+            args.put(argName, !isIgnoreArgs(argValue) ? argValue : "[ignore]");
+        }
+        return JsonUtils.toJsonString(args);
+    }
+
+    private static String obtainResultData(Object result) {
+        // TODO 提升:结果脱敏和忽略
+        if (result instanceof CommonResult) {
+            result = ((CommonResult<?>) result).getData();
+        }
+        return JsonUtils.toJsonString(result);
+    }
+
+    private static boolean isIgnoreArgs(Object object) {
+        Class<?> clazz = object.getClass();
+        // 处理数组的情况
+        if (clazz.isArray()) {
+            return IntStream.range(0, Array.getLength(object))
+                    .anyMatch(index -> isIgnoreArgs(Array.get(object, index)));
+        }
+        // 递归,处理数组、Collection、Map 的情况
+        if (Collection.class.isAssignableFrom(clazz)) {
+            return ((Collection<?>) object).stream()
+                    .anyMatch((Predicate<Object>) OperateLogV2Aspect::isIgnoreArgs);
+        }
+        if (Map.class.isAssignableFrom(clazz)) {
+            return isIgnoreArgs(((Map<?, ?>) object).values());
+        }
+        // obj
+        return object instanceof MultipartFile
+                || object instanceof HttpServletRequest
+                || object instanceof HttpServletResponse
+                || object instanceof BindingResult;
+    }
+
+    private static RequestMethod obtainFirstMatchRequestMethod(RequestMethod[] requestMethods) {
+        if (ArrayUtil.isEmpty(requestMethods)) {
+            return null;
+        }
+        // 优先,匹配最优的 POST、PUT、DELETE
+        RequestMethod result = obtainFirstLogRequestMethod(requestMethods);
+        if (result != null) {
+            return result;
+        }
+        // 然后,匹配次优的 GET
+        result = Arrays.stream(requestMethods).filter(requestMethod -> requestMethod == RequestMethod.GET)
+                .findFirst().orElse(null);
+        if (result != null) {
+            return result;
+        }
+        // 兜底,获得第一个
+        return requestMethods[0];
+    }
+
+    private static RequestMethod[] obtainRequestMethod(ProceedingJoinPoint joinPoint) {
+        RequestMapping requestMapping = AnnotationUtils.getAnnotation( // 使用 Spring 的工具类,可以处理 @RequestMapping 别名注解
+                ((MethodSignature) joinPoint.getSignature()).getMethod(), RequestMapping.class);
+        return requestMapping != null ? requestMapping.method() : new RequestMethod[]{};
+    }
+
+    private static RequestMethod obtainFirstLogRequestMethod(RequestMethod[] requestMethods) {
+        if (ArrayUtil.isEmpty(requestMethods)) {
+            return null;
+        }
+        return Arrays.stream(requestMethods).filter(requestMethod ->
+                        requestMethod == RequestMethod.POST
+                                || requestMethod == RequestMethod.PUT
+                                || requestMethod == RequestMethod.DELETE)
+                .findFirst().orElse(null);
+    }
+
+}

+ 34 - 0
yudao-framework/yudao-spring-boot-starter-biz-operatelog/src/main/java/cn/iocoder/yudao/framework/operatelogv2/core/enums/OperateLogV2Constants.java

@@ -0,0 +1,34 @@
+package cn.iocoder.yudao.framework.operatelogv2.core.enums;
+
+/**
+ * 操作日志常量接口
+ *
+ * @author HUIHUI
+ */
+public interface OperateLogV2Constants {
+
+    // ========== 开关字段-如果没有值默认就是记录 ==========
+
+    /**
+     * 是否记录日志
+     */
+    String ENABLE = "enable";
+
+    /**
+     * 是否记录方法参数
+     */
+    String IS_LOG_ARGS = "isLogArgs";
+
+    /**
+     * 是否记录方法结果的数据
+     */
+    String IS_LOG_RESULT_DATA = "isLogResultData";
+
+    // ========== 扩展 ==========
+
+    /**
+     * 扩展信息
+     */
+    String EXTRA_KEY = "extra";
+
+}

+ 5 - 49
yudao-framework/yudao-spring-boot-starter-biz-operatelog/src/main/java/cn/iocoder/yudao/framework/operatelogv2/core/service/ILogRecordServiceImpl.java

@@ -1,14 +1,9 @@
 package cn.iocoder.yudao.framework.operatelogv2.core.service;
 
-import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils;
-import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
-import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
-import cn.iocoder.yudao.module.system.api.logger.OperateLogApi;
-import cn.iocoder.yudao.module.system.api.logger.dto.OperateLogV2CreateReqDTO;
+import cn.iocoder.yudao.framework.operatelogv2.core.aop.OperateLogV2Aspect;
 import com.mzt.logapi.beans.LogRecord;
+import com.mzt.logapi.context.LogRecordContext;
 import com.mzt.logapi.service.ILogRecordService;
-import jakarta.servlet.http.HttpServletRequest;
-import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 
 import java.util.Collections;
@@ -17,56 +12,17 @@ import java.util.List;
 /**
  * 操作日志 ILogRecordService 实现类
  *
- * 基于 {@link OperateLogApi} 实现,记录操作日志
+ * 基于 {@link OperateLogV2Aspect} 实现,记录操作日志
  *
  * @author HUIHUI
  */
 @Slf4j
-@RequiredArgsConstructor
 public class ILogRecordServiceImpl implements ILogRecordService {
 
-    private final OperateLogApi operateLogApi;
-
     @Override
     public void record(LogRecord logRecord) {
-        OperateLogV2CreateReqDTO reqBO = new OperateLogV2CreateReqDTO();
-        // 补全通用字段
-        reqBO.setTraceId(TracerUtils.getTraceId());
-        // 补充用户信息
-        fillUserFields(reqBO);
-        // 补全模块信息
-        fillModuleFields(reqBO, logRecord);
-        // 补全请求信息
-        fillRequestFields(reqBO);
-        // 异步记录日志
-        operateLogApi.createOperateLogV2(reqBO);
-        log.info("操作日志 ===> {}", reqBO);
-    }
-
-    private static void fillUserFields(OperateLogV2CreateReqDTO reqBO) {
-        reqBO.setUserId(WebFrameworkUtils.getLoginUserId());
-        reqBO.setUserType(WebFrameworkUtils.getLoginUserType());
-    }
-
-    public static void fillModuleFields(OperateLogV2CreateReqDTO reqBO, LogRecord logRecord) {
-        reqBO.setType(logRecord.getType()); // 大模块类型如 crm-客户
-        reqBO.setSubType(logRecord.getSubType());// 操作名称如 转移客户
-        reqBO.setBizId(Long.parseLong(logRecord.getBizNo())); // 操作模块业务编号
-        reqBO.setContent(logRecord.getAction());// 例如说,修改编号为 1 的用户信息,将性别从男改成女,将姓名从芋道改成源码。
-        reqBO.setExtra(logRecord.getExtra()); // 拓展字段,有些复杂的业务,需要记录一些字段 ( JSON 格式 ),例如说,记录订单编号,{ orderId: "1"}
-    }
-
-    private static void fillRequestFields(OperateLogV2CreateReqDTO reqBO) {
-        // 获得 Request 对象
-        HttpServletRequest request = ServletUtils.getRequest();
-        if (request == null) {
-            return;
-        }
-        // 补全请求信息
-        reqBO.setRequestMethod(request.getMethod());
-        reqBO.setRequestUrl(request.getRequestURI());
-        reqBO.setUserIp(ServletUtils.getClientIP(request));
-        reqBO.setUserAgent(ServletUtils.getUserAgent(request));
+        OperateLogV2Aspect.setContent(logRecord); // 操作日志
+        OperateLogV2Aspect.addExtra(LogRecordContext.getVariables()); // 扩展信息
     }
 
     @Override

+ 2 - 2
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java

@@ -67,7 +67,7 @@ public class CrmCustomerController {
     }
 
     @PutMapping("/update")
-    //@Operation(summary = "更新客户")
+    @Operation(summary = "更新客户")
     @PreAuthorize("@ss.hasPermission('crm:customer:update')")
     public CommonResult<Boolean> updateCustomer(@Valid @RequestBody CrmCustomerUpdateReqVO updateReqVO) {
         customerService.updateCustomer(updateReqVO);
@@ -131,7 +131,7 @@ public class CrmCustomerController {
     }
 
     @PutMapping("/transfer")
-    //@Operation(summary = "客户转移")
+    @Operation(summary = "客户转移")
     @PreAuthorize("@ss.hasPermission('crm:customer:update')")
     public CommonResult<Boolean> transfer(@Valid @RequestBody CrmCustomerTransferReqVO reqVO) {
         customerService.transferCustomer(reqVO, getLoginUserId());

+ 0 - 1
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/bizlog/package-info.java

@@ -1 +0,0 @@
-package cn.iocoder.yudao.module.crm.framework.bizlog;

+ 1 - 2
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/bizlog/function/CrmIndustryParseFunction.java → yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/operatelog/function/CrmIndustryParseFunction.java

@@ -1,4 +1,4 @@
-package cn.iocoder.yudao.module.crm.framework.bizlog.function;
+package cn.iocoder.yudao.module.crm.framework.operatelog.function;
 
 import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.dict.core.util.DictFrameworkUtils;
@@ -8,7 +8,6 @@ import org.springframework.stereotype.Component;
 
 import static cn.iocoder.yudao.module.crm.enums.DictTypeConstants.CRM_CUSTOMER_INDUSTRY;
 
-// TODO @puhui999:包名使用 operatelog 更合适哈;
 /**
  * 自定义函数-通过行业编号获取行业信息
  *

+ 1 - 1
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/bizlog/function/CrmLevelParseFunction.java → yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/operatelog/function/CrmLevelParseFunction.java

@@ -1,4 +1,4 @@
-package cn.iocoder.yudao.module.crm.framework.bizlog.function;
+package cn.iocoder.yudao.module.crm.framework.operatelog.function;
 
 import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.dict.core.util.DictFrameworkUtils;

+ 1 - 1
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/bizlog/function/CrmSourceParseFunction.java → yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/operatelog/function/CrmSourceParseFunction.java

@@ -1,4 +1,4 @@
-package cn.iocoder.yudao.module.crm.framework.bizlog.function;
+package cn.iocoder.yudao.module.crm.framework.operatelog.function;
 
 import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.dict.core.util.DictFrameworkUtils;

+ 1 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/operatelog/package-info.java

@@ -0,0 +1 @@
+package cn.iocoder.yudao.module.crm.framework.operatelog;

+ 38 - 1
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/logger/dto/OperateLogV2CreateReqDTO.java

@@ -5,6 +5,9 @@ import jakarta.validation.constraints.NotEmpty;
 import jakarta.validation.constraints.NotNull;
 import lombok.Data;
 
+import java.time.LocalDateTime;
+import java.util.Map;
+
 /**
  * 系统操作日志 Create Req BO
  *
@@ -58,7 +61,7 @@ public class OperateLogV2CreateReqDTO {
      * 拓展字段,有些复杂的业务,需要记录一些字段 ( JSON 格式 )
      * 例如说,记录订单编号,{ orderId: "1"}
      */
-    private String extra;
+    private Map<String, Object> extra;
 
     /**
      * 请求方法名
@@ -81,4 +84,38 @@ public class OperateLogV2CreateReqDTO {
     @NotEmpty(message = "浏览器 UA 不能为空")
     private String userAgent;
 
+    /**
+     * Java 方法名
+     */
+    private String javaMethod;
+    /**
+     * Java 方法的参数
+     */
+    private String javaMethodArgs;
+
+    /**
+     * 开始时间
+     */
+    private LocalDateTime startTime;
+
+    /**
+     * 执行时长,单位:毫秒
+     */
+    private Integer duration;
+
+    /**
+     * 结果码
+     */
+    private Integer resultCode;
+
+    /**
+     * 结果提示
+     */
+    private String resultMsg;
+
+    /**
+     * 结果数据
+     */
+    private String resultData;
+
 }

+ 4 - 2
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/logger/dto/OperateLogV2RespDTO.java

@@ -2,6 +2,8 @@ package cn.iocoder.yudao.module.system.api.logger.dto;
 
 import lombok.Data;
 
+import java.time.LocalDateTime;
+
 /**
  * 系统操作日志 Resp DTO
  *
@@ -65,8 +67,8 @@ public class OperateLogV2RespDTO {
     private String userAgent;
 
     /**
-     * 创建时间, 直接返回字符串
+     * 创建时间
      */
-    private String createTime;
+    private LocalDateTime createTime;
 
 }

+ 1 - 2
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/logger/OperateLogApiImpl.java

@@ -7,7 +7,6 @@ import cn.iocoder.yudao.module.system.api.logger.dto.OperateLogCreateReqDTO;
 import cn.iocoder.yudao.module.system.api.logger.dto.OperateLogV2CreateReqDTO;
 import cn.iocoder.yudao.module.system.api.logger.dto.OperateLogV2PageReqDTO;
 import cn.iocoder.yudao.module.system.api.logger.dto.OperateLogV2RespDTO;
-import cn.iocoder.yudao.module.system.convert.logger.OperateLogConvert;
 import cn.iocoder.yudao.module.system.dal.dataobject.logger.OperateLogV2DO;
 import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO;
 import cn.iocoder.yudao.module.system.service.logger.OperateLogService;
@@ -63,7 +62,7 @@ public class OperateLogApiImpl implements OperateLogApi {
     private static List<OperateLogV2RespDTO> setUserInfo(List<OperateLogV2DO> logList, List<AdminUserDO> userList) {
         Map<Long, AdminUserDO> userMap = convertMap(userList, AdminUserDO::getId);
         return convertList(logList, item -> {
-            OperateLogV2RespDTO respDTO = OperateLogConvert.INSTANCE.convert(item);
+            OperateLogV2RespDTO respDTO = BeanUtils.toBean(item, OperateLogV2RespDTO.class);
             findAndThen(userMap, item.getUserId(), user -> {
                 respDTO.setUserName(user.getNickname());
             });

+ 0 - 8
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/logger/OperateLogConvert.java

@@ -3,20 +3,15 @@ package cn.iocoder.yudao.module.system.convert.logger;
 import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
 import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
-import cn.iocoder.yudao.module.system.api.logger.dto.OperateLogV2RespDTO;
 import cn.iocoder.yudao.module.system.controller.admin.logger.vo.operatelog.OperateLogRespVO;
 import cn.iocoder.yudao.module.system.dal.dataobject.logger.OperateLogDO;
-import cn.iocoder.yudao.module.system.dal.dataobject.logger.OperateLogV2DO;
 import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO;
 import org.mapstruct.Mapper;
-import org.mapstruct.Mapping;
 import org.mapstruct.factory.Mappers;
 
 import java.util.List;
 import java.util.Map;
 
-import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
-
 @Mapper
 public interface OperateLogConvert {
 
@@ -30,7 +25,4 @@ public interface OperateLogConvert {
         });
     }
 
-    @Mapping(target = "createTime", source = "logV2DO.createTime", dateFormat = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
-    OperateLogV2RespDTO convert(OperateLogV2DO logV2DO);
-
 }

+ 56 - 6
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/logger/OperateLogV2DO.java

@@ -1,13 +1,19 @@
 package cn.iocoder.yudao.module.system.dal.dataobject.logger;
 
 import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
 import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
+import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
 
+import java.time.LocalDateTime;
+import java.util.Map;
+
 /**
  * 操作日志表 V2
  *
@@ -19,6 +25,16 @@ import lombok.EqualsAndHashCode;
 @EqualsAndHashCode(callSuper = true)
 public class OperateLogV2DO extends BaseDO {
 
+    /**
+     * {@link #javaMethodArgs} 的最大长度
+     */
+    public static final Integer JAVA_METHOD_ARGS_MAX_LENGTH = 8000;
+
+    /**
+     * {@link #resultData} 的最大长度
+     */
+    public static final Integer RESULT_MAX_LENGTH = 4000;
+
     /**
      * 日志主键
      */
@@ -65,8 +81,8 @@ public class OperateLogV2DO extends BaseDO {
      *
      * 例如说,记录订单编号,{ orderId: "1"}
      */
-    // TODO @puhui999:看看能不能类似 exts 搞 json 格式;
-    private String extra;
+    @TableField(typeHandler = JacksonTypeHandler.class)
+    private Map<String, Object> extra;
     /**
      * 请求方法名
      */
@@ -84,9 +100,43 @@ public class OperateLogV2DO extends BaseDO {
      */
     private String userAgent;
 
-    // TODO @芋艿:requestUrl、requestMethod
-    // TODO @芋艿:javaMethod、javaMethodArgs
-    // TODO @芋艿:startTime、duration
-    // TODO @芋艿:resultMsg、resultData
+    /**
+     * Java 方法名
+     */
+    private String javaMethod;
+    /**
+     * Java 方法的参数
+     *
+     * 实际格式为 Map<String, Object>
+     * 不使用 @TableField(typeHandler = FastjsonTypeHandler.class) 注解的原因是,数据库存储有长度限制,会进行裁剪,会导致 JSON 反序列化失败
+     * 其中,key 为参数名,value 为参数值
+     */
+    private String javaMethodArgs;
+    /**
+     * 开始时间
+     */
+    private LocalDateTime startTime;
+    /**
+     * 执行时长,单位:毫秒
+     */
+    private Integer duration;
+    /**
+     * 结果码
+     *
+     * 目前使用的 {@link CommonResult#getCode()} 属性
+     */
+    private Integer resultCode;
+    /**
+     * 结果提示
+     *
+     * 目前使用的 {@link CommonResult#getMsg()} 属性
+     */
+    private String resultMsg;
+    /**
+     * 结果数据
+     *
+     * 如果是对象,则使用 JSON 格式化
+     */
+    private String resultData;
 
 }

+ 0 - 11
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/logger/OperateLogV2Mapper.java

@@ -4,23 +4,12 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
 import cn.iocoder.yudao.module.system.api.logger.dto.OperateLogV2PageReqDTO;
-import cn.iocoder.yudao.module.system.controller.admin.logger.vo.operatelog.OperateLogPageReqVO;
 import cn.iocoder.yudao.module.system.dal.dataobject.logger.OperateLogV2DO;
 import org.apache.ibatis.annotations.Mapper;
 
-import java.util.Collection;
-
 @Mapper
 public interface OperateLogV2Mapper extends BaseMapperX<OperateLogV2DO> {
 
-    default PageResult<OperateLogV2DO> selectPage(OperateLogPageReqVO reqVO, Collection<Long> userIds) {
-        LambdaQueryWrapperX<OperateLogV2DO> query = new LambdaQueryWrapperX<OperateLogV2DO>()
-                .likeIfPresent(OperateLogV2DO::getType, reqVO.getModule())
-                .inIfPresent(OperateLogV2DO::getUserId, userIds);
-        query.orderByDesc(OperateLogV2DO::getId); // 降序
-        return selectPage(reqVO, query);
-    }
-
     default PageResult<OperateLogV2DO> selectPage(OperateLogV2PageReqDTO pageReqVO) {
         return selectPage(pageReqVO, new LambdaQueryWrapperX<OperateLogV2DO>()
                 .eqIfPresent(OperateLogV2DO::getType, pageReqVO.getBizType())

+ 0 - 1
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/bizlog/package-info.java

@@ -1 +0,0 @@
-package cn.iocoder.yudao.module.system.framework.bizlog;

+ 1 - 1
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/bizlog/function/AdminUserParseFunction.java → yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/operatelog/function/AdminUserParseFunction.java

@@ -1,4 +1,4 @@
-package cn.iocoder.yudao.module.system.framework.bizlog.function;
+package cn.iocoder.yudao.module.system.framework.operatelog.function;
 
 import cn.hutool.core.util.ObjUtil;
 import cn.hutool.core.util.StrUtil;

+ 1 - 1
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/bizlog/function/AreaParseFunction.java → yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/operatelog/function/AreaParseFunction.java

@@ -1,4 +1,4 @@
-package cn.iocoder.yudao.module.system.framework.bizlog.function;
+package cn.iocoder.yudao.module.system.framework.operatelog.function;
 
 import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.ip.core.utils.AreaUtils;

+ 1 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/operatelog/package-info.java

@@ -0,0 +1 @@
+package cn.iocoder.yudao.module.system.framework.operatelog;

+ 2 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/logger/OperateLogServiceImpl.java

@@ -71,6 +71,8 @@ public class OperateLogServiceImpl implements OperateLogService {
     @Override
     public void createOperateLogV2(OperateLogV2CreateReqDTO createReqBO) {
         OperateLogV2DO log = BeanUtils.toBean(createReqBO, OperateLogV2DO.class);
+        log.setJavaMethodArgs(StrUtils.maxLength(log.getJavaMethodArgs(), JAVA_METHOD_ARGS_MAX_LENGTH));
+        log.setResultData(StrUtils.maxLength(log.getResultData(), RESULT_MAX_LENGTH));
         operateLogV2Mapper.insert(log);
     }