打造持久化数据的Pomodoro应用:从SQLite集成到功能实现
一、技能练习与应用拓展
在深入开发之前,我们可以先对已学技能进行练习。以下是一些建议:
1.
探索Termdash的其他小部件
:了解Termdash中可用的其他小部件,这有助于为未来项目积累资源。
2.
测试小部件替换
:在后续应用中会使用BarChart和LineChart添加活动摘要,在此之前,可以进行一些小部件的测试替换。例如,用Gauge小部件替换Donut小部件来表示间隔内已过去的时间;使用SegmentDisplay小部件代替文本小部件来表示时间。
二、Pomodoro应用的数据持久化需求
最初开发的Pomodoro计时器应用虽然功能完备,但存在数据无法持久化的问题,每次启动都会从第一个Pomodoro间隔开始,无法记录之前的时间间隔。为了解决这个问题,我们将使用结构化查询语言(SQL)把数据存储到关系型数据库中,以此来改进这个应用。
由于该应用采用了存储库模式进行开发,我们可以通过添加新的存储库来集成新的数据存储方式,而无需更改业务逻辑和应用本身。这种模式具有很强的灵活性,能让应用根据不同需求以不同方式持久化数据。例如,在测试时可以使用内存数据存储,在生产环境中则使用数据库引擎。
考虑到Pomodoro应用属于个人应用,嵌入式数据库是一个不错的选择,我们将使用SQLite来实现数据存储。SQLite具有速度快、体积小且支持多操作系统的优点。
三、搭建新的开发环境
为了便于后续开发,我们可以将现有的Pomodoro应用复制到新的工作环境中,使应用源文件与开发步骤相匹配。具体操作步骤如下:
1. 切换到指定的根目录并创建新目录:
$ cd $HOME/pragprog.com/rggo/
$ mkdir -p $HOME/pragprog.com/rggo/persistentDataSQL
- 递归复制原应用目录到新目录并切换到新目录:
$ cp -r $HOME/pragprog.com/rggo/interactiveTools/pomo $HOME/pragprog.com/rggo/persistentDataSQL
$ cd $HOME/pragprog.com/rggo/persistentDataSQL/pomo
由于使用了Go模块,无需额外操作,模块会自动解析到当前目录。
四、SQLite的安装与使用
4.1 安装SQLite
不同操作系统的安装方式不同:
-
Linux
:许多应用会使用SQLite存储数据,因此很可能已经安装。可以使用发行版的包管理器检查并安装,大多数流行的Linux发行版都提供了SQLite。
-
macOS
:使用Homebrew安装:
$ brew install sqlite3
- Windows :可以从下载页面下载预编译版本,也可以使用Chocolatey安装:
C:\> choco install SQLite
4.2 验证SQLite安装并操作数据库
安装完成后,需要验证其是否正常工作。首先切换到应用目录:
$ cd $HOME/pragprog.com/rggo/persistentDataSQL/pomo
SQLite数据库通常以单个
.db
文件形式存在,这种方式使数据库更具可移植性。使用以下命令启动SQLite客户端并创建数据库文件:
$ sqlite3 pomo.db
启动后,可以使用以
.
开头的命令来操作SQLite,例如:
-
.tables
:查看表列表。
-
.quit
:退出客户端。
-
.help
:查看可用命令列表。
接下来创建一个名为
interval
的表来存储Pomodoro应用的数据:
sqlite> CREATE TABLE "interval" (
...> "id" INTEGER,
...> "start_time" DATETIME NOT NULL,
...> "planned_duration" INTEGER DEFAULT 0,
...> "actual_duration" INTEGER DEFAULT 0,
...> "category" TEXT NOT NULL,
...> "state" INTEGER DEFAULT 1,
...> PRIMARY KEY("id")
...> );
再次使用
.tables
命令确认表是否创建成功。
然后向表中插入一些数据:
sqlite> INSERT INTO interval VALUES(NULL, date('now'),25,25,"Pomodoro",3);
sqlite> INSERT INTO interval VALUES(NULL, date('now'),5,5,"ShortBreak",3);
sqlite> INSERT INTO interval VALUES(NULL, date('now'),15,15,"LongBreak",3);
使用SQL SELECT语句查询数据:
- 查询所有行和列:
sqlite> SELECT * FROM interval;
- 查询类别为Pomodoro的行:
sqlite> SELECT * FROM interval WHERE category='Pomodoro';
测试完成后,使用DELETE语句删除表中的数据:
sqlite> DELETE FROM interval;
sqlite> SELECT COUNT(*) FROM interval;
最后使用
.quit
命令退出SQLite客户端。
4.3 表结构与数据类型映射
数据库表结构与
pomodoro/Interval
类型相对应,各字段的映射关系如下表所示:
| Field | Field Type | Column | Column Data Type | Comment |
| — | — | — | — | — |
| ID | int64 | id | INTEGER | 表的主键,SQLite会自动为INTEGER类型的主键列设置自增 |
| StartTime | time.Time | start_time | DATETIME | sqlite3驱动会自动处理Go的time.Time类型与SQLite DATETIME之间的转换 |
| PlannedDuration | time.Duration | planned_duration | INTEGER | 驱动会隐式处理数据类型之间的转换 |
| ActualDuration | time.Duration | actual_duration | INTEGER | 驱动会隐式处理数据类型之间的转换 |
| Category | string | category | TEXT | 由于类别是必需的,设置了NOT NULL约束 |
| State | int | state | INTEGER |
五、Go与SQLite的连接
5.1 Go与SQL数据库的交互基础
Go通过
database/sql
包与SQL数据库进行交互,该包为使用SQL的数据库提供了通用接口,允许通过提供要执行的查询和语句来连接和操作不同的数据库。
database/sql
包在低级和高级接口之间取得了平衡,它抽象了数据类型、连接等连接数据库引擎的底层细节,但仍需要通过SQL语句执行查询。这为开发依赖数据库的应用提供了很大的灵活性,但也需要编写自己的函数来处理数据。
除了
database/sql
包,还需要特定的驱动来连接所需的数据库。这些数据库驱动并非Go标准库的一部分,通常由开源社区开发和维护。对于本应用,我们将使用
go-sqlite3
驱动。
5.2 配置开发环境
由于
go-sqlite3
驱动使用C绑定来连接SQLite,因此需要启用CGO并确保有可用的C编译器。不同操作系统的配置方法如下:
-
Linux
:大多数Linux发行版默认提供gcc编译器,若未安装,可使用发行版的包管理器进行安装。
-
macOS
:安装XCode以获取Apple的C编译器和其他开发工具。
-
Windows
:需要安装C编译器和工具链,如TDM - GCC或MINGW。若使用Chocolatey,可直接安装MINGW gcc工具链:
C:\> choco install mingw
安装gcc工具链后,要确保其在系统PATH中,以便Go编译器能够访问。
在下载和构建驱动之前,需要确保CGO已启用:
$ go env CGO_ENABLED
若未启用,可使用以下命令永久启用:
$ go env -w CGO_ENABLED=1
若想临时启用CGO来构建SQLite驱动,可使用shell的export命令:
$ export CGO_ENABLED=1
由于在编写时发现SQLite库与GCC 10或更高版本存在问题,每次连接数据库时驱动会显示警告信息。为避免这种情况,可在安装驱动前设置GCC标志:
$ go env -w CGO_CFLAGS="-g -O2 -Wno-return-local-addr"
最后,使用
go
命令下载并安装
go-sqlite3
驱动:
$ go get github.com/mattn/go-sqlite3
$ go install github.com/mattn/go-sqlite3
安装驱动后,会编译并缓存库,这样在构建应用时无需再次使用GCC,也无需每次都重新编译,节省了开发和测试时间。
六、在数据库中持久化数据
6.1 选择数据存储方式
当前应用仅支持
inMemory
存储库,当添加更多存储库时,需要为用户提供选择数据存储方式的途径,可以在编译时或运行时进行选择。
-
运行时选择
:使应用更灵活,允许用户在每次执行时选择数据存储方式。需要编译支持所有所需数据存储的应用,并通过命令行参数或配置选项让用户选择。
-
编译时选择
:创建的二进制文件依赖项更少、体积更小,应用灵活性降低但效率更高。可以根据不同标准(如测试、生产环境或特定操作系统)在构建时包含特定文件。
在本示例中,我们选择在编译时定义数据存储方式。为了在无法安装SQLite时仍能使用
inMemory
存储库进行构建和测试,我们将使用构建标签来实现。将数据库存储作为默认选项,即不使用任何标签构建应用时,将包含SQLite存储库而不是
inMemory
存储库;若要构建支持
inMemory
存储的应用,则使用
inmemory
构建标签。
6.2 添加构建标签
为相关文件添加
inmemory
构建标签:
1. 编辑
pomodoro/repository/inMemory.go
文件,在文件顶部添加
// +build inmemory
并留空行,确保Go能识别该构建标签:
// +build inmemory
package repository
-
对
pomodoro/inmemory_test.go和cmd/repoinmemory.go文件进行相同操作:
// +build inmemory
package pomodoro_test
// +build inmemory
package cmd
6.3 创建SQLite存储库文件
在
pomodoro/repository
目录下创建
sqlite3.go
文件,并添加构建标签,当
inmemory
标签不可用时包含该文件:
// +build !inmemory
package repository
import (
"database/sql"
"sync"
"time"
// Blank import for sqlite3 driver only
_ "github.com/mattn/go-sqlite3"
"pragprog.com/rggo/interactiveTools/pomo/pomodoro"
)
这里使用空白标识符
_
导入SQLite驱动,确保Go不会因未直接使用该包的函数而抛出构建错误,同时使
database/sql
包能够与所需数据库进行交互。
6.4 定义数据库表创建语句
定义一个常量字符串来表示创建
interval
表的SQL语句,使用
CREATE TABLE IF NOT EXISTS
确保表仅在不存在时创建,避免额外检查:
const (
createTableInterval string = `CREATE TABLE IF NOT EXISTS "interval" (
"id" INTEGER,
"start_time" DATETIME NOT NULL,
"planned_duration" INTEGER DEFAULT 0,
"actual_duration" INTEGER DEFAULT 0,
"category" TEXT NOT NULL,
"state" INTEGER DEFAULT 1,
PRIMARY KEY("id")
);`
)
6.5 定义SQLite存储库类型和构造函数
定义
dbRepo
类型来表示SQLite存储库,该类型实现了
pomodoro.Repository
接口的方法。它包含一个指向
sql.DB
类型的未导出字段
db
表示数据库句柄,同时嵌入了
sync.Mutex
类型以防止对数据库的并发访问:
type dbRepo struct {
db *sql.DB
sync.RWMutex
}
func NewSQLite3Repo(dbfile string) (*dbRepo, error) {
db, err := sql.Open("sqlite3", dbfile)
if err != nil {
return nil, err
}
db.SetConnMaxLifetime(30 * time.Minute)
db.SetMaxOpenConns(1)
if err := db.Ping(); err != nil {
return nil, err
}
if _, err := db.Exec(createTableInterval); err != nil {
return nil, err
}
return &dbRepo{
db: db,
}, nil
}
在构造函数中,使用
sql.Open
函数打开数据库连接,设置连接的最大生命周期和最大打开连接数,使用
Ping
方法验证连接是否建立,最后使用之前定义的常量语句初始化数据库。
6.6 实现存储库接口方法
6.6.1 Create方法
用于向存储库中添加新的时间间隔:
func (r *dbRepo) Create(i pomodoro.Interval) (int64, error) {
r.Lock()
defer r.Unlock()
insStmt, err := r.db.Prepare("INSERT INTO interval VALUES(NULL, ?,?,?,?,?)")
if err != nil {
return 0, err
}
defer insStmt.Close()
res, err := insStmt.Exec(i.StartTime, i.PlannedDuration, i.ActualDuration, i.Category, i.State)
if err != nil {
return 0, err
}
var id int64
if id, err = res.LastInsertId(); err != nil {
return 0, err
}
return id, nil
}
在方法中,首先锁定存储库以防止并发访问,使用
Prepare
方法准备INSERT语句,执行插入操作并获取插入行的ID。
6.6.2 Update方法
用于修改存储库中现有的时间间隔:
func (r *dbRepo) Update(i pomodoro.Interval) error {
r.Lock()
defer r.Unlock()
updStmt, err := r.db.Prepare("UPDATE interval SET start_time=?, actual_duration=?, state=? WHERE id=?")
if err != nil {
return err
}
defer updStmt.Close()
res, err := updStmt.Exec(i.StartTime, i.ActualDuration, i.State, i.ID)
if err != nil {
return err
}
_, err = res.RowsAffected()
return err
}
该方法与
Create
方法结构类似,使用UPDATE语句根据ID更新单行数据,并检查受影响的行数。
6.6.3 ByID方法
根据ID从存储库中返回单个时间间隔:
func (r *dbRepo) ByID(id int64) (pomodoro.Interval, error) {
r.RLock()
defer r.RUnlock()
row := r.db.QueryRow("SELECT * FROM interval WHERE id=?", id)
i := pomodoro.Interval{}
err := row.Scan(&i.ID, &i.StartTime, &i.PlannedDuration, &i.ActualDuration, &i.Category, &i.State)
return i, err
}
使用
RLock
方法锁定数据库进行读取,使用
QueryRow
方法执行SELECT查询,使用
Scan
方法将返回的列解析到
Interval
结构体的字段指针中。
6.6.4 Last方法
查询并返回存储库中的最后一个时间间隔:
func (r *dbRepo) Last() (pomodoro.Interval, error) {
r.RLock()
defer r.RUnlock()
last := pomodoro.Interval{}
err := r.db.QueryRow("SELECT * FROM interval ORDER BY id desc LIMIT 1").Scan(&last.ID, &last.StartTime, &last.PlannedDuration, &last.ActualDuration, &last.Category, &last.State)
if err == sql.ErrNoRows {
return last, pomodoro.ErrNoIntervals
}
if err != nil {
return last, err
}
return last, nil
}
该方法与
ByID
方法类似,通过排序和限制查询返回最后一行数据。
6.6.5 Breaks方法
查询并返回类别为
ShortBreak
或
LongBreak
的n个时间间隔:
func (r *dbRepo) Breaks(n int) ([]pomodoro.Interval, error) {
r.RLock()
defer r.RUnlock()
stmt := `SELECT * FROM interval WHERE category LIKE '%Break'
ORDER BY id DESC LIMIT ?`
rows, err := r.db.Query(stmt, n)
if err != nil {
return nil, err
}
defer rows.Close()
data := []pomodoro.Interval{}
for rows.Next() {
i := pomodoro.Interval{}
err = rows.Scan(&i.ID, &i.StartTime, &i.PlannedDuration, &i.ActualDuration, &i.Category, &i.State)
if err != nil {
return nil, err
}
data = append(data, i)
}
err = rows.Err()
if err != nil {
return nil, err
}
return data, nil
}
使用
RLock
方法锁定数据库进行读取,定义SELECT查询语句,使用
Query
方法执行查询,将结果解析到
Interval
结构体切片中。
6.6.6 CategorySummary方法
返回指定日期和过滤条件下的每日摘要:
func (r *dbRepo) CategorySummary(day time.Time, filter string) (time.Duration, error) {
r.RLock()
defer r.RUnlock()
stmt := `SELECT sum(actual_duration) FROM interval
WHERE category LIKE ? AND
strftime('%Y-%m-%d', start_time, 'localtime')=
strftime('%Y-%m-%d', ?, 'localtime')`
var ds sql.NullInt64
err := r.db.QueryRow(stmt, filter, day).Scan(&ds)
var d time.Duration
if ds.Valid {
d = time.Duration(ds.Int64)
}
return d, err
}
该方法使用
RLock
方法锁定数据库进行读取,定义SELECT查询语句,执行查询并将结果解析到
sql.NullInt64
类型的变量中,最后根据有效性转换为
time.Duration
类型。
至此,SQLite存储库的开发已完成,接下来可以使用该存储库对
pomodoro
包进行测试。
七、整体流程总结
graph LR
A[技能练习] --> B[数据持久化需求分析]
B --> C[搭建新开发环境]
C --> D[安装与配置SQLite]
D --> E[连接Go与SQLite]
E --> F[选择数据存储方式]
F --> G[创建SQLite存储库]
G --> H[实现存储库接口方法]
H --> I[测试pomodoro包]
通过以上步骤,我们成功地为Pomodoro应用添加了数据持久化功能,使其能够更好地满足用户的需求。在实际开发过程中,我们可以根据具体情况对代码进行优化和扩展,进一步提升应用的性能和功能。
八、测试Pomodoro包
完成SQLite存储库的开发后,接下来需要对
pomodoro
包进行测试,以确保存储库的功能正常。以下是测试的一般步骤:
1.
编写测试用例
:针对
dbRepo
实现的每个方法,编写相应的测试用例,覆盖正常情况和异常情况。
2.
初始化测试环境
:在测试开始前,创建一个测试用的SQLite数据库文件,并初始化数据库表结构。
3.
执行测试
:运行测试用例,检查每个方法的返回结果是否符合预期。
4.
清理测试环境
:测试结束后,删除测试用的数据库文件,避免对开发环境造成影响。
以下是一个简单的测试示例,用于测试
Create
方法:
package repository
import (
"os"
"testing"
"time"
"pragprog.com/rggo/interactiveTools/pomo/pomodoro"
)
func TestCreate(t *testing.T) {
// 创建测试用的数据库文件
dbfile := "test.db"
defer os.Remove(dbfile)
// 初始化SQLite存储库
repo, err := NewSQLite3Repo(dbfile)
if err != nil {
t.Fatalf("Failed to create SQLite repository: %v", err)
}
// 创建一个新的Interval实例
interval := pomodoro.Interval{
StartTime: time.Now(),
PlannedDuration: 25 * time.Minute,
ActualDuration: 25 * time.Minute,
Category: "Pomodoro",
State: 3,
}
// 调用Create方法
id, err := repo.Create(interval)
if err != nil {
t.Fatalf("Failed to create interval: %v", err)
}
// 检查返回的ID是否大于0
if id <= 0 {
t.Errorf("Expected positive ID, got %d", id)
}
}
在上述测试用例中,我们首先创建了一个测试用的数据库文件,然后初始化了SQLite存储库。接着,我们创建了一个新的
Interval
实例,并调用
Create
方法将其插入到数据库中。最后,我们检查返回的ID是否大于0,以确保插入操作成功。
九、性能优化与注意事项
9.1 性能优化
-
连接池管理
:合理设置数据库连接的最大数量和最大生命周期,避免频繁创建和销毁连接,提高性能。例如,在
NewSQLite3Repo函数中,我们设置了最大打开连接数为1和最大连接生命周期为30分钟。 -
使用预编译语句
:在执行SQL查询时,使用预编译语句可以避免SQL注入问题,并提高查询效率。例如,在
Create和Update方法中,我们使用了Prepare方法来准备SQL语句。 -
批量操作
:如果需要插入或更新大量数据,可以考虑使用批量操作来减少数据库交互次数。例如,使用
INSERT INTO ... VALUES (...)语句一次性插入多条记录。
9.2 注意事项
-
并发访问
:虽然我们使用了
sync.RWMutex来防止并发访问数据库,但在高并发场景下,仍需考虑更复杂的并发控制机制。 -
错误处理
:在实际开发中,需要对数据库操作中的错误进行详细的处理,避免程序崩溃。例如,在
sql.Open和db.Ping方法调用后,我们检查了返回的错误,并进行了相应的处理。 - 数据库文件管理 :定期备份数据库文件,避免数据丢失。同时,注意数据库文件的存储位置和权限,确保数据的安全性。
十、功能扩展建议
10.0 增加统计功能
- 周总结和月总结 :除了每日摘要,还可以实现周总结和月总结功能,让用户了解自己在更长时间范围内的工作情况。
- 不同类别的统计 :除了按类别统计实际时长,还可以统计不同类别的次数、平均时长等信息。
10.1 可视化展示
-
图表展示
:使用第三方库(如
termdash)将统计数据以图表的形式展示出来,让用户更直观地了解自己的工作情况。 - 报表生成 :生成详细的报表,包含各种统计信息和图表,方便用户导出和分享。
10.2 用户交互优化
- 命令行参数扩展 :增加更多的命令行参数,让用户可以更灵活地控制应用的行为,如选择不同的存储库、指定统计日期范围等。
- 配置文件支持 :支持使用配置文件来配置应用的参数,如数据库文件路径、默认统计类别等。
十一、总结与展望
11.1 总结
通过本文的介绍,我们成功地为Pomodoro应用添加了数据持久化功能,使用SQLite作为数据库存储数据,并实现了一个SQLite存储库。具体步骤包括:
1. 分析数据持久化需求,选择SQLite作为嵌入式数据库。
2. 搭建开发环境,安装SQLite和相关驱动。
3. 使用构建标签选择数据存储方式,将数据库存储作为默认选项。
4. 创建SQLite存储库文件,定义数据库表结构和存储库接口方法。
5. 实现存储库接口方法,包括
Create
、
Update
、
ByID
、
Last
、
Breaks
和
CategorySummary
方法。
6. 对
pomodoro
包进行测试,确保存储库的功能正常。
11.2 展望
在未来的开发中,我们可以进一步扩展Pomodoro应用的功能,如增加更多的统计功能、实现可视化展示和优化用户交互等。同时,我们也可以考虑将应用部署到不同的环境中,如服务器端或移动设备上,以满足更多用户的需求。
总之,数据持久化是应用开发中非常重要的一环,通过合理选择数据库和实现存储库,我们可以让应用更加稳定和可靠。希望本文对大家在开发类似应用时有所帮助。
十二、相关操作步骤汇总
12.1 环境搭建与配置
| 操作 | 命令或代码 | 说明 |
|---|---|---|
| 创建新目录 |
$ cd $HOME/pragprog.com/rggo/
$ mkdir -p $HOME/pragprog.com/rggo/persistentDataSQL
| 切换到指定根目录并创建新目录 |
| 复制应用目录 |
$ cp -r $HOME/pragprog.com/rggo/interactiveTools/pomo $HOME/pragprog.com/rggo/persistentDataSQL
$ cd $HOME/pragprog.com/rggo/persistentDataSQL/pomo
| 递归复制原应用目录到新目录并切换到新目录 |
| 安装SQLite(macOS) |
$ brew install sqlite3
| 使用Homebrew安装SQLite |
| 安装SQLite(Windows) |
C:\> choco install SQLite
| 使用Chocolatey安装SQLite |
| 启用CGO |
$ go env CGO_ENABLED
$ go env -w CGO_ENABLED=1
或
$ export CGO_ENABLED=1
| 检查并启用CGO |
| 设置GCC标志 |
$ go env -w CGO_CFLAGS="-g -O2 -Wno-return-local-addr"
| 避免SQLite库与GCC 10或更高版本的警告问题 |
安装
go-sqlite3
驱动
|
$ go get github.com/mattn/go-sqlite3
$ go install github.com/mattn/go-sqlite3
| 下载并安装驱动 |
12.2 数据库操作
| 操作 | 命令或代码 | 说明 |
|---|---|---|
| 启动SQLite客户端 |
$ sqlite3 pomo.db
| 启动SQLite客户端并创建数据库文件 |
| 创建表 |
sqlite> CREATE TABLE "interval" (...)
|
创建
interval
表
|
| 插入数据 |
sqlite> INSERT INTO interval VALUES(...)
| 向表中插入数据 |
| 查询数据 |
sqlite> SELECT * FROM interval;
sqlite> SELECT * FROM interval WHERE category='Pomodoro';
| 查询数据 |
| 删除数据 |
sqlite> DELETE FROM interval;
| 删除表中的数据 |
12.3 代码实现
| 操作 | 代码 | 说明 |
|---|---|---|
| 创建SQLite存储库 |
go <br> repo, err := NewSQLite3Repo(dbfile) <br>
| 创建SQLite存储库实例 |
| 插入数据 |
go <br> id, err := repo.Create(interval) <br>
| 向存储库中插入新的时间间隔 |
| 更新数据 |
go <br> err := repo.Update(interval) <br>
| 修改存储库中现有的时间间隔 |
| 根据ID查询数据 |
go <br> interval, err := repo.ByID(id) <br>
| 根据ID从存储库中返回单个时间间隔 |
| 查询最后一个数据 |
go <br> interval, err := repo.Last() <br>
| 查询并返回存储库中的最后一个时间间隔 |
| 查询指定类别的数据 |
go <br> intervals, err := repo.Breaks(n) <br>
|
查询并返回类别为
ShortBreak
或
LongBreak
的n个时间间隔
|
| 查询每日摘要 |
go <br> duration, err := repo.CategorySummary(day, filter) <br>
| 返回指定日期和过滤条件下的每日摘要 |
十三、总结操作流程
graph LR
A[测试环境初始化] --> B[编写测试用例]
B --> C[执行测试]
C --> D{测试结果是否通过}
D -- 通过 --> E[清理测试环境]
D -- 未通过 --> F[调试代码]
F --> B
E --> G[性能优化与注意事项检查]
G --> H[功能扩展]
通过以上步骤,我们完成了Pomodoro应用的数据持久化功能开发,并对其进行了测试、优化和扩展。在实际开发中,我们可以根据具体需求对应用进行进一步的完善和改进。
超级会员免费看
29

被折叠的 条评论
为什么被折叠?



