Spring AOP 完成日志记录

1、技术目标

 

 

  • 掌握Spring AOP基本用法
  • 使用Spring AOP完成日志记录功能

 

 

提示:本文所用项目为"影片管理",参看

http://hotstrong.iteye.com/blog/1160153

本文基于"影片管理"项目进行了日志记录功能扩充

 

注意:本文所实现的项目(MyEclipse工程)已提供下载,数据库

脚本可参看《MyBatis 1章 入门(使用MyBatis完成CRUD)》

 

 

2、什么是AOP

 

AOP是Aspect Oriented Programming的缩写,意思是面向方面编程,AOP实际是GoF设计模式的延续

 

注意:关于AOP的详细介绍不是本文重点

 

 

3、关于Spring AOP的一些术语

 

  • 切面(Aspect):在Spring AOP中,切面可以使用通用类或者在普通类中以@Aspect 注解(@AspectJ风格)来实现
  • 连接点(Joinpoint):在Spring AOP中一个连接点代表一个方法的执行
  • 通知(Advice):在切面的某个特定的连接点(Joinpoint)上执行的动作。通知有各种类型,其中包括"around"、"before”和"after"等通知。许多AOP框架,包括Spring,都是以拦截器做通知模型, 并维护一个以连接点为中心的拦截器链
  • 切入点(Pointcut):定义出一个或一组方法,当执行这些方法时可产生通知,Spring缺省使用AspectJ切入点语法。

 

 

4、通知类型

 

  • 前置通知(@Before):在某连接点(join point)之前执行的通知,但这个通知不能阻止连接点前的执行(除非它抛出一个异常)
  • 返回后通知(@AfterReturning):在某连接点(join point)正常完成后执行的通知:例如,一个方法没有抛出任何异常,正常返回
  • 抛出异常后通知(@AfterThrowing):方法抛出异常退出时执行的通知
  • 后通知(@After):当某连接点退出的时候执行的通知(不论是正常返回还是异常退出)
  • 环绕通知(@Around):包围一个连接点(join point)的通知,如方法调用。这是最强大的一种通知类型,环绕通知可以在方法调用前后完成自定义的行为,它也会选择是否继续执行连接点或直接返回它们自己的返回值或抛出异常来结束执行

 

 

5、@AspectJ风格的AOP配置

 

Spring AOP配置有两种风格:

 

  • XML风格 = 采用声明形式实现Spring AOP 
  • AspectJ风格 = 采用注解形式实现Spring AOP

 

 

注意:本文采用AspectJ风格

 

6、使用准备

闲话少说,下面开始日志记录的准备工作

 

6.1)创建日志记录表(MySQL),

 

 

Sql代码   收藏代码
  1. CREATE TABLE `t_log` (  
  2.   `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,  
  3.   `userid` bigint(20) unsigned NOT NULL,  
  4.   `createdate` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建日期',  
  5.   `content` varchar(8000) NOT NULL DEFAULT '' COMMENT '日志内容',  
  6.   `operation` varchar(250) NOT NULL DEFAULT '' COMMENT '用户所做的操作',  
  7.   PRIMARY KEY (`id`)  
  8. ) ENGINE=InnoDB DEFAULT CHARSET=utf8;  

 

 

 

6.2)在经过了Spring Security的权限验证后,可以从Security中获取到

登录管理员的帐号,而日志记录表t_log中存储的是管理员id,所以需要通

过管理员的帐号查询出管理员id,创建管理员POJO、Mapper、Service,

代码及配置如下:

 

管理员POJO类:

 

 

Java代码   收藏代码
  1. package com.xxx.pojo;  
  2. public class Admin extends BaseDomain {  
  3.   
  4.     private String nickname;//管理员帐号  
  5.     private String passwd;//管理员密码  
  6.     private String phoneno;//联系电话  
  7.   
  8.     public String getNickname() {  
  9.         return nickname;  
  10.     }  
  11.     public void setNickname(String nickname) {  
  12.         this.nickname = nickname;  
  13.     }  
  14.     public String getPasswd() {  
  15.         return passwd;  
  16.     }  
  17.     public void setPasswd(String passwd) {  
  18.         this.passwd = passwd;  
  19.     }  
  20.     public String getPhoneno() {  
  21.         return phoneno;  
  22.     }  
  23.     public void setPhoneno(String phoneno) {  
  24.         this.phoneno = phoneno;  
  25.     }  
  26. }  

 

 

 

管理员Mapper接口与XML配置文件:

 

 

Java代码   收藏代码
  1. package com.xxx.dao;  
  2.   
  3. import com.xxx.pojo.Admin;  
  4. /** 
  5.  * 管理员Mapper接口 
  6.  */  
  7. public interface AdminMapper {  
  8.     /** 
  9.      * 获取指定帐号名的管理员 
  10.      */  
  11.      public Admin findAdminByNickname(String userName);  
  12. }  

 

 

 

Xml代码   收藏代码
  1. <?xml version="1.0" encoding="UTF-8"?>  
  2. <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"   
  3. "http://mybatis.org/dtd/mybatis-3-mapper.dtd">  
  4. <mapper namespace="com.xxx.dao.AdminMapper">  
  5.     <!-- 通过账号名称查询管理员 -->  
  6.     <select id="findAdminByNickname" parameterType="string" resultType="Admin">  
  7.     select * from t_admin where nickname=#{userName}  
  8.     </select>  
  9. </mapper>  

 

 

 

管理员Service接口与实现类:

 

 

Java代码   收藏代码
  1. package com.xxx.service;  
  2.   
  3. import com.xxx.pojo.Admin;  
  4.   
  5. /** 
  6.  * 管理员信息业务逻辑接口 
  7.  */  
  8. public interface AdminService {  
  9.     /** 
  10.      * 获取指定帐号名的管理员 
  11.      */  
  12.      public Admin findAdminByNickname(String userName);  
  13. }  

 

 

Java代码   收藏代码
  1. package com.xxx.service;  
  2.   
  3. import org.springframework.beans.factory.annotation.Autowired;  
  4. import com.xxx.dao.AdminMapper;  
  5. import com.xxx.pojo.Admin;  
  6.   
  7. public class AdminServiceImpl implements AdminService {  
  8.     @Autowired  
  9.     private AdminMapper adminMapper;//Mapper接口  
  10.       
  11.     public Admin findAdminByNickname(String userName) {  
  12.         return adminMapper.findAdminByNickname(userName);  
  13.     }  
  14. }  

 

 

6.3)创建日志记录POJO、Mapper、Service,代码及配置如下:

 

日志记录POJO类:

 

 

Java代码   收藏代码
  1. package com.xxx.pojo;  
  2.   
  3. import java.io.Serializable;  
  4. import java.util.Date;  
  5.   
  6. /** 
  7.  * 日志记录POJO 
  8.  */  
  9. public class Log extends BaseDomain implements Serializable{  
  10.       
  11.     private static final long serialVersionUID = 1024792477652984770L;  
  12.   
  13.     private Long userid;//管理员id  
  14.     private Date createdate;//日期  
  15.     private String content;//日志内容  
  16.     private String operation;//操作(主要是"添加"、"修改"、"删除")  
  17.       
  18.     //getter、setter,此处省略N字(你懂的)  
  19. }  

 

 

 

日志记录Mapper接口与XML配置文件:

 

 

Java代码   收藏代码
  1. package com.xxx.dao;  
  2.   
  3. import com.xxx.pojo.Log;  
  4.   
  5. /** 
  6.  * 日志记录Mapper 
  7.  */  
  8. public interface LogMapper {  
  9.       
  10.     public void insert(Log log);//添加日志记录  
  11. }  

 

 

 

Xml代码   收藏代码
  1. <?xml version="1.0" encoding="UTF-8"?>  
  2. <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"   
  3. "http://mybatis.org/dtd/mybatis-3-mapper.dtd">  
  4. <mapper namespace="com.xxx.dao.LogMapper">  
  5.     <!-- 添加日志记录 -->  
  6.     <insert id="insert" parameterType="Log">  
  7.         INSERT INTO t_log(userid,createdate,operation,content)  
  8.         VALUES(#{userid},NOW(),#{operation},#{content});  
  9.     </insert>  
  10. </mapper>  

 

 

日志记录Service接口与实现类:

 

 

Java代码   收藏代码
  1. package com.xxx.service;  
  2.   
  3. import org.springframework.transaction.annotation.Transactional;  
  4. import com.xxx.pojo.Log;  
  5.   
  6. /** 
  7.  * 日志记录业务逻辑接口 
  8.  */  
  9. public interface LogService {  
  10.       
  11.     /** 
  12.      * 日志记录 
  13.      * @param log 
  14.      */  
  15.     @Transactional  
  16.     public void log(Log log);  
  17.       
  18.     /** 
  19.      * 获取登录管理员ID 
  20.      */  
  21.     public Long loginUserId();  
  22. }  

 

 

Java代码   收藏代码
  1. package com.xxx.service;  
  2.   
  3. import org.springframework.beans.factory.annotation.Autowired;  
  4. import org.springframework.security.core.context.SecurityContextHolder;  
  5. import org.springframework.security.core.userdetails.UserDetails;  
  6. import com.xxx.dao.LogMapper;  
  7. import com.xxx.pojo.Admin;  
  8. import com.xxx.pojo.Log;  
  9.   
  10. /** 
  11.  * 日志记录业务逻辑接口实现类 
  12.  * @author HotStrong 
  13.  */  
  14. public class LogServiceImpl implements LogService{  
  15.       
  16.     @Autowired  
  17.     private AdminService adminService;  
  18.       
  19.     @Autowired  
  20.     private LogMapper logMapper;  
  21.       
  22.     public void log(Log log) {  
  23.         logMapper.insert(log);  
  24.     }  
  25.       
  26.     /** 
  27.      * 获取登录管理员ID 
  28.      *  
  29.      * @return 
  30.      */  
  31.     public Long loginUserId() {  
  32.   
  33.         if(SecurityContextHolder.getContext() == null){  
  34.             return null;  
  35.         }  
  36.           
  37.         if(SecurityContextHolder.getContext().getAuthentication() == null){  
  38.             return null;  
  39.         }  
  40.           
  41.         UserDetails userDetails = (UserDetails) SecurityContextHolder  
  42.                 .getContext().getAuthentication().getPrincipal();  
  43.           
  44.         if(userDetails == null){  
  45.             return null;  
  46.         }  
  47.           
  48.         //获取登录管理员帐号名  
  49.         String userName = userDetails.getUsername();  
  50.           
  51.         if(userName == null || userName.equals("")){  
  52.             return null;  
  53.         }  
  54.           
  55.         // 根据管理员帐号名获取帐号ID  
  56.         Admin admin = this.adminService.findAdminByNickname(userName);  
  57.           
  58.         if(admin == null){  
  59.             return null;  
  60.         }  
  61.           
  62.         return admin.getId();  
  63.     }  
  64. }  

 

 

 

7、在MyBatis配置文件mybatis-config.xml中配置POJO,如下:

 

 

Xml代码   收藏代码
  1. <?xml version="1.0" encoding="UTF-8"?>  
  2. <!DOCTYPE configuration  
  3.     PUBLIC "-//mybatis.org//DTD Config 3.0//EN"  
  4.     "http://mybatis.org/dtd/mybatis-3-config.dtd">  
  5. <configuration>  
  6.     <settings>  
  7.         <!-- changes from the defaults -->  
  8.         <setting name="lazyLoadingEnabled" value="false" />  
  9.     </settings>  
  10.     <typeAliases>  
  11.         <typeAlias alias="Film" type="com.xxx.pojo.Film"/>  
  12.         <typeAlias alias="Admin" type="com.xxx.pojo.Admin"/>  
  13.         <typeAlias alias="Log" type="com.xxx.pojo.Log"/>  
  14.     </typeAliases>  
  15. </configuration>  

 

 

 

 

8、创建aop包,在aop包下创建切面类LogAspect

 

 

Java代码   收藏代码
  1. package com.xxx.aop;  
  2.   
  3. import java.lang.reflect.Method;  
  4. import java.util.Date;  
  5.   
  6. import org.aspectj.lang.JoinPoint;  
  7. import org.aspectj.lang.ProceedingJoinPoint;  
  8. import org.aspectj.lang.annotation.AfterReturning;  
  9. import org.aspectj.lang.annotation.Around;  
  10. import org.aspectj.lang.annotation.Aspect;  
  11. import org.aspectj.lang.annotation.Pointcut;  
  12. import org.springframework.beans.factory.annotation.Autowired;  
  13.   
  14. import com.xxx.pojo.Film;  
  15. import com.xxx.pojo.Log;  
  16. import com.xxx.service.FilmService;  
  17. import com.xxx.service.LogService;  
  18.   
  19. /** 
  20.  * 日志记录,添加、删除、修改方法AOP 
  21.  * @author HotStrong 
  22.  *  
  23.  */  
  24. @Aspect  
  25. public class LogAspect {  
  26.       
  27.     @Autowired  
  28.     private LogService logService;//日志记录Service  
  29.       
  30.     @Autowired  
  31.     private FilmService filmService;//影片Service  
  32.       
  33.     /** 
  34.      * 添加业务逻辑方法切入点 
  35.      */  
  36.     @Pointcut("execution(* com.xxx.service.*.insert*(..))")  
  37.     public void insertServiceCall() { }  
  38.       
  39.     /** 
  40.      * 修改业务逻辑方法切入点 
  41.      */  
  42.     @Pointcut("execution(* com.xxx.service.*.update*(..))")  
  43.     public void updateServiceCall() { }  
  44.       
  45.     /** 
  46.      * 删除影片业务逻辑方法切入点 
  47.      */  
  48.     @Pointcut("execution(* com.xxx.service.FilmService.deleteFilm(..))")  
  49.     public void deleteFilmCall() { }  
  50.       
  51.     /** 
  52.      * 管理员添加操作日志(后置通知) 
  53.      * @param joinPoint 
  54.      * @param rtv 
  55.      * @throws Throwable 
  56.      */  
  57.     @AfterReturning(value="insertServiceCall()", argNames="rtv", returning="rtv")  
  58.     public void insertServiceCallCalls(JoinPoint joinPoint, Object rtv) throws Throwable{  
  59.           
  60.         //获取登录管理员id  
  61.         Long adminUserId = logService.loginUserId();  
  62.           
  63.         if(adminUserId == null){//没有管理员登录  
  64.             return;  
  65.         }  
  66.           
  67.         //判断参数  
  68.         if(joinPoint.getArgs() == null){//没有参数  
  69.             return;  
  70.         }  
  71.           
  72.         //获取方法名  
  73.         String methodName = joinPoint.getSignature().getName();  
  74.           
  75.         //获取操作内容  
  76.         String opContent = adminOptionContent(joinPoint.getArgs(), methodName);  
  77.           
  78.         //创建日志对象  
  79.         Log log = new Log();  
  80.         log.setUserid(logService.loginUserId());//设置管理员id  
  81.         log.setCreatedate(new Date());//操作时间  
  82.         log.setContent(opContent);//操作内容  
  83.         log.setOperation("添加");//操作  
  84.           
  85.         logService.log(log);//添加日志  
  86.     }  
  87.       
  88.      /** 
  89.      * 管理员修改操作日志(后置通知) 
  90.      * @param joinPoint 
  91.      * @param rtv 
  92.      * @throws Throwable 
  93.      */  
  94.     @AfterReturning(value="updateServiceCall()", argNames="rtv", returning="rtv")  
  95.     public void updateServiceCallCalls(JoinPoint joinPoint, Object rtv) throws Throwable{  
  96.           
  97.         //获取登录管理员id  
  98.         Long adminUserId = logService.loginUserId();  
  99.           
  100.         if(adminUserId == null){//没有管理员登录  
  101.             return;  
  102.         }  
  103.           
  104.         //判断参数  
  105.         if(joinPoint.getArgs() == null){//没有参数  
  106.             return;  
  107.         }  
  108.           
  109.         //获取方法名  
  110.         String methodName = joinPoint.getSignature().getName();  
  111.           
  112.         //获取操作内容  
  113.         String opContent = adminOptionContent(joinPoint.getArgs(), methodName);  
  114.           
  115.         //创建日志对象  
  116.         Log log = new Log();  
  117.         log.setUserid(logService.loginUserId());//设置管理员id  
  118.         log.setCreatedate(new Date());//操作时间  
  119.         log.setContent(opContent);//操作内容  
  120.         log.setOperation("修改");//操作  
  121.           
  122.         logService.log(log);//添加日志  
  123.     }  
  124.       
  125.     /** 
  126.      * 管理员删除影片操作(环绕通知),使用环绕通知的目的是 
  127.      * 在影片被删除前可以先查询出影片信息用于日志记录 
  128.      * @param joinPoint 
  129.      * @param rtv 
  130.      * @throws Throwable 
  131.      */  
  132.     @Around(value="deleteFilmCall()", argNames="rtv")  
  133.     public Object deleteFilmCallCalls(ProceedingJoinPoint pjp) throws Throwable {  
  134.           
  135.         Object result = null;  
  136.          //环绕通知处理方法  
  137.          try {  
  138.               
  139.             //获取方法参数(被删除的影片id)  
  140.             Integer id = (Integer)pjp.getArgs()[0];  
  141.             Film obj = null;//影片对象  
  142.             if(id != null){  
  143.                 //删除前先查询出影片对象  
  144.                 obj = filmService.getFilmById(id);  
  145.             }  
  146.               
  147.             //执行删除影片操作  
  148.             result = pjp.proceed();  
  149.               
  150.             if(obj != null){  
  151.                   
  152.                 //创建日志对象  
  153.                 Log log = new Log();  
  154.                 log.setUserid(logService.loginUserId());//用户编号  
  155.                 log.setCreatedate(new Date());//操作时间  
  156.                   
  157.                 StringBuffer msg = new StringBuffer("影片名 : ");  
  158.                 msg.append(obj.getFname());  
  159.                 log.setContent(msg.toString());//操作内容  
  160.                   
  161.                 log.setOperation("删除");//操作  
  162.                   
  163.                 logService.log(log);//添加日志  
  164.             }  
  165.               
  166.          }  
  167.          catch(Exception ex) {  
  168.             ex.printStackTrace();  
  169.          }  
  170.            
  171.          return result;  
  172.     }  
  173.       
  174.     /** 
  175.      * 使用Java反射来获取被拦截方法(insert、update)的参数值, 
  176.      * 将参数值拼接为操作内容 
  177.      */  
  178.     public String adminOptionContent(Object[] args, String mName) throws Exception{  
  179.   
  180.         if (args == null) {  
  181.             return null;  
  182.         }  
  183.           
  184.         StringBuffer rs = new StringBuffer();  
  185.         rs.append(mName);  
  186.         String className = null;  
  187.         int index = 1;  
  188.         // 遍历参数对象  
  189.         for (Object info : args) {  
  190.               
  191.             //获取对象类型  
  192.             className = info.getClass().getName();  
  193.             className = className.substring(className.lastIndexOf(".") + 1);  
  194.             rs.append("[参数" + index + ",类型:" + className + ",值:");  
  195.               
  196.             // 获取对象的所有方法  
  197.             Method[] methods = info.getClass().getDeclaredMethods();  
  198.               
  199.             // 遍历方法,判断get方法  
  200.             for (Method method : methods) {  
  201.                   
  202.                 String methodName = method.getName();  
  203.                 // 判断是不是get方法  
  204.                 if (methodName.indexOf("get") == -1) {// 不是get方法  
  205.                     continue;// 不处理  
  206.                 }  
  207.                   
  208.                 Object rsValue = null;  
  209.                 try {  
  210.                       
  211.                     // 调用get方法,获取返回值  
  212.                     rsValue = method.invoke(info);  
  213.                       
  214.                     if (rsValue == null) {//没有返回值  
  215.                         continue;  
  216.                     }  
  217.                       
  218.                 } catch (Exception e) {  
  219.                     continue;  
  220.                 }  
  221.                   
  222.                 //将值加入内容中  
  223.                 rs.append("(" + methodName + " : " + rsValue + ")");  
  224.             }  
  225.               
  226.             rs.append("]");  
  227.               
  228.             index++;  
  229.         }  
  230.           
  231.         return rs.toString();  
  232.     }  
  233.       
  234. }  

 

 

 

9、对管理员登录操作进行日志记录

 

还记得《使用Spring Security实现权限管理》一文中第7步提到的两个类吗?其中LoginSuccessHandler类中可以记录管理员的登录操作,代码如下:

 

 

Java代码   收藏代码
  1. package com.xxx.security;  
  2.   
  3. import java.io.IOException;  
  4. import java.util.Date;  
  5.   
  6. import javax.servlet.ServletException;  
  7. import javax.servlet.http.HttpServletRequest;  
  8. import javax.servlet.http.HttpServletResponse;  
  9.   
  10. import org.springframework.beans.factory.annotation.Autowired;  
  11. import org.springframework.security.core.Authentication;  
  12. import org.springframework.security.core.userdetails.UserDetails;  
  13. import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;  
  14.   
  15. import com.xxx.pojo.Log;  
  16. import com.xxx.service.LogService;  
  17.   
  18. /** 
  19.  * 处理管理登录日志 
  20.  * 
  21.  */  
  22. public class LoginSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler{  
  23.       
  24.     @Autowired  
  25.     private LogService logService;//日志记录Service  
  26.       
  27.     @Override  
  28.     public void onAuthenticationSuccess(HttpServletRequest request,  
  29.             HttpServletResponse response, Authentication authentication) throws IOException,  
  30.             ServletException {  
  31.           
  32.         UserDetails userDetails = (UserDetails)authentication.getPrincipal();  
  33.           
  34.         //创建日志对象  
  35.         Log log = new Log();  
  36.         log.setUserid(logService.loginUserId());//设置管理员id  
  37.         log.setCreatedate(new Date());//操作时间  
  38.         log.setContent("管理员 " + userDetails.getUsername());//操作内容  
  39.         log.setOperation("登录");//操作  
  40.           
  41.         logService.log(log);//添加日志  
  42.           
  43.         super.onAuthenticationSuccess(request, response, authentication);  
  44.     }  
  45.       
  46. }  

 

 

 

10、在applicationContext-services.xml中加入新的配置

 

applicationContext-services.xml中加入了Aspectj配置以及新增的管理员Service、日志记录Service配置:

 

 

Xml代码   收藏代码
  1. <?xml version="1.0" encoding="UTF-8"?>  
  2. <beans xmlns="http://www.springframework.org/schema/beans"  
  3.         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
  4.         xmlns:aop="http://www.springframework.org/schema/aop"  
  5.         xmlns:tx="http://www.springframework.org/schema/tx"  
  6.         xsi:schemaLocation="  
  7.             http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd  
  8.             http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd  
  9.             http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd">  
  10.           
  11.         <!-- 加入Aspectj配置 -->  
  12.         <aop:aspectj-autoproxy />  
  13.         <bean id="logAspect" class="com.xxx.aop.LogAspect" />  
  14.           
  15.         <!-- 电影业务逻辑对象 -->  
  16.         <bean id="filmService" class="com.xxx.service.FilmServiceImpl"></bean>    
  17.           
  18.         <!-- 管理员业务逻辑对象 -->  
  19.         <bean id="adminService" class="com.xxx.service.AdminServiceImpl"></bean>  
  20.           
  21.         <!-- 日志记录业务逻辑对象 -->  
  22.         <bean id="logService" class="com.xxx.service.LogServiceImpl"></bean>  
  23.           
  24. </beans>  

 

 

 

11、配置成功后分别进行登录、添加、修改、删除影片操作,日志记录表的内容如下:

 

 

 

 

参考文章:

MyBatis 1章 入门(使用MyBatis完成CRUD)

使用Spring Security实现权限管理

使用ajax gson增强用户体验

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值