拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python+pytest+requests接口自动化测试框架搭建实战与踩坑指南

Python+pytest+requests接口自动化测试框架搭建实战与踩坑指南

这套组合市面上教程不少,但大多教的是pytest怎么用、requests怎么调用,很少讲清楚一件事:为什么你的接口用例一旦超过50条就维护不动了?我在多个项目里迭代过测试框架,从最早几百行的Python脚本一路拆到现在分层清晰、支持多环境切换、能接入CI的执行框架,中间踩过不少坑。这篇就围绕“Python+pytest+requests”这套技术栈,聊一聊框架从0到能扛住业务测试需求的完整搭建思路、关键代码、以及那些不跑到第200条用例根本发现不了的坑。

如果你正准备自己搭一套接口自动化测试框架,或者已经在维护一个越改越乱的测试项目,这篇应该能帮你省下几个周末的摸索时间。

1. 为什么最终选定 pytest + requests,而不是其它组合

先说结论,再聊细节。做接口自动化可以用的技术组合很多,常见的有下面几种,我都实际用过或用团队维护过。

方案优点痛点适合场景
Postman + Newman上手快,导入导出方便,界面直观复杂断言不好做,数据驱动不灵活,逻辑复用困难临时验证接口,小型项目冒烟
Java + TestNG + RestAssured功能全,生态成熟Java语法相对重,写测试用例成本高,改造成本大大型Java技术栈团队,接口和开发同语言
Python + unittest + requests标准库,无额外学习成本用例组织相对死板,断言写法啰嗦,fixture功能弱轻量自动化,纯脚本跑通流程
RobotFramework关键字驱动,易读复杂逻辑表达式受限,出问题后排查链路深非技术团队协作,对可读性有强诉求
Python + pytest + requests简洁、灵活、fixture强大、插件生态丰富需要团队有一定Python基础绝大多数Web接口测试场景

最终我选第三行,核心原因有三点。

第一,pytest的fixture机制比其他方案灵活太多。unittest里想封装一个“登录后拿到token给后续用例复用”的逻辑,通常得写setUpClass、或者借助类级变量,代码绕来绕去。pytest里一个session作用域的fixture就能解决,而且通过参数传递让依赖关系清清楚楚。

第二,requests库的API设计极其顺手。一个session对象可以自动管理Cookie,超时配置、重试配置、SSL关闭、代理切换都有现成参数。相比urllib或者http.client那些需要手写一堆模板代码的方案,requests在处理HTTP协议细节时更省心,后续维护体验也好。

第三,pytest的插件生态能补齐几乎所有真实项目需求。比如pytest-xdist做并发执行,pytest-html和allure做报告,pytest-assume做软断言,这些插件虽然都是独立维护的,但是和pytest核心集成得很好。对一个测试框架来说,能随着项目成长而不推翻重来,是很重要的事。

提示:如果你的团队完全没有Python基础,并且业务人员也要参与维护用例,RobotFramework可能是更合适的选择。测试框架没有银弹,只有和你团队情况匹配的。

2. 框架的整体结构设计:不是堆文件,而是分层

很多初学者搭建测试框架时,习惯把所有的请求都写在test_xxx.py的文件里,刚开始没几条用例还好,一旦用例数量上来,会立刻面临两个问题:公共步骤改动时,几十个文件都要跟着改;接口返回结构变化时,断言散落在各处,根本改不动。

我的做法是,把框架拆成明显分层,目录结构大概长这样:

api_testing_framework/ ├── config/ │ ├── __init__.py │ ├── settings.py # 全局配置:环境地址、超时时间、重试次数 │ └── env_config.yaml # 多环境配置:dev/staging/prod ├── common/ │ ├── __init__.py │ ├── http_client.py # 基于requests二次封装的请求客户端 │ ├── logger.py # 日志封装 │ ├── assertion.py # 自定义断言扩展 │ ├── retry.py # 请求重试与限流处理 │ └── read_data.py # yaml/excel测试数据读取 ├── testcases/ │ ├── __init__.py │ ├── conftest.py # pytest fixture定义 │ ├── test_user_api.py # 用户模块接口用例 │ └── test_order_api.py # 订单模块接口用例 ├── data/ │ ├── user_test_data.yaml │ └── order_test_data.yaml ├── reports/ │ └── ... # 测试报告目录 ├── pytest.ini ├── requirements.txt └── run.py # 测试入口脚本

分层思路从下往上梳理其实是这样的:

配置层负责放环境地址、账号密码、超时和重试参数。公共层封装所有重复动作,包括HTTP请求、日志、断言、重试机制。测试数据层放用例的数据驱动文件,把“数据”和“代码”分开,业务人员维护数据时不需要碰代码。测试用例层只负责描述“测什么”,包括请求参数怎么拼、断言结果怎么验、fixture怎么注入。

这个结构最重要的原则是:用例层不应该出现requests.get或requests.post这样的调用,所有直接和HTTP打交道的代码都收敛在http_client.py里。这样将来如果想把requests换成httpx,只需要改一个文件。

在conftest.py中,我会把公共fixture都集中管理:

import pytest from common.http_client import HttpClient @pytest.fixture(scope="session") def http_client(env_config): """整个测试会话共用的HTTP客户端,内部自动管理token和cookie""" client = HttpClient(base_url=env_config["base_url"]) yield client @pytest.fixture(scope="session") def auth_token(http_client): """登录获取token,整个session只执行一次""" resp = http_client.post("/api/v1/login", json={ "username": http_client.username, "password": http_client.password }) assert resp.status_code == 200 token = resp.json()["data"]["token"] http_client.update_headers({"Authorization": f"Bearer {token}"}) return token

session作用域的http_client可以保证整个测试会话只有一个连接池、一套统一请求头,用例之间不会因为重复创建客户端而产生额外的握手开销。

3. 核心模块的落地细节:请求封装、数据驱动和断言扩展

3.1 请求封装能根除哪些脏代码

先看一段反面代码,很多测试脚本里都有这种影子:

def test_get_user_info(): r = requests.post("http://192.168.1.10:8080/api/v1/login", data={"username": "admin", "password": "123456"}) token = r.json()["data"]["token"] headers = {"Authorization": "Bearer " + token} r2 = requests.get("http://192.168.1.10:8080/api/v1/user/10001", headers=headers) assert r2.status_code == 200

这个用例跑一次没问题,但如果你有200条用例,就要写200遍“登录、拼headers、拼接URL”。如果接口地址变了,要全部改一遍;如果登录接口加了验证码参数,又是全部改一遍。这不是自动化测试,是变相的手工维护。

我在http_client.py里做这些事:

import requests from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry from common.logger import logger class HttpClient: def __init__(self, base_url, username=None, password=None, timeout=10, max_retries=3): self.base_url = base_url.rstrip("/") self.username = username self.password = password self.timeout = timeout self.session = requests.Session() # 挂载自动重试策略,细节下一章展开 retry = Retry( total=max_retries, connect=max_retries, read=max_retries, status=max_retries, status_forcelist=(429, 500, 502, 503, 504), backoff_factor=0.5, raise_on_status=False ) adapter = HTTPAdapter(max_retries=retry) self.session.mount("http://", adapter) self.session.mount("https://", adapter) def request(self, method, path, **kwargs): url = self.base_url + path kwargs.setdefault("timeout", self.timeout) # 捕获完整的请求日志,方便排查 logger.info(f"{method.upper()} {url} 参数: {kwargs.get('json') or kwargs.get('params') or ''}") try: resp = self.session.request(method, url, **kwargs) logger.info(f"响应状态: {resp.status_code} 耗时: {resp.elapsed.total_seconds():.3f}s") return resp except requests.exceptions.RequestException as e: logger.error(f"请求异常: {str(e)}") raise def get(self, path, **kwargs): return self.request("GET", path, **kwargs) def post(self, path, **kwargs): return self.request("POST", path, **kwargs) def update_headers(self, headers: dict): self.session.headers.update(headers)

这样设计后有四个好处。统一超时设置,不会出现某个用例忘记传timeout、卡住整个测试过程的情况。统一日志记录,每条请求的URL、参数、状态码、耗时都有迹可循。统一服务地址配置,用例里只写路径,环境切换只改配置文件。统一重试策略,后续讲429限流时你会看到这有多关键。

3.2 yaml文件做数据驱动,维护用例不需要打开IDE

数据驱动是让测试用例“长不大”的关键手段。我的习惯是每个模块建一个yaml文件,文件名和用例模块对应。以用户模块为例:

test_get_user_by_id: - case_name: "查询已存在用户" method: GET path: /api/v1/user/{user_id} path_params: user_id: 10001 expected: status_code: 200 code: 0 user_name: "张三" - case_name: "查询不存在的用户" method: GET path: /api/v1/user/{user_id} path_params: user_id: 999999 expected: status_code: 200 code: 10001 message: "user not found"

然后在conftest.py写一个fixture读取数据:

import pytest import yaml @pytest.fixture() def user_test_data(): with open("data/user_test_data.yaml", "r", encoding="utf-8") as f: return yaml.safe_load(f)

用例层通过pytest的parametrize参数化输入数据:

import pytest from common.assertion import assert_response class TestUserApi: @pytest.mark.parametrize("case_data", pytest.mark.usefixtures("user_test_data")([ { "id": "test_get_user_by_id", "data": None } ])) def test_demo(self): pass

不过parametrize直接套yaml里嵌套的列表会有一些语法上绕的地方。我后来更推荐在读取层就把数据组织成pytest的参数列表,让用例层更干净:

def load_case_data(file_path, case_key): """从yaml文件中取指定case_key下的所有测试数据""" with open(file_path, "r", encoding="utf-8") as f: all_data = yaml.safe_load(f) return all_data[case_key]

然后用例文件里这样写:

class TestUserApi: @pytest.mark.parametrize("case_data", load_case_data("data/user_test_data.yaml", "test_get_user_by_id")) def test_get_user_by_id(self, http_client, case_data): path = case_data["path"].format(**case_data.get("path_params", {})) resp = http_client.get(path) assert_response(resp, case_data["expected"])

路径中的{user_id}通过format动态替换,这其实就是路径参数的常规处理方式。如果项目里的查询条件更复杂,可以把query_params也放进yaml,在请求时传递。

提示:这里有个很实用的小技巧,yaml文件里不要写入任何“期望值以外的业务逻辑”,比如不要写什么“登录后调用”,不要把断言表达式也放进去。yaml只负责数据,越纯粹越好维护。

3.3 断言体系:不要到处写assert xxx == yyy

接口测试的断言,最基础的是状态码,然后是业务返回码。很多项目还会校验数据库落库结果、异步任务结果等。如果把所有的断言逻辑都堆在用例文件里,代码可读性会变得很差。

我的做法是把常用断言封装到一个模块里,比如:

def assert_response(resp, expected: dict): """统一断言入口,expected是yaml中读取的期望值字典""" assert resp.status_code == expected["status_code"], \ f"状态码不符,期望{expected['status_code']},实际{resp.status_code}" body = resp.json() if "code" in expected: assert body.get("code") == expected["code"], \ f"业务码不符,期望{expected['code']},实际{body.get('code')}" if "message" in expected: assert expected["message"] in body.get("message", ""), \ f"错误信息不符,期望包含{expected['message']},实际{body.get('message')}" # 逐字段校验,通常用于结果稳定、需要精确匹配的字段 for field, value in expected.get("fields", {}).items(): assert body.get(field) == value, \ f"字段 {field} 不符,期望{value},实际{body.get(field)}"

这里需要注意一个常见误区:凡是用in做字符串包含断言的地方,都要注意预期值不能太短,否则可能出现“期望输入一个很短的字符串却匹配到错误信息”的误判。比如服务器抛出一个通用错误"system error, please retry later",你断言message等于"error"也会通过,这就失去断言的意义了。

如果涉及“返回列表长度 > 0”“返回的数值在一个区间内”这类断言,断言模块里也可以加对应函数。这部分完全看业务需要,但整体原则只有一个:用例文件只描述业务期望,不重复造断言轮子。

4. 接口自动化跑起来之后,429限流和重试机制是绕不开的问题

如果你在写爬虫或者频繁调用第三方接口,一定见过这个错误:429 Too Many Requests。接口自动化测试项目里同样很容易碰到,尤其是用例数量多、跑得又勤的时候,服务端的网关就会启动限流。我实际遇到的情况是:本地跑100条用例没问题,一上Jenkins每小时跑一次,跑几次之后开始出现零零散散的失败,点开日志一看全是429。

4.1 429这个错误码到底在说什么

HTTP 429不是接口业务报错,而是服务端主动降负载。它的意思是:“你太频繁了,我不处理你的请求了。”很多测试人员第一反应是——是不是我代码写错了?其实不是,代码逻辑完全没问题,但服务端为了不影响线上用户,只能牺牲自动化测试的请求。

处理429通常有三个思路。降低整体请求频率,最简单,但会让测试执行时间变长。对失败的请求做重试,配合退避时间,这是最常用的做法。改造测试策略,错峰执行、分批跑,这是CI层面的优化。

4.2 用requests的Retry机制优雅处理429

requests库本身不带内置的自动重试机制,但可以通过urllib3的Retry类来挂载。我在前面的HttpClient里已经写了相关代码,这里展开说明一下每个参数的含义:

retry = Retry( total=3, # 最大重试次数,连接失败和响应错误总共算在一起 connect=3, # 连接失败时单独的重试次数 read=3, # 读取超时时的重试次数 status=3, # 状态码触发重试的次数 status_forcelist=(429, 500, 502, 503, 504), backoff_factor=0.5, # 退避因子,决定每次重试的间隔时间 raise_on_status=False )

退避时间的具体算法是:

退避时间 = backoff_factor * (2 ^ (重试次数 - 1))

在这个配置下,第一次重试前的等待时间是0.5 * 2^0 = 0.5秒,第二次是0.5 * 2^1 = 1秒,第三次是0.5 * 2^2 = 2秒。这样设计是为了让请求频率呈指数下降,给服务端留出恢复窗口。

提示:backoff_factor不要设置得太大,也别设成0。设成0意味着重试之间没有间隔,连续打过去反而更容易触发限流。我一般习惯用0.3到0.8之间的值,执行时间和稳定性之间比较平衡。

还有个容易被忽略的点:status_forcelist里除了429,最好把500、502、503、504这类服务端错误也放进去。因为在接口测试场景中,服务端偶尔抖动导致500,加个重试通常能直接把用例跑绿。但4xx的客户端错误,比如400、401、403、404,绝对不要加进重试。请求参数错了重试多少次都一样,反而会把错误掩藏起来,让排查问题的成本变高。

4.3 重试会带来新问题:重复提交风险

这里必须提醒一下。如果被测接口不是幂等的,比如创建订单、支付回调、发送短信这些操作,自动重试意味着服务端可能已经处理成功了,但响应在网络中被拦了一下,客户端又重发了一次请求,结果产生了两条订单、两条优惠券。

遇到这种情况,不能盲目依赖通用重试。更安全的做法是,这类接口在测试数据设计时就做好幂等控制,比如创建订单时传入唯一的order_sn,重复提交相同order_sn时服务端只处理一次。如果被测服务不支持幂等,那重试机制只对GET等幂等请求开启,写操作坚决不重试,宁可用例失败人工排查。

我自己的处理方式是,在HttpClient里加一个开关:

self.session.mount("http://", adapter) # 默认开启重试,用于GET为主的基础请求 # 如果用例明确不想要重试,可以在请求时传入retry_enabled=False def request(self, method, path, retry_enabled=True, **kwargs): ... if not retry_enabled: kwargs.setdefault("timeout", self.timeout) resp = self.session.request(method, url, **kwargs) return resp

在创建订单、删除资源这类用例中,显式关闭重试,从根上避免重复提交。

5. 从零搭框架时,我踩过的五个神坑和一次完整排查链路

5.1 conftest.py的层级作用域把你坑了

pytest里conftest.py不是只能放在根目录的。放在根目录的conftest.py对全局生效,放在testcases子目录下的conftest.py只对当前目录及子目录下的用例生效。这意味着你如果在一个模块的conftest.py里定义了一个fixture,然后另一个模块下也想用,会直接报fixture not found。

我当时遇到的更隐蔽的问题是:我在根目录conftest.py里定义了一个session作用域的fixture,返回登录token,然后在testcases目录下的某个子目录conftest.py里又定义了一个同名fixture,覆盖了根目录的。结果所有依赖这个fixture的用例,都跑成了子目录里那个返回的值。排查了很久才发现,同一个fixture名字在多个conftest.py中出现时,离用例更近的那个会覆盖更远的那个。

经验是:全局公共fixture只放根目录conftest.py,模块级私有fixture放对应模块的conftest.py,尽量不要重名。如果实在要重名,通过指定fixture位置来规避:

@pytest.fixture() def auth_token(http_client): # ...

这里依赖的是参数名http_client,只要这个fixture名字在作用域内可见,pytest就会自动找到它并根据作用域层级决定使用哪个定义。

5.2 用例间依赖导致偶发性失败:一次完整排查记录

这是一个真实项目里的问题,现象非常典型。我接手一个支付项目的接口自动化测试,套件一共80多条用例,单条跑全部通过,整个套件跑的时候总有4到5条随机失败。失败不是固定的用例,这次A失败,下次B失败,再跑又全部通过。

排查链路是这样的。

第一步,先看失败用例的日志。发现一个共同特点:失败的用例都依赖“先创建一条带特定金额的订单”,而创建订单的用例刚好在它们前面执行。如果前面创建订单的用例偶发失败,后面依赖订单数据的用例全挂。

第二步,再往前走。为什么创建订单的用例会偶发失败?日志显示失败原因是登录token过期。80多条用例跑下来大概要40多分钟,而登录token的有效期是30分钟。当token超过有效期后,所有后续接口都返回401。

第三步,这就把问题定位清楚了:session级fixture拿到的token贯穿整个测试会话,但被测系统的token有效期比测试总时长还短。不修改被测系统的情况下,最稳妥的方案是在HttpClient里监听401状态码,发现token失效就自动重新登录一次,再重放当前请求。

class HttpClient: def request(self, method, path, **kwargs): resp = self.session.request(method, url, **kwargs) if resp.status_code == 401 and self._auth_mode == "auto": self.login() resp = self.session.request(method, url, **kwargs) return resp

这个方案的风险点是重新登录后必须重新携带新token重放一次原请求,并且要避免登录接口本身返回401导致无限递归。所以在login方法内部调用request时要绕过自动重新登录的逻辑,或者加一个标志位。

这次排查的核心教训是:接口测试框架的偶发失败,十有八九不是业务Bug,而是用例间依赖、token生命周期、并发抢占这些外部因素引起的,排查时要顺着依赖链从结果一层层往回倒,不要一上来就怀疑断言逻辑写错。

5.3 参数化数据中包含特殊字符导致读取失败

有一次我在yaml文件里写了一个很长很长的测试数据,里面包含了大量特殊字符,比如{}, [], 等,用pytest的parametrize读取数据时,直接报了奇怪的语法错误。查下来发现是yaml解析时,反斜杠被当作转义字符处理了。

解决办法有两个。数据里如果有反斜杠,yaml字符串加单引号包裹,单引号内的反斜杠不会被转义。如果数据非常复杂,改用json文件做数据源,json的转义规则更容易预测。我自己后来的习惯是:简单数据用yaml,复杂结构数据直接用json。

5.4 断言中时间字段导致的不稳定

接口返回中经常有timestamp这类动态字段,每次请求返回的时间都不同。如果期望值里写死了时间,用例必然偶发失败。这个问题不算难,但很多人第一次遇到时容易卡住。解决方案是断言模块里支持动态字段忽略机制:

def assert_response(resp, expected: dict): body = resp.json() # 忽略动态字段 for field in expected.get("ignore_fields", []): if field in body: body[field] = None # 再逐字段断言

yaml里这样配置即可:

expected: status_code: 200 code: 0 ignore_fields: - create_time - update_time - token

这样写用例时不用关心动态字段的变化,同时又能保证其他核心字段被精确校验。

5.5 多环境切换时踩的坑:base_url硬编码

这条很基础,但我必须写。我在代码评审时见过太多把base_url写死在用例文件里的情况:

BASE_URL = "http://192.168.1.10:8080"

一旦要切到预发布环境测,就要全局搜索替换。更麻烦的是,如果不同环境使用的测试账号也不同,还要连账号密码一起改。

我的做法是通过pytest的ini机制读取环境变量:

[pytest] addopts = -p no:cacheprovider

然后运行命令时指定环境:

pytest --env=staging

在conftest.py中读取这个参数,加载不同环境配置,再传给http_client。这样环境切换只需要改命令行参数,框架代码完全不用动。

执行命令时我们一般会写一个run.py,里面把常用的执行参数都封装好,避免团队里每个人记一长串pytest命令。

6. 框架能跑通只是开始:并发、报告和CI集成

6.1 pytest-xdist并发执行时,fixture作用域要格外小心

用例数量超过200条,单线程执行要跑将近1个小时,这个执行速度对日常迭代来说是不够快的。pytest-xdist插件可以解决这个问题:

pytest -n auto

用auto参数会让pytest根据CPU核数决定并行worker数量。

这里有个大坑:pytest-xdist的并发模式下,session作用域的fixture并不是全局只执行一次,而是每个worker各执行一次。如果你的session级别token是登录一次完事,在多worker模式下,每个worker都会各自登录一次,这本身没关系,但如果登录接口有限流,并发同时登录N次反而容易把自己限流打出来。

解决思路有几种。如果登录接口不抗压,并发时给worker数设一个合理值,比如- n 4,而不是auto。token失效自动重登机制在这种模式下非常重要,否则某个worker的token只要过期,该worker上所有用例都会一起失败。如果需要多个worker共享同一个token,可以用pytest-base-url配合外部缓存,但成本较高,一般不推荐在初期就这么干。

6.2 报告:pytest-html和allure怎么选

报告是测试框架的门面,也是团队推广自动化的关键。pytest自带终端输出是给人看的,不是给业务方看的。我一般这样处理。

pytest-html是轻量级方案,一条命令生成单html文件,直接可以用浏览器打开,适合团队内部快速查看。

pytest --html=reports/report.html --self-contained-html

allure功能更全面,支持历史趋势、测试步骤图形化展示、失败截图分类,适合需要把自动化测试报告对接到管理团队或者做长期质量看板的场景。

pytest --alluredir=reports/allure-results allure generate reports/allure-results -o reports/allure-report --clean

我个人经验是:团队还小、目标只是回归测试快速看结果时,用pytest-html就够了。如果是为了建设质量度量体系、让非技术人员也能看懂测试覆盖情况,那就直接上allure,省得后面再从html转到allure时的迁移成本。

6.3 Jenkins或GitLab CI里跑起来之后要注意的细节

把自动化测试接入CI是框架发挥长期价值的最后一步。实操中几个高频问题值得提前注意。

CI机器的时区和本地不同,测试报告里的时间戳看起来会很奇怪,在CI机器上设置TZ环境变量可以解决。

CI环境一般没有GUI,但requests是纯HTTP库不依赖浏览器,不会受影响。

CI执行机如果也是跑其他任务的共用机器,要避免测试运行时占用大量内存,在构建配置里限制并发worker数。

每次CI失败,如果只看到一个红色标记,排查效率很低。所以框架里一定要有“失败用例日志自动打包”的插件或脚本,至少要把pytest的output日志置为always,这样打开构建日志就能直接定位到是哪条用例、哪个环节断言失败。

6.4 让CLI入口更贴心:用run.py封装日常操作

团队里不是每个人都精通pytest命令参数,而且一长串参数容易记混。我习惯在项目根目录放一个run.py,把常用操作封装成命令:

import argparse import subprocess def run_case(case_name=None, env="staging", workers=1): cmd = ["pytest", "-n", str(workers), "--env", env] if case_name: cmd.append(f"testcases/{case_name}.py") subprocess.run(cmd) if __name__ == "__main__": parser = argparse.ArgumentParser(description="接口自动化测试入口") parser.add_argument("--case", help="指定用例模块,如test_user_api") parser.add_argument("--env", default="staging", choices=["dev", "staging", "prod"]) parser.add_argument("--workers", type=int, default=1) args = parser.parse_args()

这样新同事接手项目,跑python run.py --case test_user_api --env dev就知道怎么用了,不用记pytest那些复杂的参数组合。

回到开头说的那个问题:为什么很多自动化测试项目跑着跑着就废了?根据我的经验,不是pytest和requests不好用,而是框架设计时没有把“变化”考虑进去。接口地址会变,token会过期,服务会限流,数据会有动态字段,并发会带来依赖问题。提前把这一层一层的变化处理掉,框架才能真正稳定跑下去。希望这篇对你搭建自己的接口自动化测试框架有帮助,少走一些我走过的弯路。

返回列表