Jenkins教程

Jenkins是一个开源的自动化服务器,用于实现软件开发过程中的持续集成和持续交付。它提供了一种灵活的方式来构建、测试和部署软件项目,以及监控项目的整个开发过程。

Jenkins可以通过插件扩展其功能,支持各种编程语言和工具。它可以与版本控制系统(如Git、Subversion)、构建工具(如Apache Maven、Gradle)、测试框架(如JUnit、Selenium)以及部署工具(如Docker、Kubernetes)等集成。

Jenkins的核心概念是作业(Job)。作业定义了构建过程的一系列步骤,包括代码的获取、编译、测试和部署等。通过配置作业,您可以指定何时触发构建、如何获取代码、构建步骤的顺序以及构建产物的处理方式。

Jenkins还提供了一个可视化的用户界面,使用户可以方便地管理和监控作业、查看构建历史和日志,并设置触发条件和通知机制。它还支持分布式构建,可以在多台计算机上同时执行构建任务,以加快构建速度和提高效率。

通过使用Jenkins,团队可以实现持续集成和持续交付的最佳实践,将软件的开发、测试和交付过程自动化,减少手动操作和人为错误,提高软件质量和交付速度。它广泛应用于各种软件开发项目和持续集成环境中。

CI、CD

image-20230625200853997

安装Jenkins

Windows安装

Jenkins中文网:http://www.jenkins.org.cn/

进群下载.msi文件进行安装

Docker安装

  1. 编写docker-compose.yml

    version: "3.1"
    services:
      jenkins:
        image: jenkins/jenkins:2.401.1-lts
        container_name: jenkins
        ports:
          - 8080:8080
   volumes:
     - ./data/:/var/jenkins_home/

2. 启动docker-compose

docker-compose up -d


![image-20230621174121654](https://qny.chengxuyi.top//typora_img/image-20230621174121654.png)

这里看到启动失败了,查看容器日志发现是权限问题,无法进行write

![image-20230621174345882](https://qny.chengxuyi.top//typora_img/image-20230621174345882.png)

对data文件夹赋予777权限并启动再次容器

chmod -R 777 data
docker start jenkins


3. 浏览器访问8080端口进入首页,密码可在容器日志中或者文件中查看

![image-20230621175229386](https://qny.chengxuyi.top//typora_img/image-20230621175229386.png)

4. 安装插件,随便点击一个并等待插件安装完成

> 这里插件大概率会安装失败,不要紧,等下我们进去安装我们需要的插件,不用在这个地方纠结很久

![image-20230621175504592](https://qny.chengxuyi.top//typora_img/image-20230621175504592.png)

5. 创建用户,这里随便输,记住账号密码即可

![image-20230621175903669](https://qny.chengxuyi.top//typora_img/image-20230621175903669.png)

6. 实例配置使用默认值,点击保存并完成

![image-20230621175954040](https://qny.chengxuyi.top//typora_img/image-20230621175954040.png)

## 安装插件

Jenkins的大部分功能都来自于它的插件,插件越多,功能越强大

进入插件页面

![image-20230621180351804](https://qny.chengxuyi.top//typora_img/image-20230621180351804.png)

我们需要安装`Git Parameter`和`Publish Over SSH`2个插件

![image-20230621180523507](https://qny.chengxuyi.top//typora_img/image-20230621180523507.png)

安装完并重启

![image-20230621180544156](https://qny.chengxuyi.top//typora_img/image-20230621180544156.png)

`Git Parameter`插件用于在连接Git仓库时添加额外的参数

`Publish Over SSH`插件用于连接远程服务器

## Jenkins配置Maven、JDK、SSH

进入全局配置页面

![image-20230625201915125](https://qny.chengxuyi.top//typora_img/image-20230625201915125.png)

### Maven

填写Name和Home路径

路径在填写错误时会有提示

![image-20230625202056876](https://qny.chengxuyi.top//typora_img/image-20230625202056876.png)

### JDK

![image-20230625202237465](https://qny.chengxuyi.top//typora_img/image-20230625202237465.png)

### SSH

![image-20230625202325738](https://qny.chengxuyi.top//typora_img/image-20230625202325738.png)

滑到最底部找到 `Publish over SSH`并配置需要连接的服务器信息

![image-20230625202855508](https://qny.chengxuyi.top//typora_img/image-20230625202855508.png)

使用密码进行验证

![image-20230625202912132](https://qny.chengxuyi.top//typora_img/image-20230625202912132.png)

## CI

CI(持续集成)是指在团队开发过程中,频繁地将代码集成到共享代码仓库中,并自动执行构建、测试和代码质量检查等过程。其目标是确保团队成员的代码能够及时合并,并尽早发现和解决集成问题。CI的关键是自动化构建和测试,以便尽早发现代码中的错误和问题。通过持续集成,团队可以更快地交付软件,减少集成问题的风险,并提高团队的协作效率。

> 本次案例使用项目为RuoYi前后端分离多模块项目:https://gitee.com/y_project/RuoYi-Vue
>
> 采用Docker容器的方式进行部署

1. 创建一个自定义风格的项目

![image-20230625201202830](https://qny.chengxuyi.top//typora_img/image-20230625201202830.png)

2. 配置项目源码信息

![image-20230625201700751](https://qny.chengxuyi.top//typora_img/image-20230625201700751.png)

测试代码是否拉取成功

![image-20230625203259138](https://qny.chengxuyi.top//typora_img/image-20230625203259138.png)

查看控制台日志

![image-20230625203325532](https://qny.chengxuyi.top//typora_img/image-20230625203325532.png)

拉取代码成功

![image-20230625203346572](https://qny.chengxuyi.top//typora_img/image-20230625203346572.png)

拉取成功之后会在Jenkins的工作空间中看到项目文件夹

使用docker部署时在挂载的data/workspace文件夹中查看。

使用windows安装版本在图中的目录中查看

![image-20230625203550793](https://qny.chengxuyi.top//typora_img/image-20230625203550793.png)

3. 添加项目构建步骤:调用顶层Maven目标

![image-20230625203714474](https://qny.chengxuyi.top//typora_img/image-20230625203714474.png)

![image-20230625203927742](https://qny.chengxuyi.top//typora_img/image-20230625203927742.png)

再次进行构建并查看控制台日志

![image-20230625204030927](https://qny.chengxuyi.top//typora_img/image-20230625204030927.png)

项目构建成功之后会在对应的工作空间的项目中生成target目录

4. 在项目根目录新建docker文件夹并编写Dockerfile和docker-compose文件

`/docker/Dockerfile`

FROM daocloud.io/library/java:8u40-jdk

COPY ruoyi-admin.jar /usr/local/

WORKDIR /usr/local

CMD java -jar ruoyi-admin.jar


`/docker/docker-compose.yml`

version: "3.1"
services:

 bms-project:
   build:
     context: ./
     dockerfile: Dockerfile
   image: bms:v2.2
   container_name: bms
   ports:
     - "4001:4001"

**编写完代码记得提交到自己Git仓库**

5. 添加构建后操作:通过SSH发送构建工件

![image-20230625205524397](https://qny.chengxuyi.top//typora_img/image-20230625205524397.png)

![image-20230626093529864](https://qny.chengxuyi.top//typora_img/image-20230626093529864.png)

6. 保存并执行任务

- 超时

![image-20230626094020061](https://qny.chengxuyi.top//typora_img/image-20230626094020061.png)

<img src="https://qny.chengxuyi.top//typora_img/image-20230626094112712.png" alt="image-20230626094112712"  />![image-20230626094227457](https://qny.chengxuyi.top//typora_img/image-20230626094227457.png)

修改超时时间保存后再次执行

![image-20230626094424578](https://qny.chengxuyi.top//typora_img/image-20230626094424578.png)

在目标服务器中使用`docker ps`查看已经部署成功

> **至此,CI操作就已经完成了**

## CD

CD(持续交付/持续部署)是CI的延伸,指的是在通过持续集成构建、测试和验证代码后,自动将代码交付给生产环境或可用于生产部署的环境。持续交付的目标是使软件的交付过程自动化、可靠且可重复,以减少人工干预和减少交付的时间间隔。持续交付的流程通常包括自动化构建、自动化测试、自动化部署和自动化验证等步骤。持续部署则更进一步,指的是将经过验证的代码自动部署到生产环境中,以实现全自动的软件交付。

1. 在Gitee中配置标签

![image-20230626095308436](https://qny.chengxuyi.top//typora_img/image-20230626095308436.png)

2. 在任务配置中选择参数化,并选择之前安装的`Git Parameter`

![image-20230626095442344](https://qny.chengxuyi.top//typora_img/image-20230626095442344.png)



![image-20230628164540903](https://qny.chengxuyi.top//typora_img/image-20230628164540903.png)

3. 新增一行构建之前的命令,切换到指定的tag分支。$tag取上面Git Parameter名称的值

![image-20230628170215381](https://qny.chengxuyi.top//typora_img/image-20230628170215381.png)

> windows环境中的Jenkins无法调用shell脚本问题

在系统配置里面中配置shell为Git的sh.exe执行文件路径

![image-20230628170343041](https://qny.chengxuyi.top//typora_img/image-20230628170343041.png)

4. 在配置完参数后,构建按钮变成了Build with Parameters

![image-20230626095851626](https://qny.chengxuyi.top//typora_img/image-20230626095851626-16877447685341.png)

5. 选择对应的tag并开始构建

![image-20230626095956108](https://qny.chengxuyi.top//typora_img/image-20230626095956108.png)



控制台查看执行了切换分支的命令

![image-20230628170551950](https://qny.chengxuyi.top//typora_img/image-20230628170551950.png)





## SonarQube

SonarQube(前身为Sonar)是一个开源的代码质量管理平台。它提供了一个集中的平台,用于对源代码进行静态代码分析、测量代码质量、跟踪技术债务、管理代码复杂性以及监控代码规范和安全性等方面的指标。SonarQube支持多种编程语言,包括Java、C/C++、C#、Python、JavaScript等,并提供了丰富的插件生态系统,以扩展和定制功能。SonarQube还提供了直观的仪表板和报告,用于可视化和监控代码质量的变化趋势。

### Docker形式安装SonarQube

编写docker-compose.yml

version: '3.1'
services:
db:

image: postgres
container_name: db
ports:
  - 5432:5432
networks:
  - sonarnet
environment:
  POSTGRES_USER: sonar
  POSTGRES_PASSWORD: sonar

sonarqube:

image: sonarqube:9.9-community
container_name: sonarqube
depends_on:
  - db
ports:
  - 9000:9000
networks:
  - sonarnet
environment:
  SONAR_JDBC_URL: jdbc:postgresql://db:5432/sonar
  SONAR_JDBC_USERNAME: sonar
  SONAR_JDBC_PASSWORD: sonar

networks:
sonarnet:

driver: bridge

启动命令:docker-compose up -d

**Windows启动失败查看SonarQube容器日志**

bootstrap check failure [1] of [1]: max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144]

**启动失败解决办法:https://blog.csdn.net/qq_38680405/article/details/126700722**

> 浏览器打开端口为9000

### Maven与SonarQube集成

#### 修改Maven配置文件:settings.xml

在profiles标签中新增SonarQube配置信息并激活配置

<profile>
    <id>sonar</id>
    <activation>
        <activeByDefault>true</activeByDefault>
    </activation>
    <properties>
        <sonar.login>admin</sonar.login>
        <sonar.password>123456</sonar.password>
        <sonar.host.url>http://localhost:9000</sonar.host.url>
    </properties>
</profile>

<activeProfile>sonar</activeProfile>


#### 在项目根目录运行:mvn sonar:sonar

![image-20230625193319595](https://qny.chengxuyi.top//typora_img/image-20230625193319595.png)

在SonarQube提供的可视化界面查看检查结果

![image-20230625193442430](https://qny.chengxuyi.top//typora_img/image-20230625193442430.png)

## SonarScanner

SonarScanner是SonarQube的命令行工具,用于将源代码发送到SonarQube服务器进行分析。它是一个独立的客户端工具,可以与SonarQube进行集成,以便在分析阶段捕获和处理代码。SonarScanner通过读取配置文件(如`sonar-project.properties`)或通过命令行参数传递配置信息,确定需要分析的项目和相关设置。它会扫描项目的源代码,执行静态代码分析,并将结果提交到SonarQube服务器进行处理和展示。

SonarScanner支持多种构建工具和编程语言,包括Maven、Gradle、Ant、MSBuild等。它可以轻松地集成到持续集成(CI)和持续交付(CD)流程中,使开发团队能够在每次代码提交或构建时自动进行代码质量分析。

下载地址:https://docs.sonarqube.org/latest/analyzing-source-code/scanners/sonarscanner/

下载自己对应的操作系统文件

![image-20230625165342274](https://qny.chengxuyi.top//typora_img/image-20230625165342274.png)



### 命令行方式检查代码

#### 打开SonarScanner配置文件:sonar-scanner.properties

![image-20230625165522702](https://qny.chengxuyi.top//typora_img/image-20230625165522702.png)

更改后的文件内容为

Configure here general information about the environment, such as SonarQube server connection details for example

No information about specific project should appear here

----- Default SonarQube server

对应sonarQube的url地址

sonar.host.url=http://localhost:9000

----- Default source code encoding

sonar.sourceEncoding=UTF-8


#### 执行检查

切换到SonarScanner目录

cd E:environmentsonar-scanner-4.8.0.2856-windowsbin

执行

./sonar-scanner.bat -D sonar.java.binaries=.//target -D sonar.source=./ -D sonar.login=sqa_684223ae6877050107f0b3e3368e72fc06759874 -D sonar.projectKey=bms-project -D sonar.projectBaseDir=E:overload-workingbms-dianshang-project -D sonar.exclusions=RuoYi-Vue3/

注释

sonar.java.binaries 编译后的class文件位置

sonar.source 源码位置

sonar.login token

sonar.projectKey 项目的Key

sonar.projectBaseDir 项目的基本路径

sonar.exclusions 需要排除的文件夹或者文件


## Jenkins集成SonarQubeScanner

在构建项目时对代码进行检查

1. Jenkins安装SonarQubeScanner插件

   ![image-20230701093415322](https://qny.chengxuyi.top//typora_img/image-20230701093415322.png)

2. 生成密钥:打开SonarQube服务端

   ![image-20230701094153984](https://qny.chengxuyi.top//typora_img/image-20230701094153984.png)

   复制令牌

   ![image-20230701094229335](https://qny.chengxuyi.top//typora_img/image-20230701094229335.png)

   

3. 配置SonarQube服务端

   类型选择Secret text,输入复制的令牌

   ![image-20230701094304636](https://qny.chengxuyi.top//typora_img/image-20230701094304636.png)

   

   ![image-20230701093949642](https://qny.chengxuyi.top//typora_img/image-20230701093949642.png)

4. 构建项目时添加SonarQubeScanner步骤

   

   ![image-20230701094621711](https://qny.chengxuyi.top//typora_img/image-20230701094621711.png)

   ![image-20230701094814580](https://qny.chengxuyi.top//typora_img/image-20230701094814580.png)

5. 重新构建

   > 在构建成功之后执行了SonarScanner命令进行检查

   ![image-20230701095008286](https://qny.chengxuyi.top//typora_img/image-20230701095008286.png)

   SonarQube服务端也新增了一个检查

   ![image-20230701095126527](https://qny.chengxuyi.top//typora_img/image-20230701095126527.png)

## Harbor

Harbor是一个开源的企业级容器镜像仓库,用于存储、分发和管理Docker镜像。它提供了一个安全可靠的集中式存储库,使团队能够方便地共享和管理容器镜像。

下面是Harbor的一些主要特性:

1. **安全性和权限管理**:Harbor支持用户身份验证和授权机制,可以通过集成LDAP、Active Directory等进行用户认证。它还提供细粒度的访问控制,使管理员能够精确地管理用户对镜像的访问权限。
2. **镜像复制和同步**:Harbor支持镜像的复制和同步功能,可以在多个Harbor实例之间同步镜像。这有助于实现分布式部署和高可用性需求。
3. **漏洞扫描和安全审计**:Harbor集成了漏洞扫描工具,可以对上传到仓库中的镜像进行安全扫描,识别潜在的漏洞和安全风险。此外,Harbor还提供审计日志功能,记录了对仓库的操作和访问,方便进行安全审计。
4. **复制策略和镜像签名**:Harbor支持灵活的复制策略,可以定义哪些镜像需要复制到其他Harbor实例。此外,Harbor还支持镜像签名,以确保镜像的完整性和来源可信。
5. **项目和命名空间**:Harbor支持将镜像进行组织和管理,使用项目和命名空间来划分不同的逻辑单元。这有助于团队对镜像进行分类、管理和权限控制。
6. **可视化界面和API支持**:Harbor提供了易于使用的Web界面,使用户可以通过图形化界面进行镜像的上传、浏览和管理。此外,Harbor还暴露了一组RESTful API,方便与其他工具和系统进行集成。

总而言之,Harbor是一个功能丰富、安全可靠的容器镜像仓库,适用于企业级的容器镜像管理和分发需求。它提供了许多有用的特性,使团队能够轻松地构建、共享和管理容器镜像。

### 安装Harbor

[GitHub传送门](https://github.com/goharbor/harbor/releases):选择离线或者在线版本

![image-20230704194905334](https://qny.chengxuyi.top//typora_img/image-20230704194905334.png)

1. 将[harbor-offline-installer-v2.8.2.tgz](https://github.com/goharbor/harbor/releases/download/v2.8.2/harbor-offline-installer-v2.8.2.tgz)上传到服务器或虚拟机中并解压

2. 将`harbor.yml.tmpl`拷贝一份并重命名为`harbor.yml`。harbor启动时只会识别`harbor.yml`配置文件

3. 修改`harbor.yml`内容

   ![image-20230704195702869](https://qny.chengxuyi.top//typora_img/image-20230704195702869.png)

4. 执行`install.sh`。harbor部署方式是使用docker容器部署,所有install之前需要先安装好Docker和Docker compose

5. 部署好查看Docker容器运行情况

   ![image-20230704195931227](https://qny.chengxuyi.top//typora_img/image-20230704195931227.png)

6. 浏览器访问89端口

   ![image-20230704200004857](https://qny.chengxuyi.top//typora_img/image-20230704200004857.png)

### Harbor基本操作

#### 新建项目

![image-20230714163418205](https://qny.chengxuyi.top//typora_img/image-20230714163418205.png)

![image-20230714163428904](https://qny.chengxuyi.top//typora_img/image-20230714163428904.png)

输入项目名称、访问级别、项目配合限制:-1为不限制

### 将镜像推送到Harbor

需要推送的目标服务器设置`daemon.json`

`/etc/docker/daemon.json`

{
"registry-mirrors": [

    "https://registry.hub.docker.com",
    "http://hub-mirror.c.163.com",
    "https://docker.mirrors.ustc.edu.cn",
    "https://registry.docker-cn.com"
],
"insecure-registries":["111.230.199.xxx:89"]

}


**重启docker**

`systemctl restart docker`

1. 先给要推送的镜像打一个tag

# 格式: docker tag 镜像ID HarborIP:端口/项目名/镜像名:版本号
docker tag 91b53e2624b4 111.230.199.xxx:89/test/mysql:v1.0.0


![image-20230714164321072](https://qny.chengxuyi.top//typora_img/image-20230714164321072.png)

2. 登录到Harbor

docker login 111.230.199.xxx:89 -u 用户名 -p 密码


![image-20230714164353104](https://qny.chengxuyi.top//typora_img/image-20230714164353104.png)

3. 推送

docker push 111.230.199.xxx:89/test/mysql:v1.0.0


![image-20230714164520134](https://qny.chengxuyi.top//typora_img/image-20230714164520134.png)

4. 推送完毕之后就可以在Harbor中看到

![image-20230714164605854](https://qny.chengxuyi.top//typora_img/image-20230714164605854.png)

### 将镜像拉取到本地

Harbor进入到要拉取镜像的页面中复制拉取命令

![image-20230714164642412](https://qny.chengxuyi.top//typora_img/image-20230714164642412.png)

复制后的命令

docker pull 111.230.199.xxx:89/test/mysql@sha256:29ea1f451c3aad88df5a24288f76442286dc7fa6874d96a022e93a8a7efda880

需要将后面的hash码替换成需要拉取的版本号

docker pull 111.230.199.xxx:89/test/mysql:v1.0.0






## Jenkins集成部署

当我们需要在多台服务器中同时部署项目时,如果按之前的方式从目标服务器中进行构建Docker镜像,就会造成在每一台服务器中都需要进行构建镜像的操作,为了避免这种情况。

我们可以在Jenkins中先将Docker镜像构建好,并推送到Harbor中,这样我们在目标服务器只需执行命令,将镜像从Harbor中拉取下来,并运行即可。这样就省去了重复构建的操作。

### Jenkin制作自定义镜像并推送到Harbor

1. 创建一个自定义风格的任务

2. 设置Git源码地址

3. 调用顶层Maven目标进行打包

   ![image-20230716210054716](https://qny.chengxuyi.top//typora_img/image-20230716210054716.png)

package -Dmaven.test.skip=true


4. 在项目的Dockerfile路径执行构建镜像命令

![image-20230717150433979](https://qny.chengxuyi.top//typora_img/image-20230717150433979.png)

**问题:Jenkins无法直接执行的Docker命令(本案例Jenkins在WIndows环境中使用Docker进行部署)**

![image-20230716205949661](https://qny.chengxuyi.top//typora_img/image-20230716205949661.png)

**解决:让Jenkins可以执行宿主机的Docker命令**

1. 修改Docker核心的sock文件所属组

   路径:`/var/run/docker.sock`
  chown root:root docker.sock
  ```
  1. 更改docker.sock的权限,使其对其他用户可读可写

    chmod o+rw docker.sock
  2. 更改docker-compose.yml挂载信息,修改后:

    version: "3.1"
    services:
      jenkins:
        image: jenkins/jenkins:2.401.1-lts
        container_name: jenkins
        restart: always
        ports:
          - 8080:8080
      volumes:
        - ./data/:/var/jenkins_home/
        - /var/run/docker.sock:/var/run/docker.sock
        - /usr/bin/docker:/usr/bin/docker
        - /etc/docker/daemon.json:/etc/docker/daemon.json
      environment:
        - TZ=Asia/Shanghai
  ```
    1. 进入Jenkins容器内部使用docker命令进行测试

      成功

      image-20230717145945606

    1. 再次进行构建发现已经执行了Docker构建命令的操作

      image-20230717150705664

    2. 文件不存在

      image-20230717150946711

      在构建之前需要将jar包移动到docker文件夹中

      mv ruoyi-admin/target/*.jar docker/
      docker build -t ${JOB_NAME}:$tag docker/

      image-20230717151116665

    3. 构建成功,查看宿主机镜像

      image-20230717151240060

      image-20230717151250557

    4. 登录到Harbor,将镜像打一个tag并推送到Harbor

      mv ruoyi-admin/target/*.jar docker/
      docker build -t ${JOB_NAME}:$tag docker/
      docker login -u admin -p Harbor1xxxx 111.230.199.xxx:89 
      docker tag ${JOB_NAME}:$tag 111.230.199.xxx:89/test/${JOB_NAME}:$tag
      docker push 111.230.199.xxx:89/test/${JOB_NAME}:$tag
      docker image prune -f

      image-20230717151656645

    5. 查看宿主机镜像和Harbor是否推送成功

      image-20230717153707766

      image-20230717153716504

    到此推送流程就已经结束了

    目标服务器准备脚本文件

    脚本文件需要放置/usr/bin路径下,确保在服务器的哪个地方都可以运行此脚本文件

    harbor_addr=$1
    harbor_repo=$2
    project=$3
    version=$4
    container_port=$5
    host_port=$6
    
    imageName=$harbor_addr/$harbor_repo/$project:$version
    
    echo $imageName
    
    containerId=`docker ps -a | grep ${project} | awk '{print $1}'`
    
    echo $containerId
    
    if [ "$containerId" != "" ] ; then
      docker stop $containerId
      docker rm $containerId
    fi
    
    tag=`docker images | grep ${project} | awk '{print $2}'`
    
    echo $tag
    
    if [[ "$tag" =~ "$version" ]] ; then
      docker rmi $imageName
    fi
    
    docker pull $imageName
    
    docker run -d -p $host_port:$container_port --name $project $imageName
    
    echo "SUCCESS"
    

    完成基于Harbor的最终部署

    1. 添加2个字符串参数(容器内部端口、宿主机端口)

      image-20230717155302469

    2. Send build artifacts over SSH

      如果需要同时在多台服务器上部署,只需添加多个Send build artifacts over SSH即可。

      前提是每台服务器都安装好了Docker

      image-20230717155356682

      执行脚本文件

      deploy.sh 111.230.199.xxx:89 test ${JOB_NAME} $tag $container_port $host_port
    3. 构建运行

      image-20230717155444740

    Jenkins流水线任务初体验

    最后修改:2024 年 06 月 16 日
    如果觉得我的文章对你有用,请随意赞赏