- 测试管理
- Apifox CLI
- Apifox CLI 命令行运行
- JSON Schema 介绍
- 一、如何描述 JSON ?
- 二、JSON Schema 的举例
- 1. 空 schema
- 2. type 指定 JSON 数据类型
- 3. type 可以包含多个类型
- 4. string 限定长度
- 5. string 模式匹配
- 6. string 值的枚举
- 7. integer
- 8. multipleOf 数字倍数
- 9. number 限定范围
- 10. object 不允许有额外的字段
- 11. object 允许有额外的字段,并限定类型
- 12. object 必填字段
- 13. object 指定属性个数
- 14. Dependencies 依赖
- 15. Object 属性的模式匹配
- 16. array 数组
- 17. array 指定数组成员类型
- 18. array 指定数组成员类型,逐个指定
- 19. array 指定数组成员类型,逐个指定,严格限定
- 20. array 数组长度限制
- 21. array element uniqueness 数组元素的唯一性
- 22. boolean
- 23. null
- 24. schema 的合并
- 25. allOf、oneOf
- 26. oneOf
- 27. not
- JSON Path 介绍
- XPath 介绍
- 正则表达式
- CSV 格式规范
- Socket 粘包和分包问题
- 安装 Java 环境
- 其他
- 后续功能规划
- 更新日志
测试管理
测试用例
测试用例是将多个接口有序地组合在一起运行,用来测试一个完整业务流程。
新建测试用例
路径:自动化测试-测试用例
点击新建测试用例,根据需要新建一个测试用例。
导入接口用例
选中某个测试用例,进入编辑页面。
在测试用例的编辑页面,把鼠标移动到添加步骤上,会展示菜单。
添加用例有两种方式:从接口导入和从接口用例导入 (推荐)
- 从【接口】导入:根据接口参数自动生成一个用例,其参数值为空,需要手动填写。
- 从【接口用例】导入:有两种模式复制和绑定。将接口用例以复制的方式导入,接口用例里的参数也会一同复制过来,和原来用例数据相互独立,各自改动后互不影响。将接口用例以绑定的方式导入,会直接引用原来的用例,两边的改动都会相互实时同步。
注意
- 从接口导入后,需要手动设置接口参数,否则运行的时候,接口参数是空的。
- 从接口用例导入后,会同步导入接口用例里的参数,会方便很多。
从接口用例导入例图
从接口导入例图
导入成功后,一定要记得点击保存哦。
运行测试用例
测试报告
运行完成后,如图所示,可以看到哪些接口没有通过测试,可以点击对应的接口展开详情;点击更多详情,可以查看该接口的运行结果,方便定位问题。
运行结束后可以从下面两个入口,查看之前的测试报告,也可以导出。
常见问题
B 接口请求参数依赖于 A 接口返回的数据,如何实现?
使用后置脚本和变量(普通变量、环境变量或全局变量)。
- A 接口的用例里编写后置脚本,将接口请求返回的数据写入变量,示例: ```javascript // 获取 JSON 格式的请求返回数据 var jsonData = pm.response.json();
// 将 jsonData.token 的值写入变量 pm.variables.set(“token”, jsonData.token);
2. B 接口对应的参数值,设置为对应的变量,如{{token}},即可直接引用前面设置的变量token的值。
<a name="OnzHb"></a>
## 测试套件
测试套件为测试用例的集合,每个测试套件包含多个测试用例。<br />**主要用途:**
1. 实现测试用例的复用。
1. 业务流程复杂时,可避免将所有步骤都写在单个用例里,防止造成单个用例里的步骤过多,难以管理。
<a name="uMrLI"></a>
## 测试数据
测试用例和测试套件支持测试数据集。当用例或套件运行时,系统会循环运行数据文件里所有的数据集,并且会将数据集里的数据赋值给对应的变量。
1. 每个数据集可包含多个变量,接口运行时 [使用变量](https://www.apifox.cn/help/app/api-manage/variables/) 的地方会读取对应的值(变量优先级:临时变量 > 测试数据变量 > 环境变量 > 全局变量)。
1. 可创建多个数据集,系统会遍历运行所有的数据集(每个数据集都会被运行一次)。
1. 数据集云端同步,成员之间共享测试数据。
1. 可根据不同环境设置不同的数据集。
<a name="WSWXU"></a>
### 编辑测试数据
打开测试用例或测试套件详情页就可以看到测试数据页。通过添加数据集、批量编辑、添加变量等直接编辑测试数据;点击导入可以导入本地csv文件的数据。<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/26803611/1653150195048-37b51d44-0b9c-41c4-aef6-613d7dc4e3ff.png#clientId=ue424eaa9-6977-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=ua63fe637&margin=%5Bobject%20Object%5D&name=image.png&originHeight=1600&originWidth=2560&originalType=url&ratio=1&rotation=0&showTitle=false&size=460425&status=done&style=none&taskId=ueacd73fd-9190-4377-b489-a6957b01336&title=)
<a name="BzvwH"></a>
### 使用测试数据
测试步骤导入的接口或用例,通过引用变量的方式获取测试数据。<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/26803611/1653150194967-3f1d2e97-c11c-4f8e-9da2-3f68e199aff9.png#clientId=ue424eaa9-6977-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=ud16e387a&margin=%5Bobject%20Object%5D&name=image.png&originHeight=778&originWidth=1816&originalType=url&ratio=1&rotation=0&showTitle=false&size=163132&status=done&style=none&taskId=u0604175c-80a9-4f42-a1ba-0560b852ae9&title=) ![image.png](https://cdn.nlark.com/yuque/0/2022/png/26803611/1653150194940-318c55f2-77f4-4238-b03b-2b4442ffd2b3.png#clientId=ue424eaa9-6977-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=u8604b143&margin=%5Bobject%20Object%5D&name=image.png&originHeight=794&originWidth=1816&originalType=url&ratio=1&rotation=0&showTitle=false&size=150901&status=done&style=none&taskId=u0593af2f-58c8-42d4-b92a-5d17b0bd102&title=)
<a name="w6dGV"></a>
### 运行测试数据
在运行前需要打开测试数据的开关,再点击运行<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/26803611/1653150195110-522b984d-8526-4f92-b51f-75b6359fe247.png#clientId=ue424eaa9-6977-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=u6127847c&margin=%5Bobject%20Object%5D&name=image.png&originHeight=1600&originWidth=2560&originalType=url&ratio=1&rotation=0&showTitle=false&size=468245&status=done&style=none&taskId=u5108cb6f-c8ac-497e-a714-27a62f7285e&title=)
<a name="TzcUi"></a>
### 常见问题
<a name="Rg2c6"></a>
#### 1. 中文导入后乱码的问题
是因为 windows 默认导出 csv 是 GBK,并且旧版本的 Excel 2016 前会不保存 Bom (byte order mark)。
- Windows 可以使用记事本打开 csv 文件后另存为 utf-8 格式。
- Mac 上可以使用 iconv -f GBK -t UTF-8 xxx.csv > utf-8.csv。
<a name="O20Bt"></a>
## 性能测试
性能测试有 3 种方式。
<a name="dCnku"></a>
### 一、Apifox 应用内测试
运行测试用例的时候,设置线程数大于1即可实现性能测试。<br />线程数即同时【并发】运行的线程数,每个线程都会按顺序运行选中的所有步骤。
> **注意**
> 1. 该功能为 beta 阶段,还在优化中,高并发测试建议导出 JMeter 文件的方式来测试。
<a name="l1l1L"></a>
### 二、Apifox CLI 方式测试
Apifox CLI 是 Apifox 的命令行运行工具,主要用来做持续集成和压力测试,其压力测试功能目前正在开发中,敬请期待!
<a name="N1il4"></a>
### 三、导出 JMeter 测试
测试用例和测试套件可以导出JMeter格式数据,然后可以导入 JMeter 做性能测试。
<a name="PneuZ"></a>
## 对比测试 (todo)
相同的接口和参数在两个不同环境下运行,对比其返回的数据差异。
> **注意**
> 该功能还未上线,敬请期待...
<a name="cIJVi"></a>
### 使用场景
后端系统重构(或架构升级)时,对比新代码环境和旧代码环境接口返回的数据是否完全一致,用以确保系统重构没有没有带来接口问题。
---
<a name="X4scQ"></a>
# 持续集成
Apifox 的测试用例和测试套件支持导出Apifox CLI、Postman、Jmeter 格式数据做持续集成。
<a name="rL0Kb"></a>
## 一、Apifox CLI 方式
Apifox CLI 是 [Apifox](https://www.apifox.cn/) 的命令行运行工具,主要用来做持续集成。 Apifox 支持实时运行在线数据和导出数据运行2 种方式。
> **注意**
> - Apifox 版本号大于等于 1.0.25 才支持导出Apifox CLI格式数据。
> - Apifox 版本号大于等于 1.4.3 才支持直接实时运行在线数据。
<a name="VC21m"></a>
### 安装 Apifox CLI
使用以下命令安装 Apifox CLI
```latex
npm install -g apifox-cli
实时运行在线数据
在 Apifox 的测试用例和测试套件选择持续集成,生成如下命令
点击即复制命令,运行即可
apifox run http://xxx/api/v1/api-test/ci-config/xxxx/detail?token=xxxx -r html,cli
导出数据运行
在 Apifox 的测试用例和测试套件导出Apifox CLI格式数据
运行以下命令
apifox run examples/sample.apifox-cli.json -r cli,html
测试报告
运行完成后测试报告会保存在当前目录下的apifox-reports目录里。
查看Apifox CLI 使用说明。
二、Newman 方式(Postman)
使用参考教程:Web API 持续集成:PostMan+Newman+Jenkins(图文讲解)
三、JMeter 方式
导出 JMeter 数据主要用来做性能测试,不过也可以做持续集成,参考教程:性能测试与持续集成(JMeter+Jenkins)
注意 由于 JMeter 不支持 JS 脚本,所以 Apifox 导出 JMeter 数据不包含前置/后置脚本。 后续 Apifox 会将接口返回数据提取和断言功能做成界面配置,这样就可以做到导出数据和 JMeter 兼容了。
Apifox CLI
Apifox CLI 命令行运行
Apifox CLI 主要用来以命令行方式运行 Apifox 的 测试用例或测试套件。
开始
Apifox CLI 依赖于 Node.js >= v10。使用前请先安装 Node.js.
安装
使用以下命令安装 Apifox CLI
$ npm install -g apifox-cli
实时运行在线数据
在 Apifox 的测试用例和测试套件选择持续集成,生成如下命令
点击即复制命令,运行即可
apifox run http://xxx/api/v1/api-test/ci-config/xxxx/detail?token=xxxx -r html,cli
运行测试用例或测试套件
$ apifox run examples/sample.apifox-cli.json -r cli,html,json
运行完成后测试报告会保存在当前目录下的 apifox-reports 目录里。
- 如果想要自定义报告,可以通过 json 文件的结果集来定制自己想要的报告
结合 Jenkins 做持续集成
Jenkins 配置 NodeJS 环境
打开 Jenkins,安装 NodeJS 插件 在全局工具配置中设置 NodeJS 路径 任务 build 环境中设置 node Jenkins 新建一个任务,添加构建步骤- Excute shell,将 ApifoxCli 的命令拷贝进去,保存并运行即可。Jenkins 报告展示
在命令中指定生成报告名 ${JOBNAME}${BUILDNUMBER}( Jenkins 内置变量),结合HTML Publisher 插件方便展示报告
apifox run https://api.apifox.cn/api/v1/api-test/ci-config/XXX/detail?token=xxxxx -r html,cli —out-file ${JOB_NAME}${BUILD_NUMBER}
CLI 如何实现文件参数传递
- 首先要回到 接口文档-修改文档 的请求参数处,通过批量编辑,把上传文件的路径改为变量。
- -h, —help
使用帮助 -
apifox run
[options] file-source 为从 Apifox 导出的测试用例或测试套件数据文件存放路径。
更多选项:-r, --reporters [reporters] 指定测试报告类型, 支持 cli,html,json (default: ["cli"]) -n, --iteration-count
设置循环次数 -d, --iteration-data 设置用例循环额数据 (JSON 或 CSV) --external-program-path 指定 [外部程序] 的所处文件路径,默认值为命令当前执行目录 --out-dir 输出测试报告目录,默认为当前目录下的 ./apifox-reports --out-file 输出测试报告文件名,不需要添加后缀,默认格式为 apifox-report-{当前时间戳}-0 --database-connection 指定 [数据库配置] 的所处文件路径,使用 URL 测试的时候必须指定 --ignore-redirects 阻止 Apifox 自动重定向返回 3XX 状态码的请求 --silent 阻止 Apifox CLI 输出到控制台 --color 开启/关闭控制台彩色输出 (auto|on|off) (default: "auto") --delay-request [n] 指定请求之间停顿间隔 (default: 0) --timeout-request [n] 指定接口请求超时时间 (default: 0) --timeout-script [n] 指定脚本预执行/后执行接口运行超时时间 (default: 0) -k, --insecure 关闭 SSL 校验 --ssl-client-cert-list 指定客户端证书配置路径 (JSON) --ssl-client-cert 指定客户端证书路径 (PEM) --ssl-client-key 指定客户端证书私钥路径 --ssl-client-passphrase 指定客户端证书密码 (for protected key) --ssl-extra-ca-certs 指定额外受信任的 CA 证书 (PEM) --verbose 显示所有接口请求的详细信息 -h, --help display help for command SSL
客户端证书
使用单个 SSL 客户端证书
—ssl-client-cert
公共客户端证书文件的路径- —ssl-client-key
私有客户端密钥的路径(可选) —ssl-client-passphrase
用于保护私有客户端密钥的密码(可选)使用 SSL 客户端证书 配置文件(支持多个证书)
—ssl-client-cert-list
SSL 客户端证书列表配置文件的路径(JSON 格式)。示例如下 ssl-client-cert-list.json ```json ssl-client-cert-list.json
[ { “name”: “domain1”, “matches”: [“https://test.domain1.com/*“, “https://www.domain1/*“], “key”: {“src”: “./client.domain1.key”}, “cert”: {“src”: “./client.domain1.crt”}, “passphrase”: “changeme” }, { “name”: “domain2”, “matches”: [“https://domain2.com/*“], “key”: {“src”: “./client.domain2.key”}, “cert”: {“src”: “./client.domain2.crt”}, “passphrase”: “changeme” } ]
此选项允许根据 URL 或主机名设置不同的 SSL 客户端证书。 此选项优先于 --ssl-client-cert, --ssl-client-key 和 --ssl-client-passphrase 选项。如果列表中的 URL 没有匹配项,这些选项将用作后备选项。
<a name="fa5rD"></a>
### 升级版本
使用以下命令升级 Apifox CLI<br />$ npm install apifox-cli@latest -g
---
<a name="XHOZY"></a>
# Apifox API
<a name="Nampr"></a>
## Apifox 开放 API (todo)
开发者可通过开放 API 读取、修改自己 [Apifox](https://www.apifox.cn/) 账号下的数据。
> **注意**
> 该功能还未上线,敬请期待...
---
<a name="gg9ax"></a>
# Web 版
<a name="px8aZ"></a>
## 浏览器扩展
Apifox 浏览器扩展用于 Web 版接口调试,须使用 Chrome 浏览器,暂只能使用本地安装的方式。
> **下载地址**
> [下载地址](https://apifox-generic.pkg.coding.net/apifox/apifox-desktop/Apifox-browser-extension.zip)
<a name="oWAlz"></a>
### 安装方法
1. 扩展 zip 后解压
1. 浏览器打开 chrome://extensions
> 必须开启开发者模式
![image.png](https://cdn.nlark.com/yuque/0/2022/png/26803611/1653200583068-45dd81c0-dad8-4a92-b20a-c98d97f41110.png#clientId=u22741251-f7dd-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=u2d584693&margin=%5Bobject%20Object%5D&name=image.png&originHeight=884&originWidth=1000&originalType=url&ratio=1&rotation=0&showTitle=false&size=127415&status=done&style=none&taskId=ubcebde39-a14e-4974-a2db-7b0563f4123&title=)
3. 然后点击加载已解压的扩展程序,加载后需要刷新访问中的 Apifox Web 网页,Agent 才能生效
![image.png](https://cdn.nlark.com/yuque/0/2022/png/26803611/1653200583393-b65a17dd-3f9c-4cb2-b2de-38c4f8e3dc9a.png#clientId=u22741251-f7dd-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=ue82b9b8e&margin=%5Bobject%20Object%5D&name=image.png&originHeight=884&originWidth=1000&originalType=url&ratio=1&rotation=0&showTitle=false&size=118349&status=done&style=none&taskId=uca0f7e49-84f3-44a9-838b-72644d149be&title=)
4. 部分浏览器版本,需要手动修改赋予权限,否则会请求失败
![image.png](https://cdn.nlark.com/yuque/0/2022/png/26803611/1653200583391-cad385b7-090c-41dc-87b7-6cd37e6a3d7e.png#clientId=u22741251-f7dd-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=uef893023&margin=%5Bobject%20Object%5D&name=image.png&originHeight=1888&originWidth=3024&originalType=url&ratio=1&rotation=0&showTitle=false&size=305725&status=done&style=none&taskId=u021e7993-02f6-499e-a610-a45cd41c47e&title=)
<a name="rZAw2"></a>
### FAQ
1. 为什么我安装不成功?
检测是否在 chrome://extensions 开启了开发者模式。
2. Web 版调试不支持调用数据库、执行本地代码?
这是浏览器限制,目前版本若需要则建议使用 [桌面版](https://apifox.cn/)。
3. Web 版调试不支持 get、head 方法请求中带 body ?
这是浏览器限制,目前版本调试发起请求会去除 Get、Head 方法请求的 body,若需要调试此类型接口建议使用 [桌面版](https://apifox.cn/)。
---
<a name="sNA3t"></a>
# 插件
<a name="lGvp3"></a>
## 插件安装、开发 (todo)
> **注意**
> 该功能还未上线,敬请期待...
<a name="S4cIJ"></a>
## 数据导入插件 (todo)
> **注意**
> 该功能还未上线,敬请期待...
<a name="fcx20"></a>
## 数据导出插件 (todo)
> **注意**
> 该功能还未上线,敬请期待...
---
<a name="s7ipt"></a>
# 更多功能
<a name="TAcFB"></a>
## 快捷键
| 功能 | Windows / Linux | macOS |
| --- | --- | --- |
| 新建接口 | **Ctrl + N** | **⌘ + N** |
| 新建快捷调试 | **Ctrl + T** | **⌘ + T** |
| 保存接口 / 保存用例 | **Ctrl + S** | **⌘ + S** |
| 发送请求 | **Ctrl + Enter** | **⌘ + Enter** |
| 切换到【运行】Tab | **Ctrl + Enter** | **⌘ + Enter** |
| 关闭 Tab | **Ctrl + W** | **⌘ + W** |
| 强制关闭 Tab | **Ctrl + Alt + W** | **⌘ + Option + W** |
| 切换到下一个 Tab | **Ctrl + Tab** 或 Ctrl + PageDown | **⌘ + Option + 向右箭头键** 或 ⌘ + Shift + ] |
| 切换到上一个 Tab | **Ctrl + Shift + Tab** 或 Ctrl + PageUp | **⌘ + Option + 向左箭头键** 或 ⌘ + Shift + [ |
| 跳转到特定标签页 | **Ctrl + 1** 到 Ctrl + 8 | **⌘ + 1** 到 ⌘ + 8 |
| 跳转到最后一个标签页 | **Ctrl + 9** | **⌘ + 9** |
| 导入数据 | **Ctrl + O** | **⌘ + O** |
| 导入抓包数据 (cURL) | **Ctrl + I** | **⌘ + I** |
| 查找接口 | **Ctrl + F** | **Ctrl + F** |
<a name="gQ51w"></a>
## 私有化部署
关于 [Apifox](https://www.apifox.cn/) 私有化部署方案及价格,请扫二维码加我们工作人员微信咨询,加微信请备注:私有化+公司名<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/26803611/1653200754139-026962cd-a8ad-498e-9519-3122dda36a5e.png#clientId=uf6587ae1-3da2-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=449&id=u7fef1d82&margin=%5Bobject%20Object%5D&name=image.png&originHeight=1968&originWidth=1500&originalType=url&ratio=1&rotation=0&showTitle=false&size=989795&status=done&style=none&taskId=ue9a69840-f7cd-4079-a02d-7ca7d253c43&title=&width=342)
<a name="aAyTc"></a>
### 申请攻略
需要 [Apifox](https://www.apifox.cn/) 功能介绍 PPT 版本的(可用于团队内部分享/推广 Apifox)
---
<a name="IELJc"></a>
# 参考资料
<a name="reKn2"></a>
## Apifox Swagger 扩展
<a name="Px5EM"></a>
### 一、指定某个接口所属目录:x-apifox-folder
1. 多级目录使用斜杠/分隔。其中\和/为特殊字符,需要转义,\/表示字符/,\\表示字符\。
```json
"paths": {
"/pets": {
"post": {
...
"operationId": "addPet",
"x-apifox-folder": "宠物店/宠物信息"
}
}
}
Swagger 注解示例:
@Operation(extensions = {
@Extension(properties = {
@ExtensionProperty(name = "apifox-folder", value = "宠物店/宠物信息")})
})
public Response createPet() {...}
二、接口状态:x-apifox-status
状态 | 英文 |
---|---|
设计中 | designing |
待确定 | pending |
开发中 | developing |
联调中 | integrating |
测试中 | testing |
已测完 | tested |
已发布 | released |
已废弃 | deprecated |
有异常 | exception |
"paths": {
"/pets": {
"post": {
...
"operationId": "addPet",
"x-apifox-status": "released"
}
}
}
Swagger 注解示例:
@Operation(extensions = {
@Extension(properties = {
@ExtensionProperty(name = "apifox-status", value = "released")})
})
public Response createPet() {...}
JSON Schema 介绍
JSON Schema 规范参考文档:JSON Schema 规范中文版
一、如何描述 JSON ?
JSON (JavaScript Object Notation) 缩写,JSON 是一种数据格式,具有简洁、可读性高、支持广泛的特点。JSON 有以下基本数据类型
// # 1. object
{ "key1": "value1", "key2": "value2" }
// # 2. array
[ "first", "second", "third" ]
// # 3. number
42
// # 4. string
"This is a string"
// # 5. boolean
true
false
// # 6. null
null
在其它语言中也有类似的内建数据类型,但是由于 JavaScript的广泛应用,而 JSON 作 为 JavaScript原生的数据类型,具备更加广泛的支持。
有了上面列举的基本数据类型,JSON 能非常灵活的表示任意复杂的数据结构。举个例子:
{
"name": "George Washington",
"birthday": "February 22, 1732",
"address": "Mount Vernon, Virginia, United States"
}
如何描述上面 JSON 对象呢?
首先,它是一个 object;
其次,它拥有 name、birthday、address 这三个字段
并且,name 、address 的字段值是一个字符串 String,birthday 的值是一个日期。
最后,将上面的信息如何用 JSON 来表示?如下:
{
"type": "object",
"properties": {
"name": { "type": "string" },
"birthday": { "type": "string", "format": "date" },
"address": { "type": "string" }
}
}
这个表示就是一个 JSON Schema ,JSON Schema 用于描述 JSON 数据。
相同的数据,可能有不同的表示,比如下面的两种表示,包含的信息量基本是一致的:
// # 1. 表示一
{
"name": "George Washington",
"birthday": "February 22, 1732",
"address": "Mount Vernon, Virginia, United States"
}
// # 2. 表示二
{
"first_name": "George",
"last_name": "Washington",
"birthday": "1732-02-22",
"address": {
"street_address": "3200 Mount Vernon Memorial Highway",
"city": "Mount Vernon",
"state": "Virginia",
"country": "United States"
}
}
在特定的应用场景中,应用程序对数据的结构要求是确定的,出于对数据描述的规范化需 求,需要用 JSON Schema 来规范化。使用 JSON Schema 可以描述 JSON 数据所包含的字 段、以及字段值的类型,以及依赖关系等。
相同信息量的数据,采用不同的形式来表达,用 JSON Schema 来描述也是不一样的,表示二的 JSON Schema 如下:
{
"type": "object",
"properties": {
"first_name": { "type": "string" },
"last_name": { "type": "string" },
"birthday": { "type": "string", "format": "date-time" },
"address": {
"type": "object",
"properties": {
"street_address": { "type": "string" },
"city": { "type": "string" },
"state": { "type": "string" },
"country": { "type" : "string" }
}
}
}
}
从上面的描述,可以很自然的想到 JSON Schema 可以用来做数据校验,比如前后端先把数 据接口约定好,写好 JSON Schema,等后端把接口输出完毕,直接用 JSON Schema 来对接 口做验收。
关于 JSON Schema 的应用,对 JSON Schema 有过了解的人可以直接跳到第三、四部分。
接下来对 JSON Schema 做一些举例说明。
二、JSON Schema 的举例
1. 空 schema
{}
以下都是合法的 JSON
42
"I'm a string"
[{"an": "aaa","bbb":{"nest":"data"}}]
2. type 指定 JSON 数据类型
{ "type": "string" }
"I'm a string"
42
{ "type": "number" }
42
"42"
type` 的可能取值: `string` 、`number` 、`object`、 `array`、 `boolean`、 `null`
3. type 可以包含多个类型
{ "type": ["number", "string"] }
"I'm a string" // 合法
42 // 合法
["Life", "the universe", "and everything"] // 不合法
4. string 限定长度
{
"type": "string",
"minLength": 2,
"maxLength": 3
}
"AA" // 合法
"AAA" // 合法
"A" // 不合法
"AAAA" // 不合法
5. string 模式匹配
{
"type": "string",
"pattern": "^(\\([0-9]{3}\\))?[0-9]{3}-[0-9]{4}$"
}
"555-1212" // ok
"(888)555-1212" // ok
"(888)555-1212 ext. 532" // not ok
"(800)FLOWERS" // not ok
6. string 值的枚举
{
"type": "string",
"enum": ["red", "amber", "green"]
}
"red" // ok
"blue" // not ok: blue 没有在 enum 枚举项中
7. integer
integer 一定是整数类型的 number
{ "type": "integer" }
42 // ok
1024 // ok
8. multipleOf 数字倍数
{ "type": "number", "multipleOf": 2.0 }
42 // ok
21 // not ok
9. number 限定范围
{
"type": "number",
"minimum": 0,
"maximum": 100,
"exclusiveMaximum": true
}
exclusiveMaximum 为 true 表示不包含边界值 maximum,类似的还有 exclusiveMinimum 字段.
10. object 不允许有额外的字段
{
"type": "object",
"properties": {
"number": { "type": "number" },
"street_name": { "type": "string" },
"street_type": {
"type": "string",
"enum": ["Street", "Avenue", "Boulevard"]
}
},
"additionalProperties": false
}
{ "number": 1600, "street_name": "Pennsylvania", "street_type": "Avenue" } // ok
{ "number": 1600, "street_name": "Pennsylvania", "street_type": "Avenue","direction": "NW" } // not ok
因为包含了额外的字段 direction,而 schema 规定了不允许额外的字段 “additionalProperties”: false
11. object 允许有额外的字段,并限定类型
{
"type": "object",
"properties": {
"number": { "type": "number" },
"street_name": { "type": "string" },
"street_type": {
"type": "string",
"enum": ["Street", "Avenue", "Boulevard"]
}
},
"additionalProperties": { "type": "string" }
}
{ "number": 1600, "street_name": "Pennsylvania", "street_type": "Avenue","direction": "NW" } // ok
{ "number": 1600, "street_name": "Pennsylvania", "street_type": "Avenue", "office_number": 201 } // not ok
额外字段 `"office_number": 201` 是 number 类型,不符合 schema
12. object 必填字段
{
"type": "object",
"properties": {
"name": { "type": "string" },
"email": { "type": "string" },
"address": { "type": "string" },
"telephone": { "type": "string" }
},
"required": ["name", "email"]
}
// ok
{
"name": "William Shakespeare",
"email": "bill@stratford-upon-avon.co.uk"
}
多出字段也是 ok 的
// ok
{
"name": "William Shakespeare",
"email": "bill@stratford-upon-avon.co.uk",
"address": "Henley Street, Stratford-upon-Avon, Warwickshire, England",
"authorship": "in question"
}
少了字段,就是不行
// not ok
{
"name": "William Shakespeare",
"address": "Henley Street, Stratford-upon-Avon, Warwickshire, England",
}
13. object 指定属性个数
{
"type": "object",
"minProperties": 2,
"maxProperties": 3
}
{ "a": 0, "b": 1 } // ok
{ "a": 0, "b": 1, "c": 2, "d": 3 } // not ok
14. Dependencies 依赖
略复杂,不提供示例
15. Object 属性的模式匹配
{
"type": "object",
"patternProperties": {
"^S_": { "type": "string" },
"^I_": { "type": "integer" }
},
"additionalProperties": false
}
{ "S_25": "This is a string" } // ok
{ "I_0": 42 } // ok
// not ok
{ "I_42": "This is a string" }
{ "keyword": "value" }
16. array 数组
// ok
{ "type": "array" }
[1, 2, 3, 4, 5]
[3, "different", { "types" : "of values" }]
// not ok:
{"Not": "an array"}
17. array 指定数组成员类型
{
"type": "array",
"items": {
"type": "number"
}
}
[1, 2, 3, 4, 5] // ok
[1, 2, "3", 4, 5] // not ok
18. array 指定数组成员类型,逐个指定
{
"type": "array",
"items": [{
"type": "number"
},{
"type": "string"
},{
"type": "string",
"enum": ["Street", "Avenue", "Boulevard"]
},{
"type": "string",
"enum": ["NW", "NE", "SW", "SE"]
}]
}
// ok
[1600, "Pennsylvania", "Avenue", "NW"]
[10, "Downing", "Street"] // 缺失一个也是可以的
[1600, "Pennsylvania", "Avenue", "NW", "Washington"] // 多出一个也是可以的
// not ok
[24, "Sussex", "Drive"]
["Palais de l'Élysée"]
19. array 指定数组成员类型,逐个指定,严格限定
{
"type": "array",
"items": [{
"type": "number"
},
{
"type": "string"
},
{
"type": "string",
"enum": ["Street", "Avenue", "Boulevard"]
},
{
"type": "string",
"enum": ["NW", "NE", "SW", "SE"]
}
],
"additionalItems": false
}
[1600, "Pennsylvania", "Avenue", "NW"] // ok
[1600, "Pennsylvania", "Avenue"] // ok
[1600, "Pennsylvania", "Avenue", "NW", "Washington"] // not ok 多出了字段就是不行
20. array 数组长度限制
{
"type": "array",
"minItems": 2,
"maxItems": 3
}
[1, 2] // ok
[1, 2, 3, 4] // not ok
21. array element uniqueness 数组元素的唯一性
{
"type": "array",
"uniqueItems": true
}
[1, 2, 3, 4, 5] // ok
[1, 2, 3, 3, 4] // not ok:出现了重复的元素 3
22. boolean
{ "type": "boolean" }
true // ok
0 // not ok
23. null
{ "type": "null" }
null // ok
"" // not ok
24. schema 的合并
string 类型,最大长度为 5 ;或 number 类型,最小值为 0
{
"anyOf": [
{ "type": "string", "maxLength": 5 },
{ "type": "number", "minimum": 0 }
]
}
`anyOf` 包含了两条规则,符合任意一条即可
"short" // ok
42 // ok
"too long" // not ok 长度超过 5
-5 // not ok 小于了 0
25. allOf、oneOf
`anyOf` 是满足任意一个 Schema 即可,而 `allOf` 是要满足所有 Schema
`oneOf` 是满足且只满足一个
26. oneOf
{
"oneOf": [
{ "type": "number", "multipleOf": 5 },
{ "type": "number", "multipleOf": 3 }
]
}
10 // ok
15 // not ok 因为它既是 3 又是 5 的倍数
上面的 schema 也可以写为:
{
"type": "number",
"oneOf": [
{ "multipleOf": 5 },
{ "multipleOf": 3 }
]
}
27. not
{ "not": { "type": "string" } }
只要是非 string 类型即可
42 // ok
{"key" : "value"} // ok
"This is a string" // not ok
JSON Path 介绍
JSONPath 之于 JSON,就如 XPath 之于 XML。JSONPath 可以方便对 JSON 数据结构进行内容提取。
概览
- 根对象使用$来表示,而无需区分是对象还是数组。
- 表达式可以使用.,也可以使用[]。如:$.store.book[0].title 或 $[‘store’][‘book’][0][‘title’]
- 表达式(
)可用作显式名称或索引的替代,如:$.store.book[(@.length-1)].title 表示获取最后一个 book 的 title。 - 使用符号@表示当前对象。过滤器表达式通过语法支持,?(
)如:$.store.book[?(@.price < 10)].title 表示获取价格小于 10 的所有 book 的 title。 语法
要点:
- $ 表示文档的根元素
- @ 表示文档的当前元素
- .node_name 或 [‘node_name’] 匹配下级节点
- [index] 检索数组中的元素
- [start:end:step] 支持数组切片语法
- 作为通配符,匹配所有成员
- .. 子递归通配符,匹配成员的所有子元素
- (
) 使用表达式 - ?(
)进行数据筛选
JSONPath 语法和 XPath 对比:
XPath | JsonPath | 说明 |
---|---|---|
/ | $ | 文档根元素 |
. | @ | 当前元素 |
/ | .或[] | 匹配下级元素 |
.. | N/A | 匹配上级元素,JsonPath不支持此操作符 |
// | .. | 递归匹配所有子元素 |
* | * | 通配符,匹配下级元素 |
@ | N/A | 匹配属性,JsonPath不支持此操作符 |
[] | [] | 下标运算符,根据索引获取元素,XPath索引从1开始,JsonPath索引从0开始 |
| | [,] | 连接操作符,将多个结果拼接成数组返回,可以使用索引或别名 |
N/A | [start:end:step] | 数据切片操作,XPath不支持 |
[] | ?() | 过滤表达式 |
N/A | () | 脚本表达式,使用底层脚本引擎,XPath不支持 |
() | N/A | 分组,JsonPath不支持 |
注意:
- JsonPath 的索引从0开始计数
- JsonPath 中字符串使用单引号表示,例如:$.store.book[?(@.category==’reference’)]中的’reference’
JsonPath示例
下面是相应的JsonPath的示例,代码来源于https://goessner.net/articles/JsonPath/,JSON文档如下:
接下来我们看一下如何对这个文档进行解析:{
"store": {
"book": [{
"category": "reference",
"author": "Nigel Rees",
"title": "Sayings of the Century",
"price": 8.95
}, {
"category": "fiction",
"author": "Evelyn Waugh",
"title": "Sword of Honour",
"price": 12.99
}, {
"category": "fiction",
"author": "Herman Melville",
"title": "Moby Dick",
"isbn": "0-553-21311-3",
"price": 8.99
}, {
"category": "fiction",
"author": "J. R. R. Tolkien",
"title": "The Lord of the Rings",
"isbn": "0-395-19395-8",
"price": 22.99
}
],
"bicycle": {
"color": "red",
"price": 19.95
}
}
}
XPath | JsonPath | Result |
---|---|---|
/store/book/author | $.store.book[*].author | 所有book的author节点 |
//author | $..author | 所有author节点 |
/store/* | $.store.* | store下的所有节点,book数组和bicycle节点 |
/store//price | $.store..price | store下的所有price节点 |
//book[3] | $..book[2] | 匹配第3个book节点 |
//book[last()] | $..book[(@.length-1)],或 $..book[-1:] | 匹配倒数第1个book节点 |
//book[position()<3] | $..book[0,1],或 $..book[:2] | 匹配前两个book节点 |
//book[isbn] | $..book[?(@.isbn)] | 过滤含isbn字段的节点 |
//book[price<10] | $..book[?(@.price<10)] | 过滤price<10的节点 |
//* | $..* | 递归匹配所有子节点 |
你可以在 http://jsonpath.com/ 站点进行验证JsonPath的执行效果。
参考文档:
- https://goessner.net/articles/JsonPath/
- https://www.newtonsoft.com/json/help/html/SelectToken.htm
XPath 介绍
XPath 使用路径表达式来选取 XML 文档中的节点或节点集。节点是通过沿着路径 (path) 或者步 (steps) 来选取的。XML 实例文档
我们将在下面的例子中使用这个 XML 文档。实例
<?xml version="1.0" encoding="UTF-8"?>
<bookstore>
<book>
<title lang="eng">Harry Potter</title>
<price>29.99</price>
</book>
<book>
<title lang="eng">Learning XML</title>
<price>39.95</price>
</book>
</bookstore>
选取节点
XPath 使用路径表达式在 XML 文档中选取节点。节点是通过沿着路径或者 step 来选取的。 下面列出了最有用的路径表达式:
表达式 | 描述 |
---|---|
nodename | 选取此节点的所有子节点。 |
/ | 从根节点选取。 |
// | 从匹配选择的当前节点选择文档中的节点,而不考虑它们的位置。 |
. | 选取当前节点。 |
.. | 选取当前节点的父节点。 |
@ | 选取属性。 |
在下面的表格中,我们已列出了一些路径表达式以及表达式的结果:
路径表达式 | 结果 |
---|---|
bookstore | 选取 bookstore 元素的所有子节点。 |
/bookstore | 选取根元素 bookstore。注释:假如路径起始于正斜杠( / ),则此路径始终代表到某元素的绝对路径! |
bookstore/book | 选取属于 bookstore 的子元素的所有 book 元素。 |
//book | 选取所有 book 子元素,而不管它们在文档中的位置。 |
bookstore//book | 选择属于 bookstore 元素的后代的所有 book 元素,而不管它们位于 bookstore 之下的什么位置。 |
//@lang | 选取名为 lang 的所有属性。 |
谓语(Predicates)
谓语用来查找某个特定的节点或者包含某个指定的值的节点。
谓语被嵌在方括号中。
在下面的表格中,我们列出了带有谓语的一些路径表达式,以及表达式的结果:
路径表达式 | 结果 |
---|---|
/bookstore/book[1] | 选取属于 bookstore 子元素的第一个 book 元素。 |
/bookstore/book[last()] | 选取属于 bookstore 子元素的最后一个 book 元素。 |
/bookstore/book[last()-1] | 选取属于 bookstore 子元素的倒数第二个 book 元素。 |
/bookstore/book[position()❤️] | 选取最前面的两个属于 bookstore 元素的子元素的 book 元素。 |
//title[@lang] | 选取所有拥有名为 lang 的属性的 title 元素。 |
//title[@lang=’eng’] | 选取所有 title 元素,且这些元素拥有值为 eng 的 lang 属性。 |
/bookstore/book[price>35.00] | 选取 bookstore 元素的所有 book 元素,且其中的 price 元素的值须大于 35.00。 |
/bookstore/book[price>35.00]//title | 选取 bookstore 元素中的 book 元素的所有 title 元素,且其中的 price 元素的值须大于 35.00。 |
选取未知节点
XPath 通配符可用来选取未知的 XML 元素。
通配符 | 描述 |
---|---|
* | 匹配任何元素节点。 |
@* | 匹配任何属性节点。 |
node() | 匹配任何类型的节点。 |
在下面的表格中,我们列出了一些路径表达式,以及这些表达式的结果:
路径表达式 | 结果 |
---|---|
/bookstore/* | 选取 bookstore 元素的所有子元素。 |
//* | 选取文档中的所有元素。 |
//title[@*] | 选取所有带有属性的 title 元素。 |
选取若干路径
通过在路径表达式中使用”|”运算符,您可以选取若干个路径。
在下面的表格中,我们列出了一些路径表达式,以及这些表达式的结果:
路径表达式 | 结果 |
---|---|
//book/title | //book/price | 选取 book 元素的所有 title 和 price 元素。 |
//title | //price | 选取文档中的所有 title 和 price 元素。 |
/bookstore/book/title | //price | 选取属于 bookstore 元素的 book 元素的所有 title 元素,以及文档中所有的 price 元素。 |
正则表达式
参考文档:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Regular_Expressions
CSV 格式规范
下面的格式规范定义来源于 RFC 4180,一共七点。
- 文件中的各条记录必须位于不同行,其间以换行符 CRLF 分隔。例如: ```latex aaa,bbb,ccc zzz,yyy,xxx
2. 最后一条记录的末尾可以不包括换行符。例如:
```latex
aaa,bbb,ccc
zzz,yyy,xxx
文件中的首条记录可以是字段名(但这不是必要的),且其所含的名称数量及存储规则须与其他记录保持一致。
field_name,field_name,field_name aaa,bbb,ccc zzz,yyy,xxx
每条记录中可以包含一个或多个字段,每个字段以半角逗号分隔。文件中的所有记录必须拥有相同数量的字段。字段中的空格属于字段取值,不可忽略。每条记录的最后一个字段之后不应再添加半角逗号。例如:
aaa,bbb,ccc
每个字段可以用半角双引号括起来(但这不一定是必要的)。如果字段没有被双引号括起来,那么字段中不应该出现双引号。例如:
"aaa","bbb","ccc" zzz,yyy,xxx
含有换行符、半角双引号或半角逗号的字段应该用半角双引号括起来。例如:
"aaa","b bb","ccc" zzz,yyy,xxx
如果字段被半角双引号括起来了,那么在表示字段取值中本身含有的半角双引号时,需要在其前方增加一个半角双引号。例如:
"aaa","b""bb","ccc"
Socket 粘包和分包问题
概念
Socket通信时会对发送的字节数据进行分包和粘包处理,属于一种Socket内部的优化机制。 粘包: 当发送的字节数据包比较小且频繁发送时,Socket内部会将字节数据进行粘包处理,既将频繁发送的小字节数据打包成 一个整包进行发送,降低内存的消耗。 分包: 当发送的字节数据包比较大时,Socket内部会将发送的字节数据进行分包处理,降低内存和性能的消耗。
例子解释
当前发送方发送了两个包,两个包的内容如下: 123456789 ABCDEFGH
我们希望接收方的情况是:收到两个包,第一个包为:123456789,第二个包为:ABCDEFGH。但是在粘包和分包出现的情况就达不到预期情况。
粘包情况
两个包在很短的时间间隔内发送,比如在0.1秒内发送了这两个包,如果包长度足够的话,那么接收方只会接收到一个包,如下:
123456789ABCDEFGH 1
分包情况
假设包的长度最长设置为5字节(较极端的假设,一般长度设置为1000到1500之间),那么在没有粘包的情况下,接收方就会收到4个包,如下:
12345 6789 ABCDE FGH 1234
处理方式
因为存在粘包和分包的情况,所以接收方需要对接收的数据进行一定的处理,主要解决的问题有两个:
在粘包产生时,要可以在同一个包内获取出多个包的内容。
- 在分包产生时,要保留上一个包的部分内容,与下一个包的部分内容组合。
一、给数据包的头尾加上标记。
比如在数据包的头部加上“START”字符串,尾部加上”END”字符串,这样可以解析出START和END之间的字符串就是接收方需要接收的内容。(当然真正处理的时候不可能使用START和END这种混效率较高的字符串,此处只是个例子) 上边两个包的例子就可以如下:
START123456789END STARTABCDEFGHEND 12
二、在数据包头部加上内容的长度
发送方在发送的时候就可以在包头加上包的长度,接收方每次接收的时候都根据头部的长度去获取后面的内容。 上边两个包的例子就可以如下:
PACKAGELENGTH:0009123456789 PACKAGELENGTH:0008ABCDEFGH 12
处理例子
头尾标记处理
粘包
START123456789ENDSTARTABCDEFGHEND 1
获取第一个START和第一个END的位置,然后获取他们之间的内容,第二个包的内容就是获取第二个START和第二个END的位置。
分包
START1234567 89END 12
每个包要判断最后是否是END结尾,如果没有找到END,那么就保留上一个包START之后的内容,与下一个包第一个END之前的内容组合。
头部长度处理
粘包
PACKAGELENGTH:0009123456789PACKAGELENGTH:0008ABCDEFGH 1
获取“PACKAGELENGTH:”这个字符串后面4个字符,转化为数字就是包的长度,根据包的长度获取后面的内容,第二个内容的长度就是获取第二个“PACKAGELENGTH:”字符串后面的4个字符。
分包
PACKAGELENGTH:0009123456 789 12
获取“PACKAGELENGTH:”这个字符串后面4个字符,转化为数字就是包的长度,如果包结尾还没有获取完,那么就要获取下一个包前面的部分内容。
部分细节情况
看了前面的例子,比较善于思考的读者肯定已经想到了一些其他问题,这些问题处理起来方式和上面相似,笔者在此罗列一下,就不重复解释了,相信聪明的读者能够自己解决:
1、粘包和分包问题一起出现
START123456789ENDSTARTAB CDEFGHEND 12
2、头尾标志由于分包获取不完整
START123456789E ND
安装 Java 环境
一、 Java 安装包下载地址
Windows 版(64位):https://repo.huaweicloud.com/java/jdk/8u192-b12/jdk-8u192-windows-x64.exe
Windows 版(32位):https://repo.huaweicloud.com/java/jdk/8u192-b12/jdk-8u192-windows-i586.exe
macOS 版:https://repo.huaweicloud.com/java/jdk/8u192-b12/jdk-8u192-macosx-x64.dmg
其他版本:https://repo.huaweicloud.com/java/jdk/8u192-b12/
注意: 上面推荐下载的是 JDK,其实大多数 Windows 电脑安装JRE也能运行,但是也有部分电脑(大多数 Mac 电脑)JRE下无法运行,所以保险起见推荐JDK。
检查 Java 环境是否安装成功:打开命令行,运行java -version,如能正确显示 java 版本号,则表示安装成功。
二、注意点
如果安装后,Apifox 还是没有检查到 java 环境,请尝试以下 2 个方法:
- 关掉 Apifox,过一会再重新打开。
- 如果上面方法没有解决,请重启电脑,一般重启电脑就既可正常使用。
其他
常见问题
1. Apifox 是否收费?
Apifox 公网版 (SaaS版) 免费,私有化部署版收费。
2. 登录(Auth)态如何实现?
请参考文档:登录态(Auth)如何处理
3. 接口发送请求前需要调用登录接口获取 token 放在 header,如何实现?
请参考文档:登录态(Auth)如何处理
4. B 接口请求参数依赖于 A 接口返回的数据,如何实现?
请参考文档:接口之间如何传递数据
5. 同项目下有不同域名的接口,如何处理?
方案一:在环境里新增多个服务,分别设置不同的前置 URL ,接口分组和接口维度可以指定对应的前置 URL。推荐本方案!
方案二:把域名设置成环境变量如DOMAIN_1,接口路径这样填写:https:///users。接口路径是以http://或https://起始的,系统会自动忽略里环境里前置 URL。
方案三:给不同域名接口设置不同环境,通过切换环境来运行不同域名下的接口。不推荐本方案!
6. 脚本如何读取或修改接口请求信息?
请参考文档:脚本读取/修改接口请求信息
7. 是否支持查询数据库字段作为参数传给接口?
支持,请参考文档:数据库操作
8. 数据是存储在本地还是云端?可否离线使用?可否私有化部署?
目前 Apifox 有 Saas 版 和私有化部署版 。
Saas 版 是免费的,数据都是存在云端的,需要联网才能使用。
私有化部署版 是收费的,数据存在使用者企业内部,不连外网也可以使用。
注意
环境变量/全局变量里的 本地值 仅存放在本地,不会同步到云端,团队成员之间也不会相互同步,适合存放token、账号、密码之类的敏感数据。
9. 使用 Postman 调用接口返回正常,而 Apifox 返回错误
解决方法:对比 postman 和 apifox 实际发出的请求内容(url、参数、body、header)是否完全一样。
查看实际请求内容方法:
- Apifox:返回内容下的实际请求 tab 里查看
-
10. 为什么修改了环境变量(或全局变量)值,而引用的地方没有生效?
请检查环境变量、全局变量、临时变量里是不是有多个地方定义了相同名称的变量,如果有,系统会根据优先级来取值。优先级顺序如下:临时变量>环境变量>全局变量。
请检查修改的是否是本地值,环境变量(或全局变量)仅读取本地值,而不会读取远程值。
后续功能规划
Apifox 后续功能规划主要如下:
支持接口性能测试(类似 JMeter)
- 支持插件市场,可以自己开发插件
- 支持更多接口协议,如GraphQL、websocket等
- 支持离线使用,项目可选择在线同步(团队协作)还是仅本地存储(单机离线使用)
- 提供私有化部署方案
更新日志
升级方法:Apifox 软件内“检查更新”,或从官网手动下载。Alpha 版说明 Alpha 版为新功能尝鲜版,需要加官方微信群、QQ 群或钉钉群,才能参与 Alpha 版内测。 加群方式:apifox.cn/help/app/contact-us
- Apifox 新功能都会先在 Alpha 版上线,等稳定后才会合到正式版。
- Alpha 版可能会有一些 bug,如遇到问题及时在群里反馈,我们会第一时间解决。
- Alpha 版和正式版数据是互通的。
- 已经是 Alpha 版的,直接点击软件内更新。