第六章 日志管理

一、使用log4j记录日志

 

新建log4j配置文件

#log4j.rootLogger=CONSOLE,info,error,DEBUG

log4j.rootLogger=info,error,CONSOLE,DEBUG

log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender     

log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout     

log4j.appender.CONSOLE.layout.ConversionPattern=%d{yyyy-MM-dd-HH-mm} [%t] [%c] [%p] - %m%n     

log4j.logger.info=info

log4j.appender.info=org.apache.log4j.DailyRollingFileAppender

log4j.appender.info.layout=org.apache.log4j.PatternLayout     

log4j.appender.info.layout.ConversionPattern=%d{yyyy-MM-dd-HH-mm} [%t] [%c] [%p] - %m%n  

log4j.appender.info.datePattern='.'yyyy-MM-dd

log4j.appender.info.Threshold = info   

log4j.appender.info.append=true   

#log4j.appender.info.File=/home/admin/pms-api-services/logs/info/api_services_info

log4j.appender.info.File=/Users/dddd/Documents/testspace/pms-api-services/logs/info/api_services_info

log4j.logger.error=error  

log4j.appender.error=org.apache.log4j.DailyRollingFileAppender

log4j.appender.error.layout=org.apache.log4j.PatternLayout     

log4j.appender.error.layout.ConversionPattern=%d{yyyy-MM-dd-HH-mm} [%t] [%c] [%p] - %m%n  

log4j.appender.error.datePattern='.'yyyy-MM-dd

log4j.appender.error.Threshold = error   

log4j.appender.error.append=true   

#log4j.appender.error.File=/home/admin/pms-api-services/logs/error/api_services_error

log4j.appender.error.File=/Users/dddd/Documents/testspace/pms-api-services/logs/error/api_services_error

log4j.logger.DEBUG=DEBUG

log4j.appender.DEBUG=org.apache.log4j.DailyRollingFileAppender

log4j.appender.DEBUG.layout=org.apache.log4j.PatternLayout     

log4j.appender.DEBUG.layout.ConversionPattern=%d{yyyy-MM-dd-HH-mm} [%t] [%c] [%p] - %m%n  

log4j.appender.DEBUG.datePattern='.'yyyy-MM-dd

log4j.appender.DEBUG.Threshold = DEBUG   

log4j.appender.DEBUG.append=true   

#log4j.appender.DEBUG.File=/home/admin/pms-api-services/logs/debug/api_services_debug

log4j.appender.DEBUG.File=/Users/dddd/Documents/testspace/pms-api-services/logs/debug/api_services_debug

 

二、使用logback记录日志 

 

在resources 文件夹下,新建logback-spring.xml 文件,将文件文本内容复制进去。

<?xml version="1.0" encoding="UTF-8"?>
<configuration> <!--
 说明:
 1、日志级别及文件
 日志记录采用分级记录,级别与日志文件名相对应,不同级别的日志信息记录到不同的日志文件中
 例如:error级别记录到log_error_xxx.log或log_error.log(该文件为当前记录的日志文件),而log_error_xxx.log为归档日志,
 日志文件按日期记录,同一天内,若日志文件大小等于或大于2M,则按0、1、2...顺序分别命名
 例如log-level-2013-12-21.0.log
 其它级别的日志也是如此。
 2、文件路径
 若开发、测试用,在Eclipse中运行项目,则到Eclipse的安装路径查找logs文件夹,以相对路径../logs。
 若部署到Tomcat下,则在Tomcat下的logs文件中
 3、Appender
 FILEERROR对应error级别,文件名以log-error-xxx.log形式命名
 FILEWARN对应warn级别,文件名以log-warn-xxx.log形式命名
 FILEINFO对应info级别,文件名以log-info-xxx.log形式命名
 FILEDEBUG对应debug级别,文件名以log-debug-xxx.log形式命名
 stdout将日志信息输出到控制上,为方便开发测试使用
 -->
 <contextName>logback</contextName>
    <!--生成日志目录,dubug模式生成在项目根目录,运行jar模式生成目录在jar的同级目录-->
 <property name="LOG_PATH" value="logs" />
    <!-- 日志记录器,日期滚动记录 -->
 <appender name="FILEERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 正在记录的日志文件的路径及文件名 -->
 <file>${LOG_PATH}/log_error.log</file>
        <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
 <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 归档的日志文件的路径,例如今天是2013-12-21日志,当前写的日志文件路径为file节点指定,可以将此文件与file指定文件路径设置为不同路径,从而将当前日志文件或归档日志文件置不同的目录。
 而2013-12-21的日志文件在由fileNamePattern指定。%d{yyyy-MM-dd}指定日期格式,%i指定索引 -->
 <fileNamePattern>${LOG_PATH}/error/log-error-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <!-- 除按日志记录之外,还配置了日志文件不能超过2M,若超过2M,日志文件会以索引0开始,
 命名日志文件,例如log-error-2013-12-21.0.log -->
 <timeBasedFileNamingAndTriggeringPolicy
 class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>2MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--只保留最近n天的日志-->
 <maxHistory>7</maxHistory>
            <!--用来指定日志文件的上限大小,那么到了这个值,就会删除旧的日志-->
 <totalSizeCap>1GB</totalSizeCap>
            <!--启动清除超过上限的历史日志-->
 <cleanHistoryOnStart>true</cleanHistoryOnStart>
        </rollingPolicy>
        <!-- 追加方式记录日志 -->
 <append>true</append>
        <!-- 日志文件的格式 -->
 <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %logger Line:%-3L - %msg%n</pattern>
            <charset>utf-8</charset>
        </encoder>
        <!-- 此日志文件只记录info级别的 -->
 <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>error</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>
    <!-- 日志记录器,日期滚动记录 -->
 <appender name="FILEWARN" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 正在记录的日志文件的路径及文件名 -->
 <file>${LOG_PATH}/log_warn.log</file>
        <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
 <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 归档的日志文件的路径,例如今天是2013-12-21日志,当前写的日志文件路径为file节点指定,可以将此文件与file指定文件路径设置为不同路径,从而将当前日志文件或归档日志文件置不同的目录。
 而2013-12-21的日志文件在由fileNamePattern指定。%d{yyyy-MM-dd}指定日期格式,%i指定索引 -->
 <fileNamePattern>${LOG_PATH}/warn/log-warn-%d{yyyy-MM-dd}.%i.log
            </fileNamePattern>
            <!-- 除按日志记录之外,还配置了日志文件不能超过2M,若超过2M,日志文件会以索引0开始,
 命名日志文件,例如log-error-2013-12-21.0.log -->
 <timeBasedFileNamingAndTriggeringPolicy
 class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>2MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--只保留最近n天的日志-->
 <maxHistory>7</maxHistory>
            <!--用来指定日志文件的上限大小,那么到了这个值,就会删除旧的日志-->
 <totalSizeCap>1GB</totalSizeCap>
            <!--启动清除超过上限的历史日志-->
 <cleanHistoryOnStart>true</cleanHistoryOnStart>
        </rollingPolicy>
        <!-- 追加方式记录日志 -->
 <append>true</append>
        <!-- 日志文件的格式 -->
 <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %logger Line:%-3L - %msg%n</pattern>
            <charset>utf-8</charset>
        </encoder> <!-- 此日志文件只记录info级别的 -->
 <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>warn</level>
            <level>debug</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>
    <!-- 日志记录器,日期滚动记录 -->
 <appender name="FILEINFO" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 正在记录的日志文件的路径及文件名 -->
 <file>${LOG_PATH}/log_info.log</file>
        <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
 <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 归档的日志文件的路径,例如今天是2013-12-21日志,当前写的日志文件路径为file节点指定,可以将此文件与file指定文件路径设置为不同路径,从而将当前日志文件或归档日志文件置不同的目录。
 而2013-12-21的日志文件在由fileNamePattern指定。%d{yyyy-MM-dd}指定日期格式,%i指定索引 -->
 <fileNamePattern>${LOG_PATH}/info/log-info-%d{yyyy-MM-dd}.%i.log
            </fileNamePattern>
            <!-- 除按日志记录之外,还配置了日志文件不能超过2M,若超过2M,日志文件会以索引0开始,
 命名日志文件,例如log-error-2013-12-21.0.log -->
 <timeBasedFileNamingAndTriggeringPolicy
 class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>2MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--只保留最近n天的日志-->
 <maxHistory>7</maxHistory>
            <!--用来指定日志文件的上限大小,那么到了这个值,就会删除旧的日志-->
 <totalSizeCap>1GB</totalSizeCap>
            <!--启动清除超过上限的历史日志-->
 <cleanHistoryOnStart>true</cleanHistoryOnStart>
        </rollingPolicy>
        <!-- 追加方式记录日志 -->
 <append>true</append>
        <!-- 日志文件的格式 -->
 <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %logger Line:%-3L - %msg%n</pattern>
            <charset>utf-8</charset>
        </encoder>
        <!-- 此日志文件只记录info级别的 -->
 <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>info</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <!--encoder 默认配置为PatternLayoutEncoder-->
 <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %logger Line:%-3L - %msg%n</pattern>
            <charset>utf-8</charset>
        </encoder>
        <!--此日志appender是为开发使用,只配置最底级别,控制台输出的日志级别是大于或等于此级别的日志信息-->
 <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>debug</level>
        </filter>
    </appender>
    <logger name="org.springframework" level="WARN" />
    <logger name="org.hibernate" level="WARN" />
    <logger name="org.hibernate.type.descriptor.sql.BasicBinder" level="TRACE"/>
    <logger name="org.hibernate.type.descriptor.sql.BasicExtractor" level="TRACE"/>
    <logger name="org.hibernate.SQL" level="DEBUG"/>
    <logger name="org.hibernate.engine.QueryParameters" level="DEBUG"/>
    <logger name="org.hibernate.engine.query.HQLQueryPlan" level="DEBUG"/>
    <!-- 生产环境下,将此级别配置为适合的级别,以免日志文件太多或影响程序性能 -->
 <root level="INFO">
        <appender-ref ref="FILEERROR" />
        <appender-ref ref="FILEWARN" />
        <appender-ref ref="FILEINFO" />
        <!-- 生产环境将请stdout,testfile去掉 -->
 <appender-ref ref="STDOUT" />
    </root>
</configuration>

三、使用AOP统一处理Web请求日志。

 

POM文件新增依赖

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-aop</artifactId>

</dependency>

切面拦截器,增加日志记录内容。

@Aspect

@Component

public class WebLogAspect {

private Logger logger = LoggerFactory.getLogger(getClass());

@Pointcut("execution(public * com.itmayiedu.controller..*.*(..))")

public void webLog() {

}

@Before("webLog()")

public void doBefore(JoinPoint joinPointthrows Throwable {

// 接收到请求,记录请求内容

ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

HttpServletRequest request = attributes.getRequest();

// 记录下请求内容

logger.info("URL : " + request.getRequestURL().toString());

logger.info("HTTP_METHOD : " + request.getMethod());

logger.info("IP : " + request.getRemoteAddr());

Enumeration<String> enu = request.getParameterNames();

while (enu.hasMoreElements()) {

String name = (String) enu.nextElement();

logger.info("name:{},value:{}"namerequest.getParameter(name));

}

}

@AfterReturning(returning = "ret", pointcut = "webLog()")

public void doAfterReturning(Object retthrows Throwable {

// 处理完请求,返回内容

logger.info("RESPONSE : " + ret);

}

}

四、记录每个接口的响应时间。

 记录每个接口的响应时间,在实际使用中可以用来查询接口的运行效率。也可以通过elk的方式,将接口的响应的响应效率通过es等工具进行展示,监控接口的运行情况。

 

ElkFormatLog 类
//ELK日志记录实体类
public class ElkFormatLog {

    //接口名称
 public String interfacename;
    //日志级别,info信息,errinfo,错误信息
 public String logLevel;
    //日志记录时间
 public String logTime;
    //访问者Ip
 public String Ip ;
    //服务端Ip
 public String serverIp ;
    //服务端计算机名称
 public String servercomputername ;
    //调用的接口路径
 public String function ;
    //执行开始时间
 public String starttime;
    //执行结束时间,接口返回时间
 public String endtime;
    //接口方式执行耗时
 public long usetime;
    //接口入参
 public String inputParameters;
    //接口出参
 public String outputParameters;
    //接口信息,出错记录错误信息
 public String messageinfo;

}

 

切面拦截器优化

package com.winning.sx.microframework.config;

import com.alibaba.fastjson.JSON;
import com.winning.sx.microframework.common.constant.BaseConstant;
import com.winning.sx.microframework.common.util.ElkFormatLog;
import com.winning.sx.microframework.common.util.LogInfo;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.joda.time.DateTime;
import org.joda.time.Days;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.text.SimpleDateFormat;
import java.util.Date;
/*
使用记录ELK日志
 */
@Aspect
@Slf4j
@Component
public class AccessLogAspect {

    @Pointcut("execution(public * com.winning..controller.*.*(..))")
    public void accessLog() {
    }

    @Around("accessLog()")
    public Object around(ProceedingJoinPoint joinPoint){
        DateTime startTime =DateTime.now();
        // 定义返回对象、得到方法需要的参数
 Object resultData = null;
        Object[] args = joinPoint.getArgs();
        String uri = "";
        String methodName = joinPoint.getSignature().getName();
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        try {
            uri = request.getServletPath();
            if (joinPoint != null && joinPoint.getArgs() != null) {
                StringBuffer buf = new StringBuffer("[");
                for (int i = 0; i < joinPoint.getArgs().length; i++) {
                    Object arg = joinPoint.getArgs()[i];
                    if (arg != null && !(arg instanceof ServletRequest) && !(arg instanceof ServletResponse)) {
                        String argStr = arg.toString();
                        //针对登录密码日志,做特殊处理
 if (argStr.contains("password") && argStr.contains("LoginForm")){
                            int pwIndex = argStr.indexOf("password=");
                            argStr = argStr.substring(0,pwIndex+10)+"****"+argStr.substring(argStr.indexOf("'",pwIndex+11));
                        }
                        buf.append(argStr);
                    }
                }
// log.info("args{}", buf.append("]").toString());
 }
            resultData = joinPoint.proceed(args);
            DateTime endTime = DateTime.now();
            long diff = endTime.getMillis() - startTime.getMillis();
            //log.info("======>用户:{},完成请求[{}],开始时间:{},结束时间:{},耗时:{},返回:{}", "", uri,startTime.toString(),endTime.toString(), diff, resultData.toString());
 ElkFormatLog elk = new ElkFormatLog();
            elk.interfacename="JAVAVue系统框架";
            elk.logLevel = "loginfo";
            Date date = new Date();
            SimpleDateFormat sd = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
            elk.logTime = sd.format(date);
            elk.Ip = request.getRemoteAddr();
            elk.serverIp = request.getLocalAddr();
            elk.servercomputername =request.getLocalName();
            elk.function = request.getRequestURL().toString();
            elk.starttime = startTime.toString("yyyy-MM-dd HH:mm:ss.SSS");
            elk.endtime = endTime.toString("yyyy-MM-dd HH:mm:ss.SSS");
            elk.usetime = diff;
            elk.inputParameters = request.getQueryString();
            elk.outputParameters = JSON.toJSONString(resultData);
            elk.messageinfo = "";
            // 记录ELK日志信息
 LogInfo.writeLog(elk);
        } catch (Throwable e) {
            e.printStackTrace();
            DateTime endTime = DateTime.now();
            long diff = endTime.getMillis() - startTime.getMillis();
            ElkFormatLog elk = new ElkFormatLog();
            elk.interfacename="JAVA系统框架";
            elk.logLevel = "errinfo";
            Date date = new Date();
            SimpleDateFormat sd = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
            elk.logTime = sd.format(date);
            elk.Ip = request.getRemoteAddr();
            elk.serverIp = request.getLocalAddr();
            elk.servercomputername =request.getLocalName();
            elk.function = request.getRequestURL().toString();
            elk.starttime = startTime.toString("yyyy-MM-dd HH:mm:ss.SSS");
            elk.endtime = DateTime.now().toString("yyyy-MM-dd HH:mm:ss.SSS");
            elk.usetime = diff;
            elk.inputParameters = request.getQueryString();
            elk.outputParameters = JSON.toJSONString(BaseConstant.ERR_MSG);
            elk.messageinfo = e.toString();
            // 记录异常信息
 LogInfo.writeLog(elk);
            //log.error("======>用户:{},完成请求[{}],开始时间:{},结束时间:{},耗时:{},返回:{}", "", uri,startTime.toString(),endTime.toString(), diff, resultData.toString());
// log.error("======>请求[{}]异常!耗时:{}", uri, (endTime - startTime));
 }
        return resultData;
    }
}

 

记录日志类

package com.winning.sx.microframework.common.util;

import com.alibaba.fastjson.JSON;
import com.winning.sx.microframework.common.util.ElkFormatLog;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.text.SimpleDateFormat;
import java.util.Date;
/*
记录ELK可分析接口日志,方便问题追踪
 */
public class LogInfo {

    /**
 * 将信息写入到日志
 *
 * @param
 * @return
 * @throws IOException
 */
 public static boolean writeLog(ElkFormatLog elk) {
        //先进行日志内容拼接
 String content = info(elk);
        //创建文件夹及日志文件,每天一个文件夹,1小时一个日志文件
 Date date = new Date();
        SimpleDateFormat dtnow = new SimpleDateFormat("yyyyMMdd");
        SimpleDateFormat dtnowhour = new SimpleDateFormat("HH");
        String logfilepath = dtnow.format(date);
        String logfiel = dtnowhour.format(date);
        File file1 = new File(System.getProperty("user.dir") + "/WinningLog");
        //如果文件夹不存在则创建
 if (!file1.exists() && !file1.isDirectory()) {
            file1.mkdir();
        }
        File file = new File(System.getProperty("user.dir") + "/WinningLog/" + logfilepath);
        //如果文件夹不存在则创建
 if (!file.exists() && !file.isDirectory()) {
            file.mkdir();
        }
        File fileName = new File(System.getProperty("user.dir") + "/WinningLog/" + logfilepath + "/" + logfiel + ".wnlog");
        RandomAccessFile mm = null;
        boolean flag = false;
        FileOutputStream o = null;
        try {
            o = new FileOutputStream(fileName, true);
            o.write(content.getBytes("utf-8"));
            o.close();
            flag = true;
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (mm != null) {
                try {
                    mm.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        try {
            //使用线程进行删除过期的日志文件
 MyThread myThread = new MyThread();
            Thread t1 = new Thread(myThread);
            t1.start();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return flag;
    }

    /*
 使用线程进行删除过期日志文件,保留2天
 */
 public static class MyThread extends Thread {
        public void run() {
            removeOldFile(System.getProperty("user.dir") + "/WinningLog/", 2);
        }
    }

    /*
 拼接日志消息
 */
 private static String info(ElkFormatLog elk) {

        StringBuffer buffer = new StringBuffer();
        buffer.append(JSON.toJSONString(elk) + "\r\n");
        return buffer.toString();
    }

    private static void removeOldFile(String filepath, int keepDays) {
        File file = new File(filepath);
        File[] filelist = file.listFiles();
        for (int i = 0; i < filelist.length; i++) {
            File filedetail = new File(filelist[i].getPath());
            File[] filedetaillist = filedetail.listFiles();
            for (int j = 0; j < filedetaillist.length; j++) {
                long newtime = System.currentTimeMillis();
                long txttime = filedetaillist[j].lastModified();
                long time = newtime - txttime;
                time=time / (1000 * 60 * 60 * 24);
                if (time >= keepDays) {
                    filedetaillist[j].delete();
                }
            }
            //最后删除空文件夹
 filedetaillist = filedetail.listFiles();
            if(filedetaillist.length==0) {
                filelist[i].delete();
            }
        }
    }
}

五、将日志信息信息存储到数据库表中。

 代码优化

六、关于接口日志的细节思考。

 

1、接口必须进行日志存储,否则无法排查问题和处理问题。

2、接口日志一般建议保存7-30天,再长占用空间过多。

3、接口一定要进行性能分析,即存储服务的响应时间进行分析。

4、服务如果文件存储,建议按照小时存储,将一天的日志文件按照一个小时或者半个小时进行存储,方便排查问题。

 

1、日志存储在数据库还是通过文件格式存储。 

存储在数据库的优势在于方便进行结构化查询和分析。

缺陷在于需要对于日志进行定时清理,要不然数据库日志越来越大。

2、使用elk的方案。

将日志通过文件json格式进行存储,再异步进行文件提出,抽取到日志分析的数据库进行分析。因为是异步的即使日志平台崩溃了也不影响正常业务的使用。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值