利用svn的补丁文件打包生成增量文件的两种方法

1 篇文章 0 订阅
1 篇文章 0 订阅

需求缘起

很多情况下,项目是不允许全量发布的,所以你得把有做修改的文件一个个挑出来,如果有成千上百的文件,你是不是要头大了? 前提是你是用装有svn plugin的eclipse上做开发。 这样减少了一些琐碎,重复,没有任何技术含量的工作了,避免开发人员的宝贵时间浪费在一个个挑增量文件的痛苦中。下面会介绍利用svn的增量补丁文件如何实现自动化增量打包的原理及实现方法

解决方法

实现原理

讲简单点,主要包括几个步骤
1、生成增量的补丁文件
2、读补丁文件,遍历文件,如果是java文件,需要找到对应的class文件,其他文件如jsp,config配置文件 ,然后复制一份到指定的目标路径下即可

两个实现方法
在学习有关 增量发布的过程中,发现网上主要是两种方法

  1. 编写Ant脚步实现
  2. 纯java代码

两种方法的区别在于
1、利用ant脚步需要先把整个项目重新编译一遍(主要是将java文件编译成class文件),然后再去class文件目录下找到对应class文件,最后讲class文件复制出来到一个指定的文件目录下
2、纯java代码的话,不需要去编译整个项目,而是利用Eclipse的自动编译的class文件
3、ant脚步相对于纯java代码的方式更复杂,配置的内容较多,编译的时候比较耗时(如果项目很大的话),而且不允许项目中有任何编译出差的java代码,要不能整个项目编译会报错的,这种情况是经常会出现的,比如从svn更新代码后,就会出现某些java类出现异常的情况,这是因为其他同事提交代码的时候并未提交完全导致。编译的过程中可能会出现很奇怪的问题,可能都看不懂,对于不懂ant脚本的人来说,这也是很头痛的问题。
4、纯java代码就相对更灵活,没有任何配置,只有一个java类,直接运行main方法即可,耗时特别少。

废话少说,下面就结合我目前的项目分别介绍两种方法

编写纯java代码

1. 创建patch.txt增量文件

这里写图片描述

这里写图片描述

2. 编写java代码

package com.xyq.maventest;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;

public class FreePatchUtil {


    public static String patchFile="E:\\workplaces\\EC_TAOBAO\\patch.txt";//补丁文件,由eclipse svn plugin生成  

    public static String projectPath="E:\\workplaces\\EC_TAOBAO";//项目文件夹路径  

    public static String webContent="WebContent";//web应用文件夹名  

    public static String classPath="E:\\workplaces\\EC_TAOBAO\\WebContent\\WEB-INF\\classes";//class存放路径  

    public static String desPath="C:\\Users\\youqiang.xiong\\Desktop\\update_pkg";//补丁文件包存放路径  

    public static String version="20170908";//补丁版本  


    /** 
     * @param args 
     * @throws Exception  
     */  
    public static void main(String[] args) throws Exception {  
        copyFiles(getPatchFileList());  
    }  

    /****
     * 读取补丁配置文件解析出修改的文件并返回到list集合
     * @return
     * @throws Exception
     */
    public static List<String> getPatchFileList() throws Exception{  
        List<String> fileList=new ArrayList<String>();  
        FileInputStream f = new FileInputStream(patchFile);   
        BufferedReader dr=new BufferedReader(new InputStreamReader(f,"utf-8"));  
        String line;  
        while((line=dr.readLine())!=null){   
            if(line.indexOf("Index:")!=-1){  
                line=line.replaceAll(" ","");  
                line=line.substring(line.indexOf(":")+1,line.length());  
                fileList.add(line);  
            }  
        }  
        dr.close();
        return fileList;  
    }  

    /***
     * 
     * @param list 修改的文件
     */
    public static void copyFiles(List<String> list){  

        for(String fullFileName:list){  
            if(fullFileName.indexOf("src/")!=-1){//对源文件目录下的文件处理  
                String fileName=fullFileName.replace("src","");  
                fullFileName=classPath+fileName;  
                if(fileName.endsWith(".java")){  
                    fileName=fileName.replace(".java",".class");  
                    fullFileName=fullFileName.replace(".java",".class");  
                }  
                String tempDesPath=fileName.substring(0,fileName.lastIndexOf("/"));  
                String desFilePathStr=desPath+"/"+version+"/WEB-INF/classes"+tempDesPath;  
                String desFileNameStr=desPath+"/"+version+"/WEB-INF/classes"+fileName;  
                File desFilePath=new File(desFilePathStr);  
                if(!desFilePath.exists()){  
                    desFilePath.mkdirs();  
                } 
                copyFile(fullFileName, desFileNameStr);  
                System.out.println(fullFileName+"复制完成");  
            }else{//对普通目录的处理  
                String desFileName=fullFileName.replaceAll(webContent,"");  
                fullFileName=projectPath+"/"+fullFileName;//将要复制的文件全路径  
                String fullDesFileNameStr=desPath+"/"+version+desFileName;  
                String desFilePathStr=fullDesFileNameStr.substring(0,fullDesFileNameStr.lastIndexOf("/"));  
                File desFilePath=new File(desFilePathStr);  
                if(!desFilePath.exists()){  
                    desFilePath.mkdirs();  
                }  
                copyFile(fullFileName, fullDesFileNameStr);  
                System.out.println(fullFileName+"复制完成");  

            }  

        }  

    }  



    private static void copyFile(String sourceFileNameStr, String desFileNameStr) {  
        File srcFile=new File(sourceFileNameStr);  
        File desFile=new File(desFileNameStr);  
        try {  
            copyFile(srcFile, desFile);  
        } catch (IOException e) {  
            e.printStackTrace();  
        }  
    }  




    public static void copyFile(File sourceFile, File targetFile) throws IOException {  
        BufferedInputStream inBuff = null;  
        BufferedOutputStream outBuff = null;  
        try {  
            // 新建文件输入流并对它进行缓冲  
            inBuff = new BufferedInputStream(new FileInputStream(sourceFile));  

            // 新建文件输出流并对它进行缓冲  
            outBuff = new BufferedOutputStream(new FileOutputStream(targetFile));  

            // 缓冲数组  
            byte[] b = new byte[1024 * 5];  
            int len;  
            while ((len = inBuff.read(b)) != -1) {  
                outBuff.write(b, 0, len);  
            }  
            // 刷新此缓冲的输出流  
            outBuff.flush();  
        } finally {  
            // 关闭流  
            if (inBuff != null)  
                inBuff.close();  
            if (outBuff != null)  
                outBuff.close();  
        }  
    }  
}

下面这段代码根据你们的实际情况进行修改

  public static String patchFile="E:\\workplaces\\EC_TAOBAO\\patch.txt";//补丁文件,由eclipse svn plugin生成  

    public static String projectPath="E:\\workplaces\\EC_TAOBAO";//项目文件夹路径  

    public static String webContent="WebContent";//web应用文件夹名  

    public static String classPath="E:\\workplaces\\EC_TAOBAO\\WebContent\\WEB-INF\\classes";//class存放路径  

    public static String desPath="C:\\Users\\youqiang.xiong\\Desktop\\update_pkg";//补丁文件包存放路径  

    public static String version="20170908";//补丁版本  

3 .运行main方法就能在指定的目录下生成相应的增量文件

这里写图片描述

以上这段代码还存在一个问题,如果java类中存在内部类的情况下,貌似就不行了,可以对这段代码进行优化,新增一个方法专门用来处理内部类情况。

package com.xyq.maventest;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;

public class FreePatchUtil {


    public static String patchFile="E:\\workplaces\\EC_TAOBAO\\patch.txt";//补丁文件,由eclipse svn plugin生成  

    public static String projectPath="E:\\workplaces\\EC_TAOBAO";//项目文件夹路径  

    public static String webContent="WebContent";//web应用文件夹名  

    public static String classPath="E:\\workplaces\\EC_TAOBAO\\WebContent\\WEB-INF\\classes";//class存放路径  

    public static String desPath="C:\\Users\\youqiang.xiong\\Desktop\\update_pkg";//补丁文件包存放路径  

    public static String version="20170908";//补丁版本  


    /** 
     * @param args 
     * @throws Exception  
     */  
    public static void main(String[] args) throws Exception {  
        copyFiles(getPatchFileList());  
    }  

    /****
     * 读取补丁配置文件解析出修改的文件并返回到list集合
     * @return
     * @throws Exception
     */
    public static List<String> getPatchFileList() throws Exception{  
        List<String> fileList=new ArrayList<String>();  
        FileInputStream f = new FileInputStream(patchFile);   
        BufferedReader dr=new BufferedReader(new InputStreamReader(f,"utf-8"));  
        String line;  
        while((line=dr.readLine())!=null){   
            if(line.indexOf("Index:")!=-1){  
                line=line.replaceAll(" ","");  
                line=line.substring(line.indexOf(":")+1,line.length());  
                fileList.add(line);  
            }  
        }  
        dr.close();
        return fileList;  
    }  

    /***
     * 
     * @param list 修改的文件
     */
    public static void copyFiles(List<String> list){  

        for(String fullFileName:list){  
            if(fullFileName.indexOf("src/")!=-1){//对源文件目录下的文件处理  
                String fileName=fullFileName.replace("src","");  
                fullFileName=classPath+fileName;  
                if(fileName.endsWith(".java")){  
                    fileName=fileName.replace(".java",".class");  
                    fullFileName=fullFileName.replace(".java",".class");  
                }  
                String tempDesPath=fileName.substring(0,fileName.lastIndexOf("/"));  
                String desFilePathStr=desPath+"/"+version+"/WEB-INF/classes"+tempDesPath;  
                String desFileNameStr=desPath+"/"+version+"/WEB-INF/classes"+fileName;  
                File desFilePath=new File(desFilePathStr);  
                if(!desFilePath.exists()){  
                    desFilePath.mkdirs();  
                } 
                copyFile(fullFileName, desFileNameStr);  
                System.out.println(fullFileName+"复制完成");  
                //遍历目录,是否存在内部类,如果有内部,则将所有的额内部类挑选出来放到
                copyInnerClassFile(fullFileName, desFileNameStr);
            }else{//对普通目录的处理  
                String desFileName=fullFileName.replaceAll(webContent,"");  
                fullFileName=projectPath+"/"+fullFileName;//将要复制的文件全路径  
                String fullDesFileNameStr=desPath+"/"+version+desFileName;  
                String desFilePathStr=fullDesFileNameStr.substring(0,fullDesFileNameStr.lastIndexOf("/"));  
                File desFilePath=new File(desFilePathStr);  
                if(!desFilePath.exists()){  
                    desFilePath.mkdirs();  
                }  
                copyFile(fullFileName, fullDesFileNameStr);  
                System.out.println(fullFileName+"复制完成");  

            }  

        }  

    }  

    /***
     * 处理内部类的情况
     * 解析源路径名称,遍历此文件路径下是否存在这个类的内部类
     * 内部类编译后的格式一般是 OuterClassName$InnerClassName.class
     * @param sourceFullFileName 原路径
     * @param desFullFileName 目标路径
     */
    private static void copyInnerClassFile(String sourceFullFileName,String desFullFileName){

        String sourceFileName = sourceFullFileName.substring(sourceFullFileName.lastIndexOf("/")+1);
        String sourcePackPath = sourceFullFileName.substring(0,sourceFullFileName.lastIndexOf("/"));
        String destPackPath = desFullFileName.substring(0,desFullFileName.lastIndexOf("/"));
        String tempFileName = sourceFileName.split("\\.")[0];
        File packFile = new File(sourcePackPath);
        if(packFile.isDirectory()){
            String[] listFiles = packFile.list();
            for(String fileName:listFiles){
                //可以采用正则表达式处理
                if(fileName.indexOf(tempFileName+"$")>-1 && fileName.endsWith(".class")){
                    String newSourceFullFileName = sourcePackPath+"/" +fileName;
                    String newDesFullFileName = destPackPath + "/" + fileName;
                    copyFile(newSourceFullFileName, newDesFullFileName);  
                    System.out.println(newSourceFullFileName+"复制完成");  
                }
            }
        }

    }

    private static void copyFile(String sourceFileNameStr, String desFileNameStr) {  
        File srcFile=new File(sourceFileNameStr);  
        File desFile=new File(desFileNameStr);  
        try {  
            copyFile(srcFile, desFile);  
        } catch (IOException e) {  
            e.printStackTrace();  
        }  
    }  




    public static void copyFile(File sourceFile, File targetFile) throws IOException {  
        BufferedInputStream inBuff = null;  
        BufferedOutputStream outBuff = null;  
        try {  
            // 新建文件输入流并对它进行缓冲  
            inBuff = new BufferedInputStream(new FileInputStream(sourceFile));  

            // 新建文件输出流并对它进行缓冲  
            outBuff = new BufferedOutputStream(new FileOutputStream(targetFile));  

            // 缓冲数组  
            byte[] b = new byte[1024 * 5];  
            int len;  
            while ((len = inBuff.read(b)) != -1) {  
                outBuff.write(b, 0, len);  
            }  
            // 刷新此缓冲的输出流  
            outBuff.flush();  
        } finally {  
            // 关闭流  
            if (inBuff != null)  
                inBuff.close();  
            if (outBuff != null)  
                outBuff.close();  
        }  
    }  
}

至此通过java代码块自动生成增量脚本就已经完成了,是不是超级简单呀 。。

编写Ant脚步实现

1.创建patch.txt文件,参考上面

2.创建PatchFileKit.class

/*jadclipse*/// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.

package com.trendy.fw.tools.integration;

import java.io.*;

public class PatchFileKit
{

    private static final String prefix = "Index:";
    public PatchFileKit()
    {
    }

    private static void export(String fileStr, String baseDir, String destDir)
        throws Exception
    {
        String srcFile = (new StringBuilder()).append(baseDir).append(fileStr).toString();
        String desFile = (new StringBuilder()).append(destDir).append(fileStr).toString();
        int lastIndex = desFile.lastIndexOf("/");
        String desPath = lastIndex != -1 ? desFile.substring(0, lastIndex) : destDir.toString();
        File srcF = new File(srcFile);
        if(srcF.exists())
        {
            File desF = new File(desFile);
            File desP = new File(desPath);
            if(!desP.exists())
                desP.mkdirs();
            System.out.println((new StringBuilder()).append("\u5BFC\u51FA\u6587\u4EF6\uFF1A").append(fileStr).toString());
            FileInputStream fis = new FileInputStream(srcF);
            FileOutputStream fos = new FileOutputStream(desF);
            byte buf[] = new byte[1024];
            for(int len = 0; (len = fis.read(buf)) != -1;)
                fos.write(buf, 0, len);

            fos.flush();
            fos.close();
            fis.close();
        }
    }

    public static void main(String args[])
    {
        if(args != null && args.length > 0)
            if(args.length < 3)
            {
                System.out.println("args[0] is patch file full path");
                System.out.println("args[1] is workspace project base path");
                System.out.println("args[2] is increment files export path");
            } else
            {
                String configPath = args[0];
                String baseDir = args[1];
                String destDir = args[2];
                System.out.println((new StringBuilder()).append("\u8865\u4E01\u6587\u4EF6\uFF1A").append(configPath).toString());
                System.out.println((new StringBuilder()).append("\u5DE5\u7A0B\u8DEF\u5F84\uFF1A").append(baseDir).toString());
                System.out.println((new StringBuilder()).append("\u589E\u91CF\u6587\u4EF6\u5B58\u653E\u8DEF\u5F84\uFF1A").append(destDir).toString());
                System.out.println("--------------\u5F00\u59CB\u5BFC\u51FA\u589E\u91CF\u6587\u4EF6-----------------");
                try
                {
                    BufferedReader br = new BufferedReader(new FileReader(configPath));
                    String s = null;
                    do
                    {
                        if((s = br.readLine()) == null)
                            break;
                        s = s.trim();
                        if(s != null && s.startsWith("Index:"))
                        {
                            String fileStr = s.replaceFirst("Index:", "").trim();
                            export(fileStr, baseDir, destDir);
                        }
                    } while(true);
                    br.close();
                }
                catch(Exception e)
                {
                    e.printStackTrace();
                }
                System.out.println("------------- \u5BFC\u51FA\u589E\u91CF\u6587\u4EF6\u7ED3\u675F-----------------");
            }
    }


}

3 编写ant的build.properties属性配置文件

根据各种项目不同这里的属性配置

webapp.name=tpp
web.root=WebContent
src.dir = src

# project class file path
dist.classes = WebContent/WEB-INF/classes
dist.dir = build/dist
# distribute jar file name
dist.jar.name = trendy-jst
# distribute jar file version
dist.version = 0.1.0

# list of the file diff from svn
patch.file=patch.txt
# util(java class) to export diff files
patch.kit.classname = com.trendy.fw.tools.integration.PatchFileKit
# jar of the util. must update with ivy.xml
patch.kit.jar.name = trendy-tools
patch.kit.jar.version = 1.6.3
increment.dist = build/dist_increment/

javac.debuglevel=source,lines,vars

findbugs.home = ../EC_BASE/findbugs-2.0.3
ivy.instance = ../EC_BASE/ivysettings.xml
ivy.repository.jar.path = ../EC_BASE/ecrepository/trendy/trendy-jst-client/jars

4编写ant的build.xml脚本文件

<?xml version="1.0" encoding="UTF-8"?>
<project name="${webapp.name}" default="package" basedir="." xmlns:ivy="antlib:org.apache.ivy.ant">
    <property file="build.properties" />
    <property environment="env" />
    <property name="dist.jar" value="${dist.jar.name}-${dist.version}.jar" />
    <property name="debuglevel" value="${javac.debuglevel}" />

    <!-- svn增量文件保存目录  -->
    <property name="increment.files" location="${increment.dist}/files/" />
    <!-- svn增量文件编译后保存目录  -->
    <property name="increment.classes" location="${increment.dist}/classes/" />
    <property name="increment.webapp" location="${increment.dist}/webapp/" />

    <ivy:settings id="ivy.instance" file="${ivy.instance}" />

    <taskdef resource="checkstyletask.properties" />
    <taskdef name="findbugs" classname="edu.umd.cs.findbugs.anttask.FindBugsTask" />

    <path id="trendy.classpath">
        <pathelement location="${dist.classes}" />
        <fileset dir="lib">
            <include name="**/*.jar" />
        </fileset>
    </path>

    <target name="clean">
        <delete dir="${dist.classes}" />
        <delete dir="lib" />
    </target>

    <target name="clean_increment">
        <delete dir="${increment.files}" />
        <delete dir="${increment.classes}" />
        <delete dir="${increment.webapp}" />
    </target>

    <target name="init" depends="clean">
        <mkdir dir="${dist.classes}" />
        <mkdir dir="${dist.dir}" />
        <mkdir dir="lib" />
        <copy includeemptydirs="false" todir="${dist.classes}">
            <fileset dir="src">
                <exclude name="**/*.launch" />
                <exclude name="**/*.java" />
            </fileset>
        </copy>
    </target>

    <target name="init_increment" depends="clean_increment">
        <mkdir dir="${increment.dist}" />
        <mkdir dir="${increment.files}" />
        <mkdir dir="${increment.files}/${src.dir}" />
        <mkdir dir="${increment.classes}" />
        <mkdir dir="${increment.webapp}/WEB-INF/" />
    </target>

    <target name="compile" depends="init,resolve" description="compile project">
        <javac debug="true" 
            debuglevel="${debuglevel}" 
            destdir="${dist.classes}" 
            encoding="utf-8" 
            includeantruntime="on">
            <compilerarg value="-XDignore.symbol.file" />
            <src path="${src.dir}" />
            <exclude name="**/test/**" />
            <classpath refid="trendy.classpath" />
        </javac>
    </target>

    <target name="compile_increment" depends="compile,export_increment" description="compile increment files">
        <javac debug="true" 
            debuglevel="${debuglevel}" 
            destdir="${increment.classes}" 
            encoding="utf-8" 
            includeantruntime="on">
            <compilerarg value="-XDignore.symbol.file" />
            <src path="${increment.files}" />
            <exclude name="**/test/**" />
            <classpath refid="trendy.classpath" />
        </javac>
        <!-- copy jsp js ... to webapp dir -->
        <copy todir="${increment.webapp}/" failonerror="false">
            <fileset dir="${increment.files}/${web.root}" includes="**" />
        </copy>
        <!-- copy class file to webapp dir -->
        <copy todir="${increment.webapp}/WEB-INF/classes" failonerror="true">
            <fileset dir="${increment.classes}/" includes="**/*.class" />
            <fileset dir="${increment.files}/${src.dir}/" includes="**/*.xml, **/*.properties, **/*.xsd" />
        </copy>
    </target>

    <!-- 全部打包 -->
    <target name="war" depends="compile" description="package war file">
        <echo>create war file</echo>
        <!--得到当前日期-->
        <tstamp>
            <format property="DSTAMP" pattern="yyyyMMdd" locale="zh" />
            <format property="TSTAMP" pattern="HHmmss" locale="zh" />
        </tstamp>
        <war destfile="${dist.dir}/${webapp.name}-${DSTAMP}-${TSTAMP}.war" basedir="${basedir}/${web.root}/" webxml="${basedir}/${web.root}/WEB-INF/web.xml" />
    </target>

    <target name="war_increment" depends="compile_increment" description="package war file of increment files">
        <echo>create increment war file</echo>
        <!--得到当前日期-->
        <tstamp>
            <format property="DSTAMP" pattern="yyyyMMdd" locale="zh" />
            <format property="TSTAMP" pattern="HHmmss" locale="zh" />
        </tstamp>
        <war destfile="${increment.dist}/${webapp.name}-${DSTAMP}-${TSTAMP}.patch.war" basedir="${increment.webapp}" webxml="${basedir}/${web.root}/WEB-INF/web.xml" />
    </target>

    <!-- 导出增量文件 -->
    <target name="export_increment" depends="init_increment" description="export increment files">
        <property name="patch.kit.jar" value="${patch.kit.jar.name}-${patch.kit.jar.version}.jar" />
        <echo>export increment files</echo>
        <echo>check the version of [${patch.kit.jar.name}] in ivy.xml if throws error</echo>
        <echo>patch kit jar:${patch.kit.jar.name}-${patch.kit.jar.version}.jar</echo>
        <echo>patch kit classname:${patch.kit.classname}</echo>
        <java classname="${patch.kit.classname}" classpath="lib/${patch.kit.jar}" fork="true">
            <sysproperty key="file.encoding" value="UTF-8" />
            <arg value="${patch.file}" />
            <arg value="${basedir}\" />
            <arg value="${increment.files}\" />
        </java>
    </target>

    <target name="package" depends="compile" description="package jar file">
        <jar jarfile="${dist.dir}/${dist.jar}">
            <fileset dir="${dist.classes}">
                <!-- add class file here -->
            </fileset>
        </jar>
    </target>

    <target name="hudson" depends="compile,package,cs,findbugs">
        <copy todir="${ivy.repository.jar.path}" file="${dist.dir}/${dist.jar}" />
        <delete dir="lib" />
    </target>

    <target name="cs">
        <checkstyle failonviolation="false" config="..\EC_BASE\sun_checks.xml">
            <fileset dir="${src.dir}" includes="**/*.java" />
            <formatter type="plain" />
            <formatter type="xml" toFile="checkstyle_report.xml" />
        </checkstyle>
    </target>

    <target name="findbugs">
        <findbugs home="${findbugs.home}" output="xml" outputFile="findbugs_errors.xml">
            <sourcePath path="${src.dir}" />
            <class location="${dist.classes}" />
            <auxClasspath>
                <path refid="trendy.classpath" />
            </auxClasspath>
        </findbugs>
    </target>

    <target name="resolve" description="resolve ivy dependency">
        <ivy:retrieve />
    </target>

</project>
  1. 运行build.xml文件

这里写图片描述

这里写图片描述

  1. 点击run ,控制台就会编译运行,等待一段时间就可以在指定的目录下找到增量文件

这里写图片描述

把上面的WEB-INFO文件夹丢到服务器上覆盖即可。

备注:配置文件中的一些属性是需要根据你们项目中的实际情况进行修改的。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值