作业网址:CLI 命令行实用程序开发实战 – Agenda
代码传送门:https://github.com/zz9629/go/tree/master/agenda
1.概述
命令行实用程序并不是都象 cat、more、grep 是简单命令。go 项目管理程序,类似 java 项目管理 maven、Nodejs 项目管理程序 npm、git 命令行客户端、 docker 与 kubernetes 容器管理工具等等都是采用了较复杂的命令行。即一个实用程序同时支持多个子命令,每个子命令有各自独立的参数,命令之间可能存在共享的代码或逻辑,同时随着产品的发展,这些命令可能发生功能变化、添加新命令等。因此,符合 OCP 原则 的设计是至关重要的编程需求。
任务目标
熟悉 go 命令行工具管理项目
综合使用 go 的函数、数据结构与接口,编写一个简单命令行应用 agenda
使用面向对象的思想设计程序,使得程序具有良好的结构命令,并能方便修改、扩展新的命令,不会影响其他命令的代码
项目部署在 Github 上,合适多人协作,特别是代码归并
支持日志(原则上不使用debug调试程序)
参考文档
官方文档 推荐
golang命令行库cobra的使用 中文翻译
2. 准备Cobra
Cobra既是一个用来创建强大的现代CLI命令行的golang库,也是一个生成程序应用和命令行文件的程序。此命令行工具基于cobra开发.
2.1安装 cobra
直接执行以下命令,可能安装不成功:(因为cobra用到的一些依赖包被墙了)
go get -v github.com/spf13/cobra/cobra
所以可以首先安装其依赖包: 在$GOPATH/src/golang.org/x目录下(如果没有,则自行创建)用git clone下载sys和text项目。或者直接去网站下载,这个比较快:
然后手动解压到指定目录:
最后再到$GOPATH/bin目录下获得可执行文件。还是之前的命令:
go get -v github.com/spf13/cobra/cobra
bin文件夹下成功生成exe可执行文件:
然后输入cobra即可,如下:
2.2 Cobra使用
2.2.1 生成agenda项目
新版cobra需要带–pkg-name参数
新建一个工作路径为$GOPATH/src/zz
进入此目录,使用命令
λ cobra init agenda --pkg-name=zz/agenda
会生成一个叫zz/agenda的项目
文件夹结构如下:
2.2.2 添加agenda工具命令
除了生成应用程序框架,还可以通过 cobra add 命令生成子命令的代码文件。
在agenda的目录下,添加子命令register,就是
λ cobra add register
这条命令生成了 agenda程序中 register 子命令的代码,当然了,还没有什么具体的功能,后面会具体介绍写法。
到现在为止,已经给agenda程序添加了两个command,分别为rootCmd
(cobra init 命令默认生成)和registerCmd
2.3 cobra工作原理
下面介绍cobra的工作原理,部分通过刚刚创建的子命令registerCmd
介绍。
main.go文件如下:
package main
import "zz/agenda/cmd"
func main() {
cmd.Execute()
root.go根命令,是整个应用命令的入口,通过在main函数中调用rootCmd.Execute()启动,内部实现中监听了所有命令。root.go代码:
var rootCmd = &cobra.Command{
Use: "agenda",
Short: "A brief description of your application",
Long: `A longer description that spans multiple lines and likely contains examples and usage of using your application. For example:
Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files to quickly create a Cobra application.`,
Run: func(cmd *cobra.Command, args []string) {
// Do Stuff Here
},
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
2.3.1 cobra中的重要概念
cobra 中有个重要的概念,分别是 commands、arguments 和 flags。其中 commands 代表行为,arguments 就是命令行参数(或者称为位置参数),flags 代表对行为的改变(也就是我们常说的命令行选项)。执行命令行程序时的一般格式为:
[appName] [command] [arguments] –[flag]
比如下面的例子:
# server是 commands,port 是 flag
hugo server --port=1313
# clone 是 commands,URL 是 arguments,brae 是 flag
git clone URL --bare
如果是一个简单的程序(功能单一的程序),使用 commands 的方式可能会很啰嗦,但是像 git、docker 等应用,把这些本就很复杂的功能划分为子命令的形式,会方便使用。
在本程序中,如果要使用子命令register来创建一个用户时,比如输入:
# agenda是程序名,register是commands,-u/-p/-e/-t均是flag,(u的配置为name)
$ agenda register -u=name -p=pass -e=email@qq.com -t=8644274
ps:对于命令行参数,有多种输入方式,比如输入用户名的命令行选项-u, 可以是 -uname
或者 -u name
或者 -u=name
,三种方式都可以。
2.3.2 参数解释
- Use: 命令
- Short: 命令的简短描述。命令表中,会显示此条消息,如输入
$ agenda -h
register this command can register user
- Long: 命令的详细描述,在帮助命令,如
$ [appName] -h
或者$ [appName] [command] -h
时会显示此程序和此命令的字符串;
- Run: 命令执行入口,函数主要写在这个模块中。
- 函数initConfig(): 用来初始化viper配置文件位置,监听变化
- 函数init(): 定义flag和配置处理
2.3.3 为 Command 添加选项(flags)
选项(flags)用来控制 Command 的具体行为。根据选项的作用范围,可以把选项分为两类:
- persistent
- local
对于 persistent 类型的选项,既可以设置给该 Command,又可以设置给该 Command 的子 Command。对于一些全局性的选项,比较适合设置为 persistent 类型,比如控制输出的 verbose 选项:
var Verbose bool
rootCmd.PersistentFlags().BoolVarP(&Verbose, "verbose", "v", false, "verbose output")
local 类型的选项只能设置给指定的 Command,比如下面定义的 source 选项:
var Source string
rootCmd.Flags().StringVarP(&Source, "source", "s", "", "Source directory to read from")
又比如在子命令register.go中,命令register需要新建用户的信息,使用flag进行控制不同的参数。
var registerUser models.User
func init() {
rootCmd.AddCommand(registerCmd)
registerCmd.Flags().StringVarP(®isterUser.Username, "username", "u", "", "The User's Username")
registerCmd.Flags().StringVarP(®isterUser.Password, "password", "p", "", "The User's Password")
registerCmd.Flags().StringVarP(®isterUser.Email, "email", "e", "", "The User's Email")
registerCmd.Flags().StringVarP(®isterUser.Telephone, "telephone", "P", "", "The User's telephone")
}
那么用户在使用的时候,可以输入如 $ agenda register -u[name] -p[password] -e[email] -P[tel]
创建新用户。
该选项不能指定给 rootCmd 之外的其它 Command。
默认情况下的选项都是可选的,但一些用例要求用户必须设置某些选项,这种情况 cobra 也是支持的,通过 Command 的 MarkFlagRequired 方法标记该选项即可:
var Name Region
rootCmd.Flags().StringVarP(&Region, "region", "r", "", "AWS region (required)")
rootCmd.MarkFlagRequired("region")
此项目中需要用到。
在register命令中,我们需要name和password非空,否则无法创建账户,那么添加两行代码。前面的函数最后的具体代码如下:
PS:注意后两行的参数是位置参数username,不是数据结构registerUSer.Username。
2.3.4 命令行参数(arguments)
首先我们来搞清楚命令行参数(arguments)
与命令行选项(flags/options)
的区别。以常见的 ls 命令来说,其命令行的格式为:
ls [OPTION]... [FILE]…
其中的 OPTION 对应本文中介绍的 flags,以 – 或 – 开头;而 FILE 则被称为参数(arguments)或位置参数。一般的规则是参数在所有选项的后面,上面的 … 表示可以指定多个选项和多个参数。
cobra 默认提供了一些验证方法:
- NoArgs – 如果存在任何位置参数,该命令将报错 ArbitraryArgs – 该命令会接受任何位置参数 OnlyValidArgs
- 如果有任何位置参数不在命令的 ValidArgs 字段中,该命令将报错 MinimumNArgs(int) – 至少要有 N 个位置参数,否则报错 MaximumNArgs(int)
- 如果位置参数超过 N 个将报错 ExactArgs(int)
- 必须有 N个位置参数,否则报错 ExactValidArgs(int) 必须有 N 个位置参数,且都在命令的 ValidArgs 字段中,否则报错
- RangeArgs(min, max) – 如果位置参数的个数不在区间 min 和 max 之中,报错
比如要让 Command cmdTimes 至少有一个位置参数,可以这样初始化它:
var cmdTimes = &cobra.Command{
Use: …
Short: …
Long: …
Args: cobra.MinimumNArgs(1),
Run: …
}
不过这个功能本项目没有用到。
2.3.5 run模块
这个模块就是具体代码,比如在register中,我们需要对已有的用户名和新用户名进行一些比较等等步骤。
2.3.6 帮助信息(help command)
cobra 会自动添加 –help(-h)选项,所以我们可以不必添加该选项而直接使用:
cobra 同时还自动添加了 help 子命,默认效果和使用 –help 选项相同。如果为 help 命令传递其它命令作为参数,则会显示对应命令的帮助信息,下面的命令输出 register子命令的帮助信息:
3. agenda
3.1 agenda命令介绍
本次实验,我选择实现的命令式register 、login、logout、userquery、ru ,接下来将分别介绍 。
用户注册
- 注册新用户时,用户需设置一个唯一的用户名和一个密码。另外,还需登记邮箱及电话信息。
- 如果注册时提供的用户名已由其他用户使用,应反馈一个适当的出错信息;成功注册后,亦应反馈一个成功注册的信息。
用户登录
- 用户使用用户名和密码登录 Agenda 系统。
- 用户名和密码同时正确则登录成功并反馈一个成功登录的信息。否则,登录失败并反馈一个失败登录的信息。
用户登出
- 已登录的用户登出系统后,只能使用用户注册和用户登录功能。
用户查询
- 已登录的用户可以查看已注册的所有用户的用户名、邮箱及电话信息。
用户删除
- 已登录的用户可以删除本用户账户(即销号)。
- 操作成功,需反馈一个成功注销的信息;否则,反馈一个失败注销的信息。
删除成功则退出系统登录状态。删除后,该用户账户不再存在。
3.2 项目结构
agenda
├── cmd //各个命令
| ├── login.go //登陆
| ├── logout.go //登出
| ├── register.go //注册一个用户
| ├── root.go //主函数
| ├── ru.go //删除某个用户
| └── userquery.go //列出所有用户
├── docs
| ├── commandIntro.md //命令说明文档
| ├── entity.md //entity说明文档
| └── models.md //model说明文档
├── entity
| └── userInfoOp.go //存放 User 对象读写与处理逻辑
├── LICENSE
├── log
| └── logFile.txt //log 包记录命令执行情况
├── main.go
├── models
| ├── logger.go //日志处理函数
| └── user.go //user数据结构
├── README.md
└── storage //user实体存储
├── curUser.txt
└── users.json
directory: 6 file: 19
3.3 register命令实现
以register为例,简单过一下项目的流程。
这个指令比较复杂,我们首先要读取新用户信息,接着遍历所有用户的名字,检查是否与新用户信息冲突。若不冲突则可以注册,否则则返回失败。
- 读取用户信息使用到的是
agenda/entity
包 ,这个包存放 User (和 Meeting) 对象读写与处理逻辑。 - 用户对象的处理放在
agenda/entity/userInfoOp.go
这个代码文件中。(如果有会议的处理,也会单股用meetingInfoOp.go代码文件,这样是增加文件的内聚性和减小各个文件间的耦合性) entity
包会读取文件storage/user.json
,这个json文件相当于用户的数据库,所有用户的信息都保存在里面。entity
函数会返回所有用户信息。接着我们就可以在这个数组中遍历所有的用户名,来查询冲突。- 主要用到的文件为如下结构:
agenda ├── cmd | ├── register.go //注册一个用户 | └── root.go ├── entity //存放对象读写与处理逻辑 | └── userInfoOp.go //User对象 ├── main.go ├── models | └── user.go //user数据结构 └── storage //数据实体存储 └── users.json //所有user信息
users.json
保存所有用户的信息,register.go
是通过entity包
读写用户集来访问这个文件的,user.json和register.go两个文件并没有直接的接口。
创建register命令后,cmd命令中的register.go初始结构如下:
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
// registerCmd represents the register command
var registerCmd = &cobra.Command{
Use: "register",
Short: "A brief description of your command",
Long: `A longer description that spans multiple lines and likely contains examples
and usage of using your command. For example:
Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("register called")
},
}
func init() {
rootCmd.AddCommand(registerCmd)
}
在这个命令中,添加代码之后如下:
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
"zz/agenda/entity"
"zz/agenda/models"
)
var registerUser models.User
// registerCmd represents the register command
var registerCmd = &cobra.Command{
Use: "register",
Short: "This command can register user",
Long: `You can use agenda register to sign up one user`,
Run: func(cmd *cobra.Command, args []string) {
models.Logger.SetPrefix("[agenda register]")
users := entity.ReadUserInfoFromFile()
for _, user := range users {
if user.Username == registerUser.Username {
models.Logger.Println(registerUser.Username, "has been registered!")
fmt.Println(registerUser.Username, "has been registered!")
os.Exit(0)
}
}
users = append(users, registerUser)
entity.WriteUserInfoToFile(users)
models.Logger.Println("Register", registerUser.Username, "successfully!")
fmt.Println("Register", registerUser.Username, "successfully!")
},
}
func init() {
rootCmd.AddCommand(registerCmd)
registerCmd.Flags().StringVarP(®isterUser.Username, "username", "u", "", "The User's Username")
registerCmd.Flags().StringVarP(®isterUser.Password, "password", "p", "", "The User's Password")
registerCmd.Flags().StringVarP(®isterUser.Email, "email", "e", "", "The User's Email")
registerCmd.Flags().StringVarP(®isterUser.Telephone, "telephone", "P", "", "The User's telephone")
registerCmd.MarkFlagRequired("username")
registerCmd.MarkFlagRequired("password")
}
下面介绍一下这个具体代码的补充。
首先这个函数需要一些用户的参数,然后创建一个用户类型存储这些信息,方便之后的比较。
-
首先是用户的数据结构,因此定义这样一个数据结构,在
agenda/models/user.go
中。因此在register.go
中可以直接使用这个数据结构,我们需要做的仅仅是import
这个包即可。
ps:本来完整的项目需要实现会议,那么会议的数据结构meeting.go
也是可以直接打包在models文件夹中的。 -
有了用户的数据结构之后,就可以在register.go中使用了。
首先定义一个新的用户registerUser
,类型为models.User
即var registerUser models.User
-
现在需要把命令行中的参数传入
registerUser
中。
原理在flag中已经解释过了,这里不再赘述。因为用户名和密码是必须的,而邮箱和电话号码是选填,因此在后两行需要添加MarkFlagRequired
函数。否则的话,可能出现用户名和密码为空的情况。(以防万一,也可以在run模块中对用户名密码的内容进行判断。不过我觉得MarkFlagRequired
已经足够了)func init() { rootCmd.AddCommand(registerCmd) registerCmd.Flags().StringVarP(®isterUser.Username, "username", "u", "", "The User's Username") registerCmd.Flags().StringVarP(®isterUser.Password, "password", "p", "", "The User's Password") registerCmd.Flags().StringVarP(®isterUser.Email, "email", "e", "", "The User's Email") registerCmd.Flags().StringVarP(®isterUser.Telephone, "telephone", "P", "", "The User's telephone") registerCmd.MarkFlagRequired("username") registerCmd.MarkFlagRequired("password") }
-
现在已经有了新用户对象,接下来就是注册用户的操作了。我们需要数据库(所有用户信息)中所有用户的信息,然后判断是否可以加入这个新用户。
- 如果重名了,直接退出。
- 否则,扩展用户,并将新用户集写入数据库中。
- ps:失败或成功,都需要写入日志log并在终端上输出。
run模块:
Run: func(cmd *cobra.Command, args []string) { models.Logger.SetPrefix("[agenda register]")//日志前缀 users := entity.ReadUserInfoFromFile() //从数据库中读取用户集 for _, user := range users { //遍历每一个用户 if user.Username == registerUser.Username { models.Logger.Println(registerUser.Username, "has been registered!") fmt.Println(registerUser.Username, "has been registered!") os.Exit(0) } } users = append(users, registerUser) //添加用户 entity.WriteUserInfoToFile(users) //将新的用户集写入数据库 models.Logger.Println("Register", registerUser.Username, "successfully!") //日志记录 fmt.Println("Register", registerUser.Username, "successfully!") }, //终端输出
-
用户集的获取与写入均用到了
agenda/entity/userInfoOp.go
中的read,write函数,这个文件封装了user对象的读写。 -
日志的路径处理用到了
agenda/models/logger.go
这些放在后面讲。
3.4 其他指令
其他指令和register差不多,先添加指令,然后在cmd/[command].go指令代码中修改代码即可,指令如下:
λ cobra add login
λ cobra add logout
λ cobra add userquery
λ cobra add ru
如图:
最后在相应的cmd/[command].go指令代码中修改代码即可。
3.41 login
- login命令的实现比较简单,用户的输入和register一样,不过只需要用户名和密码。
- 在函数实现中,首先需要查询现在是否已经有用户登陆,无法同时登陆两个账号。
- 然后再查询用户集,当前用户是否再用户集中,密码是否相同。
- 如果登陆成功,需要更新当前登陆用户的信息curUser.txt.这也是通过
entity
包实现的。
package cmd
import (
"os"
"fmt"
"zz/agenda/models"
"zz/agenda/entity"
"github.com/spf13/cobra"
)
var loginUser models.User
var loginCmd = &cobra.Command{
Use: "login",
Short: "This command can login user",
Long: `You can use agenda login to login one user.`,
Run: func(cmd *cobra.Command, args []string) {
users := entity.ReadUserInfoFromFile()
models.Logger.SetPrefix("[agenda login]")
// 判断是否已经登陆过
isLoggedIn, user := entity.IsLoggedIn()
if isLoggedIn == true {
// 已经登陆
fmt.Println(user.Username + " has already in")
os.Exit(0)
}
for _, userInfo := range users {
if userInfo.Username == loginUser.Username && userInfo.Password == loginUser.Password {
entity.SaveCurUserInfo(userInfo)
models.Logger.Println("Login", loginUser.Username, "successfully!")
fmt.Println("Login successfully")
os.Exit(0)
} else {
models.Logger.Println("Login", loginUser.Username, "error!")
fmt.Println("Username or Password error, please check your input")
os.Exit(0)
}
}
models.Logger.Println("Login", loginUser.Username, "no such an user!")
fmt.Println("No such an user")
},
}
func init() {
rootCmd.AddCommand(loginCmd)
loginCmd.Flags().StringVarP(&loginUser.Username, "username", "u", "", "The User's Username")
loginCmd.Flags().StringVarP(&loginUser.Password, "password", "p", "", "The User's Password")
loginCmd.MarkFlagRequired("username")
loginCmd.MarkFlagRequired("password")
}
3.42 logout
logout是最简单的一部分,先判断当前是否有人登陆,如果有的话,清空登陆信息即可,均用到了entity
包的函数。
package cmd
import (
"fmt"
"zz/agenda/models"
"zz/agenda/entity"
"github.com/spf13/cobra"
)
var logoutCmd = &cobra.Command{
Use: "logout",
Short: "This command can logout user",
Long: `You can use agenda logout to logout user.`,
Run: func(cmd *cobra.Command, args []string) {
models.Logger.SetPrefix("[agenda logout]")
isLoggedIn, user := entity.IsLoggedIn()
if isLoggedIn == true { //有用户登陆
entity.ClearCurUserInfo() //清空登陆信息
fmt.Println(user.Username, "log out")
models.Logger.Println(user.Username, "log out")
} else {
fmt.Println("No user login")//没人登录,这个就不计入日志了
}
},
}
func init() {
rootCmd.AddCommand(logoutCmd)
}
3.43 userquery
这个命令实现的逻辑和上面相同。如果当前有人登陆,直接列出所有用户的信息即可。
其中调用entity
包查看当前是否有人登陆,以及获得所有用户信息。
package cmd
import (
"fmt"
"zz/agenda/models"
"zz/agenda/entity"
"github.com/spf13/cobra"
)
var userqueryCmd = &cobra.Command{
Use: "userquery",
Short: "This command can query all user information only for logged in users",
Long: `You can use agenda userquery to get all user information only for logged in users.`,
Run: func(cmd *cobra.Command, args []string) {
models.Logger.SetPrefix("[agenda userquery]")
isLoggedIn, user := entity.IsLoggedIn()
if isLoggedIn == true {
models.Logger.Println("UserQuery", user.Username, "query all users infomation!")
users := entity.ReadUserInfoFromFile()
fmt.Println("Name\tPhone\t\tEmail")
for _, userInfo := range users {
fmt.Printf("%-8s%-16s%s\n", userInfo.Username, userInfo.Telephone, userInfo.Email)
}
} else {
fmt.Println("Please login")
}
},
}
func init() {
rootCmd.AddCommand(userqueryCmd)
}
3.44 ru
这个命令只针对当前登陆的用户。会清除当前登陆信息,相当于logout;然后删除当前用户。
package cmd
import (
"fmt"
"zz/agenda/models"
"zz/agenda/entity"
"github.com/spf13/cobra"
)
var ruCmd = &cobra.Command{
Use: "ru",
Short: "This command is used to clear the account for the user who has logged in.",
Long: `You can use agenda ru to Clear your account information [use with caution]`,
Run: func(cmd *cobra.Command, args []string) {
models.Logger.SetPrefix("[agenda remove user]")
isLoggedIn, user := entity.IsLoggedIn()
if isLoggedIn == true {
// delete login info
entity.ClearCurUserInfo()
// delete user info
entity.RemoveUser(user.Username)
models.Logger.Println(user.Username, "clear account")
fmt.Println("Remove user ["+ user.Username + "] successfully")
} else {
fmt.Println("Please login first")
}
},
}
func init() {
rootCmd.AddCommand(ruCmd)
}
3 .5 entity/userInfoOp.go
-
ReadUserInfoFromFile()
/** * @arguments: nil * @return: []models.User */
此函数用于从users.json文件中读取所有用户信息。 通过利用文件读操作,包括os、bufio、json-iterator/go等库的使用,我们遍历整个用户信息文件,获取所有用户的models.Meeting切片然后返回。
-
WriteUserInfoToFile()
/** * @arguments: []models.User * @return: nil */
此函数用于将当前列表中的更新后的所有用户信息重新写入users.json文件中。 通过利用文件写操作,包括os、bufio、json-iterator/go等库的使用,我们可以将所有用户信息编码为json格式的字符串并存储到users.json文件中。
-
SaveCurUserInfo()
/** * @arguments: loginUser models.User * @return: nil */
此函数用于将当前登陆的用户信息存储到curUser.txt文件中,方便登陆用户信息的存储。
-
ClearCurUserInfo
/** * @arguments: nil * @return: nil */
当登陆用户登出的时候,我们利用os库Truncate函数来将登录用户信息从curUser.txt文件中删除。
-
IsLoggedIn
/** * @arguments: nil * @return: bool, models.User */
此函数判断当前是否已经已经有用户登录,并且返回登录用户信息。 我们可以利用此函数来加一些限定,因为未登录的用户不能进行cm、mtcancel等操作。
-
IsUser
/** * @arguments: name string * @return: bool */
此函数用于判端当前用户名是否为已注册的用户,调用ReadUserInfoFromFile并加以判断即可。可以用于在创建、删除会议时判断用户是否存在;或者注册用户时判断该用户名是否已经被注册等。
-
RemoveUser
/** * @arguments: name string * @return: nil */
此函数用于移除用处,主要是方便ru操作。调用ReadUserInfoFromFile获取用户信息,加以处理后再调用WriteUserInfoToFile更新用户信息即可。
3.6 日志处理
4. 项目测试
- 在
agenda
目录中go install
之后,可以在任何目录下启动该agenda
λ agenda
- register
λ agenda register -u=zz -p=zz -e=123@qq.com -t=8763526
λ agenda register -u=aa -p=aa -e=1221@qq.com -t=83243526
λ agenda register -u=bb -p=bb -e=12as21@qq.com -t=8312352
现在查看agenda/storage/users.json
:
成功写入新用户信息。
- login
λ agenda login -uzz -pzz
查看当前登陆用户信息,curUser.txt
成功登陆。
- userquery
λ agenda userquery
- logout
λ agenda logout
curUser.txt为空,成功登出。
- 这个时候,没有用户登陆,那么是无法获取所有用户信息的。
λ agenda userquery
- ru
λ agenda ru
现在查看agenda/storage/users.json
:
已经成功删除zz用户。
测试结束。