简单的docker学习 第13章 CI/CD与Jenkins(下)

第13章 CI/CD 与 Jenkins

13.13 自由风格的 CI 操作(最终架构)

前面的架构存在的问题是,若有多个目标服务器都需要使用该镜像,那么每个目标服务器都需要在本地构建镜像,形成系统资源浪费。若能够在 Jenkins 中将镜像相撞构建好并推送到 Harbor 镜像中心,那么无论有多少目标服务器需要该镜像,都只需要从 Harbor 拉取即可。

13.13.1 Jenkins 容器化实现
  • Jenkins 容器化实现方案

  • DioD:要容器内部安装 Docker 引擎

  • DooD:与宿主机共享 Docker 引擎

  • 修改 docker.sock 权限

    /var/run/docker.sock 文件是 docker client 和 docker daemon 在本地进行通信的 socket文件。默认的组为 docker,且 other 用户不具有读写权限,这样 Jenkins 是无法来操作该文件的。

    image-20240723134522840

    所以这里需要将其组调整为 root,且为其分配读写权限

    chown root:root docker.sock
    chmod o+rw docker.sock
    
       
       
    • 1
    • 2

    image-20240723135715036

  • 修改 Jenkins 启动命令后重启

    首先强制删除正在运行的 Jenkins 容器

    docker rm -f jenkins
    
       
       

    然后在 Jenkins 启动命令中新增**/var/run/docker.sock**,docker 命令文件**/usr/bin/docker**,及**/etc/docker/daemon.json** 文件为数据卷。重启 Jenkins 容器

        docker run --name jenkins \
        --restart always \
        -p 8080:8080 \
        -p 50000:50000 \
        -v /var/jenkins_home:/var/jenkins_home \
        -v /var/run/docker.sock:/var/run/docker.sock \
        -v /usr/bin/docker:/usr/bin/docker \
        -v /etc/docker/daemon.json:/etc/docker/daemon.json \
        -d jenkins/jenkins
    
       
       

    启动jenkins服务后,可以使用命令行进行互动,进入控制台使用docker version查看docker 版本信息,证明jenkins服务器内可以执行docker命令

    docker exec -it jenkins /bin/bash
    docker version
    
       
       

    image-20240723140544506

13.13.2 构建并推送镜像到 Harbor

这里要实现 Jenkins 将来自 GitLab 的 jar 包构建为镜像,并推送到 Harbor。

  • 修改 daemon.json 文件

    Jenkins 是 Harbor 的客户端,需要修改/etc/docker/daemon.json 文件。修改后重启 Docker。

    {
      "registry-mirrors": ["https://docker.m.daocloud.io","https://huecker.io","https://dockerhub.timeweb.cloud","https://noohub.ru"],
      "insecure-registries": ["192.168.138.134:80"]
    }
    
       
       

    image-20240723141020532

  • Jenkins 删除构建后操作

    原来的 Jenkins 中配置的“构建后操作”完成的是将代码推送到目标服务器后,让目标服务器通过 docker compose 完成镜像的构建与启动。但现在不需要了,因为镜像构建任务要由 Jenkins 自己完成了。在 Jenkins 当前任务下的“配置”中删除。

    在这里插入图片描述

  • Jenkins 添加 shell 命令

    再添加一个“构建步骤”。这个构建步骤通过 shell命令方式完成。

    image-20240723141659468

    image-20240723143323439

    mv target/*.jar docker/
    cd docker
    mv docker-hello-1.0-SNAPSHOT.jar docker-hello.jar
    docker build -t docker-hello .
    docker login -u admin -p 101022 192.168.138.134:80
    # 此处的tag可以在后面标记上版本编号,比如时间戳等,方便后续版本管理
    docker tag docker-hello 192.168.138.134:80/jks/docker-hello
    docker image prune -f
    docker push 192.168.138.134:80/jks/docker-hello
    
       
       
  • 重新构建

    Jenkins 中在返回的任务首页中,再次执行立即构建。构建成功后,在 Jenkins 主机中可以查看到构建好的镜像与重 tag 过的镜像

    image-20240723143458895

    image-20240723143423957

    在 harbor 的仓库中也可以看到推送来的镜像

    image-20240723143719491

13.13.3 通知目标服务器
  • 修改 daemon.json 文件

    目标服务器是 Harbor 的客户端,需要修改/etc/docker/daemon.json 文件。修改后重启Docker。

    image-20240723143830583

  • 定义脚本文件

    在目标服务器 PATH 路径下的任意目录中定义一个脚本文件 deploy.sh。例如,定义在/usr/local/bin 目录下。然后再为其赋予可执行权限。这样该 deploy 命令就可以在任意目录下运行了。

    image-20240723145113131

    harbor_addr=$1
    harbor_proj=$2
    image_repo=$3
    image_tag=$4
    app_port=$5
    export_port=$6
    exist_cont_id=`docker ps -a | grep $image_repo | awk '{print $1}'`
    echo $exist_cont_id
    

if [ " e x i s t c o n t i d < / s p a n > " < / s p a n > < s p a n c l a s s = " t o k e n o p e r a t o r " > ! = < / s p a n > < s p a n c l a s s = " t o k e n s t r i n g " > " " < / s p a n > < s p a n c l a s s = " t o k e n p u n c t u a t i o n " > ] < / s p a n > < s p a n c l a s s = " t o k e n p u n c t u a t i o n " > ; < / s p a n > < s p a n c l a s s = " t o k e n k e y w o r d " > t h e n < / s p a n > < s p a n c l a s s = " t o k e n f u n c t i o n " > d o c k e r < / s p a n > s t o p < s p a n c l a s s = " t o k e n v a r i a b l e " > exist_cont_id</span>"</span> <span class="token operator">!=</span> <span class="token string">""</span> <span class="token punctuation">]</span> <span class="token punctuation">;</span> <span class="token keyword">then</span> <span class="token function">docker</span> stop <span class="token variable"> existcontid</span>"</span><spanclass="tokenoperator">!=</span><spanclass="tokenstring">""</span><spanclass="tokenpunctuation">]</span><spanclass="tokenpunctuation">;</span><spanclass="tokenkeyword">then</span><spanclass="tokenfunction">docker</span>stop<spanclass="tokenvariable">exist_cont_id
docker rm $exist_cont_id
fi

exist_image_tag=</span><span class="token function">docker</span> images <span class="token operator">|</span> <span class="token function">grep</span> $harbor_addr/$harbor_proj/$image_repo <span class="token operator">|</span> <span class="token function">awk</span> <span class="token string">'{print $2}'</span><span class="token variable">
image= h a r b o r a d d r < / s p a n > / < s p a n c l a s s = " t o k e n v a r i a b l e " > harbor_addr</span>/<span class="token variable"> harboraddr</span>/<spanclass="tokenvariable">harbor_proj/ i m a g e r e p o < / s p a n > < s p a n c l a s s = " t o k e n b u i l t i n c l a s s − n a m e " > : < / s p a n > < s p a n c l a s s = " t o k e n v a r i a b l e " > image_repo</span><span class="token builtin class-name">:</span><span class="token variable"> imagerepo</span><spanclass="tokenbuiltinclassname">:</span><spanclass="tokenvariable">image_tag
echo i m a g e < / s p a n > < s p a n c l a s s = " t o k e n k e y w o r d " > i f < / s p a n > < s p a n c l a s s = " t o k e n p u n c t u a t i o n " > [ < / s p a n > < s p a n c l a s s = " t o k e n p u n c t u a t i o n " > [ < / s p a n > < s p a n c l a s s = " t o k e n s t r i n g " > " < s p a n c l a s s = " t o k e n v a r i a b l e " > image</span> <span class="token keyword">if</span> <span class="token punctuation">[</span><span class="token punctuation">[</span> <span class="token string">"<span class="token variable"> image</span><spanclass="tokenkeyword">if</span><spanclass="tokenpunctuation">[</span><spanclass="tokenpunctuation">[</span><spanclass="tokenstring">"<spanclass="tokenvariable">exist_image_tag=~ $image_tag" ]] ; then
docker rmi -f simage
fi

echo 101022 | docker login -u admin --password-stdin $harbor_addr

docker pull i m a g e < / s p a n > < s p a n c l a s s = " t o k e n f u n c t i o n " > d o c k e r < / s p a n > r u n < s p a n c l a s s = " t o k e n p a r a m e t e r v a r i a b l e " > − − n a m e < / s p a n > < s p a n c l a s s = " t o k e n v a r i a b l e " > image</span> <span class="token function">docker</span> run <span class="token parameter variable">--name</span> <span class="token variable"> image</span><spanclass="tokenfunction">docker</span>run<spanclass="tokenparametervariable">name</span><spanclass="tokenvariable">image_repo -dp e x p o r t p o r t < / s p a n > < s p a n c l a s s = " t o k e n b u i l t i n c l a s s − n a m e " > : < / s p a n > < s p a n c l a s s = " t o k e n v a r i a b l e " > export_port</span><span class="token builtin class-name">:</span><span class="token variable"> exportport</span><spanclass="tokenbuiltinclassname">:</span><spanclass="tokenvariable">app_port $image

echo “SUCCESS”

  • Jenkins 添加端口号参数

    为了使用户可以随时指定容器对外暴露的参数,这里在 Jenkins 当前任务下的“配置”中“参数化构建过程”中添加一个字符参数

    image-20240723145346433

    image-20240723145445446

    image-20240723145539776

  • Jenkins 添加构建后操作

    还是在 Jenkins 当前任务下的“配置”中,为任务添加构建后操作

    image-20240723145704952

    docker-deploy.sh 192.168.138.134:80 jks docker-hello latest 8088 $export_port
    
      
      
    • 1

    image-20240723150555388

  • 重新构建工程

    这次重新构建,可以看到出现了 export_port 的文本框。在这里可以修改容器对外暴露的端口号。

    image-20240723150345538

    image-20240723160619572

    构建成功后可以看到,目标服务器中增加了新的镜像,该镜像是从 harbor 拉取的,还可以看到,该镜像的容器也已经启动

    image-20240723160649270

  • 13.14 自由风格的 CD 操作

    现在要为 GitLab 中当前的项目主干分支 origin/master 上的代码打上一个 Tag,例如 v1.0.0。然后修改代码后仍提交到 GitLab 的主干分支 origin/master 上,此时再给项目打上一个 Tag,例如 v2.0.0。这样, hellojenkins 项目的主干分支 origin/master 上就打上了两个 Tag。

    而 Jenkins 可以根据主干分支 origin/master 上代码的不同 Tag 对项目进行分别构建。实现项目的持续交付与持续部署。

    13.14.1 发布 V1.0.0 版本
    • 修改代码并推送

      简单修改一个 Controller 中方法的返回值。修改代码后,将其推送到 GitLab

      image-20240723160946006

    • GitLab 中项目打 Tag

      image-20240723161117269

      image-20240723161130919

    image-20240723161230049
    image-20240723161305824

    13.14.2 发布 V2.0 版本
    • 修改代码

      简单修改一个 Controller 中方法的返回值。将修改后的项目源码提交到 GitLab。

      image-20240723161437182

    • GitLab 中再打 Tag

      image-20240723161535871

      image-20240723161549589

    13.14.3 Jenkins 配置 tag 参数

    由于 GitLab 中的项目具有 tag 标签,那么 Jenkins 在进行项目构建时就需要让用户选择准备构建哪个 tag 的项目。所以,需要在 Jenkins 中配置一个 Git 参数 tag 作为用户选项。

    • 添加 Git 参数

      image-20240724084339474

      image-20240724084419695

      这里选择的 Git 参数,即为前面下载的 Git Parameter 插件

      image-20240724091209956

    • 添加 checkout 命令

      然后当前页面继续下拉,找到 Build Steps

      image-20240724084751233

      git checkout $proTag
      
         
         
      • 1

      image-20240724091526359

      注意:此处位置需要在maven打包之前,打包后切换起不到代码切换作用导致打出的包都一样

    • 修改构建命令配置

      然后当前页面继续下拉,找到 Build Steps 中原来添加的构建命令。在所有涉及镜像的命令中添加上$hjtag 变量引用。然后应用保存

      mv target/*.jar docker/
      cd docker
      mv docker-hello-1.0-SNAPSHOT.jar docker-hello.jar
      docker build -t docker-hello:$proTag .
      echo 101022 | docker login -u admin --password-stdin 192.168.138.134:80
      docker tag docker-hello:$proTag 192.168.138.134:80/jks/docker-hello:$proTag
      docker image prune -f
      docker push 192.168.138.134:80/jks/docker-hello:$proTag
      
         
         

      image-20240724091302296

    • 修改 SSH 配置

      然后当前页面继续下拉,找到“构建后操作”中的 Send build artifacts over SSH 中的 Exec command,将原来写死的版本 latest 修改为$docker-hello-tag

      docker-deploy.sh 192.168.138.134:80 jks docker-hello $proTag 8088 $export_port
      
         
         

      image-20240724091346160

    13.14.4 部署 v1.0
    • 重新构建工程

      任务首页中再次点击 Build with Parameters 构建项目,发现增加了 $docker-hello-tag 选项。这里选择v1.0 进行构建

      image-20240724085648172

      image-20240724091602664

    • 构建结果

      构建成功后,在 Jenkins 中可以看到增加了新的镜像

      image-20240724091649661

      image-20240724091754489

      目标节点服务器上docker-hello:v1.0也正在正常的运行中

      image-20240724091836464

      在浏览器上访问到的页面内容也是 v1.0的内容了 http://192.168.138.129:8081/some/test

      image-20240724094214419

      image-20240724092027542

    13.14.5 部署v2.0

    此时再选择 v2.0.0 进行构建

    image-20240724094236381

    image-20240724094257732

    image-20240724092329607

    image-20240724094311334

    13.15 流水线任务
    13.15.1 流水线简介

    流水线是 Jenkins 对项目构建过程的一种管理方式。其将项目的构建过程按照构建阶段进行非常清晰的划分显示。用户可以通过可视化操作方式来轻松查看、管理构建过程中的各个阶段。

    13.15.2 Hello World
    • 新建流水线任务

      image-20240724094739839

      image-20240724094820489

    • Hello World 项目创建与构建

      image-20240724094949806

      点击立即构建后,就会看到阶段视图

      image-20240724095053811

      将鼠标放到各个阶段上会显示出 logs,点击 logs 可看到相关的日志

      image-20240724095122109

      image-20240724095134669

    • 修改项目脚本

      为了更好的理解脚本,这里对 hello workd 项目的脚本进行修改

      pipeline {
          agent any
          stages {
              stage('Hello1') {
                  steps {
                      echo 'Hello World1'
                  }
              }
              stage('Hello2') {
                  steps {
                      echo 'Hello World2'
                  }
              }
              stage('Hello3') {
                  steps {
                      echo 'Hello World3'
                  }
              }
          }
      }
      
         
         

      image-20240724095346156

    • 再次构建

      应用保存后,再次立即构建。阶段视图发生较大变化。每个阶段上均可看到相应的日志

      image-20240724095441696

    13.15.3 SCM 方式维护脚本

    Jenkins 流水线任务提供了两种维护脚本的方式。本地方式与 SCM 方式。在 Jenkins 中维护的方式称为本地方式

    • 代码中追加 Jenkinsfile

      每个要构建的项目采用 piple 方式进行构建管理,要求必须要有一个构建脚本,而采用 SCM 脚本维护方式时,默认该脚本文件名为 Jenkinsfile。

      对于本例,在 Idea 中的项目根目录下追加一个名为 Jenkinsfile 的文件。然后再将原来的脚本内容复制到该文件中。为了显示区别,这里对脚本内容进行了简单修改,并且提交修改到 GitLab

      pipeline {
          agent any
          stages {
              stage('第一阶段') {
                  steps {
                      echo 'Hello World1'
                  }
              }
              stage('第二阶段') {
                  steps {
                      echo 'Hello World2'
                  }
              }
              stage('第三阶段') {
                  steps {
                      echo 'Hello World3'
                  }
              }
          }
      }
      
         
         

      image-20240724100205559

      然后在 GitLab 的项目首页中就可看到多了一个 Jenkinsfile 文件。然后再复制该项目的http 地址。

      image-20240724100309695

    • Jenkins 配置

      在 Jenkins 中流水线任务的“配置”中,流水线选择 SCM 方式,SCM 选择 Git,然后再将刚才复制的 GitLab 仓库地址粘贴到这里。

      image-20240724100454921

    image-20240724100516253

    • 重新构建

      重新立即构建后会发现,除了这些阶段名称更新为了修改过的外,还新增了一个新的阶段 Checkout SCM。即从 SCM 中检出脚本

      image-20240724100618674

    13.15.4 流水线管理 docker-hello
    • 更新 Jenkinsfile

      现要将之前的 docker-hello 项目通过流水线方式进行构建管理。所以,首先需要修改 Idea 中的 Jenkinsfile 文件内容,然后再提交到 GitLab。

      Jenkinsfile 文件操作步骤大体内容如下:

      pipeline {
      	agent any
      	stages {
      		stage('从 GitLab 拉取代码') {
      			steps {
      			echo '从 GitLab 拉取代码,成功'
      			}
      		}
      		stage('将项目打为 jar 包') {
      			steps {
      				echo '将项目打为 jar 包,成功'
      			}
      		}
      		stage('代码质量检测') {
      			steps {
      				echo '代码质量检测,成功'
      			}
      		}
      		stage('构建并推送镜像到 Harbor') {
      			steps {
      				echo '构建并推送镜像到 Harbor,成功'
      			}
      		}
      		stage('通知目标服务器') {
      			steps {
      				echo '通知目标服务器,成功'
      			}
      		}
      	}
      }
      
         
         
    • 重新构建

      image-20240724101324531

    13.15.5 GitLab 拉取代码
    • 定义Git参数

      在 Jenkins 中的 pipeline 任务中定义一个 Git 参数,该参数仍为发布的 tag

      image-20240724101439837

      image-20240724101514543

      image-20240724101556937

    • 流水线语法

      在 pipeline 脚本文件中如何定义具体的命令语句来实现“从 GitLab 位取代码”“将项目打为 jar 包”等任务?pipeline 脚本文件是具有其特殊的语法的。不过,通过当前 pipeline 任务中的流水线语法,可自动生成符合 pipeline 脚本语法的脚本语句

      image-20240724101647512

    • 生成脚本命令

      下面要通过流水线语法生成“从 GitLab 拉取代码”的语句。首先从 GitLab 的项目中复制项目地址。

      image-20240724101728057

      然后在 Jenkins 的流水线语法中选择“checkout:Check out from version control”,并将复制来的 GitLab 的项目地址粘贴到 Repository URL 中。

      image-20240724101814090

      image-20240724101850979

      image-20240724101915887

    • 更新 Jenkinsfile

      复制生成的流水线脚本,并将其写入到 Idea 中的 Jenkinsfile 的相应 stage{}中,并提交到GitLab。

      image-20240724102103825

    • 重新构建

      对任务进行重新构建,发现可以对构建的版本进行选择了

      image-20240724102205000

      image-20240724102950598

    13.15.6 将项目打为 jar 包
    • 生成脚本命令

      在 Jenkins 中通过流水线脚本语法生成“将项目打为 jar 包”的脚本语句

      sh "/var/jenkins_home/maven/bin/mvn clean package -DskipTests"
      
         
         
      • 1
    • 更新 Jenkinsfile

      复制生成的流水线脚本,并将其写入到 Idea 的 Jenkinsfile 的相应 stage{}中,提交

    • 重新构建

      对任务进行重新构建,然后便可在最上层的“将项目打为 jar 包”阶段中点击 Logs,便可看到 maven 构建的日志

      在这里插入图片描述

    13.15.7 构建镜像并推送到 Harbor
    • Jenkinsfile 中定义环境变量

      在Idea中的Jenkinsfile文件中添加环境变量,这些变量将在后面生成的脚本命令中使用

      environment {
          harbor_user='admin'
          harbor_password='101022'
          harbor_url='192.168.138.134:80'
          harbor_repository='jks'
      }
      
         
         

      image-20240724121053382

    • 生成脚本命令

      在 Jenkins 中通过流水线脚本语法生成“推送镜像到 Harbor”的脚本语句。脚本语句中使用的是 Jenkinsfile 中定义的环境变量

      image-20240724120521106

    • 重新构建

      对任务进行重新立构建,然后便可在最上层的“构建镜像并推送到 Harbor”阶段中点击 Logs,便可看到推送镜像到 Harbor 的日志

      image-20240724120605993

      此时查看 harbor 的管理页面,可以看到在 jks 项目中新增加了 hello_pipeline 的仓库,且仓库中具有 v1.0 的镜像。

      image-20240724120623412

      此时在 Jenkins 中就可看到出现了 hello_pipeline 的镜像。

      在这里插入图片描述

    13.15.8 目标服务执行deploy脚本
    • 添加端口号参数

      为了使用户可以随时指定容器对外暴露的参数,这里在 Jenkins 当前任务下的“配置”中“参数化构建过程”中添加一个字符参数

      image-20240724133753690

      在这里插入图片描述

    • 生成脚本命令

      在 Jenkins 中通过流水线脚本语法生成“通知目标服务器执行 deploy 脚本语句。选择 sshPublisher:Send build artifacts over SSH,并从中找到目标服务器

      image-20240724135719017

      在exec command当中,填写执行shell 语句信息然后点击生成流水线脚本

      deploy.sh $harbor_url $harbor_repository $JOB_NAME $proTag 8088 $export_port
      
         
         

      image-20240724135956419

    • 更新 Jenkinsfile

      复制生成的流水线脚本,并将其写入到 Idea 的 Jenkinsfile 的相应 stage{}中,提交,最终Jenkinsfile文件内容如下

      pipeline {
      	agent any
          tools {
              maven 'my_maven'
              jdk 'my_jdk'
          }
          environment {
              harbor_user='admin'
              harbor_password='101022'
              harbor_url='192.168.138.134:80'
              harbor_repository='jks'
          }
      	stages {
      		stage('从 GitLab 拉取代码') {
      			steps {
                      checkout scmGit(branches: [[name: '*/master']], extensions: [], userRemoteConfigs: [[credentialsId: 'dae57456-d8ef-407a-9817-0922f9e562c9', url: 'http://192.168.138.134:8098/root/docker-hello.git']])
      			}
      		}
      		stage('将项目打为 jar 包') {
      			steps {
      			    script {
                          sh """
                            java -version
                            mvn -version
                            git checkout $proTag
                            /var/jenkins_home/maven/bin/mvn clean package -DskipTests
                          """
      				}
      			}
      		}
              stage('构建并推送镜像到 Harbor') {
                  steps {
                      sh """
                          echo job_name=${JOB_NAME}
                          mv target/*.jar docker/
                          cd docker
                          mv docker-hello-1.0-SNAPSHOT.jar docker-hello.jar
                          docker build -t ${JOB_NAME}:$proTag .
                          echo ${harbor_password} | docker login -u ${harbor_user} --password-stdin ${harbor_url}
                          docker tag ${JOB_NAME}:$proTag ${harbor_url}/${harbor_repository}/${JOB_NAME}:$proTag
                          docker image prune -f
                          docker push ${harbor_url}/${harbor_repository}/${JOB_NAME}:$proTag
                      """
                  }
              }
              stage('通知目标服务器') {
                  steps {
                      sshPublisher(publishers: [sshPublisherDesc(configName: 'my_target_server', transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: "deploy.sh $harbor_url $harbor_repository $JOB_NAME $proTag 8088 $export_port", execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: '', remoteDirectorySDF: false, removePrefix: '', sourceFiles: '')], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)])
                  }
              }
      	}
      }
      
         
         

      注意 :原本生成的脚本中的 docker-deploy.sh 命令是使用单引号括起来的,需要将这里的单引号更改为双引号。否则这些变量无法完成引用

    • 重新构建

      对任务进行重新构建,然后便可在最上层的“通知目标服务器”阶段中点击 Logs,便可看到推送镜像到 Harbor 的日志

      在这里插入图片描述

      查看harbor上传镜像信息

      在这里插入图片描述

      查看目标服务器服务运行情况

      image-20240724141317251

      通过浏览器访问目标服务器服务 http://192.168.138.129:8081/some/test

      image-20240724141339019

    • 发布v2.0版本

      image-20240724141425256

      image-20240724141452979

      通过目标服务器服务查看,已经修改为tag为v2.0的镜像信息

      image-20240724141511401

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值