[jenkins]-通过配置文件一键创建jenkins任务

前段时间无聊  又不想一直重复jenkins创建job的过程。就想写一个脚本,根据用户传入的参数或者json格式的数据创建jenkins job。目前只能靠开发填写一个json格式的文件,让后将文件给我,我去跑一下脚本才行。

由于本人python 基本不会 全是百度编程。所以比较挫。典型的人菜嫌烦花头精多。目前都是没有带if判断的。

 

 

Settings.json

{

          "以下3项为必填项": "也可以使用默认值",

 

          "是否为java项目,经过maven编译": "yes",

          "JavaSettings": {

                   "pom文件路径": "pom.xml",

                   "mvn编译命令": "clean install --DskipTests=true"

          },

          "默认构建分支": "qa_car_loan",

          "代码git地址": "git@172.16.5.77:WDHC/web-service-all.git",

          "构建的job_name": "qa-wd-management-car-loan",

 

          "以下2项为可选项": "也可以使用默认值",

 

          "创建到指定view的name": "wangdao",

          "构建后shell(没有的话不做修改)": "echo 123"

}

 

 

创建通用的模板,我这边创建了2个。一个java 一个vue。文件夹里面内容如下

一个config.xml和builds文件夹。将/root/.jenkins/config.xml 复制一份为/root/.jenkins/exchange.xml。在/root/.jenkins/下创建insert.txt jobofview.txt format_insert.txt

其中java配置文件

<?xml version='1.0' encoding='UTF-8'?>

<maven2-moduleset plugin="maven-plugin@3.0">

  <actions/>

  <description></description>

  <keepDependencies>false</keepDependencies>

  <properties>

    <jenkins.model.BuildDiscarderProperty>

      <strategy class="hudson.tasks.LogRotator">

        <daysToKeep>10</daysToKeep>

        <numToKeep>10</numToKeep>

        <artifactDaysToKeep>-1</artifactDaysToKeep>

        <artifactNumToKeep>-1</artifactNumToKeep>

      </strategy>

    </jenkins.model.BuildDiscarderProperty>

    <hudson.model.ParametersDefinitionProperty>

      <parameterDefinitions>

        <hudson.model.StringParameterDefinition>

          <name>branch</name>

          <description></description>

          <defaultValue>branch_qa</defaultValue>

          <trim>false</trim>

        </hudson.model.StringParameterDefinition>

      </parameterDefinitions>

    </hudson.model.ParametersDefinitionProperty>

  </properties>

  <scm class="hudson.plugins.git.GitSCM" plugin="git@3.9.1">

    <configVersion>2</configVersion>

    <userRemoteConfigs>

      <hudson.plugins.git.UserRemoteConfig>

        <url>gitaddress</url>

        <credentialsId>17d7678b-e44d-4649-843f-4d0dc4f16fe9</credentialsId>

      </hudson.plugins.git.UserRemoteConfig>

    </userRemoteConfigs>

    <branches>

      <hudson.plugins.git.BranchSpec>

        <name>${branch}</name>

      </hudson.plugins.git.BranchSpec>

    </branches>

    <doGenerateSubmoduleConfigurations>false</doGenerateSubmoduleConfigurations>

    <submoduleCfg class="list"/>

    <extensions/>

  </scm>

  <canRoam>true</canRoam>

  <disabled>false</disabled>

  <blockBuildWhenDownstreamBuilding>false</blockBuildWhenDownstreamBuilding>

  <blockBuildWhenUpstreamBuilding>false</blockBuildWhenUpstreamBuilding>

  <triggers/>

  <concurrentBuild>false</concurrentBuild>

  <goals>clean install -DskipTests=true</goals>

  <aggregatorStyleBuild>true</aggregatorStyleBuild>

  <incrementalBuild>false</incrementalBuild>

  <ignoreUpstremChanges>true</ignoreUpstremChanges>

  <ignoreUnsuccessfulUpstreams>false</ignoreUnsuccessfulUpstreams>

  <archivingDisabled>false</archivingDisabled>

  <siteArchivingDisabled>false</siteArchivingDisabled>

  <fingerprintingDisabled>false</fingerprintingDisabled>

  <resolveDependencies>false</resolveDependencies>

  <processPlugins>false</processPlugins>

  <mavenValidationLevel>-1</mavenValidationLevel>

  <runHeadless>false</runHeadless>

  <disableTriggerDownstreamProjects>false</disableTriggerDownstreamProjects>

  <blockTriggerWhenBuilding>true</blockTriggerWhenBuilding>

  <settings class="jenkins.mvn.FilePathSettingsProvider">

    <path>/root/.m2/settings_hengtian.xml</path>

  </settings>

  <globalSettings class="jenkins.mvn.FilePathGlobalSettingsProvider">

    <path>/root/.m2/settings_hengtian.xml</path>

  </globalSettings>

  <reporters/>

  <publishers/>

  <buildWrappers/>

  <prebuilders/>

  <postbuilders>

    <hudson.tasks.Shell>

      <command>echo hello</command>

    </hudson.tasks.Shell>

  </postbuilders>

  <runPostStepsIfResult>

    <name>SUCCESS</name>

    <ordinal>0</ordinal>

    <color>BLUE</color>

    <completeBuild>true</completeBuild>

  </runPostStepsIfResult>

</maven2-moduleset>

 

 

vue

<?xml version='1.0' encoding='UTF-8'?>

<project>

  <actions/>

  <description></description>

  <keepDependencies>false</keepDependencies>

  <properties>

    <jenkins.model.BuildDiscarderProperty>

      <strategy class="hudson.tasks.LogRotator">

        <daysToKeep>10</daysToKeep>

        <numToKeep>10</numToKeep>

        <artifactDaysToKeep>-1</artifactDaysToKeep>

        <artifactNumToKeep>-1</artifactNumToKeep>

      </strategy>

    </jenkins.model.BuildDiscarderProperty>

    <hudson.model.ParametersDefinitionProperty>

      <parameterDefinitions>

        <hudson.model.StringParameterDefinition>

          <name>branch</name>

          <description></description>

          <defaultValue>branch_qa</defaultValue>

          <trim>false</trim>

        </hudson.model.StringParameterDefinition>

      </parameterDefinitions>

    </hudson.model.ParametersDefinitionProperty>

  </properties>

  <scm class="hudson.plugins.git.GitSCM" plugin="git@3.9.1">

    <configVersion>2</configVersion>

    <userRemoteConfigs>

      <hudson.plugins.git.UserRemoteConfig>

        <url>gitaddress</url>

        <credentialsId>17d7678b-e44d-4649-843f-4d0dc4f16fe9</credentialsId>

      </hudson.plugins.git.UserRemoteConfig>

    </userRemoteConfigs>

    <branches>

      <hudson.plugins.git.BranchSpec>

        <name>${branch}</name>

      </hudson.plugins.git.BranchSpec>

    </branches>

    <doGenerateSubmoduleConfigurations>false</doGenerateSubmoduleConfigurations>

    <submoduleCfg class="list"/>

    <extensions/>

  </scm>

  <canRoam>true</canRoam>

  <disabled>false</disabled>

  <blockBuildWhenDownstreamBuilding>false</blockBuildWhenDownstreamBuilding>

  <blockBuildWhenUpstreamBuilding>false</blockBuildWhenUpstreamBuilding>

  <authToken>123456</authToken>

  <triggers/>

  <concurrentBuild>false</concurrentBuild>

  <builders>

    <hudson.tasks.Shell>

      <command>echo hello</command>

    </hudson.tasks.Shell>

  </builders>

  <publishers/>

  <buildWrappers/>

</project>

 

 

大致就是读取Settings.json文件里的信息,然后根据里面的信息,选择不同的模板文件,拷贝模板以新job的名称命名。替换新job文件config.xml里的内容,git地址这些。备份jenkins文件夹下的config.xml,然后查找jenkins的config.xml中所有的jobname的位置,获取config中jobname的位置,提取其中最靠后2个jobname的行信息,job_no1和job_no2,他们2者中间的行就是view下面所有job的名称。根据提供的行信息 提取jobname下面的jobname到jobofview.txt中,提取 jobofview.txt文件中<string></string>之间的jobname,加入新的job名称并排序,倒序排列该文件之后写入新的文件insert.txt。删exchange.xml中job_no1和job_no2行之间的内容.对排序好的insert.txt 加上<string></string>,成为format_insert.txt,在job_no1之后一行行倒序插入format_insert.txt的内容。再对exchange.xml文件格式化输出之后写入/root/.jenkins/config.xml,删除之前无用的文件(可选),重新加载jenkins配置。

 

中间多了很多插入,排序,添加的操作。主要是因为jenkins关于view视图的排序,job-name1应该排在job-name2之上,但是sort的时候job-name2会在job-name1上面 ,只能使用笨办法,一步一步将view信息修改好之后再重新插入。

#coding=utf-8

import json

import shutil

import os

import pandas

 

 

#导入json 配置文件--------------------------------------

def loadFont():

    f = open("/config_jenkins/Settings.json")

    global git_branch ,git_address ,git_address ,new_job_name ,view_name ,shell_command ,project_type , strofview_name, strofnew_job_name ,job_line ,exchange_file ,job_path ,job_path_configxml ,pom_adr ,mvn_command

    job_line='jobNames'

    exchange_file='/root/.jenkins/exchange.xml'

    setting = json.load(f)

    git_branch = setting[u'默认构建分支'] #  //注意多重结构的读取语法

    git_address= setting[u'代码git地址'] #  //注意多重结构的读取语法

    new_job_name = setting[u'构建的job_name'] #  //注意多重结构的读取语法

    view_name = setting[u'创建到指定view的name'] #  //注意多重结构的读取语法

    shell_command = setting[u'构建后shell(没有的话不做修改)'] #  //注意多重结构的读取语法

    project_type = setting[u'是否为java项目,经过maven编译']

    project_type = project_type.lower()

    pom_adr = setting["JavaSettings"][u'pom文件路径']

    mvn_command = setting["JavaSettings"][u'mvn编译命令']

    strofview_name = "'"'<name>' + view_name + '</name>'"'"

    print strofview_name

    #strofnew_job_name = '        <string>new_job_name</string>\n'

    strofnew_job_name = "<string>" + new_job_name + "</string>\n"

    print strofnew_job_name

    job_path = '/root/.jenkins/jobs/' + new_job_name

    job_path_configxml = job_path + '/config.xml'

 

 

 

#拷贝自由风格的文件夹做为模板--------------------------------------

def copyfree_example_dir():

    #coding=utf-8

    example_dir='/config_jenkins/free-example'

    path=job_path

    print(path)

    path=path.strip()

    path=path.rstrip("\\")

    # 判断路径是否存在

    isExists=os.path.exists(path)

    # 不存在

    if not isExists:

        # 创建目录操作函数

        print(path + u'已经创建')

        shutil.copytree(example_dir, path)

        return True

    #存在

    else:

        print(path + u'目录存在')

        return False

 

 

 

 

    shutil.copytree(example_dir, path)

 

#拷贝mvn的文件夹做为模板--------------------------------------

def copymvn_example_dir():

    example_dir='/config_jenkins/mvn-example'

    #path='/tmp/test/test'

    path=job_path

    path=path.strip()

    path=path.rstrip("\\")

    # 判断路径是否存在

    isExists=os.path.exists(path)

    # 不存在

    if not isExists:

        # 创建目录操作函数

        print(path+ u'已经创建')

        shutil.copytree(example_dir, path)

        return True

    #存在

    else:

        print(path+ u'目录存在')

        return False

 

 

 

 

    shutil.copytree(example_dir, path)      

         

         

#修改free模板文件中的example.xml--------------------------------------

def change_xml_freeproject():

    import xml.etree.ElementTree as ET

    tree = ET.parse(job_path_configxml)

    root = tree.getroot()

    for child in root:

        print(child.tag, child.attrib)

 

    #替换defaultValue下默认分支

    for name in root.iter('defaultValue'):

        new_name = str(git_branch)

        name.text = str(new_name)

        tree.write(job_path_configxml)

 

    #替换git代码地址

    for name in root.iter('url'):

        new_name = str(git_address)

        name.text = str(new_name)

        tree.write(job_path_configxml)

 

    #替换构建后shell

    for name in root.iter('command'):

        new_name = str(shell_command)

        name.text = str(new_name)

        tree.write(job_path_configxml)

 

 

#修改mvn模板文件中的example.xml--------------------------------------

def change_xml_mvnproject():

    import xml.etree.ElementTree as ET

    tree = ET.parse(job_path_configxml)

    root = tree.getroot()

    for child in root:

        print(child.tag, child.attrib)

 

    #替换defaultValue下默认分支

    for name in root.iter('defaultValue'):

        new_name = str(git_branch)

        name.text = str(new_name)

        tree.write(job_path_configxml)

 

    #替换git代码地址

    for name in root.iter('url'):

        new_name = str(git_address)

        name.text = str(new_name)

        tree.write(job_path_configxml)

 

    #替换构建后shell

    for name in root.iter('command'):

        new_name = str(shell_command)

        name.text = str(new_name)

        tree.write(job_path_configxml)       

 

    #替换mvn命令

    for name in root.iter('goals'):

        new_name = str(mvn_command)

        name.text = str(new_name)

        tree.write(job_path_configxml)

 

 

    #替换pom文件原路径

    #for name in root.iter(''):

    #    new_name = str(pom_adr)

    #    name.text = str(new_name)

    #    tree.write(job_path_configxml)

 

 

 

         

#备份jenkins文件夹下的config.xml--------------------------------------

def copy():

    import os,time

    oldFileName = '/root/.jenkins/config.xml'

 

    oldFile = open(oldFileName,'rb')

    # 提取文件的后缀

    fileFlagNum = oldFileName.rfind('.')

    if fileFlagNum > 0:

    # 截取文件名’.‘到最后

        fileFlag = oldFileName[fileFlagNum:]

    # 组织新的文件名字

    #newFileName = oldFileName[:fileFlagNum] + 时间 + fileFlag

    global newFileName

    newFileName = oldFileName[:fileFlagNum] + time.strftime('-%Y-%m-%d-%H:%M:%S') + fileFlag

    backupFileName =oldFileName[:fileFlagNum] + '-' + 'bak' +fileFlag

    # 创建新文件

    newFile = open(newFileName, 'wb')

    backupFile = open(backupFileName, 'wb')

    # 把旧文件的内容复制到新文件中

    for lineContent in oldFile.readlines():

        newFile.write(lineContent)

        backupFile.write(lineContent)

    # 关闭文件

    oldFile.close()

    newFile.close()

    backupFile.close()

 

 

 

#查找config中所有的jobname的位置--------------------------------------

def merge():

    file = open(newFileName,"r")

    string='<name>'+view_name +'</name>'

    string=unicode.encode(string)

    print(123)

    print string

    #string='<name>test</name>'

    #print(type(string))

    for num,value in enumerate(file):

      #if strofview_name2  in value:

      if string  in value:

          print("the nume:%s,the value is %s") %(num,value)

          break

      else:

          pass

    #print(num)

    global i

    #num =num+1

    i=num

    #print(type(num))

    return merge

    file.close()

 

         

#获取viewname之后的2个jobname的位置信息--------------------------------------

def search_job():
    global job_no1
    global job_no2
    num=merge()
    print(i)
    #print(type (i))
    count=0
    file = open(newFileName,"r")
    for no,value in enumerate(file):
      if job_line in value:
          #print no 
          if (no >i):# and (no < i+6): 
              if (count<1):
                  print(no)
                  job_no1=no
                  count =count+1
              elif (1<=count<2):
                  print(no)
                  job_no2=no
                  break

          else:
              pass
      else:
          pass
    print(job_no1)
    print(job_no2)

    file.close()
 

 

 

 

 

#根据job提供的line 提取jobname下面的jobname到jobofview.txt中--------------------------------------

def extract():

    #strofnew_job_name='        <string>qa-wy-juwan-webaaa</string>\n'

    result=[]

    lnum = 0

    with open(newFileName, 'r') as fd:

        for line in fd:

            lnum += 1;

            if (lnum >= job_no1+2) and  (lnum <= job_no2):

                result.append(line)

                ab=line.strip()

                #print(line.strip())

                print(ab)

    fd.close()

    print(result)

   

 

    result.sort()

    result.append(strofnew_job_name)

    result.sort()

    f=open('/root/.jenkins/jobofview.txt','w')

    f.writelines(result)

    f.close()      

   

         

         

def sort():

    import pandas

    import re

    f=open('/root/.jenkins/jobofview.txt')

    result= []

    iter_f=iter(f) #用迭代器循环访问文件中的每一行

    for line in iter_f:

        regex = r'<string>(.*)</string>'

        del_string = re.findall(regex, line)

        if del_string:

            del_string = del_string[0]

        else:

            continue

        print(del_string)

        string = str(del_string)

#       import pdb;pdb.set_trace();

        string = string + "\n"

        result.append(string)

        #print(type(result))

        #print(result)

    f.close()

    #print(result)

 

 

    result.sort(reverse=True)

    f=open('/root/.jenkins/insert.txt','w')

   # f.writelines('<string>')

    f.writelines(result)

    #f.writelines('</string>')

    f.writelines('\n')

    f.close()

 

#删除jobname之间的行--------------------------------------

def delete():

    data = open(newFileName, 'rt').readlines()

    with open(newFileName, 'wt') as handle:

        handle.writelines(data[:job_no1+1])

        handle.writelines(data[job_no2:])

 

#对排序好的insert.txt 加上<string>

def TXTRead_Writeline():

    #读取文件

    import os

    my_file = '/root/.jenkins/format_insert.txt'

    if os.path.exists(my_file):

    #删除文件,可使用以下两种方法。

        os.remove(my_file)

    #os.unlink(my_file)

    else:

        print 'no such file:%s'%my_file

    ms = open("/root/.jenkins/insert.txt")

    #逐行写入

    for line in ms.readlines():

        with open("/root/.jenkins/format_insert.txt","a") as mon:

            line = line.strip('\n')

            line = "<string>" + line + "</string>" +"\n"

            mon.write(line)

 

    #with open("format_insert.txt", encoding="utf-8",mode="a") as data: 

    #    data.write("朝九晚五")

    file = open("/root/.jenkins/format_insert.txt", mode="a")

    #file.write("<comparator class=\"hudson.util.CaseInsensitiveComparator\"/>\n")

    file.close()

 

 

                  

#在jobofview中插入新jobbane--------------------------------------

def insert_into_jobofview():

    print(123123123123)

    print strofnew_job_name

    str1=strofnew_job_name+"\n"

    f = open('/root/.jenkins/jobofview.txt','a')

    f.write(str1)

    f.close()

 

 

#倒序排列文件--------------------------------------

def old_sort():

    import pandas

 

    f=open('/root/.jenkins/jobofview.txt')

    result= []

    iter_f=iter(f) #用迭代器循环访问文件中的每一行

    for line in iter_f:

        result.append(line)

    f.close()

    print(result)

 

 

    result.sort(reverse=True)

    f=open('/root/.jenkins/insert.txt','w')

    f.writelines(result)

    f.writelines('\n')

    f.close()

 

 

 

 

#将jobname插入--------------------------------------

def insert_into_config():

    #f1 = open(r'/root/.jenkins/insert.txt','rb')

    #f2= open(r'/root/.jenkins/config.xml','ab')

 

    #i=0

    #while True:

     #   line = f1.readline()

      #  i+=1

       # if i>job_no1+1 and i<job_no2:

        #    f2.write(line)

       # if i>200:

        #    break

 

    fp = file(newFileName)

    lines = []

    for line in fp:

        lines.append(line.strip())

    fp.close()

 

 

#

 

    #fp2 = file('/root/.jenkins/insert.txt')

    fp2 = file('/root/.jenkins/format_insert.txt')

    lines2 = []

    for line2 in fp2:

        lines.insert(job_no1+1, line2)

        s= '\n'.join(lines)

        fp2 = file(exchange_file, 'w')

        fp2.write(s)

    fp2.close()

 

 

 

 

         

 

 

 

 

 

#对xml文件格式化操作--------------------------------------

def prettyXml(element, indent, newline, level = 0): # elemnt为传进来的Elment类,参数indent用于缩进,newline用于换行   

    if element:

    #判断element是否有子元素   

        if element.text == None or element.text.isspace(): # 如果element的text没有内容   

            element.text = newline + indent * (level + 1)

        else:

            element.text = newline + indent * (level + 1) + element.text.strip() + newline + indent * (level + 1)

    #else:  # 此处两行如果把注释去掉,Element的text也会另起一行   

        #element.text = newline + indent * (level + 1) + element.text.strip() + newline + indent * level   

    temp = list(element) # 将elemnt转成list   

    for subelement in temp:

        if temp.index(subelement) < (len(temp) - 1): # 如果不是list的最后一个元素,说明下一个行是同级别元素的起始,缩进应一致   

            subelement.tail = newline + indent * (level + 1)

        else:  # 如果是list的最后一个元素, 说明下一行是母元素的结束,缩进应该少一个   

            subelement.tail = newline + indent * level

        prettyXml(subelement, indent, newline, level = level + 1) # 对子元素进行递归操作   

  

 

 

#对格式化后的文件写入文件--------------------------------------

def doformat():

    global root

    from xml.etree import ElementTree      #导入ElementTree模块   

    tree = ElementTree.parse(exchange_file)   #解析test.xml这个文件,该文件内容如上文   

    root = tree.getroot()                  #得到根元素,Element类   

    prettyXml(root, '\t', '\n')            #执行美化方法   

    tree.write('/root/.jenkins/config.xml', encoding='UTF-8')

    ElementTree.dump(root)                 #显示出美化后的XML内容

 

 

#删除无用文件--------------------------------------

def delete_backfile():

    import os

    if os.path.exists(newFileName):

    #删除文件,可使用以下两种方法。

        print(newFileName)

        os.remove(newFileName)

    #os.unlink(my_file)

    else:

        print("no such file:" %s) %newFileName        

 

#reload jenkins

def reload_jenkins():

    import requests

    url = 'http://172.16.129.72:8080/jenkins/reload'##定义http请求的地址,即1

    headers = {'Accept': "application/xml",'Date':'Fri, 14 Apr 2017 02:07:17 GMT'}##定义header头,用dict方式定义,即3

    #data = {'channel': 'vod.tv.cn', 'dataformat': 'json','date':'2017-04-13'}##定义参数,同样用dict定义,即4

    #res = requests.post(url, data=data, headers=headers, auth=('tv2','l5f7jrRQttWdxsLmY7FV4+MA='))##post请求,且认证user=‘tv2’,password=‘sign’

    #res = requests.post(url, headers=headers, auth=('admin','1u%VOMW$er'))##post请求,且认证user=‘tv2’,password=‘sign’

    res = requests.post(url, headers=headers, auth=('admin','68d9a2ccdad1b1e60be581970c70487a'))##post请求,且认证user=‘tv2’,password=‘sign’

    #res = requests.post(url, headers=headers, auth=('jiaminxu','119230775d4a15b4aaf5862c3f1d420a1a'))##post请求,且认证user=‘tv2’,password=‘sign’

    #print res.text##就能看到打印结果了

 

 

#--------------------------------------以上为函数部分--------------------------------------

 

 

if __name__ == "__main__":

    loadFont()

 

 

 

    if 'yes' in project_type:

 

        print '构建JAVA项目'

        loadFont()

        copymvn_example_dir()

        change_xml_mvnproject()

        copy()

        merge()

        search_job()

        extract()

 

        sort() #将jobview里的文件排序后插入insert.txt

        #insert_into_jobofview()

        TXTRead_Writeline() #对排序好的insert.txt 加上<string>

        delete() #删除jobname之间的行

        insert_into_config() #在jobofview中插入新jobbane

        #prettyXml() #对xml文件格式化操作

        doformat() #对格式化后的文件写入文件

        reload_jenkins()

 

    else:

 

        print '构建自由项目'

        loadFont()

        copyfree_example_dir()

        change_xml_freeproject()

        copy()

        merge()

        search_job()

        extract()

 

        sort() #将jobview里的文件排序后插入insert.txt

        #insert_into_jobofview()

        TXTRead_Writeline() #对排序好的insert.txt 加上<string>

        delete() #删除jobname之间的行

        insert_into_config() #在jobofview中插入新jobbane

        #prettyXml() #对xml文件格式化操作

        doformat() #对格式化后的文件写入文件

        reload_jenkins()

 

执行完毕后 不报错会出现以下界面

最后来个动图

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

爷来辣

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值