UI自动化测试实时监控与智能报警系统设计与实践

发布时间:2026/7/18 10:49:14
UI自动化测试实时监控与智能报警系统设计与实践 1. 项目概述从“跑完拉倒”到“实时看护”的转变做UI自动化测试的朋友估计都经历过这样的场景下班前信心满满地点击“运行”把一整套几百上千个用例扔给测试机然后关电脑走人。第二天早上满怀期待地打开报告要么是一片象征成功的绿色让人松一口气要么就是几个刺眼的红色失败用例更糟的是有时会发现测试脚本在半夜某个节点就卡死了后面一大半用例根本没执行一晚上的算力和时间全浪费了。这种“盲盒”式的测试体验在追求快速反馈和持续交付的今天显得越来越格格不入。“实现实时监控和报警的UI自动化测试”这个标题指向的正是解决上述痛点的进阶方案。它不再是简单的脚本编写与执行而是构建一个具备“感知-决策-响应”能力的智能测试运维体系。核心目标很明确让自动化测试过程从“黑盒”变成“白盒”从“事后复盘”变成“事中干预”。当测试用例在执行过程中出现异常——无论是断言失败、元素定位超时、页面崩溃还是测试环境本身的不稳定如网络抖动、服务重启——系统都能在第一时间捕捉到这些信号并通过预设的通道如钉钉、企业微信、邮件、短信将报警信息精准推送到负责人面前。这背后的价值远不止是让测试工程师睡个安稳觉。对于敏捷团队它意味着更快的缺陷反馈闭环开发人员能在代码热度尚存时快速定位问题对于运维团队它提供了另一个维度的系统健康度监控视角一些前端性能劣化或接口隐性故障可能先于用户投诉被自动化脚本触发对于测试团队自身实时报警结合日志快照能极大缩短排查测试脚本自身“假失败”Flaky Tests的根因时间。简单说这就是给自动化测试套上了一套7x24小时不间断的“生命体征监测仪”。2. 核心设计思路构建监控报警的三层架构要实现一个健壮、实用的实时监控报警体系不能只是简单地在测试脚本里到处塞try-catch然后发邮件。我们需要一个层次清晰、职责分离的系统性设计。经过多个项目的实践我总结出一个稳定可靠的三层架构模型采集层、汇聚分析层和报警执行层。2.1 采集层无处不在的“传感器”采集层是系统的眼睛和耳朵负责在测试执行的第一现场抓取所有可能表征异常的信号。这些信号大致可以分为三类测试结果信号这是最直接的。每个测试用例的最终状态通过、失败、跳过、错误必须被捕获。但光有结果不够我们还需要关联的上下文信息比如失败时的错误堆栈StackTrace、失败截图、操作日志等。以Playwright为例它提供了丰富的fixture和事件监听机制可以在test.afterEach钩子中轻松获取到测试结果对象TestResult里面就包含了status,error,attachments自动捕获的截图和视频等关键信息。过程性能信号测试用例执行过程中的“健康指标”。例如单步操作耗时某个点击或输入操作异常缓慢可能预示着页面资源加载问题或前端性能瓶颈。断言响应时间等待某个元素出现或某个条件成立的超时情况。频繁的超时可能意味着环境不稳定或应用响应变慢。资源监控测试运行时的CPU/内存占用。如果运行一个简单的UI测试却导致内存飙升可能是有内存泄漏的脚本或被测应用本身的问题。环境健康信号测试所依赖的外部环境状态。包括测试服务器/节点状态是否存活是否失联。被测应用可访问性首页或健康检查接口能否正常响应。依赖服务状态如数据库连接、第三方API网关等。浏览器/驱动状态Chromedriver或浏览器实例是否意外崩溃。像“wincc报警记录运行系统卡住”这类问题本质上就是测试执行环境或依赖的运行环境出现了僵死状态需要在采集层被感知。实操心得采集点并非越多越好。初期建议聚焦于“测试结果”和“关键超时”这两类高价值信号。过度采集会产生大量噪音反而淹没了真正的报警。可以遵循“先核心后扩展”的原则逐步完善。2.2 汇聚分析层从数据到决策的“大脑”采集层上报的原始数据是杂乱且海量的尤其在并行执行大规模用例时。汇聚分析层的任务就是消化这些数据进行聚合、分析和判断决定“什么时候该报警”以及“报警的内容是什么”。这里有几个关键设计点报警收敛策略这是避免“报警风暴”的关键。想象一下一个服务宕机可能导致依赖它的100个用例在1分钟内全部失败。如果不加处理你会瞬间收到100条报警消息这毫无意义。常见的收敛策略有频率收敛相同类型的报警在N分钟内只发送一次。依赖收敛识别出根因故障如登录接口挂掉后续所有因登录失败而触发的用例报警被自动抑制只报告根因。分级收敛根据报警级别如P0紧急、P1高、P2中、P3低设置不同的通知频率和渠道。报警内容格式化一条好的报警信息应该让接收者能在5秒内理解“哪里出了问题”和“初步可能的原因”。它至少应包含报警标题如[UI自动化-P0] 核心登录流程在环境Prod-01大规模失败、失败用例概要数量、名称、失败时间、直接错误信息、关键日志片段或截图链接、以及可能的影响范围。状态维护与恢复系统需要维护当前报警的状态如“触发中”、“已恢复”。当监控到失败用例对应的场景已恢复正常如下一轮测试通过应自动发送一条“恢复通知”形成闭环。2.3 报警执行层精准触达的“信使”这是最终触达用户的环节选择稳定、高效、符合团队习惯的通知渠道至关重要。常见的渠道有即时通讯工具如钉钉、企业微信、飞书的群机器人。这是目前最主流的方式实时性强支持富文本可嵌入截图、跳转链接还可以特定人员。邮件适合非紧急的日报或周报汇总或者作为IM通知的备份。短信/电话仅用于最高级别P0的、需要立即唤醒人员的报警成本较高需谨慎使用。内部告警平台如果公司已有统一的监控告警平台如Prometheus Alertmanager, Zabbix可以将UI测试报警也接入其中实现告警管理的统一化。注意事项报警通道一定要有冗余设计。不能只依赖一个IM机器人万一机器人失效就全瞎了。通常采用“IM主报 邮件备报”的策略。所有发送出去的消息务必在服务端留有日志记录便于追溯和审计。3. 技术选型与核心组件实战明确了架构接下来就要选择趁手的“兵器”来实现它。技术选型没有银弹需要结合团队的技术栈、运维能力和具体需求来决定。下面我以一个基于Playwright Python Allure 钉钉机器人的流行技术栈为例拆解核心实现。3.1 测试框架与监听机制Playwright的事件驱动优势为什么选择Playwright作为例子因为它内置的强大事件监听机制为采集层提供了极大便利。相比Selenium需要手动封装Playwright的BrowserContext和Page对象可以直接监听各种事件。# 示例在Pytest conftest.py 或全局setup中设置监听器和全局page对象 import pytest from playwright.sync_api import Page, BrowserContext import allure pytest.fixture(scopesession) def browser_context_args(browser_context_args): # 全局上下文参数如视口大小、权限等 return { **browser_context_args, viewport: {width: 1920, height: 1080}, ignore_https_errors: True } pytest.fixture def page(context: BrowserContext, request): # 为每个测试用例创建一个page并附加监听器 page context.new_page() # 监听页面崩溃事件致命错误 page.on(crash, lambda: _on_page_crash(page, request.node.name)) # 监听请求失败事件如404500 page.on(requestfailed, lambda request: _on_request_failed(request, request.node.name)) # 监听控制台错误 page.on(console, lambda msg: _on_console(msg, request.node.name)) yield page page.close() def _on_page_crash(page, test_name): error_msg f测试 {test_name} 执行过程中页面崩溃 allure.attach(bodyerror_msg, namePage Crash, attachment_typeallure.attachment_type.TEXT) # 调用报警发送函数此处可集成 # send_alert(fP0-页面崩溃, error_msg, test_name) def _on_request_failed(request, test_name): if request.resource_type in [xhr, fetch, script, stylesheet]: # 只关心关键资源的失败 failure_info f请求失败: {request.url} - {request.failure.error_text} allure.attach(bodyfailure_info, nameRequest Failed, attachment_typeallure.attachment_type.TEXT) # 可根据失败频率决定是否触发报警通过上述方式我们能在测试执行过程中自动捕获到前端错误、资源加载失败等潜在问题这些往往是单纯的断言失败无法覆盖的。3.2 测试结果采集与增强Allure报告与自定义钩子测试框架如Pytest本身会产出结果但我们需要更结构化和丰富的信息。Allure报告框架是一个绝佳的选择它不仅能生成美观的HTML报告其丰富的附件截图、日志、文本功能和灵活的标签系统也为报警信息提供了素材。我们可以通过Pytest的钩子函数在用例执行的关键节点插入我们的采集逻辑# conftest.py 继续 import pytest from datetime import datetime import os # 定义一个全局状态对象用于汇聚当前测试会话的信息 class TestSessionState: def __init__(self): self.start_time datetime.now() self.failure_count 0 self.current_failures [] # 存储本次报警周期内的失败信息 self.alert_cooldown {} # 用于报警冷却的字典 session_state TestSessionState() pytest.hookimpl(tryfirstTrue, hookwrapperTrue) def pytest_runtest_makereport(item, call): # 获取基础的测试报告对象 outcome yield report outcome.get_result() # 仅当用例执行阶段且失败/错误时进行详细采集 if report.when call and report.failed: session_state.failure_count 1 failure_detail { test_name: item.nodeid, failure_time: datetime.now().isoformat(), outcome: report.outcome, longrepr: str(report.longrepr) if report.longrepr else No error info, } # 尝试获取page对象并截图如果fixture存在 page item.funcargs.get(page) if page: # 附加失败瞬间的截图和页面源代码到Allure screenshot page.screenshot(full_pageTrue, typepng) allure.attach(screenshot, nameFailure Screenshot, attachment_typeallure.attachment_type.PNG) page_source page.content() allure.attach(page_source, namePage Source, attachment_typeallure.attachment_type.HTML) failure_detail[screenshot_path] fattachments/{item.nodeid}_failure.png # 假设的路径 failure_detail[page_source_snippet] page_source[:500] # 取前500字符用于报警 # 将失败详情加入列表用于后续聚合报警 session_state.current_failures.append(failure_detail) # 判断是否达到即时报警阈值例如5分钟内失败超过3次 if _should_send_immediate_alert(): _send_aggregated_alert(session_state.current_failures) session_state.current_failures.clear() # 发送后清空当前批次3.3 报警逻辑实现聚合、收敛与发送采集到失败信息后不能来一个发一个。我们需要在_send_aggregated_alert函数中实现2.2节提到的汇聚分析逻辑。import hashlib import time def _should_send_immediate_alert(): 判断是否满足发送即时报警的条件 # 条件1当前批次失败数超过阈值例如3个 if len(session_state.current_failures) 3: return True # 条件2存在P0级别错误如页面崩溃、核心接口失败 for failure in session_state.current_failures: if crash in failure.get(longrepr, ).lower() or 500 in failure.get(longrepr, ): return True return False def _send_aggregated_alert(failure_list): 发送聚合报警信息 if not failure_list: return # 1. 生成报警唯一标识用于收敛例如用失败用例名的MD5前8位 failure_signature _generate_failure_signature(failure_list) current_time time.time() # 2. 检查冷却时间例如相同签名报警30分钟内只发一次 last_alert_time session_state.alert_cooldown.get(failure_signature, 0) if current_time - last_alert_time 30 * 60: # 30分钟 print(f报警 {failure_signature} 处于冷却期跳过发送。) return # 3. 格式化报警消息 alert_title f【UI自动化报警】{len(failure_list)}个用例失败 env os.getenv(TEST_ENV, Unknown) alert_title f - 环境{env} # 构建Markdown格式的消息体以钉钉机器人为例 markdown_text f### {alert_title}\n\n markdown_text f**发生时间** {datetime.now().strftime(%Y-%m-%d %H:%M:%S)}\n markdown_text f**失败数量** {len(failure_list)}\n\n markdown_text **失败用例摘要**\n for idx, failure in enumerate(failure_list[:5]): # 最多展示5条详情 test_name_short failure[test_name].split(::)[-1] markdown_text f{idx1}. {test_name_short}\n # 截取错误信息第一行作为概要 error_preview failure[longrepr].split(\n)[0][:100] markdown_text f 错误{error_preview}...\n if len(failure_list) 5: markdown_text f\n... 以及另外 {len(failure_list)-5} 个失败用例。\n markdown_text f\n**Allure报告链接** [点击查看详情]({os.getenv(ALLURE_REPORT_URL, #)})\n markdown_text f\n**请相关同事及时排查** 所有人 # 4. 调用发送函数此处以钉钉为例 dingding_webhook os.getenv(DINGDING_WEBHOOK) if dingding_webhook: _send_dingding_alert(dingding_webhook, markdown_text, alert_title) # 更新冷却时间 session_state.alert_cooldown[failure_signature] current_time else: print(钉钉Webhook未配置报警消息如下) print(markdown_text) def _generate_failure_signature(failure_list): 根据失败用例名称生成简单的签名用于收敛判断 names sorted([f[test_name] for f in failure_list]) combined -.join(names) return hashlib.md5(combined.encode()).hexdigest()[:8] def _send_dingding_alert(webhook, markdown_text, title): 发送消息到钉钉群机器人 import json import requests headers {Content-Type: application/json} data { msgtype: markdown, markdown: { title: title, text: markdown_text }, at: { isAtAll: True # 所有人可根据需要替换为具体手机号 } } try: resp requests.post(webhook, headersheaders, datajson.dumps(data), timeout5) if resp.status_code 200: print(钉钉报警发送成功) else: print(f钉钉报警发送失败: {resp.status_code}, {resp.text}) except Exception as e: print(f发送钉钉报警时发生异常: {e})3.4 环境健康检查与心跳机制除了测试本身的失败执行环境健康度也至关重要。我们需要一个独立于测试套件的心跳或健康检查机制。可以编写一个轻量的health_check.py脚本在测试任务开始前、结束后甚至定时执行# health_check.py import requests import psutil import socket from datetime import datetime def check_application_health(url): 检查被测应用健康状态 try: resp requests.get(url, timeout10) return resp.status_code 200, f状态码: {resp.status_code} except Exception as e: return False, f连接异常: {e} def check_test_node_resources(): 检查测试节点资源 cpu_percent psutil.cpu_percent(interval1) mem psutil.virtual_memory() return cpu_percent 90 and mem.percent 85, fCPU: {cpu_percent}%, 内存: {mem.percent}% def check_network_connectivity(host8.8.8.8, port53, timeout3): 检查网络连通性 try: socket.create_connection((host, port), timeouttimeout) return True, 网络通畅 except OSError: return False, 网络连接失败 def perform_health_check(): 执行综合健康检查 checks [ (应用健康, check_application_health, [https://your-app.com/health]), (节点资源, check_test_node_resources, []), (网络连通, check_network_connectivity, []), ] all_ok True failure_messages [] for check_name, check_func, args in checks: is_ok, message check_func(*args) if not is_ok: all_ok False failure_messages.append(f{check_name}失败: {message}) print(f[{datetime.now()}] {check_name}: {OK if is_ok else FAIL} - {message}) # 如果有检查失败触发报警 if not all_ok: alert_msg 【环境健康报警】测试环境异常\n \n.join(failure_messages) # 调用报警发送函数 # send_alert(P1-环境异常, alert_msg) return all_ok if __name__ __main__: perform_health_check()这个健康检查脚本可以配置到CI/CD流水线中作为测试任务的前置或后置步骤也可以由系统定时任务如cron定期执行实现环境状态的持续监控。4. 集成与部署让监控报警体系运转起来有了核心组件下一步就是将它们集成到完整的测试流程和运维体系中。这里的关键是自动化和可观测性。4.1 与CI/CD流水线深度集成现代软件交付离不开CI/CD。我们的监控报警体系应该无缝嵌入流水线成为质量关卡的一部分。作为流水线的一个Stage在Jenkins、GitLab CI、GitHub Actions等工具中将UI自动化测试定义为一个独立的Job或Stage。在这个Stage中按顺序执行步骤1环境健康检查。如果失败直接终止并报警避免浪费资源执行注定失败的测试。步骤2执行测试套件。使用Pytest执行用例并确保我们的监听器和钩子生效。步骤3生成并发布Allure报告。将富媒体报告发布到静态服务器或专门的服务如Allure Service生成一个可公开访问的URL。步骤4聚合分析并发送报警。根据测试执行结果通过Pytest的退出码或解析结果文件调用我们的报警逻辑。这里有个技巧报警的发送可以放在流水线Stage的最后无论测试成功还是失败都执行用于发送“测试完成”的汇总通知成功时报喜失败时报警。环境变量与配置管理所有敏感信息如钉钉Webhook URL、测试环境地址、报警阈值都应通过CI/CD平台的环境变量或配置文件注入而不是硬编码在脚本中。这保证了安全性和多环境适配。4.2 搭建简单的报警状态看板对于测试负责人或运维人员一个集中的可视化看板比零散的报警消息更利于掌控全局。我们可以用一个轻量级的方式实现方案A利用现有监控工具如果公司有Grafana可以将测试结果如通过率、失败次数、执行时长通过一个简单的脚本推送到Prometheus或InfluxDB然后在Grafana中配置图表和报警规则。这样就能在一个平台上看到业务监控、系统监控和测试监控的所有信息。方案B极简Web看板用Flask或FastAPI写一个简单的Web服务提供一个HTTP接口接收测试结果。前端用一个HTML页面定时轮询用图表如ECharts展示最近几次测试的通过率趋势、失败用例分类等。虽然简陋但对于小团队来说足够直观。# 一个极简的接收测试结果的API示例 (app.py) from flask import Flask, request, jsonify from datetime import datetime app Flask(__name__) test_results_db [] # 简单用列表存储生产环境请用数据库 app.route(/api/test-result, methods[POST]) def receive_result(): data request.json data[received_at] datetime.now().isoformat() test_results_db.append(data) # 只保留最近100条记录 if len(test_results_db) 100: test_results_db.pop(0) return jsonify({status: ok}) app.route(/api/dashboard-data) def get_dashboard_data(): # 提供看板所需的数据如最近10次执行的概况 recent_results test_results_db[-10:] return jsonify(recent_results) if __name__ __main__: app.run(host0.0.0.0, port5000)然后在测试脚本执行完毕后调用这个API上报结果# 在测试任务结束后上报 import requests summary_data { project: 你的项目名, env: os.getenv(TEST_ENV), total: session_state.total_tests, passed: session_state.passed_tests, failed: session_state.failure_count, duration: total_duration, timestamp: datetime.now().isoformat(), report_url: allure_report_url } requests.post(http://your-dashboard-host:5000/api/test-result, jsonsummary_data)4.3 配置管理与维护要点一个可维护的系统离不开好的配置管理。分级配置建议使用配置文件如config.yaml或.env管理不同环境的参数。# config.yaml environments: staging: app_url: https://staging.example.com dingding_webhook: ${DINGDING_STAGING_WEBHOOK} alert_threshold: immediate_failure_count: 3 cooldown_minutes: 30 production: app_url: https://example.com dingding_webhook: ${DINGDING_PROD_WEBHOOK} alert_threshold: immediate_failure_count: 1 # 生产环境更敏感 cooldown_minutes: 10报警联系人管理不要总是所有人。可以根据失败用例的模块、标签Tag来映射不同的负责人。例如给支付相关的用例打上payment标签当这些用例失败时报警信息只支付团队的开发人员。这需要在报警逻辑中增加一个路由匹配的环节。日志与审计所有报警的发送、接收以及健康检查的结果都应该记录到日志文件或日志系统中如ELK。这对于事后复盘、分析报警有效性、优化报警规则至关重要。5. 常见问题排查与优化实录即便设计得再完善在实际运行中还是会遇到各种问题。下面是我在实践中遇到的一些典型问题及解决方案。5.1 报警风暴与噪音抑制问题现象凌晨服务抖动一下触发了几百个用例失败手机被报警信息轰炸到没电真正醒来后反而找不到重点。根因分析报警收敛策略不足或失效。没有对同类、同根因的报警进行聚合。解决方案实施多级收敛第一级脚本内如3.3节所示在发送前进行签名和冷却时间判断。第二级报警通道利用钉钉或企业微信机器人本身的消息频率限制通常有阈值但不要依赖这个因为可能直接触发机器人被限流。第三级中间件引入一个简单的报警代理服务。所有报警请求先发到这个代理由代理进行更智能的聚合、去重和频率控制再转发给最终渠道。这个代理可以维护一个短期内存数据库记录最近报警的状态。优化报警内容聚合报警信息不要一条用例发一条。将短时间内同一模块、同一错误类型的失败用例合并成一条消息在消息体内用列表展示。设立报警静默期对于预知的维护窗口如每周二凌晨3点-4点的数据库维护可以提前在系统中配置静默规则在此期间抑制非P0级别的报警。5.2 偶发性失败Flaky Tests干扰问题现象某些用例时好时坏失败时报警了但等开发去看时又好了浪费排查精力。根因分析测试本身不稳定可能由于网络延迟、第三方依赖不稳定、前端动画未完成、时间戳断言等原因导致。解决方案识别与标记首先通过历史报告找出这些“flaky”的用例。Allure报告的历史趋势图很有用。给这些用例打上特殊的标签如flaky。差异化处理在报警逻辑中对带有flaky标签的用例失败采用不同的策略。例如提高其报警阈值比如连续失败2次才报或者将其报警级别降为P3仅记录不发送即时通知。自动重试与熔断在测试框架层面Pytest有pytest-rerunfailures插件为flaky用例配置自动重试如失败后重试2次。同时如果某个用例在一天内失败次数超过一个上限如5次则自动将其标记为“熔断”在接下来的几次运行中跳过并发送一条需要人工介入的报警提示“某Flaky用例已熔断请检查”。根本治理建立定期任务分析Flaky用例的失败截图和日志组织测试和开发一起进行根因治理将其转化为稳定用例。5.3 环境问题与“假报警”问题现象收到大量用例失败报警但排查发现是测试环境数据库连接不上、或者测试账号被锁导致的并非产品缺陷。根因分析环境准备或清理不充分测试数据污染或状态残留。解决方案强化前置健康检查如3.4节所述在执行测试套件前进行严格的环境健康检查。除了应用本身还要检查数据库、缓存、消息队列等关键依赖。任何一项不通过则阻塞测试执行并发送“环境异常”报警。实现测试数据隔离与自清理每个测试用例或测试类应该使用独立的数据集如通过唯一的用户ID、订单号前缀。用例执行前创建数据执行后通过teardown方法必须清理自己创建的数据。对于难以清理的全局状态可以考虑在每次测试任务开始时通过调用管理接口或执行数据库脚本来重置测试环境到一个干净的状态。报警分类与路由在报警信息中明确区分是“产品缺陷”还是“环境/测试框架问题”。可以通过分析错误堆栈的关键字来实现初步分类例如错误信息包含Connection refused、Timeout、Element not found可能偏向环境/脚本问题包含AssertionError且与业务逻辑相关则偏向产品缺陷。不同类型的报警可以路由到不同的群或负责人如环境问题报给运维或测试开发产品缺陷报给对应开发。5.4 报警疲劳与响应失灵问题现象报警太多团队逐渐麻木看到报警也不立即处理失去了报警的意义。根因分析报警阈值设置不合理过多低级别、非紧急的报警淹没了重要的信号。解决方案精细化报警级别建立清晰的报警级别定义并与响应SLA挂钩。级别定义示例响应SLA通知渠道P0-紧急核心功能完全不可用影响线上用户或主流程登录、支付全部失败15分钟电话/短信 IMP1-高主要功能严重受损影响大量用户或测试核心列表页无法加载30%用例失败1小时IM 相关人P2-中次要功能问题或非阻塞性缺陷某个次要按钮样式错位个别边缘用例失败下一个工作日IM 群内通知P3-低建议性改进或极低概率偶发问题控制台轻微警告已知的Flaky测试失败记录即可无需即时通知定期回顾与优化每周或每两周团队一起回顾过去一段时间的报警记录。讨论哪些报警是有效的、哪些是噪音、哪些报警被遗漏了。根据回顾结果动态调整报警阈值、收敛规则和路由策略。这是一个持续优化的过程。设立“值班”制度对于P0和P1级别的报警可以像运维一样设立“测试值班”制度确保在任何时候都有明确的责任人能够第一时间响应和处理。实现实时监控和报警的UI自动化测试本质上是在测试执行能力和测试运维能力之间架起一座桥梁。它要求我们从单纯的“脚本编写者”转变为“质量工程体系的设计者”。这套体系的搭建初期会有些工作量但一旦运转起来它所带来的快速反馈、问题透明化和团队效率提升价值是巨大的。最让我有成就感的一点是当凌晨三点收到一条报警点开一看发现是环境网络问题而非线上故障时那种能继续安心睡觉的感觉是之前“盲跑”测试时从未有过的。