ssm 实现日志 用aspectj_javaWEB SSM AOP+注解保存操作日志

本篇文章的诞生离不开这篇文章的作者:http://blog.csdn.net/czmchen/article/details/42392985。

前言

操作日志在javaWeb的业务系统中是在是太常见的功能了,主要记录用户再什么时间,什么位置进行了什么操作。如果每新增一个功能都要写一个插入代码的话,是非常不容易维护的。加一个字段就要在每个插入语句上加入这个字段。所以AOP+注解的优势就显现了出来,不仅如此,当我们有了这套代码以后,可以通用在该系统的wap端或者其他的系统中,不必修改太多的代码。针对日志这种实时性不是很高的功能,这里用了异步的方式进行,这样日志系统独立出来,不会影响业务。下面是我整理的代码,欢迎留下宝贵意见。这里再次感谢原作者提供良好的思路。

代码

一、核心类

1.自定义注解 拦截Controller/**

*

* 自定义注解 拦截Controller

* @see  [相关类/方法]

* @since  [产品/模块版本]

*/

@Target({ElementType.PARAMETER, ElementType.METHOD})

@Retention(RetentionPolicy.RUNTIME)

@Documented

public @interface SystemControllerLog

{

String description() default "";//描述

String moduleType() default "";//模块代码

String operateValue() default "";//操作类型

boolean firstParamName() default false;

}

2.自定义注解 拦截service/**

*

* 自定义注解 拦截service

* @see  [相关类/方法]

* @since  [产品/模块版本]

*/

@Target({ElementType.PARAMETER, ElementType.METHOD})

@Retention(RetentionPolicy.RUNTIME)

@Documented

public @interface SystemServiceLog

{

String description() default "";// 描述

String moduleType() default "";// 模块代码

String operateValue() default "";// 操作类型

}

3.AOP可以拦截到controller的配置

spring.xml中加入下面这句话

4.切点类

这里用的是返回通知,用来接收成功或失败。/**

*

* AOP记录操作&异常日志-切点类

* @see [相关类/方法]

* @since [产品/模块版本]

*/

@Aspect

@Component

public class SystemLogAspect

{

private static final Logger logger = LoggerFactory.getLogger(SystemLogAspect.class);

// 队列

private static BlockingQueue queue = new LinkedBlockingQueue();

// 缓存线程池

private static ExecutorService threadPool = Executors.newFixedThreadPool(3);

// 任务数

private static int taskSize = 6;

// 线程是否已启动

boolean isStartThread = false;

// 用来启动或停止线程

static boolean run = true;

@Autowired

private LogService logService;

@Autowired

private UserService userService;

// Service层切点

@Pointcut("@annotation(com.rzzl.wap.log.annotation.SystemServiceLog)")

public void serviceAspect()

{

}

// Controller层切点

@Pointcut("@annotation(com.rzzl.wap.log.annotation.SystemControllerLog)")

public void controllerAspect()

{

}

public static BlockingQueue getQueue()

{

return queue;

}

public static void setQueue(BlockingQueue queue)

{

SystemLogAspect.queue = queue;

}

public static boolean isRun()

{

return run;

}

public static void setRun(boolean run)

{

SystemLogAspect.run = run;

}

/**

*

* 返回通知 用于拦截Controller层记录用户的操作

* @param joinPoint 切点

* @param result 返回值

* @see [类、类#方法、类#成员]

*/

@AfterReturning(value = "controllerAspect()", returning = "result")

public void afterReturn(JoinPoint joinPoint, Object result)

{

// 请求的IP

User user = WebUtils.getSessionValue(LoginContact.SESSION_USER);

String params = "";

WebResult webResult = new WebResult();

webResult.setCode(FlagContact.BACK_SUCCESS);

try

{

if (WebResult.class.isInstance(result))

{

webResult = (WebResult)result;

}

String loginName = "";

InnnerBean innnerBean = getControllerMethodDescription(joinPoint);

Object[] arguments = innnerBean.getArguments();

String remark = innnerBean.getDescription();

Log log = new Log.Builder().type(LogTypes.type.operate)

.moduleType(innnerBean.getModuleType())

.operateCode(joinPoint.getSignature().getName())

.operateValue(innnerBean.getOperateValue())

.remark(remark)

.operateStatus(webResult.getCode().equals(FlagContact.BACK_SUCCESS) ? LogTypes.operateStatus.Y

: LogTypes.operateStatus.N)// 返回值1操作成功,否则失败

.method((joinPoint.getTarget().getClass().getName() + "." + joinPoint.getSignature().getName() + "()"))

.param(params)

.loginName(user.getAccountNo())

.fullName(user.getUserName())

.build();

// 放入队列

queue.put(log);

if (!isStartThread)

{

for (int i = 0; i 

{

threadPool.execute(new saveLogThread());

}

isStartThread = true;

}

}

catch (Exception e)

{

logger.error("异常信息:{}", e.toString());

}

}

/**

* 异常通知 用于拦截service层记录异常日志

* @param joinPoint

* @param e

* @see [类、类#方法、类#成员]

*/

@AfterThrowing(pointcut = "serviceAspect()", throwing = "e")

public void doAfterThrowing(JoinPoint joinPoint, Throwable e)

{

// 读取session中的用户

User user = WebUtils.getSessionValue(LoginContact.SESSION_USER);

String params = "";

try

{

if (joinPoint.getArgs() != null && joinPoint.getArgs().length > 0)

{

for (int i = 0; i 

{

params += JSONUtils.valueToString(joinPoint.getArgs()[i].toString()) + ";";

}

}

InnnerBean innnerBean = getServiceMthodDescription(joinPoint);

String loginName = "";

Log log =

new Log.Builder().type(LogTypes.type.exception)

.moduleType(innnerBean.getModuleType())

.operateCode(joinPoint.getSignature().getName())

.operateValue(innnerBean.getOperateValue())

.remark(innnerBean.getDescription())

.operateStatus(LogTypes.operateStatus.N)

.method(

(joinPoint.getTarget().getClass().getName() + "." + joinPoint.getSignature().getName() + "()"))

.param(params)

.exceptionDetail(e.toString())

.build();

// 放入队列

queue.put(log);

if (!isStartThread)

{

new Thread(new saveLogThread()).start();

isStartThread = true;

}

}

catch (Exception ex)

{

logger.error("异常信息:{}", ex.toString());

}

finally

{

logger.error("异常方法:{" + joinPoint.getTarget().getClass().getName() + "}异常代码:{"

+ joinPoint.getSignature().getName() + "}异常信息:{" + e.toString() + "}参数:{" + params + "}");

}

}

/**

* 获取注解中对方法的描述信息 用于service层注解

* @param joinPoint 切点

* @return 方法描述

* @throws Exception

* @see [类、类#方法、类#成员]

*/

@SuppressWarnings("rawtypes")

public static InnnerBean getServiceMthodDescription(JoinPoint joinPoint)

throws Exception

{

String targetName = joinPoint.getTarget().getClass().getName();

String methodName = joinPoint.getSignature().getName();

Object[] arguments = joinPoint.getArgs();

Class targetClass = Class.forName(targetName);

Method[] methods = targetClass.getMethods();

String moduleType = "";

String operateValue = "";

String description = "";

InnnerBean innnerBean = new InnnerBean(moduleType, operateValue, description);

for (Method method : methods)

{

if (method.getName().equals(methodName))

{

Class[] clazzs = method.getParameterTypes();

if (clazzs.length == arguments.length)

{

SystemServiceLog annotation = method.getAnnotation(SystemServiceLog.class);

moduleType = annotation.moduleType();

operateValue = annotation.operateValue();

description = annotation.description();

innnerBean = new InnnerBean(moduleType, operateValue, description);

break;

}

}

}

innnerBean.setArguments(arguments);

return innnerBean;

}

/**

* 获取注解中对方法的描述信息 用于Controller层注解

* @param joinPoint 切点

* @return 方法描述

* @throws Exception

* @see [类、类#方法、类#成员]

*/

@SuppressWarnings("rawtypes")

public static InnnerBean getControllerMethodDescription(JoinPoint joinPoint)

throws Exception

{

String targetName = joinPoint.getTarget().getClass().getName();

String methodName = joinPoint.getSignature().getName();

Object[] arguments = joinPoint.getArgs();

Class targetClass = Class.forName(targetName);

Method[] methods = targetClass.getMethods();

String moduleType = "";

String operateValue = "";

String description = "";

boolean firstParamName = false;

InnnerBean innnerBean = new InnnerBean(moduleType, operateValue, description);

for (Method method : methods)

{

if (method.getName().equals(methodName))

{

Class[] clazzs = method.getParameterTypes();

if (clazzs.length == arguments.length)

{

SystemControllerLog annotation = method.getAnnotation(SystemControllerLog.class);

moduleType = annotation.moduleType();

operateValue = annotation.operateValue();

description = annotation.description();

firstParamName = annotation.firstParamName();

innnerBean = new InnnerBean(moduleType, operateValue, description);

innnerBean.setFirstParamName(firstParamName);

break;

}

}

}

innnerBean.setArguments(arguments);

return innnerBean;

}

/**

*

* 内部类封装注入信息

* @see [相关类/方法]

* @since [产品/模块版本]

*/

static class InnnerBean

{

private String moduleType;// 模块代码

private String description;// 描述

private String operateValue;// 操作类型

private boolean firstParamName;

private Object[] arguments;

public InnnerBean(String moduleType, String operateValue, String description)

{

super();

this.moduleType = moduleType;

this.description = description;

this.operateValue = operateValue;

}

public String getOperateValue()

{

return operateValue;

}

public void setOperateValue(String operateValue)

{

this.operateValue = operateValue;

}

public String getModuleType()

{

return moduleType;

}

public void setModuleType(String moduleType)

{

this.moduleType = moduleType;

}

public String getDescription()

{

return description;

}

public void setDescription(String description)

{

this.description = description;

}

public Object[] getArguments()

{

return arguments;

}

public void setArguments(Object[] arguments)

{

this.arguments = arguments;

}

public boolean isFirstParamName()

{

return firstParamName;

}

public void setFirstParamName(boolean firstParamName)

{

this.firstParamName = firstParamName;

}

}

/**

*

* 异步保存日志

* @see [相关类/方法]

* @since [产品/模块版本]

*/

class saveLogThread implements Runnable

{

Lock lock = new ReentrantLock();

@Override

public void run()

{

try

{

while (run)

{

while (queue.size() != 0)

{

// 如果对插入顺序无要求,此处不需要同步可提升效率

lock.lock();

Log log = queue.take();

logService.insert(log);

lock.unlock();

}

Thread.sleep(3000);

}

}

catch (InterruptedException e)

{

logger.error("saveLogThread被唤醒:" + e.toString());

}

catch (Exception e)

{

logger.error("saveLogThread异常:" + e.toString());

}

}

}

}

二、定值类

日志系统中的定值/**

*

* 日志系统中的定值

*

* @see [相关类/方法]

* @since [产品/模块版本]

*/

public interface LogTypes

{

/**

*

* 操作状态(成功与否Y\\N)

* @see [相关类/方法]

* @since [产品/模块版本]

*/

static interface operateStatus

{

// 成功

final String Y = "Y";

// 失败

final String N = "N";

}

/**

*

* 日志类型

* @see [相关类/方法]

* @since [产品/模块版本]

*/

static interface type

{

// 操作日志

final String operate = "operate";

// 异常日志

final String exception = "exception";

}

/**

*

* 模块类型

* @see [相关类/方法]

* @since [产品/模块版本]

*/

static interface moduleType

{

// 登录模块

final String LOGIN = "LOGIN";

// 项目模块

final String PROJECT = "PROJECT";

// 客户模块

final String CUSTOMER = "CUSTOMER";

// 用户模块

final String SYS_USER = "SYS_USER";

}

/**

*

* 操作类型

* @see [相关类/方法]

* @since [产品/模块版本]

*/

static interface operateValue

{

// 查询

final String select = "select";

// 登录

final String login = "login";

// 保存

final String save = "save";

// 新增

final String add = "add";

// 修改

final String edit = "edit";

// 删除

final String delete = "delete";

// 查看

final String view = "view";

// 修改密码

final String editPassword = "editPassword";

// 上传

final String upload = "upload";

// 下载

final String down = "down";

// 下载

final String packagedown = "packagedown";

}

/**

*

* 保存描述的前缀

*          方便于批量修改该备注

* @see [相关类/方法]

* @since [产品/模块版本]

*/

static interface Prefix

{

// 查询

final String savePrefix = "新增/编辑";

}

}

三、实体类

1、日志实体

应用了Builder设计模式/**

*

* 操作日志&异常日志

* @see [相关类/方法]

* @since [产品/模块版本]

*/

public class Log implements Serializable

{

/**

* serialVersionUID

*/

private static final long serialVersionUID = 1L;

private static final String reqSource;// 请求来源,pc:pc端,wap:wap端 默认来源为pc

private static final String localAddr;// 服务器IP

private String ip;// 操作电脑ip

private String fullName;// 操作人员名字

private String loginName;// 操作人员登录账号

private Date operateDateTime;// 操作时间

private Date createDateTime;// 创建时间

private Long id;

private String type;// 日志类型,‘operate’:操作日志,‘exception’:异常日志

private String moduleType;// 模块代码

private String operateCode;// 操作代码

private String operateValue;// 操作类型

private String remark;// 操作备注(记录参数)

private String operateStatus;// 操作状态(成功与否Y\\N)

private String method;// 调用方法

private String param;// 方法的请求参数

private String exceptionDetail;// 异常信息

static{

reqSource = "wap";

localAddr = WebUtils.getLocalAddr();

}

public void init()

{

User user = WebUtils.getSessionValue(LoginContact.SESSION_USER);

ip = WebUtils.getRemoteIP();

loginName = user != null ? user.getAccountNo() : getLoginName();

fullName = user != null ? user.getUserName() : getFullName();

operateDateTime = new Date();

createDateTime = new Date();

}

public Log()

{

init();

}

public Log(Log origin)

{

init();

this.id = origin.id;

this.type = origin.type;

this.moduleType = origin.moduleType;

this.operateCode = origin.operateCode;

this.operateValue = origin.operateValue;

this.remark = origin.remark;

this.operateStatus = origin.operateStatus;

this.method = origin.method;

this.param = origin.param;

this.exceptionDetail = origin.exceptionDetail;

this.fullName = origin.fullName;

this.loginName = origin.loginName;

}

public String getIp()

{

return ip;

}

public void setIp(String ip)

{

this.ip = ip;

}

public String getFullName()

{

return fullName;

}

public void setFullName(String fullName)

{

this.fullName = fullName;

}

public String getLoginName()

{

return loginName;

}

public void setLoginName(String loginName)

{

this.loginName = loginName;

}

public Date getOperateDateTime()

{

return operateDateTime;

}

public void setOperateDateTime(Date operateDateTime)

{

this.operateDateTime = operateDateTime;

}

public Date getCreateDateTime()

{

return createDateTime;

}

public void setCreateDateTime(Date createDateTime)

{

this.createDateTime = createDateTime;

}

public Long getId()

{

return id;

}

public void setId(Long id)

{

this.id = id;

}

public String getType()

{

return type;

}

public void setType(String type)

{

this.type = type;

}

public String getModuleType()

{

return moduleType;

}

public void setModuleType(String moduleType)

{

this.moduleType = moduleType;

}

public String getOperateCode()

{

return operateCode;

}

public void setOperateCode(String operateCode)

{

this.operateCode = operateCode;

}

public String getOperateValue()

{

return operateValue;

}

public void setOperateValue(String operateValue)

{

this.operateValue = operateValue;

}

public String getRemark()

{

return remark;

}

public void setRemark(String remark)

{

this.remark = remark;

}

public String getOperateStatus()

{

return operateStatus;

}

public void setOperateStatus(String operateStatus)

{

this.operateStatus = operateStatus;

}

public String getMethod()

{

return method;

}

public void setMethod(String method)

{

this.method = method;

}

public String getParam()

{

return param;

}

public void setParam(String param)

{

this.param = param;

}

public String getExceptionDetail()

{

return exceptionDetail;

}

public void setExceptionDetail(String exceptionDetail)

{

this.exceptionDetail = exceptionDetail;

}

public String getLocalAddr()

{

return localAddr;

}

public static class Builder

{

private Log target;

public Builder()

{

target = new Log();

}

public Builder id(Long id)

{

target.id = id;

return this;

}

public Builder type(String type)

{

target.type = type;

return this;

}

public Builder moduleType(String moduleType)

{

target.moduleType = moduleType;

return this;

}

public Builder operateCode(String operateCode)

{

target.operateCode = operateCode;

return this;

}

public Builder operateValue(String operateValue)

{

target.operateValue = operateValue;

return this;

}

public Builder remark(String remark)

{

target.remark = remark;

return this;

}

public Builder operateStatus(String operateStatus)

{

target.operateStatus = operateStatus;

return this;

}

public Builder method(String method)

{

target.method = method;

return this;

}

public Builder param(String param)

{

target.param = param;

return this;

}

public Builder exceptionDetail(String exceptionDetail)

{

target.exceptionDetail = exceptionDetail;

return this;

}

public Builder loginName(String loginName)

{

target.loginName = loginName;

return this;

}

public Builder fullName(String fullName)

{

target.fullName = fullName;

return this;

}

public Log build()

{

return new Log(target);

}

}

}

2.返回结果实体/**

* @ClassName: WebResult

* @version 1.0

* @Desc: WEB返回JSON结果

* @history v1.0

*/

public class WebResult implements Serializable

{

private static final long serialVersionUID = -4776437900752507269L;

/**

* 返回消息

*/

private String msg;

/**

* 返回码

*/

private String code;

/**

* 返回数据

*/

private Object data;

private Map,?> map;

private List> list;

public WebResult()

{

}

public WebResult(String msg, String code)

{

super();

this.msg = msg;

this.code = code;

}

public WebResult(String msg, String code, Object data)

{

super();

this.msg = msg;

this.code = code;

this.data = data;

}

public String getMsg()

{

return msg;

}

public void setMsg(String msg)

{

this.msg = msg;

}

public String getCode()

{

return code;

}

public void setCode(String code)

{

this.code = code;

}

public Object getData()

{

return data;

}

public void setData(Object data)

{

this.data = data;

}

public Map, ?> getMap()

{

return map;

}

public void setMap(Map, ?> map)

{

this.map = map;

}

public List> getList()

{

return list;

}

public void setList(List> list)

{

this.list = list;

}

@Override

public String toString()

{

return "WebResult [msg=" + msg + ", code=" + code + ", data=" + data + "]";

}

/**

* 初始失败方法

*

* @author cc HSSD0473

* @see [类、类#方法、类#成员]

*/

public void invokeFail(){

this.data = null;

this.code = FlagContact.BACK_FAIL;

this.msg = "操作失败";

}

public void invokeFail(String msg){

this.data = null;

this.code = FlagContact.BACK_FAIL;

if(msg != null && !msg.equals(""))

{

this.msg = msg;

}

}

public void invokeSuccess()

{

this.code = FlagContact.BACK_SUCCESS;

this.msg = "操作成功";

}

public void invokeSuccess(String msg)

{

if(msg != null && !msg.equals(""))

{

this.msg = msg;

}

this.code = FlagContact.BACK_SUCCESS;

}

}

三、工具类

获取登录用户,客户ip主机ip等方法**

* @ClassName: WebUtils

* @version 1.0

* @Desc: WebUtils

* @history v1.0

*/

public class WebUtils

{

/**

* 描述:获取request对象

* @return

*/

public static HttpServletRequest getRequest()

{

return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();

}

/**

* 描述:获取responce对象

* @return

*/

public static HttpServletResponse getResponse()

{

return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();

}

/**

* 描述:获取session

* @return

*/

public static HttpSession getSession()

{

return getRequest().getSession();

}

/**

* 描述:设置session值

* @param key

* @param val

*/

public static  void setSessionValue(String key, T val)

{

getSession().setAttribute(key, val);

}

/**

* 描述:获取session值

* @param key

* @return

*/

@SuppressWarnings("unchecked")

public static  T getSessionValue(String key)

{

return (T) getSession().getAttribute(key);

}

/**

* 描述:移除session

* @param key

* @return

*/

@SuppressWarnings("unchecked")

public static  T removeSessionValue(String key)

{

Object obj = getSession().getAttribute(key);

getSession().removeAttribute(key);

return (T) obj;

}

/**

* 描述:获取客户端ip

* @param request

* @return

*/

public static String getRemoteIP(HttpServletRequest request)

{

String ip = request.getHeader("x-forwarded-for");

if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip))

{

ip = request.getHeader("Proxy-Client-IP");

}

if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip))

{

ip = request.getHeader("WL-Proxy-Client-IP");

}

if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip))

{

ip = request.getRemoteAddr();

}

return ip.equals("0:0:0:0:0:0:0:1") ? "127.0.0.1" : ip;

}

/**

*

* 获得本机IP

* @return

* @throws Exception

* @see [类、类#方法、类#成员]

*/

public final static String getLocalAddr()

{

String hostAddress = "";

try

{

hostAddress = InetAddress.getLocalHost().getHostAddress();

}

catch (Exception e)

{

}

return hostAddress;

}

/**

* 描述:获取客户端ip

* @return

*/

public static String getRemoteIP()

{

HttpServletRequest request = getRequest();

return getRemoteIP(request);

}

}

四、清理工作

tomcat停止前,异步日志的清理动作/**

*

* tomcat停止前,异步日志的清理动作

* @see  [相关类/方法]

* @since  [产品/模块版本]

*/

@WebListener()

public class BeforeDestoryListener implements ServletContextListener

{

@Override

public void contextInitialized(ServletContextEvent sce)

{

}

@Override

public void contextDestroyed(ServletContextEvent sce)

{

while (SystemLogAspect.getQueue().size() == 0)

{

SystemLogAspect.setRun(false);

break;

}

}

}

数据库

用的是mysql,其他数据库请自行更改语句CREATE TABLE `t_log` (

`id` bigint(20) NOT NULL AUTO_INCREMENT,

`reqSource` varchar(10) DEFAULT 'pc' COMMENT '请求来源,pc:pc端,wap:wap端 默认来源为pc',

`type` varchar(10) DEFAULT NULL COMMENT '日志类型,‘operate’:操作日志,‘exception’:异常日志',

`ip` varchar(20) NOT NULL COMMENT '操作电脑ip',

`fullName` varchar(50) NOT NULL COMMENT '操作人员名字',

`loginName` varchar(50) NOT NULL COMMENT '操作人员登录账号',

`moduleType` varchar(50) NOT NULL COMMENT '模块代码',

`operateCode` varchar(50) NOT NULL COMMENT '操作代码',

`operateValue` varchar(50) DEFAULT NULL COMMENT '操作类型',

`operateDateTime` datetime NOT NULL COMMENT '操作时间',

`createDateTime` datetime NOT NULL COMMENT '创建时间',

`remark` varchar(100) DEFAULT NULL COMMENT '操作备注(记录参数)',

`operateStatus` varchar(20) DEFAULT NULL COMMENT '操作状态(成功与否Y\\N)',

`localAddr` varchar(20) DEFAULT NULL COMMENT '服务器IP',

`method` varchar(100) DEFAULT NULL COMMENT '调用方法',

`param` varchar(2000) DEFAULT NULL COMMENT '方法的请求参数',

`exceptionDetail` varchar(1000) DEFAULT NULL COMMENT '异常信息',

PRIMARY KEY (`id`)

) ENGINE=InnoDB AUTO_INCREMENT=3430 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;

测试及结果

控制层代码/**

* 登录请求

*

* @param userName用户名

* @param password密码

* @return WebResult msg:系统反馈消息 code:登录标识码

* @see [类、类#方法、类#成员]

*/

@ResponseBody

@RequestMapping("login")

@SystemControllerLog(moduleType=LogTypes.moduleType.LOGIN,operateValue=LogTypes.operateValue.login,description = "登录动作")

public WebResult userLogin(String accoutnNo, String password) {

WebResult wt = new WebResult();

try

{

// 登录逻辑

}

catch(Exception e)

{

log.error("登录异常:" + e.toString());

wt.invokeFail();

}

return wt;

}

数据库结果

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值