修改截图保存文件夹

This commit is contained in:
2026-02-10 14:41:13 +08:00
parent 4e49793416
commit 6274c83dd5
12 changed files with 437 additions and 306 deletions

Binary file not shown.

View File

@@ -378,6 +378,107 @@ def get_user_max_variation(username: str) -> Optional[int]:
return None return None
requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning) requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning)
# def get_line_info_and_save_global(user_name: str) -> bool:
# """
# 调用get_name_all接口提取status=3的line_num和line_name存入全局字典
# :param user_name: 接口请求参数,如"wangshun"
# :return: 执行成功返回True失败/异常返回False
# """
# # 接口基础配置
# api_url = "https://engineering.yuxindazhineng.com/index/index/get_name_all"
# request_params = {"user_name": user_name} # GET请求参数
# timeout = 10 # 请求超时时间(秒),避免卡进程
# try:
# # 1. 发送GET请求
# response = requests.get(
# url=api_url,
# params=request_params, # GET参数用params传递自动拼接到URL后规范且防乱码
# timeout=timeout,
# verify=False # 禁用SSL验证适配HTTPS接口
# )
# # 2. 校验HTTP状态码先确保请求本身成功
# if response.status_code != 200:
# logging.error(f"接口请求失败HTTP状态码异常{response.status_code},响应内容:{response.text}")
# return False
# # 3. 解析JSON响应接口返回是JSON格式需解析为字典
# try:
# response_data = response.json()
# except Exception as e:
# logging.error(f"接口返回内容非合法JSON无法解析{response.text},错误:{str(e)}")
# return False
# # 4. 校验业务状态码接口约定code=0成功-1失败
# business_code = response_data.get("code")
# if business_code == 0:
# logging.info("接口业务请求成功,开始解析数据")
# elif business_code == -1:
# logging.error(f"接口业务请求失败业务状态码code=-1返回数据{response_data}")
# return False
# else:
# logging.warning(f"接口返回未知业务状态码:{business_code},请确认接口文档")
# return False
# # 5. 提取data字段校验数据是否存在
# api_data_list = response_data.get("data")
# if not api_data_list:
# logging.warning("接口业务成功但data字段为空或无数据")
# return False
# # 6. 校验data是否为列表类型
# if not isinstance(api_data_list, list):
# logging.error(f"data字段不是列表类型实际类型{type(api_data_list)},内容:{api_data_list}")
# return False
# found_valid_data = False
# # 7. 遍历列表提取所有status=3的数据
# for item in api_data_list:
# # 确保每个item是字典
# if not isinstance(item, dict):
# logging.warning(f"列表中的元素不是字典类型,跳过:{item}")
# continue
# # 获取字段值
# data_status = item.get("status")
# line_num = item.get("line_num")
# line_name = item.get("line_name")
# # 校验status是否为3且目标字段非空
# if data_status == 3 and line_num and line_name:
# # # 存入全局字典key=line_numvalue=line_name
# # global_variable.GLOBAL_UPLOAD_BREAKPOINT_DICT[line_num] = line_name
# # 存入全局字典key=line_namevalue=line_num
# global_variable.get_upload_breakpoint_dict()[line_name] = line_num
# print(f"当前全局字典数据上传线路字典数据:{global_variable.get_upload_breakpoint_dict()}")
# # 如果line_name不在列表中则添加
# if line_name not in global_variable.get_upload_breakpoint_list():
# global_variable.get_upload_breakpoint_list().append(line_name)
# logging.info(f"找到status=3的线路信息line_num={line_num}, line_name={line_name}")
# found_valid_data = True
# if found_valid_data:
# logging.info(f"成功提取所有status=3的线路信息当前全局字典数据{global_variable.get_upload_breakpoint_dict()}")
# return True
# else:
# logging.warning("data列表中未找到任何status=3且字段完整的线路信息")
# return False
# # 捕获所有请求相关异常(超时、连接失败、网络异常等)
# except requests.exceptions.Timeout:
# logging.error(f"调用get_name_all接口超时超时时间{timeout}秒,请求参数:{request_params}")
# return False
# except requests.exceptions.ConnectionError:
# logging.error(f"调用get_name_all接口连接失败检查网络或接口地址是否正确{api_url}")
# return False
# except Exception as e:
# logging.error(f"调用get_name_all接口时发生未知异常{str(e)}", exc_info=True) # exc_info=True打印异常堆栈方便排查
# return False
def get_line_info_and_save_global(user_name: str) -> bool: def get_line_info_and_save_global(user_name: str) -> bool:
""" """
调用get_name_all接口提取status=3的line_num和line_name存入全局字典 调用get_name_all接口提取status=3的line_num和line_name存入全局字典
@@ -388,7 +489,10 @@ def get_line_info_and_save_global(user_name: str) -> bool:
api_url = "https://engineering.yuxindazhineng.com/index/index/get_name_all" api_url = "https://engineering.yuxindazhineng.com/index/index/get_name_all"
request_params = {"user_name": user_name} # GET请求参数 request_params = {"user_name": user_name} # GET请求参数
timeout = 10 # 请求超时时间(秒),避免卡进程 timeout = 10 # 请求超时时间(秒),避免卡进程
max_retries = 3 # 最大重试次数
retry_interval = 2 # 重试间隔(秒)
for retry in range(max_retries):
try: try:
# 1. 发送GET请求 # 1. 发送GET请求
response = requests.get( response = requests.get(
@@ -401,6 +505,10 @@ def get_line_info_and_save_global(user_name: str) -> bool:
# 2. 校验HTTP状态码先确保请求本身成功 # 2. 校验HTTP状态码先确保请求本身成功
if response.status_code != 200: if response.status_code != 200:
logging.error(f"接口请求失败HTTP状态码异常{response.status_code},响应内容:{response.text}") logging.error(f"接口请求失败HTTP状态码异常{response.status_code},响应内容:{response.text}")
if retry < max_retries - 1:
logging.info(f"将在{retry_interval}秒后进行第{retry+2}次重试")
time.sleep(retry_interval)
continue
return False return False
# 3. 解析JSON响应接口返回是JSON格式需解析为字典 # 3. 解析JSON响应接口返回是JSON格式需解析为字典
@@ -408,6 +516,10 @@ def get_line_info_and_save_global(user_name: str) -> bool:
response_data = response.json() response_data = response.json()
except Exception as e: except Exception as e:
logging.error(f"接口返回内容非合法JSON无法解析{response.text},错误:{str(e)}") logging.error(f"接口返回内容非合法JSON无法解析{response.text},错误:{str(e)}")
if retry < max_retries - 1:
logging.info(f"将在{retry_interval}秒后进行第{retry+2}次重试")
time.sleep(retry_interval)
continue
return False return False
# 4. 校验业务状态码接口约定code=0成功-1失败 # 4. 校验业务状态码接口约定code=0成功-1失败
@@ -416,20 +528,36 @@ def get_line_info_and_save_global(user_name: str) -> bool:
logging.info("接口业务请求成功,开始解析数据") logging.info("接口业务请求成功,开始解析数据")
elif business_code == -1: elif business_code == -1:
logging.error(f"接口业务请求失败业务状态码code=-1返回数据{response_data}") logging.error(f"接口业务请求失败业务状态码code=-1返回数据{response_data}")
if retry < max_retries - 1:
logging.info(f"将在{retry_interval}秒后进行第{retry+2}次重试")
time.sleep(retry_interval)
continue
return False return False
else: else:
logging.warning(f"接口返回未知业务状态码:{business_code},请确认接口文档") logging.warning(f"接口返回未知业务状态码:{business_code},请确认接口文档")
if retry < max_retries - 1:
logging.info(f"将在{retry_interval}秒后进行第{retry+2}次重试")
time.sleep(retry_interval)
continue
return False return False
# 5. 提取data字段校验数据是否存在 # 5. 提取data字段校验数据是否存在
api_data_list = response_data.get("data") api_data_list = response_data.get("data")
if not api_data_list: if not api_data_list:
logging.warning("接口业务成功但data字段为空或无数据") logging.warning("接口业务成功但data字段为空或无数据")
if retry < max_retries - 1:
logging.info(f"将在{retry_interval}秒后进行第{retry+2}次重试")
time.sleep(retry_interval)
continue
return False return False
# 6. 校验data是否为列表类型 # 6. 校验data是否为列表类型
if not isinstance(api_data_list, list): if not isinstance(api_data_list, list):
logging.error(f"data字段不是列表类型实际类型{type(api_data_list)},内容:{api_data_list}") logging.error(f"data字段不是列表类型实际类型{type(api_data_list)},内容:{api_data_list}")
if retry < max_retries - 1:
logging.info(f"将在{retry_interval}秒后进行第{retry+2}次重试")
time.sleep(retry_interval)
continue
return False return False
found_valid_data = False found_valid_data = False
@@ -466,17 +594,33 @@ def get_line_info_and_save_global(user_name: str) -> bool:
return True return True
else: else:
logging.warning("data列表中未找到任何status=3且字段完整的线路信息") logging.warning("data列表中未找到任何status=3且字段完整的线路信息")
if retry < max_retries - 1:
logging.info(f"将在{retry_interval}秒后进行第{retry+2}次重试")
time.sleep(retry_interval)
continue
return False return False
# 捕获所有请求相关异常(超时、连接失败、网络异常等) # 捕获所有请求相关异常(超时、连接失败、网络异常等)
except requests.exceptions.Timeout: except requests.exceptions.Timeout:
logging.error(f"调用get_name_all接口超时超时时间{timeout}秒,请求参数:{request_params}") logging.error(f"调用get_name_all接口超时超时时间{timeout}秒,请求参数:{request_params}")
if retry < max_retries - 1:
logging.info(f"将在{retry_interval}秒后进行第{retry+2}次重试")
time.sleep(retry_interval)
continue
return False return False
except requests.exceptions.ConnectionError: except requests.exceptions.ConnectionError:
logging.error(f"调用get_name_all接口连接失败检查网络或接口地址是否正确{api_url}") logging.error(f"调用get_name_all接口连接失败检查网络或接口地址是否正确{api_url}")
if retry < max_retries - 1:
logging.info(f"将在{retry_interval}秒后进行第{retry+2}次重试")
time.sleep(retry_interval)
continue
return False return False
except Exception as e: except Exception as e:
logging.error(f"调用get_name_all接口时发生未知异常{str(e)}", exc_info=True) # exc_info=True打印异常堆栈方便排查 logging.error(f"调用get_name_all接口时发生未知异常{str(e)}", exc_info=True) # exc_info=True打印异常堆栈方便排查
if retry < max_retries - 1:
logging.info(f"将在{retry_interval}秒后进行第{retry+2}次重试")
time.sleep(retry_interval)
continue
return False return False
def get_accounts_from_server(yh_id): def get_accounts_from_server(yh_id):

View File

@@ -126,6 +126,8 @@ def set_upload_success_breakpoint_list(success_list):
"""设置上传成功的断点列表""" """设置上传成功的断点列表"""
thread_local.GLOBAL_UPLOAD_SUCCESS_BREAKPOINT_LIST = success_list thread_local.GLOBAL_UPLOAD_SUCCESS_BREAKPOINT_LIST = success_list
# 为了保持 ,保留原有的全局变量名称 # 为了保持 ,保留原有的全局变量名称
# 但这些将不再被直接使用,而是通过上面的函数访问 # 但这些将不再被直接使用,而是通过上面的函数访问
GLOBAL_DEVICE_ID = "" # 设备ID GLOBAL_DEVICE_ID = "" # 设备ID

18
main.py
View File

@@ -16,9 +16,7 @@ import permissions # 导入权限处理模块
import globals.apis as apis import globals.apis as apis
from globals.driver_utils import init_appium_driver, ensure_appium_server_running, safe_quit_driver, is_app_launched, launch_app_manually from globals.driver_utils import init_appium_driver, ensure_appium_server_running, safe_quit_driver, is_app_launched, launch_app_manually
from page_objects.login_page import LoginPage from page_objects.login_page import LoginPage
from page_objects.download_tabbar_page import DownloadTabbarPage
from page_objects.screenshot_page import ScreenshotPage from page_objects.screenshot_page import ScreenshotPage
from page_objects.upload_config_page import UploadConfigPage
from page_objects.more_download_page import MoreDownloadPage from page_objects.more_download_page import MoreDownloadPage
@@ -35,8 +33,14 @@ logging.basicConfig(
class DeviceAutomation(object): class DeviceAutomation(object):
def __init__(self, device_id=None): def __init__(self, device_id=None, project_name=None):
self.device_id = device_id self.device_id = device_id
self.project_name = project_name
# 设置项目名称到全局变量
if project_name:
global_variable.set_current_project_name(project_name)
logging.info(f"设备 {self.device_id} 已设置项目名称: {project_name}")
# 初始化权限 # 初始化权限
if permissions.grant_appium_permissions(self.device_id): if permissions.grant_appium_permissions(self.device_id):
@@ -62,9 +66,7 @@ class DeviceAutomation(object):
# 初始化页面对象 # 初始化页面对象
logging.info(f"设备 {self.device_id} 开始初始化页面对象") logging.info(f"设备 {self.device_id} 开始初始化页面对象")
self.login_page = LoginPage(self.driver, self.wait) self.login_page = LoginPage(self.driver, self.wait)
self.download_tabbar_page = DownloadTabbarPage(self.driver, self.wait, self.device_id)
self.screenshot_page = ScreenshotPage(self.driver, self.wait, self.device_id) self.screenshot_page = ScreenshotPage(self.driver, self.wait, self.device_id)
self.upload_config_page = UploadConfigPage(self.driver, self.wait, self.device_id)
self.more_download_page = MoreDownloadPage(self.driver, self.wait,self.device_id) self.more_download_page = MoreDownloadPage(self.driver, self.wait,self.device_id)
logging.info(f"设备 {self.device_id} 所有页面对象初始化完成") logging.info(f"设备 {self.device_id} 所有页面对象初始化完成")
@@ -246,13 +248,15 @@ class DeviceAutomation(object):
safe_quit_driver(getattr(self, 'driver', None), self.device_id) safe_quit_driver(getattr(self, 'driver', None), self.device_id)
@staticmethod @staticmethod
def start_upload(device_id=None, upload_time=None): def start_upload(device_id=None, upload_time=None, project_name=None):
""" """
供其他页面或模块调用的静态方法 供其他页面或模块调用的静态方法
执行完整的自动化流程 执行完整的自动化流程
参数: 参数:
device_id: 可选的设备ID如果为None则自动获取 device_id: 可选的设备ID如果为None则自动获取
upload_time: 上传时间
project_name: 项目名称
返回: 返回:
bool: 自动化流程执行结果True/False bool: 自动化流程执行结果True/False
@@ -260,7 +264,7 @@ class DeviceAutomation(object):
automation = None automation = None
try: try:
# 创建自动化实例 # 创建自动化实例
automation = DeviceAutomation(device_id=device_id) automation = DeviceAutomation(device_id=device_id, project_name=project_name)
# 执行自动化流程 # 执行自动化流程
success = automation.run_automation() success = automation.run_automation()

View File

@@ -4,6 +4,7 @@ import logging
import time import time
import re import re
import os import os
import threading
from datetime import datetime from datetime import datetime
from appium.webdriver.common.appiumby import AppiumBy from appium.webdriver.common.appiumby import AppiumBy
from selenium.common.exceptions import NoSuchElementException, TimeoutException from selenium.common.exceptions import NoSuchElementException, TimeoutException
@@ -19,7 +20,7 @@ import globals.global_variable as global_variable # 导入全局变量模块
class ScreenshotPage: class ScreenshotPage:
def __init__(self, driver, wait, device_id=None): def __init__(self, driver, wait, device_id=None):
self.driver = driver self.driver = driver
self.wait = wait self.wait = WebDriverWait(driver, 2)
self.device_id = device_id self.device_id = device_id
self.logger = logging.getLogger(__name__) self.logger = logging.getLogger(__name__)
self.all_items = set() self.all_items = set()
@@ -528,7 +529,7 @@ class ScreenshotPage:
self.logger.error(f"设备 {device_id} 检查WiFi状态时发生错误: {str(e)}") self.logger.error(f"设备 {device_id} 检查WiFi状态时发生错误: {str(e)}")
return None return None
def take_screenshot(self, filename_prefix="screenshot"): def take_screenshot(self, filename_prefix="screenshot", date_str=None, time_str=None):
""" """
通过Appium驱动截取设备屏幕 通过Appium驱动截取设备屏幕
@@ -539,20 +540,58 @@ class ScreenshotPage:
bool: 操作是否成功 bool: 操作是否成功
""" """
try: try:
# 创建测试结果目录 # 获取项目名称
screenshots_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../test_results/screenshots') project_name = global_variable.get_current_project_name() or "默认项目"
if not os.path.exists(screenshots_dir):
os.makedirs(screenshots_dir) # 获取当前日期(如果没有提供)
self.logger.info(f"创建截图目录: {screenshots_dir}") if not date_str:
date_str = datetime.now().strftime("%Y%m%d")
# 获取当前时间(如果没有提供),并确保格式合法(不含冒号)
if not time_str:
time_str = datetime.now().strftime("%H%M%S")
else:
# 移除时间中的冒号,确保文件名合法
time_str = time_str.replace(":", "")
# 创建D盘下的截图目录结构D:\uploadInfo\picture\项目名\年月日
screenshots_dir = os.path.join("D:\\", "uploadInfo", "picture", project_name, date_str)
# 确保目录存在
try:
os.makedirs(screenshots_dir, exist_ok=True)
self.logger.info(f"截图目录: {screenshots_dir}")
except Exception as dir_error:
self.logger.error(f"创建截图目录失败: {str(dir_error)}")
return False
line_code = global_variable.get_upload_breakpoint_dict().get(filename_prefix)
if not line_code:
self.logger.error(f"未找到与断点名称 {filename_prefix} 对应的线路编码")
line_code = "unknown"
# 截图保存 # 截图保存
screenshot_file = os.path.join( screenshot_file = os.path.join(
screenshots_dir, screenshots_dir,
f"{filename_prefix}_{datetime.now().strftime('%Y%m%d')}.png" f"{line_code}_{filename_prefix}_{time_str}.png"
) )
self.driver.save_screenshot(screenshot_file)
# 尝试保存截图
try:
success = self.driver.save_screenshot(screenshot_file)
if success:
self.logger.info(f"截图已保存: {screenshot_file}") self.logger.info(f"截图已保存: {screenshot_file}")
# 验证文件是否真的存在
if os.path.exists(screenshot_file):
self.logger.info(f"截图文件验证存在: {screenshot_file}")
else:
self.logger.warning(f"截图文件保存成功但验证不存在: {screenshot_file}")
return True return True
else:
self.logger.error(f"Appium截图保存失败: {screenshot_file}")
return False
except Exception as save_error:
self.logger.error(f"保存截图时发生错误: {str(save_error)}")
return False
except Exception as e: except Exception as e:
self.logger.error(f"截图时发生错误: {str(e)}") self.logger.error(f"截图时发生错误: {str(e)}")
@@ -591,117 +630,79 @@ class ScreenshotPage:
self.logger.error(f"写入截图状态文件时发生错误: {str(e)}") self.logger.error(f"写入截图状态文件时发生错误: {str(e)}")
return False return False
def wait_for_measurement_end(self, timeout=900): def update_file_status(self, username, from_status, to_status):
""" """
等待按钮变成"测量结束"最多15分钟包含驱动重新初始化机制 安全地更新 time.txt 中该用户的状态
例如: 将 'true' 改为 'running', 或将 'running' 改为 'done'
Args:
timeout: 超时时间默认900秒15分钟
Returns:
bool: 是否成功等到测量结束按钮
""" """
try: TIME_FILE_PATH = r"D:\uploadInfo\time.txt"
# 更新WebDriverWait等待时间为900秒 if not os.path.exists(TIME_FILE_PATH):
self.wait = WebDriverWait(self.driver, 900)
self.logger.info(f"设备等待测量结束按钮出现,最多等待 {timeout}")
start_time = time.time()
reinit_attempts = 0
max_reinit_attempts = 3 # 最大重新初始化次数
while time.time() - start_time < timeout:
try:
# 使用XPath查找文本为"测量结束"的按钮
measurement_end_button = self.driver.find_element(
AppiumBy.XPATH,
"//android.widget.Button[@text='测量结束']"
)
if measurement_end_button.is_displayed() and measurement_end_button.is_enabled():
self.logger.info(f"设备检测到测量结束按钮")
return True
except NoSuchElementException:
# 按钮未找到,继续等待
pass
except Exception as e:
error_msg = str(e)
self.logger.warning(f"设备查找测量结束按钮时出现异常: {error_msg}")
# 检测是否是UiAutomator2服务崩溃
if 'UiAutomator2 server' in error_msg and 'instrumentation process is not running' in error_msg and reinit_attempts < max_reinit_attempts:
reinit_attempts += 1
self.logger.info(f"设备检测到UiAutomator2服务崩溃尝试第 {reinit_attempts} 次重新初始化驱动")
# 尝试重新初始化驱动
if self._reinit_driver():
self.logger.info(f"设备驱动重新初始化成功")
else:
self.logger.error(f"设备驱动重新初始化失败")
# 继续尝试,而不是立即失败
# 等待一段时间后再次检查
time.sleep(3)
# 每30秒输出一次等待状态
if int(time.time() - start_time) % 30 == 0:
elapsed = int(time.time() - start_time)
self.logger.info(f"设备 {self.device_id} 已等待 {elapsed} 秒,仍在等待测量结束...")
self.logger.error(f"设备 {self.device_id} 等待测量结束按钮超时")
return False return False
success = False
file_lock = threading.Lock()
with file_lock:
try:
with open(TIME_FILE_PATH, 'r', encoding='utf-8') as f:
lines = f.readlines()
# new_lines = []
# for line in lines:
# clean_line = line.strip()
# # 匹配逻辑:包含用户名 且 以 from_status 结尾
# if f" {username} " in line and clean_line.endswith(from_status):
# line = line.replace(from_status, to_status)
# success = True
# new_lines.append(line)
new_lines = []
for line in lines:
# 使用正则确保精准匹配用户名和结尾状态
# 匹配规则:行内包含该用户名,且该行以 from_status 结尾
if re.search(rf'\b{username}\b', line) and line.strip().endswith(from_status):
# 只替换行尾的那个状态词
line = re.sub(rf'{from_status}$', to_status, line.rstrip()) + '\n'
success = True
new_lines.append(line)
with open(TIME_FILE_PATH, 'w', encoding='utf-8') as f:
f.writelines(new_lines)
if success:
print(f"📝 [文件更新] 用户 {username}: {from_status} -> {to_status}")
return success
except Exception as e: except Exception as e:
self.logger.error(f"设备 {self.device_id} 等待测量结束时发生错误: {str(e)}") print(f"❌ 更新文件状态失败 ({username}): {e}")
return False return False
def _reinit_driver(self): def update_upload_info_status(self, status):
""" """
重新初始化Appium驱动 更新D:/uploadInfo文件夹下的time.txt文件的状态
Returns: 参数:
bool: 是否成功重新初始化 status: 状态值,如"ok""again"
返回:
bool: 操作是否成功
""" """
try: try:
# 首先尝试关闭现有的驱动 # time.txt文件路径
if hasattr(self, 'driver') and self.driver: time_file_path = "D:\\uploadInfo\\time.txt"
try:
self.driver.quit()
except:
self.logger.warning("关闭现有驱动时出现异常")
# 导入必要的模块 # 确保文件夹存在
from appium import webdriver os.makedirs(os.path.dirname(time_file_path), exist_ok=True)
from appium.options.android import UiAutomator2Options
# 重新创建驱动配置 # 写入状态
options = UiAutomator2Options() with open(time_file_path, 'w', encoding='utf-8') as f:
options.platform_name = "Android" f.write(status)
options.device_name = self.device_id
options.app_package = "com.bjjw.cjgc"
options.app_activity = ".activity.LoginActivity"
options.automation_name = "UiAutomator2"
options.no_reset = True
options.auto_grant_permissions = True
options.new_command_timeout = 300
options.udid = self.device_id
# 重新连接驱动 self.logger.info(f"已更新上传状态文件: {time_file_path} -> {status}")
self.logger.info(f"正在重新初始化设备 {self.device_id} 的驱动...")
self.driver = webdriver.Remote("http://localhost:4723", options=options)
# 重新初始化等待对象
from selenium.webdriver.support.ui import WebDriverWait
self.wait = WebDriverWait(self.driver, 1)
self.logger.info(f"设备 {self.device_id} 驱动重新初始化完成")
return True return True
except Exception as e: except Exception as e:
self.logger.error(f"设备 {self.device_id} 驱动重新初始化失败: {str(e)}") self.logger.error(f"更新上传状态文件时发生错误: {str(e)}")
return False return False
def handle_confirmation_dialog(self, device_id, timeout=2): def handle_confirmation_dialog(self, device_id, timeout=2):
""" """
处理确认弹窗,点击""按钮 处理确认弹窗,点击""按钮
@@ -808,7 +809,7 @@ class ScreenshotPage:
if confirm_button and confirm_button.is_displayed() and confirm_button.is_enabled(): if confirm_button and confirm_button.is_displayed() and confirm_button.is_enabled():
self.logger.info(f"设备 {device_id} 点击确认弹窗的''按钮") self.logger.info(f"设备 {device_id} 点击确认弹窗的''按钮")
confirm_button.click() confirm_button.click()
time.sleep(1) time.sleep(0.5)
# 验证弹窗是否消失 # 验证弹窗是否消失
try: try:
@@ -828,7 +829,7 @@ class ScreenshotPage:
except Exception as e: except Exception as e:
self.logger.warning(f"设备 {device_id} 查找确认弹窗时出现异常: {str(e)}") self.logger.warning(f"设备 {device_id} 查找确认弹窗时出现异常: {str(e)}")
time.sleep(1) time.sleep(0.5)
self.logger.error(f"设备 {device_id} 等待返回确认弹窗超时") self.logger.error(f"设备 {device_id} 等待返回确认弹窗超时")
return False return False
@@ -1001,7 +1002,7 @@ class ScreenshotPage:
# 3. 验证是否成功返回到上一页面 # 3. 验证是否成功返回到上一页面
time.sleep(1) # 等待页面跳转完成 time.sleep(0.5) # 等待页面跳转完成
# 可以添加页面验证逻辑,比如检查是否返回到预期的页面 # 可以添加页面验证逻辑,比如检查是否返回到预期的页面
# 这里可以根据实际应用添加特定的页面元素验证 # 这里可以根据实际应用添加特定的页面元素验证
@@ -1013,51 +1014,6 @@ class ScreenshotPage:
self.logger.error(f"设备 {device_id} 执行返回导航步骤时发生错误: {str(e)}") self.logger.error(f"设备 {device_id} 执行返回导航步骤时发生错误: {str(e)}")
return False return False
def scroll_to_bottom_and_screenshot(self, device_id):
"""
检测到测量结束后下滑列表到最底端点击最后一个spinner再下滑一次点击平差处理按钮后截图
Args:
device_id: 设备ID
Returns:
bool: 操作是否成功
"""
try:
self.logger.info(f"设备 {device_id} 开始执行测量结束后的操作流程")
time.sleep(5)
# 1. 下滑列表到最底端
if not self.scroll_list_to_bottom(device_id):
self.logger.error(f"设备 {device_id} 下滑列表到底部失败")
return False
# 2. 点击最后一个spinner
if not self.click_last_spinner_with_retry(device_id):
self.logger.error(f"设备 {device_id} 点击最后一个spinner失败")
return False
# 3. 再下滑一次
if not self.scroll_down_once(device_id):
self.logger.warning(f"设备 {device_id} 再次下滑失败,但继续执行")
# 4. 点击平差处理按钮
if not self.click_adjustment_button(device_id):
self.logger.error(f"设备 {device_id} 点击平差处理按钮失败")
return False
# 5. 在点击平差处理按钮后截图
time.sleep(2) # 等待平差处理按钮点击后的界面变化
if not self.take_screenshot("after_adjustment_button_click"):
self.logger.error(f"设备 {device_id} 截图失败")
return False
self.logger.info(f"设备 {device_id} 测量结束后操作流程完成")
return True
except Exception as e:
self.logger.error(f"设备 {device_id} 执行测量结束后操作时发生错误: {str(e)}")
return False
def scroll_list_to_bottom(self, device_id, max_swipes=60): def scroll_list_to_bottom(self, device_id, max_swipes=60):
""" """
@@ -1129,7 +1085,7 @@ class ScreenshotPage:
if self.click_last_spinner(device_id): if self.click_last_spinner(device_id):
return True return True
self.logger.warning(f"设备 {device_id}{attempt + 1}次点击失败,准备重试") self.logger.warning(f"设备 {device_id}{attempt + 1}次点击失败,准备重试")
time.sleep(1) # 重试前等待 time.sleep(0.5) # 重试前等待
except Exception as e: except Exception as e:
self.logger.error(f"设备 {device_id}{attempt + 1}次尝试失败: {str(e)}") self.logger.error(f"设备 {device_id}{attempt + 1}次尝试失败: {str(e)}")
@@ -1280,7 +1236,7 @@ class ScreenshotPage:
'percent': 0.5 'percent': 0.5
}) })
time.sleep(1) time.sleep(0.2)
self.logger.info(f"设备 {device_id} 额外下滑完成") self.logger.info(f"设备 {device_id} 额外下滑完成")
return True return True
@@ -1470,7 +1426,7 @@ class ScreenshotPage:
self.logger.error(f"设备 {device_id} 加载线路时间映射字典失败") self.logger.error(f"设备 {device_id} 加载线路时间映射字典失败")
return False return False
time.sleep(5) # time.sleep(5)
# 循环检查数据数量是否一致,直到获取到完整数据 # 循环检查数据数量是否一致,直到获取到完整数据
retry_count = 0 retry_count = 0
@@ -1524,98 +1480,90 @@ class ScreenshotPage:
}) })
# 开始循环 # 开始循环
all_success = True
for breakpoint_name in global_variable.get_upload_breakpoint_dict().keys(): for breakpoint_name in global_variable.get_upload_breakpoint_dict().keys():
self.logger.info(f"开始处理要平差截图的断点 {breakpoint_name}") self.logger.info(f"开始处理要平差截图的断点 {breakpoint_name}")
# 把断点名称给find_keyword # 把断点名称给find_keyword
if not self.find_keyword(breakpoint_name): if not self.find_keyword(breakpoint_name):
self.logger.error(f"设备 {device_id} 未找到包含 {breakpoint_name} 的文件名") self.logger.error(f"设备 {device_id} 未找到包含 {breakpoint_name} 的文件名")
return False all_success = False
continue
if not self.handle_measurement_dialog(): if not self.handle_measurement_dialog():
self.logger.error(f"设备 {device_id} 处理测量弹窗失败") self.logger.error(f"设备 {device_id} 处理测量弹窗失败")
return False all_success = False
continue
if not self.check_apply_btn(): if not self.check_apply_btn():
self.logger.error(f"设备 {device_id} 检查平差处理按钮失败") self.logger.error(f"设备 {device_id} 检查平差处理按钮失败")
return False all_success = False
self.click_back_button(device_id)
continue
# 根据断点名称在get_upload_breakpoint_dict()中获取线路编码 # 根据断点名称在get_upload_breakpoint_dict()中获取线路编码
line_code = global_variable.get_upload_breakpoint_dict().get(breakpoint_name) line_code = global_variable.get_upload_breakpoint_dict().get(breakpoint_name)
if not line_code: if not line_code:
self.logger.error(f"设备 {device_id} 未找到断点 {breakpoint_name} 对应的线路编码") self.logger.error(f"设备 {device_id} 未找到断点 {breakpoint_name} 对应的线路编码")
return False all_success = False
continue
# 根据线路编码查找对应的时间 # 根据线路编码查找对应的时间
date_str, time_str = self.get_line_end_time(line_code) date_str, time_str = self.get_line_end_time(line_code)
if not time_str or not date_str: if not time_str or not date_str:
self.logger.error(f"设备 {device_id} 未找到线路 {line_code} 对应的时间") self.logger.error(f"设备 {device_id} 未找到线路 {line_code} 对应的时间")
return False all_success = False
continue
# 修改时间 # 修改时间
if not self.set_device_time(device_id, time_str, date_str): if not self.set_device_time(device_id, time_str, date_str):
self.logger.error(f"设备 {device_id} 设置设备时间失败") self.logger.error(f"设备 {device_id} 设置设备时间失败")
return False all_success = False
continue
# 滑动列表到底部 # 滑动列表到底部
if not self.scroll_list_to_bottom(device_id): if not self.scroll_list_to_bottom(device_id):
self.logger.error(f"设备 {device_id} 下滑列表到底部失败") self.logger.error(f"设备 {device_id} 下滑列表到底部失败")
return False all_success = False
continue
# 2. 点击最后一个spinner # 2. 点击最后一个spinner
if not self.click_last_spinner_with_retry(device_id): if not self.click_last_spinner_with_retry(device_id):
self.logger.error(f"设备 {device_id} 点击最后一个spinner失败") self.logger.error(f"设备 {device_id} 点击最后一个spinner失败")
return False all_success = False
continue
# 3. 再下滑一次 # 3. 再下滑一次
if not self.scroll_down_once(device_id): if not self.scroll_down_once(device_id):
self.logger.warning(f"设备 {device_id} 再次下滑失败,但继续执行") self.logger.warning(f"设备 {device_id} 再次下滑失败,但继续执行")
# # 4. 点击平差处理按钮
# if not self.click_adjustment_button(device_id):
# self.logger.error(f"设备 {device_id} 点击平差处理按钮失败")
# return False
# # 检查是否在测量页面在就重新执行选择断点滑动列表到底部点击最后一个spinner 再下滑一次,点击平差处理按钮平差
# if not self.handle_back_navigation(breakpoint_name, device_id):
# self.logger.error(f"{breakpoint_name}平差失败,未截图")
# return False
# # 检测并处理"是 保留成果"弹窗
# if not self.handle_adjustment_result_dialog():
# self.logger.error("处理平差结果弹窗失败")
# return False
# # 平差完成,将断点数据保存到上传列表中
# if not self.add_breakpoint_to_upload_list(breakpoint_name, line_code):
# self.logger.error(f"设备 {device_id} 保存断点 {breakpoint_name} 到上传列表失败")
# return False
# # 禁用WiFi
# if not self.disable_wifi(device_id):
# self.logger.error(f"设备 {device_id} 禁用WiFi失败")
# return False
# 平差处理完成后截图 # 平差处理完成后截图
time.sleep(3) # 等待平差处理按钮点击后的界面变化 time.sleep(0.2) # 等待平差处理按钮点击后的界面变化
logging.info("断点保存到上传列表成功,开始截图") logging.info("断点保存到上传列表成功,开始截图")
if not self.take_screenshot(breakpoint_name): # png_time = date_str + " " + time_str
if not self.take_screenshot(breakpoint_name, date_str, time_str):
self.logger.error(f"设备 {device_id} 截图失败") self.logger.error(f"设备 {device_id} 截图失败")
self.write_screenshot_status(breakpoint_name, success=False) self.write_screenshot_status(breakpoint_name, success=False)
return False all_success = False
else:
self.write_screenshot_status(breakpoint_name, success=True) self.write_screenshot_status(breakpoint_name, success=True)
# 点击返回按钮并处理弹窗 # 点击返回按钮并处理弹窗
if not self.execute_back_navigation_steps(device_id): if not self.execute_back_navigation_steps(device_id):
self.logger.error(f"设备 {device_id} 处理返回按钮确认失败") self.logger.error(f"设备 {device_id} 处理返回按钮确认失败")
return False all_success = False
# 启用WiFi # 启用WiFi
# if not self.enable_wifi(device_id): # if not self.enable_wifi(device_id):
# self.logger.error(f"设备 {device_id} 启用WiFi失败") # self.logger.error(f"设备 {device_id} 启用WiFi失败")
# return False # return False
self.logger.info(f"设备 {device_id} 截图页面操作执行完成") # 根据截图结果更新time.txt文件状态
status = "ok" if all_success else "again"
# self.update_upload_info_status(status)
username = global_variable.get_username()
self.update_file_status(username, "running", status)
self.logger.info(f"{username} 截图完成状态为 {status}")
self.logger.info(f"设备 {device_id} 截图页面操作执行完成,状态: {status}")
return True return True
except Exception as e: except Exception as e:
self.logger.error(f"设备 {device_id} 执行截图页面操作时出错: {str(e)}") self.logger.error(f"设备 {device_id} 执行截图页面操作时出错: {str(e)}")

View File

@@ -73,7 +73,7 @@ def parse_time_config():
for line in f: for line in f:
line = line.strip() line = line.strip()
# 匹配:用户名 时间 true (仅获取待处理任务) # 匹配:用户名 时间 true (仅获取待处理任务)
match = re.search(r'(\w+)\s+(\d{1,2}:\d{2}:\d{2})\s+true$', line) match = re.search(r'(\w+)\s+(\d{4}-\d{1,2}-\d{1,2}\s+\d{1,2}:\d{2}:\d{2})\s+true$', line)
if match: if match:
username, scheduled_time = match.group(1), match.group(2) username, scheduled_time = match.group(1), match.group(2)
time_map[username] = scheduled_time time_map[username] = scheduled_time
@@ -81,6 +81,37 @@ def parse_time_config():
print(f"❌ 解析 time.txt 失败: {e}") print(f"❌ 解析 time.txt 失败: {e}")
return time_map return time_map
def normalize_datetime(time_str):
"""
将时间字符串格式化为标准格式YYYY-MM-DD HH:MM:SS
补全单数字的月、日、时
例如2024-1-15 9:52:20 -> 2024-01-15 09:52:20
"""
try:
# 分割日期和时间部分
if ' ' in time_str:
date_part, time_part = time_str.split(' ', 1)
# 补全日期部分的单数字
date_parts = date_part.split('-')
if len(date_parts) == 3:
year = date_parts[0]
month = date_parts[1].zfill(2) # 月补零
day = date_parts[2].zfill(2) # 日补零
date_part = f"{year}-{month}-{day}"
# 补全时间部分的单数字小时
time_parts = time_part.split(':')
if len(time_parts) >= 1:
hour = time_parts[0].zfill(2) # 小时补零
time_part = f"{hour}:{':'.join(time_parts[1:])}"
return f"{date_part} {time_part}"
return time_str
except Exception as e:
print(f"⚠️ 时间格式标准化失败 ({time_str}): {e}")
return time_str
def get_combined_tasks(): def get_combined_tasks():
""" """
结合接口(is_ok==1)和本地文件(true)筛选任务 结合接口(is_ok==1)和本地文件(true)筛选任务
@@ -96,13 +127,14 @@ def get_combined_tasks():
return {} return {}
task_list = {} task_list = {}
today = datetime.now().strftime("%Y-%m-%d") # today = datetime.now().strftime("%Y-%m-%d")
for account in accounts: for account in accounts:
if account.get('is_ok') == 1 or account.get('username') == "czyuzongwen": if account.get('is_ok') == 1:
user = account.get('username') user = account.get('username')
ip = account.get('device_ip') ip = account.get('device_ip')
port = account.get('device_port') port = account.get('device_port')
project_name = account.get('project_name')
# 只有在 time.txt 中是 true 的账号才会被加入 # 只有在 time.txt 中是 true 的账号才会被加入
if user in local_times and ip and port: if user in local_times and ip and port:
@@ -110,21 +142,22 @@ def get_combined_tasks():
# full_time = f"{today} {local_times[user]}" # full_time = f"{today} {local_times[user]}"
# 确保时间是两位数格式 # 确保时间是两位数格式
raw_time = local_times[user] raw_time = local_times[user]
# 将时间格式化为两位数9:52:20 -> 09:52:20 # # 将时间格式化为两位数9:52:20 -> 09:52:20
if ':' in raw_time: # if ':' in raw_time:
parts = raw_time.split(':') # parts = raw_time.split(':')
if len(parts[0]) == 1: # if len(parts[0]) == 1:
raw_time = f"0{raw_time}" # 补齐前导零 # raw_time = f"0{raw_time}" # 补齐前导零
full_time = f"{today} {raw_time}" # full_time = f"{today} {raw_time}"
task_list[address] = {"time": full_time, "user": user} full_time = normalize_datetime(raw_time)
task_list[address] = {"time": full_time, "user": user, "project_name": project_name}
return task_list return task_list
except Exception as e: except Exception as e:
print(f"❌ 获取任务异常: {e}") print(f"❌ 获取任务异常: {e}")
return {} return {}
def run_task(address, target_time, username): def run_task(address, target_time, username, project_name):
""" """
单个执行线程:锁定状态 -> 等待 -> 执行 -> 完成 单个执行线程:锁定状态 -> 等待 -> 执行 -> 完成
""" """
@@ -133,7 +166,7 @@ def run_task(address, target_time, username):
if not update_file_status(username, "true", "running"): if not update_file_status(username, "true", "running"):
return f"⏭️ {username} 状态已变更,跳过执行。" return f"⏭️ {username} 状态已变更,跳过执行。"
print(f"🚀 [任务锁定] 设备: {address} | 用户: {username} | 计划时间: {target_time}") print(f"🚀 [任务锁定] 设备: {address} | 用户: {username} | 计划时间: {target_time} | 项目: {project_name}")
try: try:
# 2. 计算并执行等待逻辑 # 2. 计算并执行等待逻辑
@@ -146,7 +179,7 @@ def run_task(address, target_time, username):
# 3. 调用 main.py 中的自动化逻辑 # 3. 调用 main.py 中的自动化逻辑
print(f"▶️ [正在执行] {username} 开始自动化操作...") print(f"▶️ [正在执行] {username} 开始自动化操作...")
automation = DeviceAutomation(address) automation = DeviceAutomation(address, project_name)
result = automation.handle_app_state() result = automation.handle_app_state()
# 4. 执行完成后,将状态从 running 改为 done # 4. 执行完成后,将状态从 running 改为 done
@@ -169,9 +202,9 @@ def monitor_center():
if tasks: if tasks:
print(f"📡 发现 {len(tasks)} 个符合条件且未跑过的任务,准备启动线程池...") print(f"📡 发现 {len(tasks)} 个符合条件且未跑过的任务,准备启动线程池...")
with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor: with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
# 提交任务,将 address, time, username 传入 # 提交任务,将 address, time, username, project_name 传入
future_to_user = { future_to_user = {
executor.submit(run_task, addr, info['time'], info['user']): info['user'] executor.submit(run_task, addr, info['time'], info['user'], info.get('project_name', '')): info['user']
for addr, info in tasks.items() for addr, info in tasks.items()
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 223 KiB