ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Python自动化测试框架实战:从零搭建Web接口与UI回归测试工具

Python自动化测试框架实战:从零搭建Web接口与UI回归测试工具 1. 项目概述最近在带团队做项目回归测试每次手动点点点不仅效率低还容易漏测。于是花了点时间用Python搭了一套轻量级的自动化测试框架。这套东西不是什么高深莫测的“银弹”核心思路就是用代码模拟人工操作把重复、固定的测试流程固化下来解放测试人员的双手让他们能更专注于探索性测试和复杂场景的构建。我把它用在Web接口和部分UI的回归测试上效果立竿见影原本需要半天的手工测试现在十几分钟就能跑完还能自动生成一份清晰的测试报告。如果你也在被重复的测试工作困扰或者想从手工测试转向自动化那么用Python来搭建自己的测试工具是一个非常务实且高效的起点。这篇文章我就结合自己的实战经验把从环境搭建、框架设计、到源码实现的完整过程拆解给你看并提供可直接运行的源码。2. 为什么选择Python构建自动化测试框架在动手之前我们得先想清楚市面上有那么多成熟的测试工具比如Postman、Jmeter为什么还要自己用Python从头造轮子这绝不是为了炫技而是为了解决实际工具无法满足的痛点。2.1 通用测试工具的局限性我最早也用过不少工具但在实际企业级项目中它们常常“水土不服”。首要问题就是测试数据不可控。自动化测试的本质是对数据的测试你传入参数断言返回结果。但如果数据库里的数据是动态变化的或者被其他操作污染了你的断言就可能失败。这时你很难判断到底是接口代码有Bug还是测试数据出了问题。自己写的框架可以在每次执行用例前强制初始化测试数据确保每次测试都在一个干净、已知的数据环境下进行排除了数据干扰测试结果才真正可信。其次很多内部接口为了安全会进行签名、加密或复杂的鉴权。通用的工具往往难以灵活地模拟这些安全机制。而用Python你可以轻松地引入公司内部的加密库或者用hashlib、hmac等标准库自定义签名算法将安全逻辑无缝集成到你的测试请求中这是工具很难做到的。再者是扩展性和定制化。工具的功能是固定的但业务需求是千变万化的。比如领导想要一份更美观、包含更多业务指标的测试报告或者需要把测试结果自动同步到内部的项目管理平台又或者想在每天凌晨定时执行核心用例集。这些定制化需求在自研框架里无非就是多写几个模块、多调几个API的事但在工具里可能就需要复杂的插件开发甚至无法实现。2.2 Python在自动化测试中的天然优势Python成为测试自动化的首选语言不是没有道理的。它的语法简洁上手快即使团队里测试同学编程基础不深也能很快看懂和编写测试用例。更重要的是Python拥有一个极其庞大和活跃的生态圈。对于接口测试有requests库可以优雅地发送HTTP请求对于Web UI自动化Selenium和Playwright是行业标准对于移动端有Appium。还有pytest/unittest这样的测试组织框架Allure/HTMLTestRunner这样的报告生成工具。你可以像搭积木一样把这些优秀的库组合起来快速构建出适合自己项目的测试解决方案。3. 自动化测试框架的核心设计思路设计一个框架首先要理清它的工作流程。我设计的这个框架核心流程可以概括为“准备数据 - 执行用例 - 验证结果 - 输出报告”。听起来简单但每个环节都有不少细节要注意。3.1 框架处理流程与核心模块整个框架的运行始于一个主控程序比如run_main.py。它的任务很明确调度所有模块让测试流程有条不紊地跑起来。我画了一个简单的流程图来帮助理解数据初始化首先框架会调用数据初始化模块向测试数据库插入预设好的数据并清理旧数据确保测试环境纯净。加载测试用例接着主程序会按照预定规则例如扫描testcase目录下所有以_test.py结尾的文件发现并加载所有测试用例。执行与断言然后框架执行这些用例。每个用例会通过请求模块向被测系统发送请求拿到响应后用断言模块来验证响应数据是否符合预期状态码、关键字段值等。生成报告与日志最后无论用例成功还是失败结果都会被记录下来。框架会收集所有执行结果调用报告生成模块输出一份HTML格式的测试报告。同时整个执行过程中的关键信息如请求、响应、错误会写入日志文件方便事后排查问题。基于这个流程我将框架的代码组织成以下几个目录这也是一个比较清晰、通用的结构project_root/ ├── common/ # 公共模块 │ ├── logger.py # 日志记录器 │ └── reporter.py # 报告生成器可封装HTMLTestRunner ├── config/ # 配置管理 │ └── config.py # 存放数据库连接、接口域名、账号密码等 ├── db_init/ # 测试数据初始化 │ ├── data_init.py │ └── mysql_conn.py ├── testcase/ # 测试用例 │ ├── __init__.py │ ├── user_login_test.py │ └── order_query_test.py ├── utils/ # 工具函数 │ ├── request_client.py # 封装的请求客户端 │ └── assert_util.py # 自定义断言方法 ├── logs/ # 日志文件目录自动生成 ├── report/ # 测试报告目录自动生成 └── run_main.py # 测试执行入口文件注意logs和report目录通常配置在.gitignore文件中避免将生成的日志和报告提交到代码仓库造成冗余。3.2 测试数据管理的艺术测试数据管理是自动化测试稳定性的基石。我的原则是测试数据必须可预测、可隔离、可清理。可预测意味着你的测试用例所依赖的数据其内容和状态必须是明确的。例如测试“查询用户信息”接口你提前在数据库里插入了一条id100, name测试用户的记录那么你的用例就可以断言返回的name字段一定是“测试用户”。这避免了因数据随机变化导致的“假失败”。可隔离强烈建议为自动化测试准备独立的测试数据库或者至少是独立的Schema。千万不要直接在线上或开发库上跑自动化用例你的数据初始化操作可能会干扰其他开发、测试人员的正常工作。如果条件有限也务必确保你的数据有特殊标识比如用户名加_test后缀并且在用例中做好数据隔离。可清理每次执行前都要清理上次测试残留的数据。我的做法是在data_init.py中先执行TRUNCATE TABLE或DELETE语句清空相关表然后再插入本次测试需要的数据。这样能保证每次测试的起点都是一致的。这里分享一个我踩过的坑早期我图省事只在第一次运行时初始化数据后续运行直接复用。结果随着用例增多数据相互干扰排查一个用例失败的原因要花大量时间分析数据状态。后来改为“每次执行前都重置数据”虽然初始化多花了几秒钟但测试的稳定性和可维护性大大提升绝对是值得的。4. 从零开始搭建你的第一个自动化测试框架理论说再多不如动手做一遍。下面我就带你一步步搭建这个框架并解释每个部分为什么要这么写。4.1 环境准备与依赖安装首先确保你的电脑上安装了Python建议3.7及以上版本。然后我们通过pip安装必要的第三方库。创建一个requirements.txt文件是个好习惯。# requirements.txt requests2.25.1 # 用于发送HTTP请求 PyMySQL1.0.2 # 用于连接MySQL数据库根据你的数据库类型调整如pymongo, psycopg2 pytest7.0.0 # 测试框架用于组织和运行用例也可用unittest pytest-html3.2.0 # 生成pytest格式的HTML报告 # 如果使用unittest并想要更美观的报告可以使用HTMLTestRunner # 可以将其源码文件HTMLTestRunner.py直接放在common目录下在命令行中进入项目目录执行安装pip install -r requirements.txt实操心得建议使用虚拟环境venv或conda来管理项目依赖避免不同项目间的包版本冲突。命令很简单python -m venv venv然后激活它Windows:venv\Scripts\activate Mac/Linux:source venv/bin/activate。4.2 核心模块代码实现1. 配置管理 (config/config.py)将易变的配置信息集中管理避免硬编码在代码中方便在不同环境测试、预发布间切换。# config/config.py import os class Config: # 数据库配置 DB_HOST localhost DB_PORT 3306 DB_USER test_user DB_PASSWORD test_pass DB_NAME test_db # 被测系统基础URL BASE_URL http://api.your-test-system.com # 日志级别 LOG_LEVEL INFO # 项目根路径用于动态定位其他文件 PROJECT_ROOT os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # 可以创建不同的配置类用于不同环境 class ProductionConfig(Config): BASE_URL http://api.your-real-system.com # 生产环境数据库配置...2. 数据库连接与初始化 (db_init/mysql_conn.py data_init.py)这里封装一个简单的数据库操作类并实现数据初始化逻辑。# db_init/mysql_conn.py import pymysql from config.config import Config class DB: def __init__(self): # 从配置类读取数据库信息 self.connection pymysql.connect( hostConfig.DB_HOST, portConfig.DB_PORT, userConfig.DB_USER, passwordConfig.DB_PASSWORD, databaseConfig.DB_NAME, charsetutf8mb4, cursorclasspymysql.cursors.DictCursor # 返回字典格式的结果 ) self.cursor self.connection.cursor() def clear_and_init(self, table_data_dict): 清空表并初始化数据 Args: table_data_dict (dict): 格式为 {table_name: [{col1:val1, ...}, ...], ...} try: # 为了安全按传入字典的顺序先清空所有相关表 for table_name in table_data_dict.keys(): self.cursor.execute(fTRUNCATE TABLE {table_name};) print(f已清空表: {table_name}) self.connection.commit() # 插入新的测试数据 for table_name, data_list in table_data_dict.items(): if not data_list: continue # 构造插入语句假设所有字典键相同 columns , .join([f{key} for key in data_list[0].keys()]) placeholders , .join([%s] * len(data_list[0])) sql fINSERT INTO {table_name} ({columns}) VALUES ({placeholders}) # 提取值列表 values_to_insert [tuple(item.values()) for item in data_list] self.cursor.executemany(sql, values_to_insert) print(f已向表 {table_name} 插入 {self.cursor.rowcount} 条数据) self.connection.commit() except Exception as e: self.connection.rollback() print(f数据初始化失败: {e}) raise e finally: self.close() def close(self): if self.cursor: self.cursor.close() if self.connection: self.connection.close()# db_init/data_init.py import sys import os sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from db_init.mysql_conn import DB import datetime import time def create_test_data(): 定义你需要初始化的测试数据 past_time time.strftime(%Y-%m-%d %H:%M:%S, time.localtime(time.time() - 100000)) future_time time.strftime(%Y-%m-%d %H:%M:%S, time.localtime(time.time() 10000)) datas { user: [ {id: 1, username: test_user_01, email: user01test.com, is_active: 1}, {id: 2, username: test_user_02, email: user02test.com, is_active: 0}, ], product: [ {id: 1001, name: 测试商品A, price: 29.9, stock: 100}, {id: 1002, name: 测试商品B, price: 199.0, stock: 0}, ], # ... 可以定义更多表和数据 } return datas def init_data(): 执行数据初始化 print(开始初始化测试数据...) test_datas create_test_data() db DB() db.clear_and_init(test_datas) print(测试数据初始化完成) if __name__ __main__: # 单独运行此脚本可用于手动初始化数据 init_data()3. 封装的请求客户端 (utils/request_client.py)对requests库进行二次封装可以统一添加请求头、处理异常、记录日志等让测试用例更简洁。# utils/request_client.py import requests import json from config.config import Config import logging # 获取日志器 logger logging.getLogger(__name__) class RequestClient: def __init__(self, base_urlNone): self.base_url base_url or Config.BASE_URL self.session requests.Session() # 可以在这里设置公共请求头如Content-Type, User-Agent等 self.session.headers.update({ Content-Type: application/json; charsetutf-8, User-Agent: MyAutoTestFramework/1.0 }) def _request(self, method, endpoint, **kwargs): 发送请求的核心方法 url f{self.base_url.rstrip(/)}/{endpoint.lstrip(/)} logger.info(f请求方法: {method}, URL: {url}, 参数: {kwargs.get(json, kwargs.get(data, 无))}) try: resp self.session.request(method, url, **kwargs) resp.raise_for_status() # 如果状态码不是2xx抛出HTTPError异常 logger.info(f响应状态码: {resp.status_code}, 响应体: {resp.text[:500]}...) # 日志只记录前500字符 return resp except requests.exceptions.RequestException as e: logger.error(f请求发生异常: {e}, URL: {url}) raise e # 提供更友好的方法名 def get(self, endpoint, paramsNone, **kwargs): return self._request(GET, endpoint, paramsparams, **kwargs) def post(self, endpoint, dataNone, jsonNone, **kwargs): return self._request(POST, endpoint, datadata, jsonjson, **kwargs) def put(self, endpoint, dataNone, jsonNone, **kwargs): return self._request(PUT, endpoint, datadata, jsonjson, **kwargs) def delete(self, endpoint, **kwargs): return self._request(DELETE, endpoint, **kwargs) # 创建一个全局客户端实例方便导入使用 client RequestClient()4. 编写你的第一个测试用例 (testcase/user_login_test.py)我们用pytest来写用例它比unittest更简洁灵活。# testcase/user_login_test.py import pytest import allure # 如果安装了allure-pytest可以用它添加更详细的报告步骤 from utils.request_client import client from utils.assert_util import assert_response allure.feature(用户认证模块) class TestUserLogin: 用户登录接口测试类 allure.story(正常登录流程) allure.title(使用正确的用户名和密码登录成功) def test_login_success(self): 测试用例正确的用户名密码登录成功 login_data { username: test_user_01, password: correct_password_123 # 注意这里应该是加密后的密码或测试专用密码 } # 发送POST请求 response client.post(/api/v1/login, jsonlogin_data) # 使用自定义的断言工具进行验证 assert_response(response, expected_status200) # 进一步断言返回的JSON数据 resp_json response.json() assert resp_json[code] 0, f响应code不为0实际为: {resp_json[code]} assert token in resp_json[data], 响应数据中未包含token字段 assert len(resp_json[data][token]) 10, 返回的token长度异常 allure.story(异常登录流程) allure.title(使用错误的密码登录失败) def test_login_with_wrong_password(self): 测试用例用户名正确密码错误登录失败 login_data { username: test_user_01, password: wrong_password } response client.post(/api/v1/login, jsonlogin_data) assert_response(response, expected_status200) # 接口可能依然返回200但code是错误码 resp_json response.json() assert resp_json[code] 1001, f预期错误码1001实际为: {resp_json[code]} assert 密码错误 in resp_json[message] allure.story(异常登录流程) allure.title(使用不存在的用户名登录失败) def test_login_with_nonexistent_user(self): 测试用例用户名不存在登录失败 login_data { username: ghost_user, password: any_password } response client.post(/api/v1/login, jsonlogin_data) resp_json response.json() assert resp_json[code] 1002, f预期错误码1002实际为: {resp_json[code]}5. 自定义断言工具 (utils/assert_util.py)将常用的断言逻辑封装起来让用例更清晰。# utils/assert_util.py import json def assert_response(response, expected_status200, expected_content_typeapplication/json): 基础响应断言 Args: response: requests.Response 对象 expected_status: 期望的HTTP状态码 expected_content_type: 期望的Content-Type头部 assert response.status_code expected_status, \ fHTTP状态码断言失败预期: {expected_status}, 实际: {response.status_code}, 响应体: {response.text} if expected_content_type: # 检查Content-Type是否包含期望的值 assert expected_content_type in response.headers.get(Content-Type, ), \ fContent-Type断言失败预期包含: {expected_content_type}, 实际: {response.headers.get(Content-Type)} def assert_json_key_exists(response_json, key_path): 断言JSON响应中某个键支持嵌套路径存在 Args: response_json: 解析后的JSON对象dict/list key_path: 键的路径用点号分隔如 data.user.id keys key_path.split(.) current response_json for key in keys: assert key in current, fJSON路径 {key_path} 断言失败键 {key} 不存在于 {current} current current[key] return current # 返回找到的值可以用于进一步断言 def assert_json_value_equal(response_json, key_path, expected_value): 断言JSON响应中某个键的值等于预期值 actual_value assert_json_key_exists(response_json, key_path) assert actual_value expected_value, \ fJSON值断言失败路径: {key_path}, 预期: {expected_value}, 实际: {actual_value}6. 测试执行入口与报告生成 (run_main.py)最后我们需要一个总指挥来调度一切。这里我展示使用pytest并生成HTML报告的方式它比unittestHTMLTestRunner更现代。# run_main.py import pytest import os import sys import shutil from datetime import datetime from db_init.data_init import init_data def run_tests(): 主执行函数 print( * 50) print(开始执行自动化测试套件) print( * 50) # 步骤1: 初始化测试数据关键 print(\n 步骤1: 初始化测试数据库...) try: init_data() print(测试数据初始化成功) except Exception as e: print(f测试数据初始化失败程序终止。错误: {e}) sys.exit(1) # 步骤2: 准备报告目录 report_dir ./report if os.path.exists(report_dir): # 可选清理旧的报告这里我们选择保留历史用时间戳区分 # shutil.rmtree(report_dir) pass else: os.makedirs(report_dir) # 生成带时间戳的报告文件名 current_time datetime.now().strftime(%Y%m%d_%H%M%S) html_report_path os.path.join(report_dir, freport_{current_time}.html) # 如果你安装了allure也可以生成allure报告 # allure_report_dir os.path.join(report_dir, fallure_{current_time}) # 步骤3: 使用pytest运行测试用例并生成HTML报告 print(f\n 步骤2: 开始执行测试用例报告将生成至: {html_report_path}) # pytest 命令行参数 # -v: 详细输出 # -s: 允许终端输出如print语句 # --tbshort: 使用简短的错误回溯信息 # --html: 生成HTML报告需要pytest-html插件 # --self-contained-html: 将CSS等嵌入HTML生成单个文件 pytest_args [ ./testcase, # 测试用例目录 -v, -s, --tbshort, f--html{html_report_path}, --self-contained-html, # 如果你使用了allure # f--alluredir{allure_report_dir} ] exit_code pytest.main(pytest_args) # 步骤4: 执行后处理 print(\n 步骤3: 测试执行完成。) # 可以在这里添加发送邮件通知、归档报告等逻辑 # if exit_code 0: # send_email_success(html_report_path) # else: # send_email_failure(html_report_path) print(f测试报告已生成: file://{os.path.abspath(html_report_path)}) # 如果想用allure打开报告 # print(fAllure报告已生成使用命令查看: allure serve {allure_report_dir}) return exit_code if __name__ __main__: sys.exit(run_tests())5. 框架运行与结果分析写完代码让我们来运行一下。在项目根目录下打开终端执行python run_main.py你会看到控制台开始输出日志先是初始化数据然后pytest开始发现并执行testcase目录下的所有测试用例。执行完成后会在report目录下生成一个类似report_20231027_143022.html的文件。用浏览器打开它你就能看到一份清晰的测试报告。报告通常会包含以下信息概览总用例数、通过数、失败数、跳过数、执行时间。结果摘要以饼图或条形图展示通过率。详细结果每个测试用例的名称、状态、执行时长。点击失败的用例可以看到详细的错误信息和日志这对于调试至关重要。实操心得测试报告是给项目组所有人看的特别是领导和产品经理。一份清晰、美观的报告能直观地反映测试质量和系统稳定性。除了基础的pytest-html强烈推荐集成Allure框架。Allure生成的报告更加专业、交互性更强可以展示测试步骤、附件如图片、日志片段还能按功能模块、优先级等进行分类展示是提升测试团队专业度的利器。6. 进阶技巧与常见问题排查框架跑起来只是第一步要让它在团队中稳定、高效地发挥作用还需要考虑更多。6.1 让框架更健壮参数化、夹具与配置文件参数化测试当一个测试逻辑需要多组不同数据验证时不要写多个重复的用例。用pytest.mark.parametrize。import pytest pytest.mark.parametrize(username, password, expected_code, expected_msg, [ (test_user_01, correct_pwd, 0, 登录成功), (test_user_01, wrong_pwd, 1001, 密码错误), (ghost_user, any_pwd, 1002, 用户不存在), (, some_pwd, 1003, 用户名不能为空), ]) def test_login_params(username, password, expected_code, expected_msg): 参数化登录测试 data {username: username, password: password} resp client.post(/api/v1/login, jsondata).json() assert resp[code] expected_code assert expected_msg in resp[message]使用pytest夹具夹具可以帮你准备测试环境、清理资源。比如每个用例都需要一个唯一的用户名可以用夹具来生成。import pytest import uuid pytest.fixture def unique_username(): 生成一个唯一的用户名夹具 return fuser_{uuid.uuid4().hex[:8]} def test_register(unique_username): 测试注册接口使用唯一的用户名避免重复 data {username: unique_username, password: 123456, email: f{unique_username}test.com} resp client.post(/api/v1/register, jsondata) assert resp.status_code 201环境配置文件前面我们用了config.py但更专业的做法是使用.env文件配合python-dotenv库或者使用configparser读取.ini文件。这样可以在不修改代码的情况下切换测试、预发布、生产环境。# .env 文件示例 TEST_ENVstaging DB_HOSTstaging-db.yourcompany.com BASE_URLhttps://staging-api.yourcompany.com6.2 常见问题与排查实录在实际使用中你肯定会遇到各种问题。这里我总结了一个速查表问题现象可能原因排查步骤与解决方案用例间歇性失败1. 测试数据被并发修改。2. 接口响应慢导致超时。3. 依赖的外部服务不稳定。1.强化数据隔离确保每个用例或用例类有独立的数据集使用事务或更细粒度的清理/初始化。2.增加重试机制对于获取列表、查询等非幂等操作可以封装一个带重试的请求函数。3.Mock外部依赖对于不稳定的第三方接口在测试环境中使用unittest.mock或pytest-mock进行模拟。数据库连接失败1. 配置错误IP、端口、密码。2. 数据库服务未启动。3. 网络问题。1.检查配置文件确认config.py或.env中的配置与实际环境一致。2.手动连接测试用MySQL客户端工具如Navicat、命令行尝试连接验证网络和权限。3.添加连接池与超时在DB类中配置连接超时和自动重连逻辑。断言失败但人工验证接口正常1. 断言条件过于严格如检查完整JSON。2. 响应中有动态字段如时间戳、自增ID。3. 数据未正确初始化。1.使用更灵活的断言只断言关键字段使用assert key in resp.json()和assert resp.json()[key] value。2.忽略动态字段在断言前使用resp.json().pop(create_time, None)移除动态字段再比较。3.检查初始化脚本在用例开头打印或查询数据库确认测试数据是否存在且正确。生成的报告为空或格式错乱1.pytest-html插件未正确安装。2. 用例执行过程中发生严重错误导致中断。3. 报告保存路径权限问题。1.确认依赖运行pip list测试执行速度慢1. 每个用例都重新建立数据库连接。2. 用例之间有不必要的等待如sleep。3. 接口本身响应慢。1.使用会话级夹具用pytest.fixture(scopesession)创建一次数据库连接供所有用例使用。2.移除硬编码等待用显式等待轮询检查替代time.sleep。3.性能测试分离将自动化功能测试与性能测试分开功能测试不关心接口绝对速度只关心逻辑正确。6.3 集成到CI/CD流水线框架稳定后就可以把它集成到团队的持续集成/持续部署流程中了比如Jenkins、GitLab CI、GitHub Actions。核心就是在CI的某个阶段如代码合并后、每日构建后自动执行你的run_main.py脚本并根据测试结果决定是否继续后续流程。以GitHub Actions为例可以编写这样一个工作流文件# .github/workflows/python-test.yml name: Python Automated Tests on: push: branches: [ main, develop ] pull_request: branches: [ main ] jobs: test: runs-on: ubuntu-latest services: mysql: image: mysql:8.0 env: MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: test_db ports: - 3306:3306 options: --health-cmdmysqladmin ping --health-interval10s --health-timeout5s --health-retries3 steps: - uses: actions/checkoutv3 - name: Set up Python uses: actions/setup-pythonv4 with: python-version: 3.9 - name: Install dependencies run: | pip install -r requirements.txt - name: Run migrations and init test data run: | # 这里可以运行你的数据库迁移脚本和初始化脚本 python db_init/data_init.py - name: Run tests with pytest run: | python run_main.py - name: Upload test report uses: actions/upload-artifactv3 if: always() # 即使测试失败也上传报告 with: name: html-report path: report/这样每次代码推送或合并请求都会自动触发测试并将生成的HTML报告作为制品保存起来供团队成员查看。自动化测试的价值就从个人效率工具升级为了保障团队交付质量的重要环节。从一行代码开始到搭建一个能融入团队工作流的自动化测试框架这个过程本身就是对测试思想和工程能力的一次锤炼。这套框架的源码我已经放在了GitHub上你可以直接克隆下来根据自己项目的接口文档修改配置和测试数据很快就能跑起来。记住最好的框架不是最复杂的那个而是最适合你当前团队和项目的那一个。先解决有无问题再逐步优化扩展。
返回列表