Pytest+Requests+Allure 接口自动化测试框架从零搭建实战
手动点接口测一遍几十个用例点完至少半小时中间改一下参数又得全部重来这在项目早期还能忍等接口一多就彻底失控。我自己就是从这种“纯手动点接口”的状态走过来的后来痛定思痛用 Pytest Requests Allure 搭了一套接口自动化框架把过去一个月的人工验证压缩到几分钟跑完。这篇文章就围绕这套框架把整个搭建过程和关键代码原原本本讲清楚。内容适合刚接触接口测试、想从手动转向自动化的同学也适合已经有了零散脚本、想整理成规范化框架的人。接下来说一下这套框架能做什么把常用的 GET、POST 请求封装好用 Pytest 管理所有用例支持参数化数据驱动接着用 Allure 生成带步骤、带请求响应详情、带断言结果的可视化测试报告。整套搭下来你会得到一个结构清晰、能直接跑通的自动化测试工程后续新增接口只需要照着已有用例模板加文件就行。1. 项目概述与整体思路1.1 为什么选择 Pytest Requests Allure 这个组合先说结论这仨不是唯一选择但对绝大多数接口测试场景来说是目前性价比最高、学习曲线最平滑的组合。Requests 库不用多讲Python 里处理 HTTP 请求的事实标准它的 API 设计非常贴近人的直觉requests.get()、requests.post()两行代码就能发一个请求。相比直接用 urllib省掉了大量编码、解码、拼接参数的琐碎操作。而且 requests 的Session对象可以自动管理 Cookie对需要登录态的接口测试特别友好。Pytest 则是目前 Python 生态里最主流的测试框架之一。它的断言直接用 Python 原生assert写起来很自然fixture 机制能解决大量用例前置、后置和依赖共享问题参数化功能pytest.mark.parametrize在接口测试里基本是刚需一个用例可以跑几十组数据。另外 Pytest 有丰富的插件生态什么重试、排序、超时控制都能通过插件解决。Allure 是报告层的利器。它不只是一个 HTML 报告生成器更是一套完整的测试报告框架能把测试步骤、请求参数、响应内容、失败截图等以非常清晰的层级展示出来。我见过不少团队用 Pytest 自带的 junit.xml 或者 HTML 插件生成报告但可读性和美观度都不如 Allure尤其是给领导汇报的时候一份 Allure 报告的说服力完全不一样。所以这个组合的分工很明确Requests 负责发出真实的 HTTP 请求Pytest 负责组织测试逻辑与生命周期Allure 负责把执行过程变成一份既专业又好看的可视化报告。三者各管一段互相之间耦合度很低替换任何一个都不影响另外两个。1.2 框架目录结构与模块划分一个容易复用的自动化框架最忌讳把所有代码堆在一个文件里。我习惯把项目按功能切成这几部分api_auto_test/ ├── config/ # 配置文件存放 │ └── config.yaml # 环境地址、超时时间、账号信息 ├── common/ # 公共封装 │ ├── __init__.py │ ├── request_client.py # requests 请求封装 │ ├── read_config.py # yaml 配置读取 │ └── log_util.py # 日志封装 ├── data/ # 测试数据 │ ├── test_login.yaml # 登录接口测试数据 │ └── test_user.yaml ├── testcases/ # 测试用例 │ ├── conftest.py # 全局 fixture 和钩子函数 │ ├── test_login.py │ └── test_user.py ├── report/ # 报告输出目录 ├── pytest.ini # pytest 配置文件 └── requirements.txt # 依赖包清单这个结构是我踩了不少坑后沉淀下来的。config、data、testcases三层分开目的是让测试数据和测试逻辑解耦。你想想如果数据直接写在用例里一旦环境地址变了或者测试账号换了就得逐个文件去改这显然不是工程化的做法。common目录里放公共封装核心是请求客户端后面会详细讲。conftest.py是 Pytest 的一个特殊文件里面定义的 fixture 可以自动被同目录及其子目录的测试用例引用非常适合放登录态、数据库清理这类全局逻辑。1.3 核心流程设计整条链路可以概括成读取配置 - 封装请求 - 准备用例数据 - 通过 fixture 完成前置操作 - 执行用例 - 断言校验 - 生成报告。这里有个容易被新手忽略的点接口自动化不止是“发请求看响应”更要考虑用例之间的依赖关系。比如很多业务接口需要登录后的 token那登录这个操作就不能放在每个用例里重复执行而应该抽出来放到 fixture 中并设置合理的 scope。我的做法是登录被定义成一个session级别的 fixture整个测试会话只登录一次然后把 token 保存到共享变量里后续接口通过请求封装自动带上这个 token。这套流程设计好之后新增一个接口测试用例的步骤就变得非常机械在testcases下新建对应业务的 py 文件从公共模块导入请求客户端在data下补充测试数据用参数化把数据传给用例然后在用例里写断言。这样即使是新入职的同事稍微看一下现有代码也能很快上手。2. 环境准备与基础选型2.1 Python 环境与依赖包安装在动手写代码之前先把环境搭好。我建议使用 Python 3.9 及以上版本太老的版本对类型注解和某些新语法支持不好容易在写框架时踩坑。创建虚拟环境是一个好习惯尤其当你手头有多个项目、各自依赖不同版本的包时。我一般在项目根目录下执行python -m venv venvWindows 环境下激活虚拟环境venv\Scripts\activatemacOS / Linux 环境下激活source venv/bin/activate然后安装依赖包pip install requests pytest allure-pytest pyyaml pytest-rerunfailures pytest-timeout这里列出的包都有一个具体用途requests发 HTTP 请求pytest测试框架本体allure-pytest是 Pytest 与 Allure 的适配器没有它生成的报告里不会有步骤和附件信息pyyaml用来读取 YAML 格式的配置和测试数据pytest-rerunfailures做失败重试解决网络不稳定导致的偶发失败pytest-timeout给用例设置超时防止某个接口卡死拖垮整个测试进程。把这些依赖写进requirements.txt之后其他人拉取代码后只需要执行pip install -r requirements.txt就能复现环境这也是工程化的一部分。2.2 项目初始化和 pytest.ini 配置项目目录建好后pytest 本身还需要一点配置。我会在pytest.ini里指定测试用例目录、报告参数和公共命令行参数。下面是我常用的配置你可以直接抄[pytest] testpaths testcases python_files test_*.py python_classes Test* python_functions test_* addopts -s -v --alluredirreport/allure-results --clean-alluredirtestpaths告诉 pytest 去哪个目录找用例python_files、python_classes、python_functions分别定义了测试文件名、测试类名、测试函数名的匹配规则addopts是默认追加的命令行参数--alluredirreport/allure-results是让 pytest 把 Allure 需要的原始结果写到指定目录--clean-alluredir保证每次执行前清空旧结果避免报告数据互相污染。为什么要单独搞一个pytest.ini因为如果没有它每次执行测试都得手动敲一长串命令行参数写错一个目录名就得重新跑。把公共参数固化到配置文件里团队所有成员执行方式就完全统一了。2.3 公共模块封装前的思考写公共模块前先想清楚两件事第一你的项目里有哪些操作会被反复用到第二哪些信息是各个用例都会依赖的。想清楚这两点再动手代码才有一个清晰的边界。我自己的实践经验是公共模块能少则少只放真正高频且通用性强的逻辑。比如请求封装、配置读取、日志输出这三块就值得抽公共。相反像“生成随机手机号”这种只在一个业务里用的工具函数就不要放进 common直接放对应业务模块里即可。日志模块也很重要。很多同学执行测试失败后只知道看报错但报错只是最后那一下。如果请求发出了但响应超时或者被服务端限流没有日志很难定位。我在log_util.py里封装了一个简单的日志工具统一格式和时间戳执行完测试后可以按时间查看完整请求流程排查问题会轻松很多。3. 核心代码实现与关键细节3.1 请求封装给 Requests 穿上外衣直接用 Requests 写接口测试本身没什么问题但为了后续统一处理鉴权、日志、超时和异常我习惯在它外面包一层。下面是我实际用过的request_client.py的一个简化版本import requests import logging import time from urllib.parse import urljoin logger logging.getLogger(__name__) class RequestClient: def __init__(self, base_url, timeout10): self.base_url base_url self.timeout timeout self.session requests.Session() def _request(self, method, url, **kwargs): # 保证超时存在防止请求一直挂起 kwargs.setdefault(timeout, self.timeout) full_url urljoin(self.base_url, url) logger.info(f请求地址: {full_url}) logger.info(f请求参数: {kwargs.get(params) or kwargs.get(json) or kwargs.get(data)}) start time.time() try: resp self.session.request(method, full_url, **kwargs) logger.info(f响应耗时: {round(time.time() - start, 3)}s, 状态码: {resp.status_code}) return resp except requests.exceptions.Timeout: logger.error(f请求超时: {full_url}) raise except requests.exceptions.RequestException as e: logger.error(f请求异常: {e}) raise def get(self, url, **kwargs): return self._request(GET, url, **kwargs) def post(self, url, **kwargs): return self._request(POST, url, **kwargs)这个封装做了几件看似不起眼但非常重要的事一是用Session对象管理连接Session 会自动保存 Cookie并且底层连接复用多接口连续调用时性能明显更好二是通过setdefault(timeout, self.timeout)给每一个请求都带上默认超时时间避免某个接口异常时用例一直卡着不结束三是统一记录请求和响应日志出了问题不用猜。在实际项目中你很可能需要在这个封装里继续追加功能比如自动加鉴权 token、统计某个接口的耗时、脱敏打印敏感字段等。我的建议是尽量把这类通用逻辑都放在_request里而不是让每个用例自己写。3.2 conftest.py 与 fixture登录态到底怎么处理很多接口测试用例不是独立的它们依赖登录后拿到的 token。如果把登录逻辑复制粘贴到每个用例里代码冗余不说每次执行都要重新登录几十次既慢又容易被拦截。正确的做法是把登录过程定义成一个 fixture。下面是一个简单的conftest.py示例import pytest from common.request_client import RequestClient pytest.fixture(scopesession) def client(): 整个测试会话共用的请求客户端 base_url https://api.example.com return RequestClient(base_url, timeout10) pytest.fixture(scopesession) def login_token(client): 登录接口获取 token整个会话只执行一次 resp client.post(/login, json{username: tester, password: 123456}) assert resp.status_code 200, f登录失败: {resp.text} token resp.json().get(data, {}).get(token) assert token, f响应中未找到 token: {resp.text} return token这里的scopesession很关键。它表示这个 fixture 在整次测试执行期间只执行一次后续用例共享返回值。如果写成默认的function那每个用例执行前都会登录一次既浪费又不合理。如果你需要在每个接口请求里自动带上 token可以在刚才的RequestClient里增加一个set_token方法把 token 写入 Session 的 headers 中类似def set_token(self, token): self.session.headers.update({Authorization: fBearer {token}})然后在其他用例里通过形参方式引入login_token这个 fixturepytest 会自动执行登录并传入 token。这个机制是 Pytest 最核心的竞争力之一理解它比背一堆断言方法重要得多。3.3 数据驱动用参数化代替复制粘贴接口测试中最常见的场景是同一个接口几十组不同的参数分别验证正常、边界和异常情况。如果不做数据驱动最粗暴的写法就是复制粘贴几十个函数每个函数改几个参数。这种代码维护成本极高新增一组数据就得复制一个用例。用pytest.mark.parametrize之后代码一下子清爽了import pytest from common.request_client import RequestClient pytest.mark.parametrize(username,password,expected_code,expected_msg, [ (tester, 123456, 200, success), (tester, wrong, 400, password error), (, 123456, 400, username cannot be empty), (tester, , 400, password cannot be empty), ]) def test_login(client, username, password, expected_code, expected_msg): resp client.post(/login, json{username: username, password: password}) body resp.json() assert resp.status_code 200 assert body.get(code) expected_code assert body.get(msg) expected_msg参数化之后pytest 会把这一个函数展开成 4 条测试用例用例 ID 会带上参数值方便定位是哪一组数据失败。你也可以给参数起一个名字用pytest.param设置 ID比如pytest.param(tester, 123456, 200, success, id正常登录)这样 Allure 报告里看到的用例名就是中文可读的非常友好。如果数据量更大比如几百组那就建议把测试数据放到外部文件里比如 YAML 或 JSON。我项目中常用的做法是用一个工具函数读取 YAML 文件并动态生成参数import yaml def load_test_data(path): with open(path, encodingutf-8) as f: data yaml.safe_load(f) return data然后在测试文件里这样用test_data load_test_data(../data/test_login.yaml) pytest.mark.parametrize(case, test_data[cases]) def test_login_by_data(client, case): resp client.post(/login, jsoncase[payload]) assert resp.status_code case[expected_status] assert resp.json().get(code) case[expected_code]这样做的好处显而易见测试数据修改不再需要动代码测试和业务人员也可以参与维护数据文件。3.4 断言设计不能只比状态码很多初学者写完请求之后只检查一下状态码是不是 200然后就结束了。这种做法在接口测试里远远不够。状态码 200 只能说明 HTTP 请求通了并不代表业务成功比如登录失败时接口同样可能返回 HTTP 200但 JSON 里的code字段是 400。我的断言习惯分三层第一层验证 HTTP 状态码确保网络层和网关没问题第二层验证业务状态码和提示信息确保业务处理符合预期第三层验证关键数据字段确保返回的数据结构和关键值正确。def assert_response(resp, expected_status200, expected_codeNone, expected_msgNone): assert resp.status_code expected_status, fHTTP状态码错误: {resp.status_code}, 响应: {resp.text} body resp.json() if expected_code is not None: assert body.get(code) expected_code, f业务code错误: {body} if expected_msg is not None: assert body.get(msg) expected_msg, f提示信息错误: {body}如果接口返回的是复杂嵌套结构我还会用到jsonschema这个库来校验响应结构是否符合约定。这比手工写一个个assert去判断字段缺失要可靠得多。用它可以保证“该有的字段一个不少字段类型也都正确”非常适合后端接口在迭代过程中频繁改结构的场景。另外说一个百试不爽的技巧无论断言怎么写都要把响应内容输出到日志里。这样即使某条用例断言失败你也能从日志中直接看到当时的完整响应而不是只知道一个“断言失败”的干巴巴结果。3.5 Allure 报告集成让你的测试报告会讲故事Allure 的接入分两步执行时生成原始结果再使用allure generate生成 HTML 报告。但光接入还不够如果你不在代码里加 Allure 装饰器和步骤最终报告只会是一张用例名和通过率的表格缺少请求响应信息。我在测试用例和公共封装里会加上这些内容import allure allure.step(发送登录请求用户名: {username}) def send_login_request(client, username, password): resp client.post(/login, json{username: username, password: password}) allure.attach(resp.text, 登录接口响应, allure.attachment_type.JSON) return resp allure.title(登录接口-正常登录) allure.description(使用正确的用户名和密码验证登录成功) def test_login_success(client): resp send_login_request(client, tester, 123456) assert_response(resp, expected_code200, expected_msgsuccess)allure.step可以以函数为单位记录测试步骤而且支持在装饰器中动态拼接参数报告里能看到每一步的传参。allure.attach可以把响应内容附加到对应步骤下面这样查看报告时点击任意一步就能看到当时的请求和响应定位问题非常直观。我还会在根目录写一个环境配置文件用allure.environment方式在报告中展示被测环境、浏览器信息、执行人。对于接口自动化这个信息同样有价值。另外强烈建议把 Allure 结果和历史结果关联起来这样能查看同一套用例近几次执行的趋势对于判断“这次失败是代码变更导致还是环境不稳定导致”特别有帮助。4. 完整实操过程从零跑通一条用例4.1 编写第一个接口测试用例为了避免泛泛而谈我这里用一个登录接口作为例子完整走一遍从写代码到出报告的过程。假设被测系统是一个标准的 RESTful 接口登录接口定义如下POST /api/login 请求体JSON: { username: tester, password: 123456 } 成功响应: { code: 200, msg: success, data: { token: a1b2c3d4e5 } }按前面的框架结构先在testcases/test_login.py里写一个最简版本用例import pytest from common.request_client import RequestClient def test_login_success(): client RequestClient(https://api.example.com, timeout10) resp client.post(/api/login, json{ username: tester, password: 123456 }) assert resp.status_code 200 body resp.json() assert body.get(code) 200 assert body.get(data, {}).get(token) is not None这个用例已经能跑但如果只有这一条它还没利用到框架的能力。我们继续往工程化方向改造把 base_url 从代码里挪到配置文件中把登录凭证信息也放进data/login.yaml最后用 fixture 提供 client。修改后的test_login.py大致长这样import allure import pytest from common.request_client import RequestClient from common.read_config import get_config allure.title(登录接口-正常登录) allure.description(验证正确用户名和密码可以返回 token) def test_login_success(client): resp client.post(/api/login, json{ username: tester, password: 123456 }) allure.attach(resp.text, 登录响应, allure.attachment_type.JSON) assert resp.status_code 200 assert resp.json().get(code) 200 assert resp.json().get(data, {}).get(token) is not None4.2 使用命令行运行 Pytest代码写完后在项目根目录下执行pytest因为我们已经在pytest.ini里配置了testpaths testcases和默认的 Allure 参数所以直接执行就能自动扫描用例并生成结果。如果你想只跑某个文件可以用pytest testcases/test_login.py如果要跑某个参数化用例 ID则用pytest testcases/test_login.py::test_login_success[正常登录]通过命令行参数控制执行范围是日常调试效率的关键。每次都全量跑信息噪音大且耗时长先本地单点验证再整体回归才是正确节奏。第一次执行时如果出现ModuleNotFoundError: No module named common说明当前工作目录没被加入 Python 的查找路径。解决办法是在项目根目录下执行命令或者在pytest.ini中加一行pythonpath .需要安装 pytest-pythonpath 插件或者使用较新版本 pytest 自带的 pythonpath 配置。4.3 生成并打开 Allure 报告pytest 执行完后结果已经写在report/allure-results目录下但这还只是原始 JSON 数据。我们需要使用 Allure 命令行把原始结果转换成可视化的 HTML 报告。安装 Allure 命令行有两种方式最简单的是直接下载解压官方发行包配置系统 PATHmacOS 也可以用 Homebrewbrew install allure安装完成后执行allure generate report/allure-results -o report/allure-report --clean allure open report/allure-reportgenerate命令把原始结果渲染为 HTML--clean先清空旧报告目录避免残留文件。open会启动一个本地 Web 服务并自动打开浏览器默认地址是http://localhost:port。如果allure命令找不到八成是环境变量没配好。Windows 下需要把解压后的bin目录加到 PATH然后重新打开终端。macOS 如果使用 brew 安装一般不会有这个问题。4.4 给框架加上失败重试和超时控制接口自动化最让人头疼的问题之一就是网络抖动导致的偶发失败。接口偶尔超时几百毫秒服务本身没问题但测试用例红了既浪费排查精力又降低报告可信度。我的解决方案是使用pytest-rerunfailures插件。在测试函数上加装饰器指定重试次数和延迟时间pytest.mark.flaky(reruns2, reruns_delay2) def test_login_success(client): ...这个装饰器表示用例失败后延迟 2 秒再重试最多重试 2 次。注意重试只适合处理“环境暂时性因素”导致的失败如果服务端逻辑真的坏了重试多少次都没有意义反而掩盖问题。所以我一般把重试次数控制在 2 次以内并且针对网络超时和 5xx 状态码单独处理。超时控制同样不能少。我在前面请求封装里已经设置了 requests 的 timeout但那是单次 HTTP 请求的超时。如果整个用例还包含登录、查询、断言多个环节总执行时间是敞口的。这时候pytest-timeout插件就派上用场了pytest --timeout30或者单独给某条用例加装饰器pytest.mark.timeout(30) def test_complex_flow(client, login_token): ...这样即使某个环节无限卡住整个用例也会在 30 秒后被强制终止避免测试进程挂死。5. 常见问题与排查技巧实录5.1 接口返回乱码或中文编码错误接口返回 JSON 里包含中文但打印到终端或者写进报告是乱码通常原因有两个一个是响应内容没有按 UTF-8 解码另一个是 requests 默认使用了错误的字符集猜测。解决办法是在获取响应文本时显式指定编码resp.encoding utf-8 print(resp.text)或者直接在请求封装里对所有响应统一设置编码。另外如果你的服务端返回头里的Content-Type没有带charsetutf-8requests 的resp.text就会用默认的 ISO-8859-1 去解码中文必乱。这种情况最稳妥的做法是用resp.content.decode(utf-8)自己做解码然后把解码后的字符串交给 JSON 解析。5.2 依赖登录态的接口怎么处理前面提到了通过conftest.py定义login_tokenfixture但很多同学在实际项目中会遇到一种更复杂的情况不同角色的用户登录后能访问的资源不一样或者测试过程中 token 过期了请求返回 401用例直接失败。针对这种情况我的经验是把 token 失效时的“自动重新登录”逻辑也做进请求封装。具体来说在_request里判断如果响应状态码是 401就重新调用一次登录接口更新 token然后带着新 token 重发原请求。这样对调用方完全透明用例不需要关心 token 是否过期。当然要重试次数有个上限避免死循环。另外存储 token 时尽量不要把它硬编码在代码里更不要提交到 Git。建议通过环境变量注入或者在配置文件中做脱敏处理。安全习惯要从框架阶段就养成。5.3 Allure 报告没有步骤和附件有的同学明明在用例里加了allure.step和allure.attach但生成的 Allure 报告里看不到步骤详情只有干巴巴的用例列表。这个问题十有八九是执行测试时没有生成足够的结果数据。排查步骤很简单先看report/allure-results目录下有没有大量*-result.json文件如果只有很少的文件说明 pytest 执行时并没有通过--alluredir参数输出完整结果再确认allure-pytest插件已经安装并且被 pytest 自动加载可以通过pytest --help查看是否出现--alluredir选项。还有一个容易忽略的点如果在测试函数内部用allure.step上下文管理器但没有真正执行到那一行比如断言在前面已经失败了报告自然没有对应步骤。所以步骤附件信息尽量放在请求封装内部统一添加而不是散落在每个用例中。5.4 用例执行顺序和数据污染接口自动化用例之间有时会互相影响比如 A 用例创建了一个订单B 用例查询订单列表结果 A 没执行或者执行顺序变了B 就失败。这一般是测试数据隔离没做好。解决手段有两类。第一类是让用例尽量独立每个用例自己构造测试数据测试结束再清理。第二类如果确实存在业务流程依赖建议显示地声明依赖关系或者把有依赖的步骤放到同一个用例的不同severity步骤里而不是拆成多个物理用例。千万不要依赖 pytest 默认的文件执行顺序因为 pytest 为了提升并行效率可能改变顺序。另外设计测试数据时建议为每条数据增加业务前缀或者随机后缀比如用户名test_user_20250101_001避免不同环境、不同批次执行时产生的数据互相冲突。这也是我踩了无数次坑以后才固定下来的习惯。5.5 常见问题速查表问题现象可能原因解决办法请求一直卡住不结束服务端没有响应且未设置 timeout在 requests 请求中设置 timeout用 pytest-timeout 做用例级超时接口报 429 Too Many Requests请求频率过高被限流控制并发数增加重试延迟对全局限流情况降低整体测试频率断言的字段突然取不到后端接口结构变更用 jsonschema 校验响应结构第一时间发现字段缺失登录 token 失效用例批量报 401token 有有效期限在请求封装中实现 token 自动续期逻辑Allure 报告打开后无法显示图表allure command 版本与 allure-pytest 版本不匹配升级二者到兼容版本重新生成报告测试数据不同用例间串了全局变量被意外修改尽量减少全局状态优先通过 fixture 返回值传递数据这个表格里的最后一项其实很容易被忽视。接口自动化框架用到后期最困扰人的往往不是接口本身而是测试代码之间的状态污染。我见过有同事用模块级别的全局变量保存 token某个用例不小心改了这个变量后面所有用例全部失败。后来统一改成 fixture 返回值传递问题才彻底消失。6. 个人经验框架跑起来之后该怎么做框架搭好、第一批用例通过、报告能看了这只是开始。我实际经历中的体会是真正让这套东西发挥价值的是后续的持续迭代和工程化完善。首先一定要接入持续集成环境。不管是 Jenkins 还是 GitLab CI只要代码提交后能自动触发接口测试并把 Allure 报告作为制品输出这套框架才真正进入了“无人值守”的阶段。如果只是本地手动执行它解决的还只是一半问题。其次是尽量让测试数据和测试代码分离。我在实际项目里已经把大量接口测试数据挪到了 YAML 文件中普通测试人员也能维护。渐渐地连产品经理偶尔都会来看一下报告里的接口通过率。测试不再是开发之后一个孤立的环节而是整个交付链路里的可视化质量信号。最后一点经验是不要一上来就追求框架的“高大全”。第一次搭框架能跑通就是胜利先写 10 条最核心的冒烟用例慢慢扩展到 50 条、100 条。等到你真正被重复手工测试折腾得受不了时你会感谢当初搭了这个框架的自己。