一卓的博客

怕什么真理无穷,
进一寸有一寸的欢喜。

0%

SpringBoot 集成数据库版本管理工具 Liquibase

photo

在 Spring Boot 项目中,提供了对 Liquibase 的内置支持。

引言

什么是数据库版本控制?为什么要进行数据库版本控制?可参考我上一篇文章 SpringBoot 集成数据库版本管理工具 Flyway

Liquibase 通过在变更日志(Change Log)文件,配置每一个变更集(Change Set),实现数据库变更的管理。

Liquibase 提供了多种格式,如下:

在 Spring Boot 中,默认配置使用 YAML Format 。所以我们在入门的示例中,也使用这种格式。

下面,就让我们开始入门 Liquibase 吧。

第一步:首先创建一个 Spring Boot 项目。

第二步:pom.xml 文件引入 Liquibase 依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<dependencies>
<!-- 实现对数据库连接池的自动化配置 -->
<!-- 同时,spring-boot-starter-jdbc 支持 Liquibase 的自动化配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- 使用 MySQL -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- Liquibase 依赖 -->
<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>
</dependency>
</dependencies>

第三步:配置 application.yaml

application.yaml 配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
spring:
# datasource 数据源配置内容,对应 DataSourceProperties 配置属性类
datasource:
url: jdbc:mysql://localhost:3306/github_demo?useUnicode=true&characterEncoding=utf-8&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai
driver-class-name: com.mysql.cj.jdbc.Driver
# 数据库账号
username: root
# 数据库密码
password: ${MYSQL_PWD:123456}
# Liquibase 配置内容,对应 LiquibaseProperties 配置项
liquibase:
# 开启 Liquibase 功能。默认为 true 。
enabled: true
# Liquibase 配置文件地址
change-log: classpath:/db/changelog/db_changelog.yaml
url: ${spring.datasource.url}
user: ${spring.datasource.username}
password: ${spring.datasource.password}

常用:基于 SQL 的版本控制

resources/db/changelog目录下创建 db_changelog.yaml文件

修改 db_changelog.yaml 变更日志,增加 ID = 0 的变更集合( Change Set ) ,不执行任何 SQL 。代码如下:

1
2
3
4
5
databaseChangeLog:
- changeSet: # 对应一个 ChangeSet 对象
id: 0 # ChangeSet 编号
author: yizhuo # 作者
comments: # 备注

启动 Spring Boot 项目

1
2
3
4
5
6
@SpringBootApplication
public class LiquibaseApplication {
public static void main(String[] args) {
SpringApplication.run(LiquibaseApplication.class, args);
}
}

执行日志如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2020-07-04 20:25:22.804  INFO 70787 --- [           main] liquibase.executor.jvm.JdbcExecutor      : SELECT COUNT(*) FROM github_demo.DATABASECHANGELOGLOCK
2020-07-04 20:25:22.885 INFO 70787 --- [ main] liquibase.executor.jvm.JdbcExecutor : CREATE TABLE github_demo.DATABASECHANGELOGLOCK (ID INT NOT NULL, `LOCKED` BIT(1) NOT NULL, LOCKGRANTED datetime NULL, LOCKEDBY VARCHAR(255) NULL, CONSTRAINT PK_DATABASECHANGELOGLOCK PRIMARY KEY (ID))
2020-07-04 20:25:23.017 INFO 70787 --- [ main] liquibase.executor.jvm.JdbcExecutor : SELECT COUNT(*) FROM github_demo.DATABASECHANGELOGLOCK
2020-07-04 20:25:23.037 INFO 70787 --- [ main] liquibase.executor.jvm.JdbcExecutor : DELETE FROM github_demo.DATABASECHANGELOGLOCK
2020-07-04 20:25:23.045 INFO 70787 --- [ main] liquibase.executor.jvm.JdbcExecutor : INSERT INTO github_demo.DATABASECHANGELOGLOCK (ID, `LOCKED`) VALUES (1, 0)
2020-07-04 20:25:23.062 INFO 70787 --- [ main] liquibase.executor.jvm.JdbcExecutor : SELECT `LOCKED` FROM github_demo.DATABASECHANGELOGLOCK WHERE ID=1
2020-07-04 20:25:23.095 INFO 70787 --- [ main] l.lockservice.StandardLockService : Successfully acquired change log lock
2020-07-04 20:25:24.929 INFO 70787 --- [ main] l.c.StandardChangeLogHistoryService : Creating database history table with name: github_demo.DATABASECHANGELOG
2020-07-04 20:25:24.930 INFO 70787 --- [ main] liquibase.executor.jvm.JdbcExecutor : CREATE TABLE github_demo.DATABASECHANGELOG (ID VARCHAR(255) NOT NULL, AUTHOR VARCHAR(255) NOT NULL, FILENAME VARCHAR(255) NOT NULL, DATEEXECUTED datetime NOT NULL, ORDEREXECUTED INT NOT NULL, EXECTYPE VARCHAR(10) NOT NULL, MD5SUM VARCHAR(35) NULL, `DESCRIPTION` VARCHAR(255) NULL, COMMENTS VARCHAR(255) NULL, TAG VARCHAR(255) NULL, LIQUIBASE VARCHAR(20) NULL, CONTEXTS VARCHAR(255) NULL, LABELS VARCHAR(255) NULL, DEPLOYMENT_ID VARCHAR(10) NULL)
2020-07-04 20:25:25.007 INFO 70787 --- [ main] liquibase.executor.jvm.JdbcExecutor : SELECT COUNT(*) FROM github_demo.DATABASECHANGELOG
2020-07-04 20:25:25.012 INFO 70787 --- [ main] l.c.StandardChangeLogHistoryService : Reading from github_demo.DATABASECHANGELOG
2020-07-04 20:25:25.013 INFO 70787 --- [ main] liquibase.executor.jvm.JdbcExecutor : SELECT * FROM github_demo.DATABASECHANGELOG ORDER BY DATEEXECUTED ASC, ORDEREXECUTED ASC
2020-07-04 20:25:25.019 INFO 70787 --- [ main] liquibase.executor.jvm.JdbcExecutor : SELECT COUNT(*) FROM github_demo.DATABASECHANGELOGLOCK
2020-07-04 20:25:25.048 INFO 70787 --- [ main] liquibase.changelog.ChangeSet : ChangeSet classpath:/db/changelog/db_changelog.yaml::0::yizhuo ran successfully in 7ms
2020-07-04 20:25:25.050 INFO 70787 --- [ main] liquibase.executor.jvm.JdbcExecutor : SELECT MAX(ORDEREXECUTED) FROM github_demo.DATABASECHANGELOG
2020-07-04 20:25:25.054 INFO 70787 --- [ main] liquibase.executor.jvm.JdbcExecutor : INSERT INTO github_demo.DATABASECHANGELOG (ID, AUTHOR, FILENAME, DATEEXECUTED, ORDEREXECUTED, MD5SUM, `DESCRIPTION`, COMMENTS, EXECTYPE, CONTEXTS, LABELS, LIQUIBASE, DEPLOYMENT_ID) VALUES ('0', 'yizhuo', 'classpath:/db/changelog/db_changelog.yaml', NOW(), 1, '8:d41d8cd98f00b204e9800998ecf8427e', 'empty', '', 'EXECUTED', NULL, NULL, '3.8.9', '3865525034')
2020-07-04 20:25:25.101 INFO 70787 --- [ main] l.lockservice.StandardLockService : Successfully released change log lock
...
2020-07-04 20:25:25.387 INFO 70787 --- [ main] c.g.z.liquibase.LiquibaseApplication : Started LiquibaseApplication in 8.936 seconds (JVM running for 10.127)

在启动的日志中,我们看到 Liquibase 会自动创建两张表:

DATABASECHANGELOG :数据库变更日志。每一条记录,对应记录每个变更集合( Change Set ) 的执行日志。表结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
CREATE TABLE `DATABASECHANGELOG` (
`ID` varchar(255) NOT NULL, -- Change Set 编号
`AUTHOR` varchar(255) NOT NULL, -- 作者
`FILENAME` varchar(255) NOT NULL, -- Change Log 文件路径
`DATEEXECUTED` datetime NOT NULL, -- 执行时间
`ORDEREXECUTED` int(11) NOT NULL, -- 执行的顺序
`EXECTYPE` varchar(10) NOT NULL, -- 执行类型。枚举值有 EXECUTED/FAILED/SKIPPED/RERAN/MARK_RAN
`MD5SUM` varchar(35) DEFAULT NULL, -- MD5 校验码
`DESCRIPTION` varchar(255) DEFAULT NULL, -- 描述
`COMMENTS` varchar(255) DEFAULT NULL, -- 备注
`TAG` varchar(255) DEFAULT NULL, -- Tag 标签
`LIQUIBASE` varchar(20) DEFAULT NULL, -- LIQUIBASE 版本号
`CONTEXTS` varchar(255) DEFAULT NULL, -- 上下文
`LABELS` varchar(255) DEFAULT NULL, -- Label 标签
`DEPLOYMENT_ID` varchar(10) DEFAULT NULL -- 部署编号
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

DATABASECHANGELOGLOCK:锁记录表,用于确保多个 JVM 进程同时只能有一个可以修改数据库。表结构如下:

1
2
3
4
5
6
7
CREATE TABLE `DATABASECHANGELOGLOCK` (
`ID` int(11) NOT NULL, -- 锁的编号。
`LOCKED` bit(1) NOT NULL, -- 是否锁。1-锁,0-未锁
`LOCKGRANTED` datetime DEFAULT NULL, -- 获得锁的时间
`LOCKEDBY` varchar(255) DEFAULT NULL, -- 锁定人
PRIMARY KEY (`ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

我们去查询一下 Mysql,如下:

image

并且 DATABASECHANGELOG 表多了一条数据如下(部分字段截图)

image

修改 db_changelog.yaml 文件,增加 ID = 1 的变更集合( Change Set ) ,如下:

1
2
3
4
5
6
7
8
- changeSet: # 对应一个 ChangeSet 对象
id: 1
author: yizhuo
comments: 初始化数据库表
changes: # 对应 Change 数组。Change 是一个接口,每种操作对应一种 Change 实现类
- sqlFile: # 使用 SQL 文件,对应 SQLFileChange 对象
encoding: utf8
path: classpath:db/changelog/sql/CHANGE_LOG_INIT_DB.sql

src/main/resources 下创建目录 db/changelog/sql/ ,并创建 CHANGE_LOG_INIT_DB.sql文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
-- 创建用户表
CREATE TABLE `users` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '用户编号',
`username` varchar(64) DEFAULT NULL COMMENT '账号',
`password` varchar(32) DEFAULT NULL COMMENT '密码',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 插入一条数据
INSERT INTO `users`(username, password, create_time) VALUES('zhuo', 'password', now());

再次启动项目,日志如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
2020-07-04 21:21:49.694  INFO 72084 --- [           main] liquibase.executor.jvm.JdbcExecutor      : SELECT COUNT(*) FROM github_demo.DATABASECHANGELOGLOCK
2020-07-04 21:21:49.746 INFO 72084 --- [ main] liquibase.executor.jvm.JdbcExecutor : SELECT COUNT(*) FROM github_demo.DATABASECHANGELOGLOCK
2020-07-04 21:21:49.777 INFO 72084 --- [ main] liquibase.executor.jvm.JdbcExecutor : SELECT `LOCKED` FROM github_demo.DATABASECHANGELOGLOCK WHERE ID=1
2020-07-04 21:21:49.844 INFO 72084 --- [ main] l.lockservice.StandardLockService : Successfully acquired change log lock
2020-07-04 21:21:51.881 INFO 72084 --- [ main] liquibase.executor.jvm.JdbcExecutor : SELECT MD5SUM FROM github_demo.DATABASECHANGELOG WHERE MD5SUM IS NOT NULL LIMIT 1
2020-07-04 21:21:51.889 INFO 72084 --- [ main] liquibase.executor.jvm.JdbcExecutor : SELECT COUNT(*) FROM github_demo.DATABASECHANGELOG
2020-07-04 21:21:51.921 INFO 72084 --- [ main] l.c.StandardChangeLogHistoryService : Reading from github_demo.DATABASECHANGELOG
2020-07-04 21:21:51.921 INFO 72084 --- [ main] liquibase.executor.jvm.JdbcExecutor : SELECT * FROM github_demo.DATABASECHANGELOG ORDER BY DATEEXECUTED ASC, ORDEREXECUTED ASC
2020-07-04 21:21:52.058 INFO 72084 --- [ main] liquibase.executor.jvm.JdbcExecutor : -- 创建用户表
CREATE TABLE `users` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '用户编号',
`username` varchar(64) DEFAULT NULL COMMENT '账号',
`password` varchar(32) DEFAULT NULL COMMENT '密码',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
2020-07-04 21:21:52.260 INFO 72084 --- [ main] liquibase.executor.jvm.JdbcExecutor : -- 插入一条数据
INSERT INTO `users`(username, password, create_time) VALUES('zhuo', 'password', now())
2020-07-04 21:21:52.283 INFO 72084 --- [ main] liquibase.changelog.ChangeSet : SQL in file classpath:db/changelog/sql/CHANGE_LOG_INIT_DB.sql executed
2020-07-04 21:21:52.309 INFO 72084 --- [ main] liquibase.changelog.ChangeSet : ChangeSet classpath:/db/changelog/db_changelog.yaml::1::yizhuo ran successfully in 364ms
2020-07-04 21:21:52.310 INFO 72084 --- [ main] liquibase.executor.jvm.JdbcExecutor : SELECT MAX(ORDEREXECUTED) FROM github_demo.DATABASECHANGELOG
2020-07-04 21:21:52.321 INFO 72084 --- [ main] liquibase.executor.jvm.JdbcExecutor : INSERT INTO github_demo.DATABASECHANGELOG (ID, AUTHOR, FILENAME, DATEEXECUTED, ORDEREXECUTED, MD5SUM, `DESCRIPTION`, COMMENTS, EXECTYPE, CONTEXTS, LABELS, LIQUIBASE, DEPLOYMENT_ID) VALUES ('1', 'yizhuo', 'classpath:/db/changelog/db_changelog.yaml', NOW(), 2, '8:88a193289a4da5d24c9fde1ae442fb3d', 'sqlFile', '', 'EXECUTED', NULL, NULL, '3.8.9', '3868911933')
2020-07-04 21:21:52.377 INFO 72084 --- [ main] l.lockservice.StandardLockService : Successfully released change log lock
...
2020-07-04 21:21:52.615 INFO 72084 --- [ main] c.g.z.liquibase.LiquibaseApplication : Started LiquibaseApplication in 8.401 seconds (JVM running for 10.018)
Disconnected from the target VM, address: '127.0.0.1:54078', transport: 'socket'

Process finished with exit code 0

可以看到日志里执行了我们配置的初始化 SQL ,去数据库查看发现 users 表已经创建,并且 DATABASECHANGELOG 表又多了一条数据如下

image

我们修改一下 CHANGE_LOG_INIT_DB.sql 文件内容,新增一行注释如下

1
-- 注释

再次启动项目,会发现日志报错:

1
2
3
2020-07-04 21:34:55.789  WARN 72294 --- [           main] s.c.a.AnnotationConfigApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'liquibase' defined in class path resource [org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfiguration$LiquibaseConfiguration.class]: Invocation of init method failed; nested exception is liquibase.exception.ValidationFailedException: Validation Failed:
1 change sets check sum
classpath:/db/changelog/db_changelog.yaml::1::yizhuo was: 8:88a193289a4da5d24c9fde1ae442fb3d but is now: 8:1b6bc2489cce8400e6aa8cfd2de6725f

Liquibase 会给每个变更集合( Change Set ),计算出一个 MD5SUM 字段。这样,每次启动时,都会校验已经执行的变更集合( Change Set ),是否发生了改变。如果是,抛出异常。这样,保证不会因为 变更集合( Change Set ) 变更,导致出现问题。

测试完成,将 CHANGE_LOG_INIT_DB.sql 文件恢复原样。

再次启动项目,日志如下

1
2
3
4
5
6
7
8
9
10
11
2020-07-04 21:55:43.289  INFO 72969 --- [           main] liquibase.executor.jvm.JdbcExecutor      : SELECT COUNT(*) FROM github_demo.DATABASECHANGELOGLOCK
2020-07-04 21:55:43.319 INFO 72969 --- [ main] liquibase.executor.jvm.JdbcExecutor : SELECT COUNT(*) FROM github_demo.DATABASECHANGELOGLOCK
2020-07-04 21:55:43.337 INFO 72969 --- [ main] liquibase.executor.jvm.JdbcExecutor : SELECT `LOCKED` FROM github_demo.DATABASECHANGELOGLOCK WHERE ID=1
2020-07-04 21:55:43.371 INFO 72969 --- [ main] l.lockservice.StandardLockService : Successfully acquired change log lock
2020-07-04 21:55:45.062 INFO 72969 --- [ main] liquibase.executor.jvm.JdbcExecutor : SELECT MD5SUM FROM github_demo.DATABASECHANGELOG WHERE MD5SUM IS NOT NULL LIMIT 1
2020-07-04 21:55:45.066 INFO 72969 --- [ main] liquibase.executor.jvm.JdbcExecutor : SELECT COUNT(*) FROM github_demo.DATABASECHANGELOG
2020-07-04 21:55:45.080 INFO 72969 --- [ main] l.c.StandardChangeLogHistoryService : Reading from github_demo.DATABASECHANGELOG
2020-07-04 21:55:45.080 INFO 72969 --- [ main] liquibase.executor.jvm.JdbcExecutor : SELECT * FROM github_demo.DATABASECHANGELOG ORDER BY DATEEXECUTED ASC, ORDEREXECUTED ASC
2020-07-04 21:55:45.113 INFO 72969 --- [ main] l.lockservice.StandardLockService : Successfully released change log lock
...
2020-07-04 21:55:45.204 INFO 72969 --- [ main] c.g.z.liquibase.LiquibaseApplication : Started LiquibaseApplication in 6.208 seconds (JVM running for 6.875)

在整个日志中,我可以看到 ID = 1 的变更集合( Change Set )不会重复执行。

基于 SQL 的版本控制介绍完毕,大部分情况下我们都只使用基于 SQL 的版本控制,但是遇到复杂的业务需要根据业务逻辑处理数据,这时基于 SQL 的版本控制可能就不够用了,下面来介绍基于 Java 类的版本控制

扩展:基于 Java 类的版本控制

com.github.zhuyizhuo.liquibase.sample目录下创建 FixUsername.java 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class FixUsername implements CustomTaskChange {
@Override
public void execute(Database database) throws CustomChangeException {
//TODO 此处实现复杂业务逻辑 操作数据库之类
System.out.println("执行业务逻辑。。");
}
@Override
public String getConfirmationMessage() {
return null;
}
@Override
public void setUp() throws SetupException {
}
@Override
public void setFileOpener(ResourceAccessor resourceAccessor) {
}
@Override
public ValidationErrors validate(Database database) {
return null;
}
}

修改 db_changelog.yaml 变更日志,增加 ID = 2 的变更集合( Change Set ) 。代码如下:

1
2
3
4
5
6
- changeSet:
id: 2
author: yizhuo
comments: 自定义业务逻辑类
changes: # 对应 Change 数组。Change 是一个接口,每种操作对应一种 Change 实现类
- customChange: {class: com.github.zhuyizhuo.liquibase.sample.FixUsername}

再次启动项目。执行日志如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2020-07-04 22:09:33.489  INFO 73234 --- [           main] liquibase.executor.jvm.JdbcExecutor      : SELECT COUNT(*) FROM github_demo.DATABASECHANGELOGLOCK
2020-07-04 22:09:33.537 INFO 73234 --- [ main] liquibase.executor.jvm.JdbcExecutor : SELECT COUNT(*) FROM github_demo.DATABASECHANGELOGLOCK
2020-07-04 22:09:33.548 INFO 73234 --- [ main] liquibase.executor.jvm.JdbcExecutor : SELECT `LOCKED` FROM github_demo.DATABASECHANGELOGLOCK WHERE ID=1
2020-07-04 22:09:33.589 INFO 73234 --- [ main] l.lockservice.StandardLockService : Successfully acquired change log lock
2020-07-04 22:09:34.875 INFO 73234 --- [ main] liquibase.executor.jvm.JdbcExecutor : SELECT MD5SUM FROM github_demo.DATABASECHANGELOG WHERE MD5SUM IS NOT NULL LIMIT 1
2020-07-04 22:09:34.879 INFO 73234 --- [ main] liquibase.executor.jvm.JdbcExecutor : SELECT COUNT(*) FROM github_demo.DATABASECHANGELOG
2020-07-04 22:09:34.886 INFO 73234 --- [ main] l.c.StandardChangeLogHistoryService : Reading from github_demo.DATABASECHANGELOG
2020-07-04 22:09:34.886 INFO 73234 --- [ main] liquibase.executor.jvm.JdbcExecutor : SELECT * FROM github_demo.DATABASECHANGELOG ORDER BY DATEEXECUTED ASC, ORDEREXECUTED ASC
执行业务逻辑。。
2020-07-04 22:09:34.905 INFO 73234 --- [ main] liquibase.changelog.ChangeSet : null
2020-07-04 22:09:34.908 INFO 73234 --- [ main] liquibase.changelog.ChangeSet : ChangeSet classpath:/db/changelog/db_changelog.yaml::2::yizhuo ran successfully in 7ms
2020-07-04 22:09:34.909 INFO 73234 --- [ main] liquibase.executor.jvm.JdbcExecutor : SELECT MAX(ORDEREXECUTED) FROM github_demo.DATABASECHANGELOG
2020-07-04 22:09:34.912 INFO 73234 --- [ main] liquibase.executor.jvm.JdbcExecutor : INSERT INTO github_demo.DATABASECHANGELOG (ID, AUTHOR, FILENAME, DATEEXECUTED, ORDEREXECUTED, MD5SUM, `DESCRIPTION`, COMMENTS, EXECTYPE, CONTEXTS, LABELS, LIQUIBASE, DEPLOYMENT_ID) VALUES ('2', 'yizhuo', 'classpath:/db/changelog/db_changelog.yaml', NOW(), 3, '8:39259f2c0bad8428733b5b6c30d30a0a', 'customChange', '', 'EXECUTED', NULL, NULL, '3.8.9', '3871774892')
2020-07-04 22:09:34.943 INFO 73234 --- [ main] l.lockservice.StandardLockService : Successfully released change log lock
...

我们可以看到,ID = 2 的变更集合( Change Set )完成了执行。

此时,我们去查询下 MySQL 。如下:

image

代码示例

本文的完整工程可以查看下面仓库中的spring-boot-samples-liquibase目录:

如果您觉得本文不错,欢迎Star支持,您的关注是我坚持的动力!

请作者喝杯咖啡吧