一、项目介绍
该项目是一个在线购物的商城网站,包括用户生命周期管理,商品信息查询与展示,下单支付等相关功能。
Python+pytest+sqlalchemy+requests+allure+jsonpath+yaml+Jenkins+Linux
二、项目架构
pythonproject/
├── run.py # 【运行入口】启动测试、选择报告类型
├── pytest.ini # pytest配置文件
├── environment.xml # Allure环境配置
├── extract.yaml # 【数据共享】接口提取数据存储
├── conftest.py # 【测试配置】pytest fixtures
│
├── conf/ # 【配置层】
│ ├── setting.py # 全局配置常量(文件路径、超时时间等)
│ ├── config.ini # 环境配置(API地址、数据库连接)
│ └── operationConfig.py # 配置文件读取工具(读取*.ini文件)
│
├── common/ # 【公共工具层】
│ ├── sendrequest.py # HTTP请求封装
│ ├── readyaml.py # YAML数据读写
│ ├── assertions.py # 断言验证工具
│ ├── recordlog.py # 日志记录
│ ├── debugtalk.py # 自定义函数库(加密、时间、获取yaml数据)
│ ├── connection.py # 数据库连接
│ └── ... # 其他工具
│
├── base/ # 【核心业务层】
│ ├── apiutil.py # 接口请求处理核心类
│ ├── apiutil_business.py # 业务场景封装
│ ├── generateId.py # 测试用例ID生成
│ └── removefile.py # 文件清理模块
│
├── testcase/ # 【测试用例层】
│ ├── conftest.py # 测试用例级fixtures
│ │
│ ├── Bussiness interface/ # 业务场景测试用例
│ │ └── BussinessScenario.yml
│ │
│ ├── Single interface/ # 用户管理测试用例
│ │ ├── addUser.yaml
│ │ ├── updateUser.yaml
│ │ ├── deleteUser.yaml
│ │ └── queryUser.yaml
│ │
│ └── ProductManager/ # 商品管理用例
│ ├── login_dw.yaml
│ ├── productDetail.yaml
│ ├── orderPay.yaml
│ └── commitOrder.yaml
│
├── data/ # 【测试数据层】
│ ├── loginName.yaml # 登录测试数据
│ ├── login_data.csv # CSV测试数据
│ ├── vehicleNo.csv # 车辆编号数据
│ ├── 测试数据.xls # Excel测试数据
│ └── sql/ # SQL脚本
│
├── logs/ # 【日志输出】
│ └── test.日期.log # 运行日志
│
├── report/ # 【报告输出】
│ ├── temp/ # Allure原始数据
│ ├── tmreport/ # TM报告
│ ├── allureReport/ # Allure报告
│ └── results.xml # JUnit XML结果
│
└── venv/ # Python虚拟环境
三、项目运行
第一阶段:项目启动
1. 入口文件执行
运行命令:python run.py
2. 配置读取 (conf/setting.py)
| 配置项 | 值 | 说明 |
|---|---|---|
REPORT_TYPE |
'allure' |
报告类型(allure或tm) |
DIR_BASE |
D:\Python+Mall\pythonproject\pythonproject |
项目根目录 |
API_TIMEOUT |
60 |
接口超时时间(秒) |
FILE_PATH['EXTRACT'] |
extract.yaml 路径 |
接口提取数据存储文件 |
第二阶段:登录认证(session级别)
执行时机 :conftest.py 中的 system_login fixture(自动执行)
数据流向:
loginName.yaml → 读取登录测试数据 → 发送登录请求 → 提取token → 存入extract.yaml
具体数据示例:
📄 loginName.yaml 内容:
- baseInfo:
api_name: 用户登录
url: /dar/user/login
method: post
testCase:
- case_name: 用户名和密码正确登录验证
data:
user_name: test01
passwd: admin123
extract:
token: $.token # 从响应中提取token
转换成JSON格式:
# 返回值是一个 list,每个元素对应 yaml 中的一个用例组
case_info = [
{
"baseInfo":
# api_info[0][0] → baseInfo 部分
{
'api_name': '用户登录',
'url': '/dar/user/login',
'method': 'post',
'header': {'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'}
},
"testCase":
# api_info[0][1] → testCase 中的第一个测试用例
{
'case_name': '用户名和密码正确登录验证',
'data': {'user_name': 'test01', 'passwd': 'admin123'},
'validation': [{'contains': {'error_code': None}}, {'eq': {'msg': '登录成功'}}],
'extract': {'token': '$.token'}
}
}
]
调用函数:
replace_load(self, data)
def replace_load(self, data):
"""yaml数据替换解析"""
str_data = data
if not isinstance(data, str):
str_data = json.dumps(data, ensure_ascii=False)
# print('从yaml文件获取的原始数据:', str_data)
for i in range(str_data.count('${')):
if '${' in str_data and '}' in str_data:
start_index = str_data.index('$')
end_index = str_data.index('}', start_index)
ref_all_params = str_data[start_index:end_index + 1]
# 取出yaml文件的函数名
func_name = ref_all_params[2:ref_all_params.index("(")]
# 取出函数里面的参数
func_params = ref_all_params[ref_all_params.index("(") + 1:ref_all_params.index(")")]
# 传入替换的参数获取对应的值,类的反射----getattr,setattr,del....
extract_data = getattr(DebugTalk(), func_name)(*func_params.split(',') if func_params else "")
if extract_data and isinstance(extract_data, list):
extract_data = ','.join(e for e in extract_data)
str_data = str_data.replace(ref_all_params, str(extract_data))
# print('通过解析后替换的数据:', str_data)
# 还原数据
if data and isinstance(data, dict):
data = json.loads(str_data)
else:
data = str_data
return data
实际发送的请求数据:
📄 登录后 extract.yaml 变化:
# 登录前:文件为空或只有之前的数据
Cookie:
access_token_cookie: ...
token: 6D2Bf021DEDfe45e9D37EAfDB99c6 # ← 新增:登录后提取的token
第三阶段:测试用例执行
执行入口 :测试类(如 TestUserManager)→ 测试方法(如 test_add_user)
数据替换过程:
📄 addUser.yaml 原始数据:
- baseInfo:
api_name: 新增用户
url: /dar/user/addUser
method: POST
testCase:
- case_name: 正常新增用户
data:
username: testadduser
token: ${get_extract_data(token)} # ← 引用提取的数据
validation:
- contains: { 'msg': '新增成功' }
变量替换过程:
${get_extract_data(token)}
↓ DebugTalk().get_extract_data('token')
↓ 读取 extract.yaml 获取 token 值
↓ 替换为:6D2Bf021DEDfe45e9D37EAfDB99c6
实际发送的请求数据:
{
"username": "testadduser",
"token": "6D2Bf021DEDfe45e9D37EAfDB99c6" # ← 替换后的值
}
调用函数:
specification_yaml(self, case_info)
def specification_yaml(self, case_info):
"""
规范yaml测试用例的写法
:param case_info: list类型,调试取case_info[0]-->dict
:return:
"""
params_type = ['params', 'data', 'json']
cookie = None
try:
base_url = self.conf.get_section_for_data('api_envi', 'host')
# base_url = self.replace_load(case_info['baseInfo']['url'])
url = base_url + case_info["baseInfo"]["url"]
allure.attach(url, f'接口地址:{url}')
api_name = case_info["baseInfo"]["api_name"]
allure.attach(api_name, f'接口名:{api_name}')
method = case_info["baseInfo"]["method"]
allure.attach(method, f'请求方法:{method}')
header = self.replace_load(case_info["baseInfo"]["header"])
allure.attach(str(header), '请求头信息', allure.attachment_type.TEXT)
try:
cookie = self.replace_load(case_info["baseInfo"]["cookies"])
allure.attach(str(cookie), 'Cookie', allure.attachment_type.TEXT)
except:
pass
for tc in case_info["testCase"]:
case_name = tc.pop("case_name")
allure.attach(case_name, f'测试用例名称:{case_name}', allure.attachment_type.TEXT)
# 断言结果解析替换
val = self.replace_load(tc.get('validation'))
tc['validation'] = val
# 字符串形式的列表转换为list类型
validation = eval(tc.pop('validation'))
allure_validation = str([str(list(i.values())) for i in validation])
allure.attach(allure_validation, "预期结果", allure.attachment_type.TEXT)
extract = tc.pop('extract', None)
extract_lst = tc.pop('extract_list', None)
for key, value in tc.items():
if key in params_type:
tc[key] = self.replace_load(value)
file, files = tc.pop("files", None), None
if file is not None:
for fk, fv in file.items():
allure.attach(json.dumps(file), '导入文件')
files = {fk: open(fv, 'rb')}
res = self.run.run_main(name=api_name,
url=url,
case_name=case_name,
header=header,
cookies=cookie,
method=method,
file=files, **tc)
res_text = res.text
allure.attach(res_text, '接口响应信息', allure.attachment_type.TEXT)
status_code = res.status_code
allure.attach(self.allure_attach_response(res.json()), '接口响应信息', allure.attachment_type.TEXT)
try:
res_json = json.loads(res_text)
if extract is not None:
self.extract_data(extract, res_text)
if extract_lst is not None:
self.extract_data_list(extract_lst, res_text)
# 处理断言
assert_res.assert_result(validation, res_json, status_code)
except JSONDecodeError as js:
logs.error("系统异常或接口未请求!")
raise js
except Exception as e:
logs.error(str(traceback.format_exc()))
raise e
except Exception as e:
logs.error(e)
raise e
步骤解析:
1、读取配置,拼接URL
base_url = self.conf.get_section_for_data('api_envi', 'host')
url = base_url + case_info["baseInfo"]["url"]
2、提取接口基础信息并添加到Allure报告
api_name = case_info["baseInfo"]["api_name"]
# api_name = '用户登录'
allure.attach(api_name, f'接口名:{api_name}')
method = case_info["baseInfo"]["method"]
# method = 'post'
allure.attach(method, f'请求方法:{method}')
header = self.replace_load(case_info["baseInfo"]["header"])
# header = {'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'}
allure.attach(str(header), '请求头信息', allure.attachment_type.TEXT)
3、遍历测试用例
tc = {
'case_name': '用户名和密码正确登录验证',
'data': {'user_name': 'test01', 'passwd': 'admin123'},
'extract': {'token': '$.token'}
}
4、提取用例名称
case_name = tc.pop("case_name")
# case_name = '用户名和密码正确登录验证'
5、处理断言
validation = eval(tc.pop('validation'))
assert_res.assert_result(validation, res_json, status_code)
6、提取数据配置
extract = tc.pop('extract', None)
# extract = {'token': '$.token'}
extract_lst = tc.pop('extract_list', None)
# extract_lst = None
7、处理请求参数
params_type = ['params', 'data', 'json']
for key, value in tc.items():
if key in params_type:
tc[key] = self.replace_load(value)
# tc = {'data': {'user_name': 'test01', 'passwd': 'admin123'}}
# 因为data中没有${}变量,所以replace_load后保持不变
8、处理文件上传
file, files = tc.pop("files", None), None
if file is not None:
for fk, fv in file.items():
allure.attach(json.dumps(file), '导入文件')
files = {fk: open(fv, 'rb')}
-
open(fv, mode='rb') :以 二进制只读模式 打开文件
-
files = {fk: 文件对象} :构建用于 requests 库上传的文件字典
9、发送HTTP请求
res = self.run.run_main(
name=api_name, # '用户登录'
url=url, # 'http://127.0.0.1:8787/dar/user/login'
case_name=case_name, # '用户名和密码正确登录验证'
header=header, # {'Content-Type': 'application/x-www-form-urlencoded'}
cookies=cookie, # None
method=method, # 'post'
**tc # {'data': {'user_name': 'test01', 'passwd': 'admin123'}}
)
# 实际发送的HTTP请求:
# POST http://127.0.0.1:8787/dar/user/login
# Content-Type: application/x-www-form-urlencoded
# user_name=test01&passwd=admin123
10、获取响应
res_text = res.text
# res_text = '{"status":200,"msg":"登录成功","token":"6D2Bf021DEDfe45e9D37EAfDB99c6"}'
status_code = res.status_code
# status_code = 200
11、解析响应并提取数据
res_json = json.loads(res_text)
# res_json = {"status":200, "msg":"登录成功", "token":"6D2Bf021DEDfe45e9D37EAfDB99c6"}
# 如果有extract配置,提取数据
if extract is not None:
self.extract_data(extract, res_text)
# extract_data({'token': '$.token'}, '{"status":200,...}')
extract_lst = tc.pop('extract_list', None)
value = dictionary.pop(key, default) #安全地移除并返回字典中的值
-
key:要移除的键名
-
default:如果键不存在时返回的默认值(可选)
第四阶段:接口响应与数据提取
1. 发送HTTP请求
- baseInfo:
api_name: 新增用户
url: /dar/user/addUser
method: POST
header:
Content-Type: application/x-www-form-urlencoded;charset=UTF-8
testCase:
- case_name: 正常新增用户
data:
username: testadduser
password: tset6789890
role_id: 123456789
dates: '2023-12-31'
phone: 13800000000
token: ${get_extract_data(token)}
validation:
- contains: { 'msg': '新增成功' }
extract:
userId: $.data.userId
orderNumber: $.data.orderNumber
📄 SendRequest 发送请求:
POST http://127.0.0.1:8787/dar/user/addUser
Content-Type: application/x-www-form-urlencoded
username=testadduser&token=6D2Bf021DEDfe45e9D37EAfDB99c6
2. 获取响应
HTTP/1.1 200 OK
{
"status": 200,
"msg": "新增成功",
"data": {
"userId": "7972512823030970797",
"orderNumber": "981712696053255936860"
}
}
调用函数:从响应中读取数据写入到extract.yaml
extract_data(self, testcase_extarct, response)
def extract_data(self, testcase_extarct, response):
"""
提取接口的返回值,支持正则表达式和json提取器
:param testcase_extarct: testcase文件yaml中的extract值
:param response: 接口的实际返回值
:return:
"""
try:
pattern_lst = ['(.*?)', '(.+?)', r'(\d)', r'(\d*)']
for key, value in testcase_extarct.items():
# 处理正则表达式提取
for pat in pattern_lst:
if pat in value:
ext_lst = re.search(value, response)
if pat in [r'(\d+)', r'(\d*)']:
extract_data = {key: int(ext_lst.group(1))}
else:
extract_data = {key: ext_lst.group(1)}
self.read.write_yaml_data(extract_data)
# 处理json提取参数
if '$' in value:
ext_json = jsonpath.jsonpath(json.loads(response), value)[0]
if ext_json:
extarct_data = {key: ext_json}
logs.info('提取接口的返回值:', extarct_data)
else:
extarct_data = {key: '未提取到数据,请检查接口返回值是否为空!'}
self.read.write_yaml_data(extarct_data)
except Exception as e:
logs.error(e)
步骤解析:
正则表达式提取
|-------------|---------------------|
| 模式 | 含义 |
| (.*?) | 匹配任意字符(非贪婪) |
| (.+?) | 匹配至少一个任意字符(非贪婪) |
| (\d+) | 匹配一个或多个数字 |
| (\d*) | 匹配零个或多个数字 |
pattern_lst = ['(.*?)', '(.+?)', r'(\d+)', r'(\d*)']
...
ext_lst = re.search(value, response)
# ext_lst.group(1) = '13800000000'
extract_data = {key: ext_lst.group(1)}
# extract_data = {'phone': '13800000000'}
re.search(pattern, string, flags=0)
参数:
-
pattern: 正则表达式模式 -
string: 要搜索的字符串 -
flags: 可选标志(如re.IGNORECASE、re.MULTILINE等)
返回值:
-
匹配成功:返回 Match 对象
-
匹配失败:返回 None
JSONPath
|----------------------|-----------------|
| 表达式 | 含义 |
| .data.key** | **data对象下的key** |
| **.list[*].key | 数组中所有元素的key |
ext_json = jsonpath.jsonpath(json.loads(response), value)[0] #[0] 是取第一个匹配的值
-
如果找到:返回 ['实际值']
-
如果没找到:返回 False 或 []
3. 数据提取并存入 extract.yaml
📄 extract.yaml 更新:
# 新增提取的数据
userId: '7972512823030970797'
orderNumber: '981712696053255936860'
第五阶段:断言验证
Assertions 断言处理:
📄 addUser.yaml 中的断言配置:
validation:
- contains: { 'status_code': 200 } # 响应状态码包含200
- contains: { 'msg': '新增成功' } # 响应消息包含"新增成功"
调用函数:
contains_assert(self, value, response, status_code)
def contains_assert(self, value, response, status_code):
"""
字符串包含断言模式,断言预期结果的字符串是否包含在接口的响应信息中
:param value: 预期结果,yaml文件的预期结果值
:param response: 接口实际响应结果
:param status_code: 响应状态码
:return: 返回结果的状态标识
"""
# 断言状态标识,0成功,其他失败
flag = 0
for assert_key, assert_value in value.items():
if assert_key == "status_code":
if assert_value != status_code:
flag += 1
allure.attach(f"预期结果:{assert_value}\n实际结果:{status_code}", '响应代码断言结果:失败',
attachment_type=allure.attachment_type.TEXT)
logs.error("contains断言失败:接口返回码【%s】不等于【%s】" % (status_code, assert_value))
else:
resp_list = jsonpath.jsonpath(response, "$..%s" % assert_key)
if isinstance(resp_list[0], str):
resp_list = ''.join(resp_list)
if resp_list:
assert_value = None if assert_value.upper() == 'NONE' else assert_value
if assert_value in resp_list:
logs.info("字符串包含断言成功:预期结果【%s】,实际结果【%s】" % (assert_value, resp_list))
else:
flag = flag + 1
allure.attach(f"预期结果:{assert_value}\n实际结果:{resp_list}", '响应文本断言结果:失败',
attachment_type=allure.attachment_type.TEXT)
logs.error("响应文本断言失败:预期结果为【%s】,实际结果为【%s】" % (assert_value, resp_list))
return flag
# 接口实际响应(JSON 对象)
response = {
"code": 200,
"msg": "操作成功",
"data": {
"id": 123,
"name": "test"
}
}
# 来自 YAML 测试用例的 validation 配置
value = {
"status_code": 200,
"msg": "操作成功",
"data": "xxx"
}
1、状态码断言
for assert_key, assert_value in value.items():
if assert_key == "status_code":
if assert_value != status_code:
flag += 1
# 记录断言失败信息到 Allure 报告
2、响应体字段提取(JSONPath)
resp_list = jsonpath.jsonpath(response, "$..%s" % assert_key)
- 从响应数据中递归查找所有匹配指定键的值
3、字符串拼接
if isinstance(resp_list[0], str):
resp_list = ''.join(resp_list)
# ["操作", "成功"] → "操作成功"
4、包含断言判断
assert_value = None if assert_value.upper() == 'NONE' else assert_value
if assert_value in resp_list:
# 断言成功
else:
# 断言失败,flag += 1
断言过程:
# 伪代码演示
response = {"status": 200, "msg": "新增成功"}
validation = [
{'contains': {'status_code': 200}},
{'contains': {'msg': '新增成功'}}
]
# 验证1:response中是否包含'status_code': 200
assert 'status_code' in response and response['status_code'] == 200 # ✅ 通过
# 验证2:response中是否包含'msg': '新增成功'
assert 'msg' in response and '新增成功' in response['msg'] # ✅ 通过
第六阶段:日志记录
📄 logs/ 目录下的日志文件:
logs/test.20260514.log
📄 日志内容示例:
2026-05-14 10:30:15 - INFO - -------------接口测试开始--------------
2026-05-14 10:30:15 - INFO - 用户登录接口: 开始发送请求...
2026-05-14 10:30:16 - INFO - 用户登录接口: 请求成功,响应状态码:200
2026-05-14 10:30:16 - INFO - 提取接口的返回值:{'token': '6D2Bf021DEDfe45e9D37EAfDB99c6'}
2026-05-14 10:30:20 - INFO - 新增用户接口: 开始发送请求...
2026-05-14 10:30:21 - INFO - 新增用户接口: 请求成功,响应状态码:200
2026-05-14 10:30:21 - INFO - 提取接口的返回值:{'userId': '7972512823030970797'}
2026-05-14 10:30:21 - INFO - -------------接口测试结束--------------
核心函数:
@pytest.fixture(autouse=True)
def start_test_and_end():
logs.info('-------------接口测试开始--------------')
yield
logs.info('-------------接口测试结束--------------')
@pytest.fixture(autouse=True)
-
在每个测试函数执行前后自动运行,无需显式调用
def get_testcase_yaml(file):
testcase_list = []
try:
with open(file, 'r', encoding='utf-8') as f:
data = yaml.safe_load(f)
if len(data) <= 1:
yam_data = data[0]
base_info = yam_data.get('baseInfo')
for ts in yam_data.get('testCase'):
param = [base_info, ts]
testcase_list.append(param)
return testcase_list
else:
return data
except UnicodeDecodeError:
logs.error(f"[{file}]文件编码格式错误,--尝试使用utf-8编码解码YAML文件时发生了错误,请确保你的yaml文件是UTF-8格式!")
except FileNotFoundError:
logs.error(f'[{file}]文件未找到,请检查路径是否正确')
except Exception as e:
logs.error(f'获取【{file}】文件数据时出现未知错误: {str(e)}') -
读取和解析 YAML 测试用例文件 ,将 YAML 数据转换为测试框架可执行的参数格式。
- baseInfo:
api_name: 用户登录
url: /dar/user/login
method: post
testCase:- case_name: 正常登录
data: {user_name: test01, passwd: admin123} - case_name: 密码错误
data: {user_name: test01, passwd: wrong}
- case_name: 正常登录
- baseInfo:
转换为:
[
[{'api_name': '用户登录', 'url': '/dar/user/login', 'method': 'post'},
{'case_name': '正常登录', 'data': {'user_name': 'test01', 'passwd': 'admin123'}}],
[{'api_name': '用户登录', 'url': '/dar/user/login', 'method': 'post'},
{'case_name': '密码错误', 'data': {'user_name': 'test01', 'passwd': 'wrong'}}]
]
调用顺序:
1. conftest.py:start_test_and_end()
↓
logs.info('-------------接口测试开始--------------')
↓
2. apiutil.py:specification_yaml()
↓
3. sendrequest.py:run_main()
↓
logs.info('接口名称:用户登录')
logs.info('请求地址:/dar/user/login')
logs.info('请求方式:POST')
↓
4. apiutil.py:extract_data()
↓
logs.info('提取接口的返回值:{"token": "xxx"}')
↓
5. conftest.py:start_test_and_end()
↓
logs.info('-------------接口测试结束--------------')
第七阶段:测试报告生成
根据 REPORT_TYPE 配置生成不同报告
模式1:allure 报告(REPORT_TYPE = 'allure')
📄 执行命令:
pytest.main([
'-s', '-v',
'--alluredir=./report/temp', # allure原始数据目录
'./testcase', # 测试用例目录
'--clean-alluredir', # 清理旧报告
'--junitxml=./report/results.xml' # JUnit XML格式结果
])
# 复制环境信息
shutil.copy('./environment.xml', './report/temp')
# 启动allure服务
os.system('allure serve ./report/temp')
📁 生成的报告结构:
report/
├── temp/ # allure原始数据
│ ├── environment.xml
│ ├── suites/ # 测试套件数据
│ ├── widgets/ # 报告组件数据
│ └── index.html # 报告入口
└── results.xml # JUnit XML结果
模式2:tm 报告(REPORT_TYPE = 'tm')
📄 执行命令:
pytest.main([
'-vs',
'--pytest-tmreport-name=testReport.html', # 报告文件名
'--pytest-tmreport-path=./report/tmreport' # 报告路径
])
# 自动打开浏览器
webbrowser.open_new_tab(os.getcwd() + '/report/tmreport/testReport.html')
📁 生成的报告结构:
report/
└── tmreport/
└── testReport.html # HTML测试报告
关键数据变化汇总
| 阶段 | 数据来源 | 处理过程 | 输出/结果 |
|---|---|---|---|
| 登录 | loginName.yaml |
发送POST请求 | 提取token到extract.yaml |
| 用例执行 | addUser.yaml等 |
${get_extract_data(token)}替换 |
发送带token的请求 |
| 响应处理 | HTTP响应 | JSON解析 | 提取userId、orderNumber等 |
| 数据存储 | 响应数据 | write_yaml_data() |
更新extract.yaml |
| 断言验证 | 响应+validation | 字符串/数值比较 | 通过/失败 |
| 日志记录 | 全过程 | logs.info/error() |
logs/test.日期.log |
| 报告生成 | 测试结果 | pytest插件 | HTML报告 |
四、项目总结
基于 Python+pytest+Allure 构建的电商平台接口测试解决方案,通过 YAML 数据驱动实现测试用例与代码解耦,支持接口关联(JSONPath/正则提取)、灵活断言(包含/相等验证)和可视化报告生成,覆盖用户登录、商品管理、订单支付等核心业务流程。