一个注解搞定,记录项目日志

Spring aop + 自定义注解 实现操作日志监控 Spring Aop + 自定义注解 实现操作日志监控 阅读详情

最近正在录制 TienChin 项目视频~采用 Spring Boot+Vue3 技术栈,里边会涉及到各种好玩的技术,小伙伴们来和我一起做一个完成率超 90% 的项目。

前段时间做了一个支线任务,现在还是回到我们的主线上面来,继续完成 TienChin 项目。

小伙伴们知道我最近在录制 TienChin 项目视频教程,这是一个基于 RuoYi-Vue 脚手架的项目,用过这个脚手架的小伙伴可能知道,这个脚手架有一个功能,就是如果你需要记录下来一个接口的操作信息,那么只需要在 Controller 接口中加上一个 @Log 注解即可,非常方便,最终记录到数据库中的日志类似下面这样:

可能有小伙伴要吐槽了,这不该记录到 Elasticsearch 中吗?上 ELK 啊!怎么说呢,如果你会将日志存入数据库,那么将来存 Elasticsearch 其实非常 Easy,结合我之前录制的 Es 视频,我相信这个问题你能自己搞定。

今天我们主要来分析一下这个脚手架中 @Log 注解的玩法。

1. 日志表设计

先来看看日志表的设计。

CREATE TABLE `sys_oper_log` (
  `oper_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '日志主键',
  `title` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '模块标题',
  `business_type` int(2) DEFAULT '0' COMMENT '业务类型(0其它 1新增 2修改 3删除)',
  `method` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '方法名称',
  `request_method` varchar(10) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '请求方式',
  `operator_type` int(1) DEFAULT '0' COMMENT '操作类别(0其它 1后台用户 2手机端用户)',
  `oper_name` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '操作人员',
  `dept_name` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '部门名称',
  `oper_url` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '请求URL',
  `oper_ip` varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '主机地址',
  `oper_location` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '操作地点',
  `oper_param` varchar(2000) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '请求参数',
  `json_result` varchar(2000) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '返回参数',
  `status` int(1) DEFAULT '0' COMMENT '操作状态(0正常 1异常)',
  `error_msg` varchar(2000) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '错误消息',
  `oper_time` datetime DEFAULT NULL COMMENT '操作时间',
  PRIMARY KEY (`oper_id`)
) ENGINE=InnoDB AUTO_INCREMENT=280 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='操作日志记录';

这里各个字段的含义我来解释下:

  • oper_id:这是日志主键,自增的。
  • title:这个标题,一般就是用来说明这个操作是干嘛的,例如删除一个用户、添加一条线索等待。
  • business_type:这个是指业务类型,一般来说有:添加、修改、删除、导入、导出等等。
  • method:执行的接口方法名称。
  • request_method:这个是指请求的方法类型,如 GET、POST、PUT、DELETE 等。
  • operator_type:这个是指操作类别,分为三种:后台用户、手机用户以及其他。
  • oper_name:操作人员的名称。
  • dept_name:操作人员所属的部门。
  • oper_url:请求的 URL 地址。
  • oper_ip:请求的 IP 地址。
  • oper_location:请求 IP 地址所属的区域。
  • oper_param:请求的参数。
  • json_result:响应的 JSON 参数。
  • status:操作的状态,成功还是失败。
  • error_msg:如果是失败,失败的内容是什么。
  • oper_time:操作的时间。

这里给出来的就这些字段,基本上满足项目需求了,如果不够的话,也可以自定添加即可。

2. 注解的定义

再来看看 @Log 注解的定义,该注解位于
org.javaboy.tienchin.common.annotation.Log :

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

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

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

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

    /**
     * 是否保存响应的参数
     */
    public boolean isSaveResponseData() default true;
}

这个注解一共有五个属性,结合上面表的定义,五个属性含义很好理解,不多说。

3. 注解的解析

经典搭配:自定义注解+ AOP 切面。解析该注解的 AOP 切面是 LogAspect,位于
org.javaboy.tienchin.framework.aspectj.LogAspect :

@Aspect
@Component
public class LogAspect {
    private static final Logger log = LoggerFactory.getLogger(LogAspect.class);

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

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

    protected void handleLog(final JoinPoint joinPoint, Log controllerLog, final Exception e, Object jsonResult) {
        try {
            // 获取当前的用户
            LoginUser loginUser = SecurityUtils.getLoginUser();

            // *========数据库日志=========*//
            SysOperLog operLog = new SysOperLog();
            operLog.setStatus(BusinessStatus.SUCCESS.ordinal());
            // 请求的地址
            String ip = IpUtils.getIpAddr(ServletUtils.getRequest());
            operLog.setOperIp(ip);
            operLog.setOperUrl(ServletUtils.getRequest().getRequestURI());
            if (loginUser != null) {
                operLog.setOperName(loginUser.getUsername());
            }

            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(ServletUtils.getRequest().getMethod());
            // 处理设置注解上的参数
            getControllerMethodDescription(joinPoint, controllerLog, operLog, jsonResult);
            // 保存数据库
            AsyncManager.me().execute(AsyncFactory.recordOper(operLog));
        } catch (Exception exp) {
            // 记录本地异常日志
            log.error("==前置通知异常==");
            log.error("异常信息:{}", exp.getMessage());
            exp.printStackTrace();
        }
    }

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

    /**
     * 获取请求的参数,放到log中
     *
     * @param operLog 操作日志
     * @throws Exception 异常
     */
    private void setRequestValue(JoinPoint joinPoint, SysOperLog operLog) throws Exception {
        String requestMethod = operLog.getRequestMethod();
        if (HttpMethod.PUT.name().equals(requestMethod) || HttpMethod.POST.name().equals(requestMethod)) {
            String params = argsArrayToString(joinPoint.getArgs());
            operLog.setOperParam(StringUtils.substring(params, 0, 2000));
        } else {
            Map<?, ?> paramsMap = (Map<?, ?>) ServletUtils.getRequest().getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
            operLog.setOperParam(StringUtils.substring(paramsMap.toString(), 0, 2000));
        }
    }

    /**
     * 参数拼装
     */
    private String argsArrayToString(Object[] paramsArray) {
        String params = "";
        if (paramsArray != null && paramsArray.length > 0) {
            for (Object o : paramsArray) {
                if (StringUtils.isNotNull(o) && !isFilterObject(o)) {
                    try {
                        Object jsonObj = JSON.toJSON(o);
                        params += jsonObj.toString() + " ";
                    } catch (Exception e) {
                    }
                }
            }
        }
        return params.trim();
    }

    /**
     * 判断是否需要过滤的对象。
     *
     * @param o 对象信息。
     * @return 如果是需要过滤的对象,则返回true;否则返回false。
     */
    public boolean isFilterObject(final Object o) {
        Class<?> clazz = o.getClass();
        if (clazz.isArray()) {
            return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
        } else if (Collection.class.isAssignableFrom(clazz)) {
            Collection collection = (Collection) o;
            for (Object value : collection) {
                return value instanceof MultipartFile;
            }
        } else if (Map.class.isAssignableFrom(clazz)) {
            Map map = (Map) o;
            for (Object value : map.entrySet()) {
                Map.Entry entry = (Map.Entry) value;
                return entry.getValue() instanceof MultipartFile;
            }
        }
        return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
                || o instanceof BindingResult;
    }
}

大概跟小伙伴们捋一下这个切面的逻辑。

  1. 首先定义了两种不同类型的通知: 返回通知和异常通知 ,正常流程就是在返回通知中处理日志写入操作,但是如果系统不幸抛出异常,就在异常通知中处理日志写入操作(此时就多了一个异常对象)。
  2. 日志数据保存在 SysOperLog 对象中,采集各种日志数据都是常规操作,不多说。
  3. 当收集接口参数的时候,有两个需要注意的地方:1.如果请求类型是 PUT 或者 POST,就直接从接口的参数中获取想要的数据,但是接口的参数中可能会存在一些并不需要记录的内容,例如 HttpServletRequest、HttpServletResponse 或者文件上传对象 MultipartFile 等,这些类型的内容是不需要记录到日志中的,这里通过一个 isFilterObject 方法完成了数据过滤操作;2. 如果请求类型是 GET 或者 DELETE,则请求参数就直接从请求对象提取了。为什么设计成这样呢?很明显,直接从请求对象中提取参数是最为方便的,一行代码就搞定,但是如果请求类型是 PUT 或者 POST,就意味着请求参数是在请求体中,请求参数有可能是二进制数据(例如上传的文件),二进制数据就不好保存了,所以对于 POST 和 PUT 还是从接口参数中提取,然后过滤掉二进制数据即可。
  4. 数据收集齐了,接下来就是往数据库中写入了。因为我们现在用的 SpringMVC 这一套是线程阻塞的,也就是得等到服务端都处理完毕之后,接口才会给客户端响应,而写日志是一个业务无关的操作,所以可以直接放在一个子线程中去完成。在 RuoYi-Vue 脚手架中,使用了 Java JUC 中的 ScheduledExecutorService 去完成这个延时任务, AsyncManager.me().execute 方法实际上就是去执行一个延时任务,这个延时任务就是一个往数据库中写入一条记录。

4. 记录日志

弄好之后,日志注解具体用法如下:

@Log(title = "参数管理", businessType = BusinessType.EXPORT)
@PreAuthorize("@ss.hasPermi('system:config:export')")
@PostMapping("/export")
public void export(HttpServletResponse response, SysConfig config) {
    List<SysConfig> list = configService.selectConfigList(config);
    ExcelUtil<SysConfig> util = new ExcelUtil<SysConfig>(SysConfig.class);
    util.exportExcel(response, list, "参数数据");
}

好啦,这就是整体操作,实际上看起来并不难,后面要是有空的话,我把这个日志整成一个 ELK 的,和小伙伴们一起探讨。

我最近正在录制 TienChin 项目视频~采用 Spring Boot+Vue3 技术栈,里边会涉及到各种好玩的技术,小伙伴们来和我一起做一个完成率超 90% 的项目。

 

JAVA 使用自定义注解实现插入日志操作功能 目录1、新建数据库表:操作日志记录表 2、创建Entity实体类3、创建Annotation注解4、创建AOP切面类5、注解使用方法 阅读详情

相关推荐

电荷灵敏前置放大器噪声优化策略:从理论到实践

本文深入探讨了电荷灵敏前置放大器的噪声优化策略,从理论分析到实践应用。文章系统解析了并联电流噪声与串联电压噪声的来源,并引入噪声等效原理与“冷电容”模型作为统一分析工具。通过详解a、b、c三类噪声的频谱特性,为实战优化提供了清晰指导。核心策略包括选用高跨导、低漏电流FET,优化静态工作点,采用共源-共基结构,精心设计反馈网络,以及实施严格的PCB布局、屏蔽与接地,旨在全面提升微弱信号检测系统的信噪比。

weixin_29292431的博客 470

若依框架----源码分析(@Log)

若依框架----源码分析(@Log)

5489

用UDP协议在dos命令行里模拟一个聊天程序

/* 需求: 在dos命令行里模拟一个聊天程序 有收数据的部分,和发数据的部分,这两部分需要同时执行。 那就需要用到多线程技术。 一个线程控制收,一个线程控制发。 */ import java.io.*; import java.net.*; class C

s_x_x的记录 1956

用户操作日志(通过自定义注解和Aspect结合完成)

用户操作日志(通过自定义注解和Aspect结合完成)

xushuai2333333的博客 1151

window下移动设备/硬盘报错位置不可用

移动硬盘指示灯亮,但电脑上不显示 报错信息 : 无法访问E:\。参数错误。 报错原因: 移动硬盘打不开提示参数错误,是因为这个H盘的文件系统内部结构损坏导致的。要怎么恢复里面的数据呢? https://jingyan.baidu.com/article/cb5d6105a80242005c2fe0c3.html 解决办法 : 不用格式化修复方法 方法一: 1、单击–开始–运行–输入cmd; 2、...

qq_40907977的博客 4929

蓝鸟四轴飞控系统C程序V3.0(基础版本)深度解读

2. 解析遥控通道数据(油门 / 横滚 / 俯仰 / 偏航),转换为标准化值(-1000~+1000);3. 封装PID初始化(PID_Init,配置KP/KI/KD、输出限幅、积分限幅);,包含姿态角(Attitude_Data.Roll/Pitch/Yaw)、角速度(Gyro_Data.X/Y/Z)、遥控值(RC_Data[0~5])、电池电压(Bat_Volt)、电机PWM值(Motor_PWM[0~3])、系统状态(Sys_State),校验和通过Check_Sum()函数计算;

搏博的专栏 344

使用@LogRecordAnnotation注解搞定SpringBoot操作日志

源码仓库 https://github.com/mouzt/mzt-biz-log 直接用法 直接使用@LogRecordAnnotation注解即可记录日志。 /** * pefix:是拼接在 bizNo 上作为 log 的一个标识。避免 bizNo 都为整数 ID 的时候和其他的业务中的 ID 重复。比如订单 ID、用户 ID 等 * bizNo:就是业务的 ID,比如订单ID,我们查询的时候可以根据 bizNo 查询和它相关的操作日志 * success:方法调用成功后

DreamsArchitects的博客 6279

【AI大模型应用开发实战】大型语言模型评估指南白皮书(LLM Eval)

近年来,大型语言模型(LLM)在自然语言处理领域取得了重大进展,例如 GPT-3 和 Chat-GPT。这些模型经过大型数据集的训练,在文本相关任务中表现出卓越的能力,甚至超越了人类。本文将简要介绍如何验证 LLMs 性能的评估指标。自然语言处理(NLP)是人工智能的一个领域,涉及计算机和人类语言之间的交互。NLP 的基本任务之一是语言建模 (LM),它涉及构建统计模型来分析和生成自然语言。LM 已成为推进机器语言智能的关键方法,使机器能够执行机器翻译、情感分析和对话系统等任务。

AI天才研究院 1017

一个小小的注解,帮你搞定 SpringBoot 操作日志

至此,文章终于到了尾声。技术能力:先写岗位所需能力,再写加分能力,不要写无关能力;项目经历:只写明星项目,描述遵循 STAR 法则;简历印象:简历遵循三大原则:清晰,简短,必要,要有的放矢,不要海投;以及最后为大家准备的福利时间:简历模板+Java面试题+热门技术系列教程视频《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!” />至此,文章终于到了尾声。技术能力:先写岗位所需能力,再写加分能力,不要写无关能力;

2401_84415615的博客 1458

基于卷积神经网络的遥感图像地物分类

AI:89-基于卷积神经网络的遥感图像地物分类 基于卷积神经网络的遥感图像地物分类是一个令人兴奋的领域,它在环境监测、城市规划、农业、国防等领域具有广泛的应用。深度学习技术为我们提供了更准确、高效的工具,有望在未来进一步提高分类性能。在本文中,我们探讨了卷积神经网络的工作原理、数据准备、模型构建、训练和评估过程,并提供了多个代码示例。此外,我们讨论了地物分类的应用和挑战,以及未来的发展趋势。

一键难忘的博客 2717

NCCL 核心集体通信操作深度解析:从原理到优化实践

操作逻辑:若 N 个 GPU 各持有一段数据(大小为 S),通信后每个 GPU 会持有所有 N 段数据(总大小为 N×S)。典型场景:多卡推理结果聚合、模型并行中间结果同步。操作逻辑:N 个 GPU 各持有数据,通过归约(求和、取平均等)运算后,所有 GPU 获得相同结果。典型场景:数据并行训练的梯度聚合(如 SGD 优化器)。操作逻辑:N 个 GPU 各持有 N 份数据分片,通信后每个 GPU 获得所有 GPU 的对应分片。典型场景。

jundao1997的专栏 680

一个注解搞定 Spring Boot 日志!

一个注解搞定 Spring Boot 日志!

961

数字IC行业笔试面试全攻略

数字IC行业迎来快速发展,国产替代加速,技术创新活跃,应用领域持续扩展,但人才缺口扩大。职业发展路径清晰,从初级到首席工程师/架构师的技术路线,或转向管理岗位。薪资待遇优厚,应届生起薪25-35W,5-10年经验可达70-110W,架构师年薪超百万。薪资受公司平台、技术方向、学历背景、项目经验和地域因素影响。AI芯片、汽车电子等热门方向溢价明显,成功流片经验尤其重要。

2301_79143213的博客 761

爽!一个注解,搞定 SpringBoot 操作日志

来源:blog.youkuaiyun.com/weixin_43954303/article/details/113781801 使用方式 基本使用 源码 此组件解决的问题是:「谁」在「什么时间」对「什么」做了「什么事」 本组件目前针对 Spring-boot 做了 Autoconfig,如果是 SpringMVC,也可自己在 xml 初始化 bean 使用方式 基本使用 maven依赖添加SDK依赖 <dependency> <groupId>io.gi

m0_52987358的博客 1254

大中型超市收银系统(可直接运行)

光步前台收银系统系统功能:商品销售、商品退货、挂单、收银员权限、会员销售、单品取消、整单取消、商品查询、脱机销售、脱机数据上传,基本资料下载,开钱箱、小票打印、收款结算,支持多币种付款方式(现金、储值卡、礼券、微信等),支持多种促销。运行环境:.net 4.0登陆信息用户名:1001 密码:123456内置商品信息,商品编码可输入00001000 - 00010000,测试效果相关功能键F5 结算 F9挂单 F11商品查询 F8退货 Insert单品取消 Del整单取消 F12开钱箱F2 会员 Ctrl+Z退出系统 Home 锁屏 相关源码待公开

jre9下载 jdk9

(Java SE Runtime Environment 9.0) 9.0u175 官方最新版 【x86|x64】

【注解+aop 记录日志】关于如何使用自定义 @Log 注解实现 Spring AOP 操作日志记录功能的详细教程

📝【SpringAOP+注解实现操作日志教程】 通过自定义@Log注解结合SpringAOP,实现低侵入式操作日志记录。只需在方法上添加注解(如@Log(title="新增用户", businessType=INSERT)),即可自动记录方法参数、返回值、异常等信息,无需手动编码。 🔧 核心实现: 定义@Log注解配置模块名、操作类型等 编写切面类LogAspect,通过@AfterReturning和@AfterThrowing拦截目标方法 利用JoinPoint获取上下文信息(类

alan0721的博客 1069

怀旧版电脑GBC模拟器

再现原版GBC掌机的界面,电脑运行连游戏界面都一样大小,很怀旧,很真实,(英文版)强烈推荐,很小的文件,很难才找到的。

自定义注解妙用,一行代码搞定用户操作日志记录,你学会了吗?

所以对我而言,我要继续加倍努力,弥补自己技术上的不足,以及与科班大佬们基础上的差距。所以对我而言,我要继续加倍努力,弥补自己技术上的不足,以及与科班大佬们基础上的差距。其实我投简历的时候,都不太敢投递阿里。因为在阿里一面前已经过了字节的三次面试,投阿里的简历一直没被捞,所以以为简历就挂了。因为在阿里一面前已经过了字节的三次面试,投阿里的简历一直没被捞,所以以为简历就挂了。

2401_84437170的博客 1116

efm32 入门.pdf

本PDF共计22页,介绍的是操作驱动API,下载与否自己斟酌。

SpringBoot 一个注解,搞定业务操作日志记录

业务操作日志记录中包含了用户操作的功能名称、功能描述、操作人、操作时间和操作的参数报文,参数报文之所以选择存储在文件中,是因为正常情况下,是不需要知道具体的参数报文,只有在回滚操作的时候才会用到,可以根据上一次的参数报文逆向操作。

weixin_45738731的博客 1062

一个注解,搞定 SpringBoot 操作日志

来源:blog.youkuaiyun.com/weixin_43954303/article/details/113781801此组件解决的问题是:「谁」在「什么时间」对「什么」做了「什么事」本组件目...

Java笔记虾 996

一个注解,优雅搞定操作日志记录(含详细代码及注释)

AOP(Aspect-Oriented Programming),⾯向切⾯编程,说起AOP,几乎学过Spring框架的人都知道,它是Spring的三大核心思想之一(IOC:控制反转,DI:依赖注入,AOP:面向切面编程)。能够将那些与业务⽆关,却为业务模块所共同调⽤的逻辑或责任(例如事务处理、⽇志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。

weixin_59930657的博客 1536

Springboot使用@Aspect、自定义注解记录日志

本文主要为直接copy的工具类,开箱即用

拾一二的博客 3162
上一篇: 「Java」ArrayList 的工作原理
下一篇: 腾讯正式开源 Spring Cloud Tencent,打造一站式微服务解决方案
程序员职业指南
博客等级 码龄4年 2138粉丝 960原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值