ruoyi的spring cloud项目详解(十五)

ruoyi的spring cloud项目详解(十四)-CSDN博客

咱们本篇文章一起学习日志吧

package com.ruoyi.common.log.event;

import org.springframework.context.ApplicationEvent;

import com.ruoyi.system.domain.SysOperLog;

/**
 * 系统日志事件
 */
public class SysOperLogEvent extends ApplicationEvent
{
    //
    private static final long serialVersionUID = 8905017895058642111L;

    public SysOperLogEvent(SysOperLog source)
    {
        super(source);
    }
}

com/ruoyi/common/log/annotation/OperLog.java

package com.ruoyi.common.log.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import com.ruoyi.common.log.enums.BusinessType;
import com.ruoyi.common.log.enums.OperatorType;

/**
 * 自定义操作日志记录注解
 * 
 * @author ruoyi
 */
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OperLog
{
    /**
     * 模块 
     */
    public String title() default "";

    /**
     * 功能
     */
    public BusinessType businessType() default BusinessType.OTHER;

    /**
     * 操作人类别
     */
    public OperatorType operatorType() default OperatorType.MANAGE;

    /**
     * 是否保存请求的参数
     */
    public boolean isSaveRequestData() default true;
}

以下是对这段代码的分析:

一、注解定义概述

这段 Java 代码定义了一个自定义注解 @OperLog,用于标记需要记录操作日志的方法或参数。这个注解可以在运行时被读取和处理,通常用于实现系统的操作日志记录功能。

二、注解属性解释

  1. title()

    • 用途:用于指定操作的模块名称或描述。
    • 默认值为一个空字符串。可以在使用注解时设置具体的模块名称,以便在操作日志中清晰地标识操作所属的模块。
  2. businessType()

    • 用途:指定操作的业务类型,枚举类型为 BusinessType
    • 默认值为 BusinessType.OTHER。通过设置不同的业务类型,可以对不同类型的操作进行分类记录,方便后续的日志分析和统计。
  3. operatorType()

    • 用途:指定操作人的类别,枚举类型为 OperatorType
    • 默认值为 OperatorType.MANAGE。可以区分不同类型的操作人,例如管理员、普通用户等,有助于了解操作的发起者身份。
  4. isSaveRequestData()

    • 用途:表示是否保存请求的参数。
    • 默认值为 true。如果设置为 true,在记录操作日志时可能会保存请求的参数信息,以便更详细地了解操作的具体内容。如果设置为 false,则不会保存请求参数。

三、使用场景和意义

  1. 在企业级应用开发中,操作日志记录非常重要,可以用于审计、故障排查和追踪用户行为等目的。
  2. 通过使用 @OperLog 注解,可以方便地标记需要记录操作日志的方法,而不需要在每个方法中手动编写日志记录代码。
  3. 可以根据具体的业务需求,灵活地设置注解的属性值,以满足不同场景下的日志记录要求。例如,可以根据不同的模块、业务类型和操作人类别进行分类记录,提高日志的可读性和可分析性。

以下是对这段代码进行的代码块分析:

1. 包声明和导入

package com.ruoyi.common.log.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import com.ruoyi.common.log.enums.BusinessType;
import com.ruoyi.common.log.enums.OperatorType;

这段代码声明了包名,并导入了一些必要的 Java 注解和枚举类型,为定义自定义注解做准备。

2. 注解定义

@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OperLog
{
    /**
     * 模块 
     */
    public String title() default "";

    /**
     * 功能
     */
    public BusinessType businessType() default BusinessType.OTHER;

    /**
     * 操作人类别
     */
    public OperatorType operatorType() default OperatorType.MANAGE;

    /**
     * 是否保存请求的参数
     */
    public boolean isSaveRequestData() default true;
}

  • @Target({ElementType.PARAMETER, ElementType.METHOD}):指定这个注解可以应用于方法和参数上。
  • @Retention(RetentionPolicy.RUNTIME):表示这个注解在运行时是可访问的,可以通过反射机制读取注解信息。
  • @Documented:使得这个注解在生成文档时会被包含进去。
  • 注解内部定义了四个属性:
    • title():用于指定操作的模块名称,默认值为空字符串。
    • businessType():指定操作的业务类型,类型为BusinessType枚举,默认值为BusinessType.OTHER
    • operatorType():指定操作人的类别,类型为OperatorType枚举,默认值为OperatorType.MANAGE
    • isSaveRequestData():表示是否保存请求的参数,默认值为true

com/ruoyi/common/log/aspect/OperLogAspect.java

package com.ruoyi.common.log.aspect;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
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.stereotype.Component;

import com.alibaba.fastjson.JSON;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.log.annotation.OperLog;
import com.ruoyi.common.log.enums.BusinessStatus;
import com.ruoyi.common.log.event.SysOperLogEvent;
import com.ruoyi.common.utils.AddressUtils;
import com.ruoyi.common.utils.IpUtils;
import com.ruoyi.common.utils.ServletUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.spring.SpringContextHolder;
import com.ruoyi.system.domain.SysOperLog;

import lombok.extern.slf4j.Slf4j;

/**
 * 操作日志记录处理
 * 
 */
@Aspect
@Slf4j
@Component
public class OperLogAspect
{
    // 配置织入点
    @Pointcut("@annotation(com.ruoyi.common.log.annotation.OperLog)")
    public void logPointCut()
    {
    }

    /**
     * 处理完请求后执行
     *
     * @param joinPoint 切点
     */
    @AfterReturning(pointcut = "logPointCut()")
    public void doAfterReturning(JoinPoint joinPoint)
    {
        handleLog(joinPoint, null);
    }

    /**
     * 拦截异常操作
     * 
     * @param joinPoint 切点
     * @param e         异常
     */
    @AfterThrowing(value = "logPointCut()", throwing = "e")
    public void doAfterThrowing(JoinPoint joinPoint, Exception e)
    {
        handleLog(joinPoint, e);
    }

    protected void handleLog(final JoinPoint joinPoint, final Exception e)
    {
        try
        {
            // 获得注解
            OperLog controllerLog = getAnnotationLog(joinPoint);
            if (controllerLog == null)
            {
                return;
            }
            // *========数据库日志=========*//
            SysOperLog operLog = new SysOperLog();
            operLog.setStatus(BusinessStatus.SUCCESS.ordinal());
            // 请求的地址
            HttpServletRequest request = ServletUtils.getRequest();
            String ip = IpUtils.getIpAddr(request);
            operLog.setOperIp(ip);
            operLog.setOperUrl(request.getRequestURI());
            operLog.setOperLocation(AddressUtils.getRealAddressByIP(ip));
            String username = request.getHeader(Constants.CURRENT_USERNAME);
            operLog.setOperName(username);
            if (e != null)
            {
                operLog.setStatus(BusinessStatus.FAIL.ordinal());
                operLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000));
            }
            // 设置方法名称
            String className = joinPoint.getTarget().getClass().getName();
            String methodName = joinPoint.getSignature().getName();
            operLog.setMethod(className + "." + methodName + "()");
         // 设置请求方式
            operLog.setRequestMethod(request.getMethod());
            // 处理设置注解上的参数
            Object[] args = joinPoint.getArgs();
            getControllerMethodDescription(controllerLog, operLog, args);
            // 发布事件
            SpringContextHolder.publishEvent(new SysOperLogEvent(operLog));
        }
        catch (Exception exp)
        {
            // 记录本地异常日志
            log.error("==前置通知异常==");
            log.error("异常信息:{}", exp.getMessage());
            exp.printStackTrace();
        }
    }

    /**
     * 获取注解中对方法的描述信息 用于Controller层注解
     * 
     * @param log     日志
     * @param operLog 操作日志
     * @throws Exception
     */
    public void getControllerMethodDescription(OperLog log, SysOperLog operLog, Object[] args) throws Exception
    {
        // 设置action动作
        operLog.setBusinessType(log.businessType().ordinal());
        // 设置标题
        operLog.setTitle(log.title());
        // 设置操作人类别
        operLog.setOperatorType(log.operatorType().ordinal());
        // 是否需要保存request,参数和值
        if (log.isSaveRequestData())
        {
            // 获取参数的信息,传入到数据库中。
            setRequestValue(operLog, args);
        }
    }

    /**
     *  获取请求的参数,放到log中
     * 
     * @param operLog 操作日志
     * @throws Exception 异常
     */
    private void setRequestValue(SysOperLog operLog, Object[] args) throws Exception
    {
        List<?> param = new ArrayList<>(Arrays.asList(args)).stream().filter(p -> !(p instanceof ServletResponse))
                .collect(Collectors.toList());
        log.debug("args:{}", param);
        String params = JSON.toJSONString(param, true);
        operLog.setOperParam(StringUtils.substring(params, 0, 2000));
    }

    /**
     * 是否存在注解,如果存在就获取
     */
    private OperLog getAnnotationLog(JoinPoint joinPoint) throws Exception
    {
        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = methodSignature.getMethod();
        if (method != null)
        {
            return method.getAnnotation(OperLog.class);
        }
        return null;
    }
}

以下是对这段代码的分析:

一、代码功能概述

这段 Java 代码实现了一个面向切面编程(AOP)的切面类,用于记录操作日志。它通过拦截被@OperLog注解标记的方法,在方法执行前后或出现异常时记录操作日志信息,并将日志信息封装成SysOperLog对象,最后通过发布事件的方式进行后续处理。

二、主要方法分析

  1. logPointCut()方法:

    • 定义了一个切入点,使用@Pointcut注解指定了被@OperLog注解标记的方法作为切入点。
  2. doAfterReturning(JoinPoint joinPoint)方法:

    • 在目标方法正常执行返回后被调用。它调用handleLog方法来处理日志记录。
  3. doAfterThrowing(JoinPoint joinPoint, Exception e)方法:

    • 在目标方法抛出异常时被调用。它同样调用handleLog方法,但传入了异常对象e
  4. handleLog(JoinPoint joinPoint, Exception e)方法:

    • 核心的日志处理方法。
    • 首先获取@OperLog注解,如果没有找到注解则直接返回。
    • 创建SysOperLog对象,设置日志的各种属性,如操作 IP、URL、方法名、请求方式等。如果有异常,则设置日志状态为失败并记录异常信息。
    • 调用getControllerMethodDescription方法处理注解上的参数,并设置到日志对象中。
    • 最后发布SysOperLogEvent事件,以便其他组件可以处理这个日志事件。
  5. getControllerMethodDescription(OperLog log, SysOperLog operLog, Object[] args)方法:

    • 设置日志对象的一些属性,如业务类型、标题、操作人类别等。如果注解中指定要保存请求参数,则调用setRequestValue方法。
  6. setRequestValue(SysOperLog operLog, Object[] args)方法:

    • 过滤掉args中的ServletResponse类型对象,将其他参数转换为 JSON 字符串,并截取前 2000 个字符作为操作参数保存到日志对象中。
  7. getAnnotationLog(JoinPoint joinPoint)方法:

    • 通过反射获取方法上的@OperLog注解,如果方法上没有该注解则返回null

三、技术点和优势

  • AOP 技术:通过切面的方式实现日志记录,避免了在每个业务方法中重复编写日志记录代码,提高了代码的可维护性和可扩展性。
  • 事件驱动:使用事件发布机制,可以灵活地处理日志记录,方便与其他系统组件进行集成。
  • 日志信息丰富:记录了操作的 IP、URL、方法名、请求方式、业务类型、标题、操作人类别等信息,还可以根据注解配置是否保存请求参数,为系统的审计和故障排查提供了详细的信息。

以下是对上述代码进行分代码块的分析:

1. 包声明和导入部分

package com.ruoyi.common.log.aspect;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
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.stereotype.Component;

import com.alibaba.fastjson.JSON;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.log.annotation.OperLog;
import com.ruoyi.common.log.enums.BusinessStatus;
import com.ruoyi.common.log.event.SysOperLogEvent;
import com.ruoyi.common.utils.AddressUtils;
import com.ruoyi.common.utils.IpUtils;
import com.ruoyi.common.utils.ServletUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.spring.SpringContextHolder;
import com.ruoyi.system.domain.SysOperLog;

import lombok.extern.slf4j.Slf4j;

这部分代码声明了包名,并导入了一系列所需的类和包,包括用于 AOP 的 AspectJ 相关类、处理 HTTP 请求的类、日志工具类、常量类、自定义注解类、枚举类、工具类以及系统特定的领域类等。

2. 切面定义部分

@Aspect
@Slf4j
@Component
public class OperLogAspect
{
    //...
}

  • @Aspect注解表明这是一个切面类。
  • @Slf4j注解用于引入日志记录功能,方便在类中进行日志输出。
  • @Component注解使得这个类可以被 Spring 容器自动扫描和管理。

3. 切入点定义部分

// 配置织入点
@Pointcut("@annotation(com.ruoyi.common.log.annotation.OperLog)")
public void logPointCut()
{
}

使用@Pointcut注解定义了一个切入点,指定被com.ruoyi.common.log.annotation.OperLog注解标记的方法将被这个切面拦截。

4. 后置返回通知部分

/**
 * 处理完请求后执行
 *
 * @param joinPoint 切点
 */
@AfterReturning(pointcut = "logPointCut()")
public void doAfterReturning(JoinPoint joinPoint)
{
    handleLog(joinPoint, null);
}

当被拦截的方法正常执行返回后,这个方法会被调用。它将调用handleLog方法进行日志处理,传入的异常参数为null表示没有异常发生。

5. 异常通知部分

/**
 * 拦截异常操作
 * 
 * @param joinPoint 切点
 * @param e         异常
 */
@AfterThrowing(value = "logPointCut()", throwing = "e")
public void doAfterThrowing(JoinPoint joinPoint, Exception e)
{
    handleLog(joinPoint, e);
}

当被拦截的方法抛出异常时,这个方法会被调用。它同样调用handleLog方法进行日志处理,并将异常对象传入。

6. 日志处理核心方法部分

protected void handleLog(final JoinPoint joinPoint, final Exception e)
{
    try
    {
        // 获得注解
        OperLog controllerLog = getAnnotationLog(joinPoint);
        if (controllerLog == null)
        {
            return;
        }
        // *========数据库日志=========*//
        SysOperLog operLog = new SysOperLog();
        operLog.setStatus(BusinessStatus.SUCCESS.ordinal());
        // 请求的地址
        HttpServletRequest request = ServletUtils.getRequest();
        String ip = IpUtils.getIpAddr(request);
        operLog.setOperIp(ip);
        operLog.setOperUrl(request.getRequestURI());
        operLog.setOperLocation(AddressUtils.getRealAddressByIP(ip));
        String username = request.getHeader(Constants.CURRENT_USERNAME);
        operLog.setOperName(username);
        if (e!= null)
        {
            operLog.setStatus(BusinessStatus.FAIL.ordinal());
            operLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000));
        }
        // 设置方法名称
        String className = joinPoint.getTarget().getClass().getName();
        String methodName = joinPoint.getSignature().getName();
        operLog.setMethod(className + "." + methodName + "()");
        // 设置请求方式
        operLog.setRequestMethod(request.getMethod());
        // 处理设置注解上的参数
        Object[] args = joinPoint.getArgs();
        getControllerMethodDescription(controllerLog, operLog, args);
        // 发布事件
        SpringContextHolder.publishEvent(new SysOperLogEvent(operLog));
    }
    catch (Exception exp)
    {
        // 记录本地异常日志
        log.error("==前置通知异常==");
        log.error("异常信息:{}", exp.getMessage());
        exp.printStackTrace();
    }
}

这是日志处理的核心方法。

  • 首先尝试获取方法上的OperLog注解,如果没有找到则直接返回。
  • 创建一个SysOperLog对象,并设置初始状态为成功。
  • 从当前请求中获取 IP 地址、URL、用户名等信息,并设置到日志对象中。如果有异常发生,则将日志状态设置为失败,并记录异常信息。
  • 设置方法名称和请求方式。
  • 调用getControllerMethodDescription方法处理注解上的参数。
  • 最后通过SpringContextHolder发布一个SysOperLogEvent事件,将日志对象传递出去以便进行后续处理。

7. 获取注解方法描述部分

/**
 * 获取注解中对方法的描述信息 用于Controller层注解
 * 
 * @param log     日志
 * @param operLog 操作日志
 * @throws Exception
 */
public void getControllerMethodDescription(OperLog log, SysOperLog operLog, Object[] args) throws Exception
{
    // 设置action动作
    operLog.setBusinessType(log.businessType().ordinal());
    // 设置标题
    operLog.setTitle(log.title());
    // 设置操作人类别
    operLog.setOperatorType(log.operatorType().ordinal());
    // 是否需要保存request,参数和值
    if (log.isSaveRequestData())
    {
        // 获取参数的信息,传入到数据库中。
        setRequestValue(operLog, args);
    }
}

这个方法根据注解中的信息设置日志对象的一些属性,包括业务类型、标题、操作人类别等。如果注解中指定要保存请求参数,则调用setRequestValue方法。

8. 设置请求值部分

/**
 *  获取请求的参数,放到log中
 * 
 * @param operLog 操作日志
 * @throws Exception 异常
 */
private void setRequestValue(SysOperLog operLog, Object[] args) throws Exception
{
    List<?> param = new ArrayList<>(Arrays.asList(args)).stream().filter(p ->!(p instanceof ServletResponse))
           .collect(Collectors.toList());
    log.debug("args:{}", param);
    String params = JSON.toJSONString(param, true);
    operLog.setOperParam(StringUtils.substring(params, 0, 2000));
}

这个方法过滤掉请求参数中的ServletResponse类型对象,将其他参数转换为 JSON 字符串,并截取前 2000 个字符作为操作参数保存到日志对象中。

9. 获取注解部分

/**
 * 是否存在注解,如果存在就获取
 */
private OperLog getAnnotationLog(JoinPoint joinPoint) throws Exception
{
    Signature signature = joinPoint.getSignature();
    MethodSignature methodSignature = (MethodSignature) signature;
    Method method = methodSignature.getMethod();
    if (method!= null)
    {
        return method.getAnnotation(OperLog.class);
    }
    return null;
}

这个方法通过反射获取方法上的OperLog注解,如果方法上没有该注解则返回null

上述注解代码(@OperLog)和 AOP 代码(OperLogAspect类)是放在一起起作用的。

@OperLog注解用于标记需要记录操作日志的方法或参数。而OperLogAspect类是一个 AOP 切面,它通过定义切入点来拦截被@OperLog注解标记的方法。在方法执行前后或出现异常时,OperLogAspect类中的方法会被调用,以获取注解信息并进行相应的操作日志记录处理。

具体来说,当一个方法被@OperLog注解标记后,在运行时,AOP 框架会根据OperLogAspect中定义的切入点检测到这个方法的调用,并触发相应的通知方法(如doAfterReturningdoAfterThrowing)。这些通知方法会调用handleLog方法,在其中获取@OperLog注解的信息,创建操作日志对象,设置各种属性,并最终发布事件进行后续处理。它们是协同工作,共同实现操作日志记录功能的。

com/ruoyi/common/log/enums/BusinessStatus.java

package com.ruoyi.common.log.enums;

/**
 * 操作状态
 * 
 * @author ruoyi
 */
public enum BusinessStatus
{
    /**
     * 成功
     */
    SUCCESS,

    /**
     * 失败
     */
    FAIL,
}

以下是对这段代码的分析:

一、代码功能概述

这段 Java 代码定义了一个枚举类型BusinessStatus,用于表示业务操作的状态,目前只有两种状态:成功(SUCCESS)和失败(FAIL)。

二、枚举值解释

  1. SUCCESS:表示业务操作成功完成。
  2. FAIL:表示业务操作失败。

三、使用场景

在应用程序中,当需要记录业务操作的结果状态时,可以使用这个枚举类型。例如,在记录操作日志时,可以根据业务操作的实际结果设置相应的状态值,以便后续的分析和处理。

例如:

import com.ruoyi.common.log.enums.BusinessStatus;

public class SomeBusinessClass {
    public void doSomeBusiness() {
        // 假设这里进行了一些业务操作
        boolean operationResult = true; // 或者 false,表示业务操作成功或失败
        BusinessStatus status = operationResult? BusinessStatus.SUCCESS : BusinessStatus.FAIL;
        // 可以进一步使用这个状态值进行日志记录或其他处理
    }
}

com/ruoyi/common/log/enums/BusinessType.java

package com.ruoyi.common.log.enums;

/**
 * 业务操作类型
 * 
 * @author ruoyi
 */
public enum BusinessType
{
    /**
     * 其它
     */
    OTHER,

    /**
     * 新增
     */
    INSERT,

    /**
     * 修改
     */
    UPDATE,

    /**
     * 删除
     */
    DELETE,

    /**
     * 授权
     */
    GRANT,

    /**
     * 导出
     */
    EXPORT,

    /**
     * 导入
     */
    IMPORT,

    /**
     * 强退
     */
    FORCE,

    /**
     * 生成代码
     */
    GENCODE,
    
    /**
     * 清空
     */
    CLEAN,
}

以下是对这段代码的分析:

一、代码功能概述

这段 Java 代码定义了一个枚举类型BusinessType,用于表示不同的业务操作类型。

二、枚举值解释

  1. OTHER:表示其他未明确分类的业务操作类型。
  2. INSERT:表示新增操作,例如向数据库中插入一条记录。
  3. UPDATE:表示修改操作,更新已有的数据。
  4. DELETE:表示删除操作,移除特定的数据。
  5. GRANT:表示授权操作,可能是对用户或资源进行权限授予。
  6. EXPORT:表示导出操作,将数据导出到外部文件或其他系统。
  7. IMPORT:表示导入操作,从外部来源导入数据到系统中。
  8. FORCE:表示强制操作,例如强制退出某个进程或用户。
  9. GENCODE:表示生成代码操作,可能是根据特定的模板或规则生成代码。
  10. CLEAN:表示清空操作,清除特定的数据或资源。

三、使用场景

在应用程序中,当需要记录业务操作的具体类型时,可以使用这个枚举类型。例如,在操作日志记录中,可以根据实际执行的业务操作设置相应的业务类型值,以便后续对不同类型的操作进行统计分析或审计。

例如:

import com.ruoyi.common.log.enums.BusinessType;

public class SomeBusinessClass {
    public void performBusinessOperation() {
        // 假设这里进行了某种业务操作
        BusinessType operationType = BusinessType.INSERT; // 根据实际操作设置类型
        // 可以进一步使用这个类型值进行日志记录或其他处理
    }
}

src/main/java/com/ruoyi/common/log/enums/OperatorType.java

package com.ruoyi.common.log.enums;

/**
 * 操作人类别
 * 
 * @author ruoyi
 */
public enum OperatorType
{
    /**
     * 其它
     */
    OTHER,

    /**
     * 后台用户
     */
    MANAGE,

    /**
     * 手机端用户
     */
    MOBILE
}

以下是对这段代码的分析:

一、代码功能概述

这段 Java 代码定义了一个枚举类型 OperatorType,用于表示操作人的类别。

二、枚举值解释

  1. OTHER:表示其他未明确分类的操作人类别。
  2. MANAGE:表示后台用户,通常是指通过管理后台进行操作的用户。
  3. MOBILE:表示手机端用户,即通过手机应用进行操作的用户。

三、使用场景

在应用程序中,当需要记录操作的发起者类别时,可以使用这个枚举类型。例如,在操作日志记录中,可以根据实际的操作人类型设置相应的枚举值,以便后续对不同类别的操作人进行统计分析或审计。

import com.ruoyi.common.log.enums.OperatorType;

public class SomeBusinessClass {
    public void performOperation() {
        // 假设这里进行了某种操作,确定操作人类别
        OperatorType operatorType = OperatorType.MANAGE; // 根据实际情况设置
        // 可以进一步使用这个类型值进行日志记录或其他处理
    }
}

这三段代码分别定义了三个枚举类型,在日志记录等场景中有以下作用:

一、BusinessStatus枚举的作用

  1. 明确业务操作结果状态:提供了一种清晰的方式来表示业务操作是成功还是失败。在记录操作日志时,可以准确地记录操作的最终状态,方便后续进行问题排查和分析。
  2. 统一状态表示:使得不同部分的代码在处理业务操作结果时,有一个统一的状态表示方式,避免了使用魔法值或字符串来表示状态,提高了代码的可读性和可维护性。

二、BusinessType枚举的作用

  1. 分类业务操作类型:明确了各种常见的业务操作类型,如新增、修改、删除、授权、导入、导出等。在记录操作日志时,可以准确记录具体的业务操作类型,便于后续对不同类型的操作进行统计分析、审计和问题排查。
  2. 提高代码可读性:使得代码中对业务操作类型的表示更加清晰明了,其他开发人员阅读代码时可以快速理解正在进行的业务操作属于哪种类型。
  3. 方便扩展:如果未来有新的业务操作类型需要记录,可以方便地在这个枚举中添加新的枚举值,而不需要在代码中到处修改硬编码的字符串或数字。

三、OperatorType枚举的作用

  1. 区分操作人类别:明确了操作人的不同类别,如后台用户和手机端用户等。在记录操作日志时,可以记录操作是由哪种类型的用户发起的,有助于了解系统的使用情况和进行用户行为分析。
  2. 统一表示操作人:提供了一种统一的方式来表示操作人的类别,避免了在代码中使用不同的字符串或数字来表示操作人类别,提高了代码的可读性和可维护性。
  3. 方便后续处理:根据不同的操作人类别,可以进行不同的处理或分析。例如,可以针对不同类型的用户制定不同的安全策略或统计分析方法。

com/ruoyi/common/log/event/SysLogininforEvent.java

package com.ruoyi.common.log.event;

import org.springframework.context.ApplicationEvent;

import com.ruoyi.system.domain.SysLogininfor;

/**
 * 系统日志事件
 */
public class SysLogininforEvent extends ApplicationEvent
{
    //
    private static final long serialVersionUID = -9084676463718966036L;

    public SysLogininforEvent(SysLogininfor source)
    {
        super(source);
    }
}

以下是对这段代码的分析:

一、代码功能概述

这段 Java 代码定义了一个自定义的事件类SysLogininforEvent,用于在特定的系统日志相关场景中进行事件通知和处理。它继承自ApplicationEvent,是 Spring 框架中用于表示应用程序事件的基类。

二、主要属性和构造函数

  1. serialVersionUID:用于在序列化和反序列化过程中确保版本兼容性。
  2. 构造函数SysLogininforEvent(SysLogininfor source):接收一个SysLogininfor类型的对象作为事件的源。这个构造函数调用了父类的构造函数,将事件源传递给父类,以便在事件处理过程中可以获取到事件的发起者。

三、使用场景

在一个基于 Spring 的应用程序中,可以使用这个事件类来实现系统日志相关的事件通知机制。例如,当有用户登录系统时,可以触发这个事件,其他组件可以监听这个事件并进行相应的处理,如记录登录日志、发送通知等。

以下是一个简单的示例,展示如何使用这个事件类:

import com.ruoyi.common.log.event.SysLogininforEvent;
import com.ruoyi.system.domain.SysLogininfor;
import org.springframework.context.ApplicationContext;

public class ExampleUsage {
    public static void main(String[] args) {
        ApplicationContext context = // 获取 Spring 应用上下文
        SysLogininfor logininfor = new SysLogininfor();
        context.publishEvent(new SysLogininforEvent(logininfor));
    }
}

src/main/java/com/ruoyi/common/log/event/SysOperLogEvent.java

package com.ruoyi.common.log.event;

import org.springframework.context.ApplicationEvent;

import com.ruoyi.system.domain.SysOperLog;

/**
 * 系统日志事件
 */
public class SysOperLogEvent extends ApplicationEvent
{
    //
    private static final long serialVersionUID = 8905017895058642111L;

    public SysOperLogEvent(SysOperLog source)
    {
        super(source);
    }
}

以下是对这段代码的分析:

一、代码功能概述

这段 Java 代码定义了一个名为SysOperLogEvent的自定义事件类,用于表示与系统操作日志相关的事件。它继承自ApplicationEvent,这是 Spring 框架中用于表示应用程序事件的基类。

二、主要属性和构造函数

  1. serialVersionUID:用于在序列化和反序列化过程中确保版本兼容性。
  2. 构造函数SysOperLogEvent(SysOperLog source):接收一个SysOperLog类型的对象作为事件的源。通过调用父类的构造函数,将事件源传递给父类,以便在事件处理过程中可以获取到事件的发起者。

三、使用场景

在一个基于 Spring 的应用程序中,可以使用这个事件类来实现系统操作日志相关的事件通知机制。例如,当有一个重要的系统操作发生时,可以触发这个事件,其他组件可以监听这个事件并进行相应的处理,如将操作日志保存到数据库、发送通知给管理员等。

以下是一个简单的示例,展示如何使用这个事件类:

import com.ruoyi.common.log.event.SysOperLogEvent;
import com.ruoyi.system.domain.SysOperLog;
import org.springframework.context.ApplicationContext;

public class ExampleUsage {
    public static void main(String[] args) {
        ApplicationContext context = // 获取 Spring 应用上下文
        SysOperLog operLog = new SysOperLog();
        context.publishEvent(new SysOperLogEvent(operLog));
    }
}

这两段代码定义了两个自定义的事件类,其作用主要有以下几点:

一、实现事件驱动架构

  1. 解耦系统组件:通过定义事件类,可以将系统中的不同部分解耦。例如,系统日志的生成和处理可以由不同的组件负责,它们之间通过事件进行通信,而不是直接调用对方的方法。这样可以提高系统的可维护性和可扩展性。

二、便于扩展和定制

  1. 方便添加新的处理逻辑:如果需要对系统日志进行额外的处理,只需要添加一个新的事件监听器,而不需要修改日志生成的代码。例如,可以添加一个监听器来将日志发送到远程服务器,或者进行实时分析。
  2. 灵活定制处理方式:不同的应用场景可能需要对日志进行不同的处理。通过事件机制,可以根据具体需求定制不同的处理逻辑,而不影响系统的其他部分。

三、提高代码的可维护性和可读性

  1. 明确的事件处理流程:事件类的定义使得系统中的事件处理流程更加清晰。开发人员可以通过查看事件的发布和监听代码,快速了解系统中日志的生成和处理逻辑。
  2. 易于理解的代码结构:使用事件机制可以使代码结构更加清晰,易于理解。相比于直接在日志生成代码中进行各种处理,将处理逻辑分离到事件监听器中可以使代码更加模块化。

四、支持异步处理

  1. 提高系统性能:在某些情况下,可以将日志处理逻辑异步执行,不影响系统的主要业务流程。事件机制可以很容易地实现异步处理,提高系统的性能和响应速度。

总之,这两个事件类在系统日志处理中起到了重要的作用,帮助实现解耦、可扩展、可维护和高性能的系统架构。

com/ruoyi/common/log/listen/LogListener.java

package com.ruoyi.common.log.listen;

import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;
import org.springframework.scheduling.annotation.Async;

import com.ruoyi.common.log.event.SysLogininforEvent;
import com.ruoyi.common.log.event.SysOperLogEvent;
import com.ruoyi.system.domain.SysLogininfor;
import com.ruoyi.system.domain.SysOperLog;
import com.ruoyi.system.feign.RemoteLogService;

import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;

/**
 * 异步监听日志事件
 */
@Slf4j
@AllArgsConstructor
public class LogListener
{
    private final RemoteLogService remoteLogService;

    @Async
    @Order
    @EventListener(SysOperLogEvent.class)
    public void listenOperLog(SysOperLogEvent event)
    {
        SysOperLog sysOperLog = (SysOperLog) event.getSource();
        remoteLogService.insertOperlog(sysOperLog);
        log.info("远程操作日志记录成功:{}", sysOperLog);
    }

    @Async
    @Order
    @EventListener(SysLogininforEvent.class)
    public void listenLoginifor(SysLogininforEvent event)
    {
        SysLogininfor sysLogininfor = (SysLogininfor) event.getSource();
        remoteLogService.insertLoginlog(sysLogininfor);
        log.info("远程访问日志记录成功:{}", sysLogininfor);
    }
}

以下是对这段代码的分析:

一、代码功能概述

这段 Java 代码定义了一个名为LogListener的类,用于异步监听系统中的日志事件,并将日志信息发送到远程服务进行记录。它使用了 Spring 的事件监听机制和异步处理功能,以提高系统的性能和响应速度。

二、主要属性和方法

  1. remoteLogService:这是一个远程日志服务的接口实例,通过构造函数注入。它用于将日志信息发送到远程服务进行存储。
  2. listenOperLog(SysOperLogEvent event)方法:
    • 使用@Async注解表示该方法是异步执行的。这样可以在不阻塞主线程的情况下处理日志事件。
    • 使用@Order注解可以指定方法的执行顺序,如果有多个事件监听器,可以通过这个注解来控制它们的执行顺序。
    • 使用@EventListener(SysOperLogEvent.class)注解表示该方法监听SysOperLogEvent类型的事件。当系统中发生与系统操作日志相关的事件时,这个方法会被自动调用。
    • 在方法内部,首先从事件中获取SysOperLog对象,然后调用remoteLogServiceinsertOperlog方法将操作日志信息发送到远程服务进行记录。最后,打印一条日志信息表示远程操作日志记录成功。
  3. listenLoginifor(SysLogininforEvent event)方法:
    • listenOperLog方法类似,这个方法监听SysLogininforEvent类型的事件,用于处理系统登录日志。
    • 从事件中获取SysLogininfor对象,然后调用remoteLogServiceinsertLoginlog方法将登录日志信息发送到远程服务进行记录。最后,打印一条日志信息表示远程访问日志记录成功。

三、使用场景和优势

  1. 异步处理:通过使用异步处理,可以在不阻塞主线程的情况下处理日志事件,提高系统的性能和响应速度。特别是在高并发的系统中,异步处理可以避免日志记录操作影响系统的主要业务流程。
  2. 事件监听机制:Spring 的事件监听机制使得代码更加松散耦合。日志记录的逻辑与业务逻辑分离,通过监听事件来触发日志记录操作,而不是在业务代码中直接调用日志记录方法。这样可以提高代码的可维护性和可扩展性。
  3. 远程日志服务:将日志信息发送到远程服务进行记录,可以实现集中式的日志管理。在分布式系统中,这种方式可以方便地收集和分析来自不同节点的日志信息。同时,远程日志服务可以提供更强大的存储和查询功能,方便进行日志的审计和故障排查。

总之,LogListener类通过使用 Spring 的异步处理和事件监听机制,实现了对系统日志的异步监听和远程记录,提高了系统的性能和可维护性。

com/ruoyi/common/log/publish/PublishFactory.java

package com.ruoyi.common.log.publish;

import javax.servlet.http.HttpServletRequest;

import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.log.event.SysLogininforEvent;
import com.ruoyi.common.utils.AddressUtils;
import com.ruoyi.common.utils.IpUtils;
import com.ruoyi.common.utils.ServletUtils;
import com.ruoyi.common.utils.spring.SpringContextHolder;
import com.ruoyi.system.domain.SysLogininfor;

import eu.bitwalker.useragentutils.UserAgent;

public class PublishFactory
{
    /**
     * 记录登陆信息
     * 
     * @param username 用户名
     * @param status 状态
     * @param message 消息
     * @param args 列表
     */
    public static void recordLogininfor(final String username, final String status, final String message,
            final Object ... args)
    {
        HttpServletRequest request = ServletUtils.getRequest();
        final UserAgent userAgent = UserAgent.parseUserAgentString(request.getHeader("User-Agent"));
        final String ip = IpUtils.getIpAddr(request);
        // 获取客户端操作系统
        String os = userAgent.getOperatingSystem().getName();
        // 获取客户端浏览器
        String browser = userAgent.getBrowser().getName();
        // 封装对象
        SysLogininfor logininfor = new SysLogininfor();
        logininfor.setLoginName(username);
        logininfor.setIpaddr(ip);
        logininfor.setLoginLocation(AddressUtils.getRealAddressByIP(ip));
        logininfor.setBrowser(browser);
        logininfor.setOs(os);
        logininfor.setMsg(message);
        // 日志状态
        if (Constants.LOGIN_SUCCESS.equals(status) || Constants.LOGOUT.equals(status))
        {
            logininfor.setStatus(Constants.SUCCESS);
        }
        else if (Constants.LOGIN_FAIL.equals(status))
        {
            logininfor.setStatus(Constants.FAIL);
        }
        // 发布事件
        SpringContextHolder.publishEvent(new SysLogininforEvent(logininfor));
    }
}

以下是对这段代码的分析:

一、代码功能概述

这段 Java 代码定义了一个名为PublishFactory的类,用于记录用户登录信息并发布相应的事件。它通过获取用户的请求信息、解析用户代理字符串,获取用户的 IP 地址、操作系统、浏览器等信息,并将这些信息封装成一个SysLogininfor对象,最后发布一个SysLogininforEvent事件,以便其他组件可以监听并处理这个事件。

二、主要方法分析

  1. recordLogininfor(final String username, final String status, final String message, final Object... args)方法:
    • 这个方法接收用户名、状态、消息以及可变参数列表作为参数。
    • 首先,通过ServletUtils.getRequest()获取当前的HttpServletRequest对象。
    • 然后,使用UserAgent.parseUserAgentString(request.getHeader("User-Agent"))解析用户代理字符串,获取用户的操作系统和浏览器信息。
    • 接着,通过IpUtils.getIpAddr(request)获取用户的 IP 地址,并使用AddressUtils.getRealAddressByIP(ip)获取用户的实际地址。
    • 之后,创建一个SysLogininfor对象,并将获取到的用户信息、登录状态、消息等设置到这个对象中。
    • 根据不同的登录状态(成功、失败、退出等)设置SysLogininfor对象的状态。
    • 最后,发布一个SysLogininforEvent事件,将封装好的登录信息对象传递出去。

三、使用场景和优势

  1. 使用场景:

    • 在用户登录、退出或登录失败等场景中,可以调用这个方法来记录用户的登录信息。例如,在用户登录成功后,可以调用recordLogininfor方法,传入用户名、登录状态(成功)、消息等参数,记录用户的登录日志。
    • 其他组件可以监听SysLogininforEvent事件,对登录信息进行进一步的处理,如保存到数据库、发送通知等。
  2. 优势:

    • 代码封装性好:将记录登录信息的逻辑封装在一个方法中,方便在不同的地方调用。
    • 事件驱动:通过发布事件的方式,使得登录信息的记录与其他处理逻辑解耦,提高了代码的可维护性和可扩展性。
    • 获取用户信息方便:通过解析用户代理字符串,可以方便地获取用户的操作系统和浏览器信息,为用户行为分析提供了更多的数据。

com/ruoyi/common/log/LogAutoConfiguration.java

package com.ruoyi.common.log;

import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;

import com.ruoyi.common.log.aspect.OperLogAspect;
import com.ruoyi.common.log.listen.LogListener;
import com.ruoyi.system.feign.RemoteLogService;

import lombok.AllArgsConstructor;

@EnableAsync
@Configuration
@AllArgsConstructor
@ConditionalOnWebApplication
public class LogAutoConfiguration
{
    private final RemoteLogService logService;

    @Bean
    public LogListener sysOperLogListener()
    {
        return new LogListener(logService);
    }

    @Bean
    public OperLogAspect operLogAspect()
    {
        return new OperLogAspect();
    }
}

以下是对这段代码的分析:

一、代码功能概述

这段 Java 代码是一个 Spring 的配置类,用于配置与日志相关的组件,并在满足特定条件下(Web 应用环境)自动生效。它主要实现了以下功能:

  1. 启用异步处理:通过@EnableAsync注解启用 Spring 的异步处理功能,使得在处理日志相关操作时可以异步执行,提高系统性能。
  2. 配置类注解:
    • @Configuration:表明这是一个配置类,用于定义 Spring Bean。
    • @AllArgsConstructor:自动生成一个包含所有参数的构造函数,方便依赖注入。
    • @ConditionalOnWebApplication:只有在 Web 应用环境下这个配置类才会生效。

二、主要方法和 Bean 定义

  1. sysOperLogListener()方法:

    • 创建一个LogListener实例,并将RemoteLogService注入其中。LogListener用于异步监听日志事件,并将日志信息发送到远程服务进行记录。
    • 将这个实例作为一个 Spring Bean 返回,使得在其他地方可以通过依赖注入获取这个监听器。
  2. operLogAspect()方法:

    • 创建一个OperLogAspect实例。OperLogAspect是一个 AOP 切面,用于拦截被特定注解标记的方法,并记录操作日志。
    • 将这个实例作为一个 Spring Bean 返回,使得 AOP 框架可以自动识别并应用这个切面。

三、使用场景和优势

  1. 使用场景:

    • 在基于 Spring 的 Web 应用中,这个配置类可以自动配置日志相关的组件,包括日志切面和日志监听器。当应用中需要记录操作日志并发送到远程服务时,可以直接使用这些组件,无需手动配置。
    • 开发人员可以专注于业务逻辑的实现,而无需关心日志记录的具体实现细节。

以下是这些代码的使用场景及相关代码示例:

整体使用场景概述

这些代码主要用于在一个基于 Spring 的 Web 应用中实现操作日志和登录日志的记录,并将这些日志信息发送到远程服务进行存储和管理。通过使用自定义注解、AOP 切面、事件机制和异步处理等技术,实现了对系统操作的全面监控和日志记录,提高了系统的可维护性和可扩展性。

具体使用场景及代码示例

  1. 记录操作日志
    • 使用场景:当开发人员希望记录某个特定方法的执行情况作为操作日志时,可以在该方法上添加@OperLog注解。例如,在一个用户管理服务中,当添加用户的方法被调用时,记录这个操作。
    • 代码示例:

package com.example.service;

import com.ruoyi.common.log.annotation.OperLog;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    @OperLog(title = "添加用户", businessType = com.ruoyi.common.log.enums.BusinessType.INSERT)
    public void addUser(String username, String password) {
        // 添加用户的逻辑代码
    }
}

  1. 记录登录日志
    • 使用场景:在用户登录、退出或登录失败时,记录用户的登录信息。例如,在用户登录成功后,调用PublishFactory.recordLogininfor方法记录登录日志。
    • 代码示例:

package com.example.controller;

import com.ruoyi.common.log.publish.PublishFactory;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class LoginController {

    @PostMapping("/login")
    public String login(@RequestBody LoginRequest request) {
        // 假设这里进行登录验证逻辑
        boolean loginSuccess = true;
        if (loginSuccess) {
            PublishFactory.recordLogininfor(request.getUsername(), "LOGIN_SUCCESS", "登录成功");
            return "登录成功";
        } else {
            PublishFactory.recordLogininfor(request.getUsername(), "LOGIN_FAIL", "登录失败");
            return "登录失败";
        }
    }
}

class LoginRequest {
    private String username;
    private String password;

    // 省略 getter 和 setter 方法
}

  1. 异步处理日志事件

    • 使用场景:当系统中发生操作日志或登录日志事件时,LogListener会异步地将这些日志信息发送到远程服务进行记录。这样可以避免在用户请求的同步处理过程中进行耗时的日志记录操作,提高系统的响应速度。
    • 代码示例:在配置类中已经定义了LogListenerOperLogAspect作为 Spring Bean,当系统运行时,Spring 会自动管理这些组件,并在相应的事件发生时触发它们的处理逻辑。无需手动调用这些组件的代码,它们会在后台自动工作。
  2. 远程日志服务调用

    • 使用场景:LogListener中的listenOperLoglistenLoginifor方法会调用RemoteLogServiceinsertOperloginsertLoginlog方法,将操作日志和登录日志发送到远程服务进行存储。
    • 假设RemoteLogService的实现如下:
package com.example.service;

import com.ruoyi.system.feign.RemoteLogService;
import org.springframework.stereotype.Service;

@Service
public class RemoteLogServiceImpl implements RemoteLogService {

    @Override
    public void insertOperlog(Object operLog) {
        // 调用远程服务接口或使用其他方式将操作日志存储到远程
        System.out.println("插入操作日志到远程服务:" + operLog);
    }

    @Override
    public void insertLoginlog(Object loginLog) {
        // 调用远程服务接口或使用其他方式将登录日志存储到远程
        System.out.println("插入登录日志到远程服务:" + loginLog);
    }
}

通过以上代码示例,可以看到这些组件在一个 Web 应用中的具体使用方式,实现了对系统操作和用户登录的全面日志记录,并将日志信息异步发送到远程服务进行存储和管理。

优势:

  • 自动化配置:通过使用 Spring 的自动配置功能,减少了手动配置的工作量,提高了开发效率。
  • 可扩展性:如果需要对日志记录进行扩展或定制,可以通过修改或扩展这些组件来实现,而不影响其他部分的代码。
  • 异步处理:启用异步处理可以提高系统性能,避免日志记录操作影响系统的主要业务流程

以下是对上述两段代码的作用分析:

一、UserService类中的代码作用

  1. 引入@OperLog注解:通过在addUser方法上添加@OperLog注解,表明这个方法的执行需要被记录为操作日志。当这个方法被调用时,AOP 切面(OperLogAspect)会拦截这个方法的执行,并根据注解中的信息记录操作日志。
  2. 业务逻辑与日志记录分离:开发人员可以专注于实现业务逻辑(添加用户的功能),而无需在业务方法中直接编写日志记录代码。日志记录的逻辑由 AOP 切面和相关的日志处理组件(如OperLogAspectLogListener等)来处理,提高了代码的可维护性和可扩展性。

二、LoginController类中的代码作用

  1. 处理用户登录请求:LoginController类中的login方法处理用户的登录请求。它接收用户提交的登录信息(用户名和密码),进行登录验证逻辑。
  2. 记录登录日志:根据登录验证的结果,调用PublishFactory.recordLogininfor方法记录用户的登录日志。如果登录成功,将状态设置为"LOGIN_SUCCESS"和相应的消息;如果登录失败,将状态设置为"LOGIN_FAIL"和失败消息。
  3. 提供用户反馈:根据登录结果,返回相应的响应给用户,告知用户登录是否成功。

综上所述,这两段代码分别在不同的场景下实现了操作日志和登录日志的记录功能,为系统的监控和审计提供了重要的信息。同时,通过使用注解、AOP 切面和事件机制等技术,实现了业务逻辑与日志记录的分离,提高了代码的可维护性和可扩展性。

  • 23
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值