diff --git a/__pycache__/main.cpython-312.pyc b/__pycache__/main.cpython-312.pyc index 2e61288..544266e 100644 Binary files a/__pycache__/main.cpython-312.pyc and b/__pycache__/main.cpython-312.pyc differ diff --git a/globals/__pycache__/apis.cpython-312.pyc b/globals/__pycache__/apis.cpython-312.pyc index 8c9b284..a1bd2a1 100644 Binary files a/globals/__pycache__/apis.cpython-312.pyc and b/globals/__pycache__/apis.cpython-312.pyc differ diff --git a/globals/__pycache__/global_variable.cpython-312.pyc b/globals/__pycache__/global_variable.cpython-312.pyc index 218bd49..aebbb27 100644 Binary files a/globals/__pycache__/global_variable.cpython-312.pyc and b/globals/__pycache__/global_variable.cpython-312.pyc differ diff --git a/globals/apis.py b/globals/apis.py index 3fc3b9b..fbd0ef7 100644 --- a/globals/apis.py +++ b/globals/apis.py @@ -378,6 +378,107 @@ def get_user_max_variation(username: str) -> Optional[int]: return None 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_num,value=line_name +# # global_variable.GLOBAL_UPLOAD_BREAKPOINT_DICT[line_num] = line_name +# # 存入全局字典:key=line_name,value=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: """ 调用get_name_all接口,提取status=3的line_num和line_name存入全局字典 @@ -388,96 +489,139 @@ def get_line_info_and_save_global(user_name: str) -> bool: api_url = "https://engineering.yuxindazhineng.com/index/index/get_name_all" request_params = {"user_name": user_name} # GET请求参数 timeout = 10 # 请求超时时间(秒),避免卡进程 + max_retries = 3 # 最大重试次数 + retry_interval = 2 # 重试间隔(秒) - 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格式,需解析为字典) + for retry in range(max_retries): try: - response_data = response.json() - except Exception as e: - logging.error(f"接口返回内容非合法JSON,无法解析:{response.text},错误:{str(e)}") - return False + # 1. 发送GET请求 + response = requests.get( + url=api_url, + params=request_params, # GET参数用params传递,自动拼接到URL后,规范且防乱码 + timeout=timeout, + verify=False # 禁用SSL验证,适配HTTPS接口 + ) - # 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 + # 2. 校验HTTP状态码(先确保请求本身成功) + if response.status_code != 200: + 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 - # 5. 提取data字段,校验数据是否存在 - api_data_list = response_data.get("data") - if not api_data_list: - logging.warning("接口业务成功,但data字段为空或无数据") - return False + # 3. 解析JSON响应(接口返回是JSON格式,需解析为字典) + try: + response_data = response.json() + except Exception as 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 - # 6. 校验data是否为列表类型 - if not isinstance(api_data_list, list): - logging.error(f"data字段不是列表类型,实际类型:{type(api_data_list)},内容:{api_data_list}") - 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}") + if retry < max_retries - 1: + logging.info(f"将在{retry_interval}秒后进行第{retry+2}次重试") + time.sleep(retry_interval) + continue + return False + else: + logging.warning(f"接口返回未知业务状态码:{business_code},请确认接口文档") + if retry < max_retries - 1: + logging.info(f"将在{retry_interval}秒后进行第{retry+2}次重试") + time.sleep(retry_interval) + continue + 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}") + # 5. 提取data字段,校验数据是否存在 + api_data_list = response_data.get("data") + if not api_data_list: + logging.warning("接口业务成功,但data字段为空或无数据") + if retry < max_retries - 1: + logging.info(f"将在{retry_interval}秒后进行第{retry+2}次重试") + time.sleep(retry_interval) + continue + return False + + # 6. 校验data是否为列表类型 + if not isinstance(api_data_list, 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 + + 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_num,value=line_name + # global_variable.GLOBAL_UPLOAD_BREAKPOINT_DICT[line_num] = line_name + # 存入全局字典:key=line_name,value=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且字段完整的线路信息") + if retry < max_retries - 1: + logging.info(f"将在{retry_interval}秒后进行第{retry+2}次重试") + time.sleep(retry_interval) + continue + return False + + # 捕获所有请求相关异常(超时、连接失败、网络异常等) + except requests.exceptions.Timeout: + 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 + except requests.exceptions.ConnectionError: + 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 + except Exception as e: + 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 - - # 获取字段值 - 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_num,value=line_name - # global_variable.GLOBAL_UPLOAD_BREAKPOINT_DICT[line_num] = line_name - # 存入全局字典:key=line_name,value=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_accounts_from_server(yh_id): """从服务器获取账户信息""" diff --git a/globals/global_variable.py b/globals/global_variable.py index 454beef..cbb8d72 100644 --- a/globals/global_variable.py +++ b/globals/global_variable.py @@ -126,6 +126,8 @@ def set_upload_success_breakpoint_list(success_list): """设置上传成功的断点列表""" thread_local.GLOBAL_UPLOAD_SUCCESS_BREAKPOINT_LIST = success_list + + # 为了保持 ,保留原有的全局变量名称 # 但这些将不再被直接使用,而是通过上面的函数访问 GLOBAL_DEVICE_ID = "" # 设备ID diff --git a/main.py b/main.py index 8ab999b..66af5a0 100644 --- a/main.py +++ b/main.py @@ -16,9 +16,7 @@ import permissions # 导入权限处理模块 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 page_objects.login_page import LoginPage -from page_objects.download_tabbar_page import DownloadTabbarPage from page_objects.screenshot_page import ScreenshotPage -from page_objects.upload_config_page import UploadConfigPage from page_objects.more_download_page import MoreDownloadPage @@ -35,8 +33,14 @@ logging.basicConfig( class DeviceAutomation(object): - def __init__(self, device_id=None): + def __init__(self, device_id=None, project_name=None): 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): @@ -62,9 +66,7 @@ class DeviceAutomation(object): # 初始化页面对象 logging.info(f"设备 {self.device_id} 开始初始化页面对象") 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.upload_config_page = UploadConfigPage(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} 所有页面对象初始化完成") @@ -246,13 +248,15 @@ class DeviceAutomation(object): safe_quit_driver(getattr(self, 'driver', None), self.device_id) @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则自动获取 + upload_time: 上传时间 + project_name: 项目名称 返回: bool: 自动化流程执行结果(True/False) @@ -260,7 +264,7 @@ class DeviceAutomation(object): automation = None try: # 创建自动化实例 - automation = DeviceAutomation(device_id=device_id) + automation = DeviceAutomation(device_id=device_id, project_name=project_name) # 执行自动化流程 success = automation.run_automation() diff --git a/page_objects/__pycache__/screenshot_page.cpython-312.pyc b/page_objects/__pycache__/screenshot_page.cpython-312.pyc index ec0e133..16c8933 100644 Binary files a/page_objects/__pycache__/screenshot_page.cpython-312.pyc and b/page_objects/__pycache__/screenshot_page.cpython-312.pyc differ diff --git a/page_objects/screenshot_page.py b/page_objects/screenshot_page.py index 1b5cfe5..b465715 100644 --- a/page_objects/screenshot_page.py +++ b/page_objects/screenshot_page.py @@ -4,6 +4,7 @@ import logging import time import re import os +import threading from datetime import datetime from appium.webdriver.common.appiumby import AppiumBy from selenium.common.exceptions import NoSuchElementException, TimeoutException @@ -19,7 +20,7 @@ import globals.global_variable as global_variable # 导入全局变量模块 class ScreenshotPage: def __init__(self, driver, wait, device_id=None): self.driver = driver - self.wait = wait + self.wait = WebDriverWait(driver, 2) self.device_id = device_id self.logger = logging.getLogger(__name__) self.all_items = set() @@ -528,7 +529,7 @@ class ScreenshotPage: self.logger.error(f"设备 {device_id} 检查WiFi状态时发生错误: {str(e)}") return None - def take_screenshot(self, filename_prefix="screenshot"): + def take_screenshot(self, filename_prefix="screenshot", date_str=None, time_str=None): """ 通过Appium驱动截取设备屏幕 @@ -539,20 +540,58 @@ class ScreenshotPage: bool: 操作是否成功 """ try: - # 创建测试结果目录 - screenshots_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../test_results/screenshots') - if not os.path.exists(screenshots_dir): - os.makedirs(screenshots_dir) - self.logger.info(f"创建截图目录: {screenshots_dir}") - + # 获取项目名称 + project_name = global_variable.get_current_project_name() or "默认项目" + + # 获取当前日期(如果没有提供) + 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( 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) - self.logger.info(f"截图已保存: {screenshot_file}") - return True + + # 尝试保存截图 + try: + success = self.driver.save_screenshot(screenshot_file) + if success: + 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 + 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: self.logger.error(f"截图时发生错误: {str(e)}") @@ -591,117 +630,79 @@ class ScreenshotPage: self.logger.error(f"写入截图状态文件时发生错误: {str(e)}") 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' + """ + TIME_FILE_PATH = r"D:\uploadInfo\time.txt" + if not os.path.exists(TIME_FILE_PATH): + return False - Args: - timeout: 超时时间,默认900秒(15分钟) + 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: + print(f"❌ 更新文件状态失败 ({username}): {e}") + return False + + def update_upload_info_status(self, status): + """ + 更新D:/uploadInfo文件夹下的time.txt文件的状态 - Returns: - bool: 是否成功等到测量结束按钮 + 参数: + status: 状态值,如"ok"或"again" + + 返回: + bool: 操作是否成功 """ try: - # 更新WebDriverWait等待时间为900秒 - self.wait = WebDriverWait(self.driver, 900) - self.logger.info(f"设备等待测量结束按钮出现,最多等待 {timeout} 秒") + # time.txt文件路径 + time_file_path = "D:\\uploadInfo\\time.txt" - start_time = time.time() - reinit_attempts = 0 - max_reinit_attempts = 3 # 最大重新初始化次数 + # 确保文件夹存在 + os.makedirs(os.path.dirname(time_file_path), exist_ok=True) - 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} 秒,仍在等待测量结束...") + # 写入状态 + with open(time_file_path, 'w', encoding='utf-8') as f: + f.write(status) - self.logger.error(f"设备 {self.device_id} 等待测量结束按钮超时") - return False - - except Exception as e: - self.logger.error(f"设备 {self.device_id} 等待测量结束时发生错误: {str(e)}") - return False - - def _reinit_driver(self): - """ - 重新初始化Appium驱动 - - Returns: - bool: 是否成功重新初始化 - """ - try: - # 首先尝试关闭现有的驱动 - if hasattr(self, 'driver') and self.driver: - try: - self.driver.quit() - except: - self.logger.warning("关闭现有驱动时出现异常") - - # 导入必要的模块 - from appium import webdriver - from appium.options.android import UiAutomator2Options - - # 重新创建驱动配置 - options = UiAutomator2Options() - options.platform_name = "Android" - 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"正在重新初始化设备 {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} 驱动重新初始化完成") + self.logger.info(f"已更新上传状态文件: {time_file_path} -> {status}") return True except Exception as e: - self.logger.error(f"设备 {self.device_id} 驱动重新初始化失败: {str(e)}") + self.logger.error(f"更新上传状态文件时发生错误: {str(e)}") return False + 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(): self.logger.info(f"设备 {device_id} 点击确认弹窗的'是'按钮") confirm_button.click() - time.sleep(1) + time.sleep(0.5) # 验证弹窗是否消失 try: @@ -828,7 +829,7 @@ class ScreenshotPage: except Exception as e: self.logger.warning(f"设备 {device_id} 查找确认弹窗时出现异常: {str(e)}") - time.sleep(1) + time.sleep(0.5) self.logger.error(f"设备 {device_id} 等待返回确认弹窗超时") return False @@ -1001,7 +1002,7 @@ class ScreenshotPage: # 3. 验证是否成功返回到上一页面 - time.sleep(1) # 等待页面跳转完成 + time.sleep(0.5) # 等待页面跳转完成 # 可以添加页面验证逻辑,比如检查是否返回到预期的页面 # 这里可以根据实际应用添加特定的页面元素验证 @@ -1013,51 +1014,6 @@ class ScreenshotPage: self.logger.error(f"设备 {device_id} 执行返回导航步骤时发生错误: {str(e)}") 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): """ @@ -1129,7 +1085,7 @@ class ScreenshotPage: if self.click_last_spinner(device_id): return True self.logger.warning(f"设备 {device_id} 第{attempt + 1}次点击失败,准备重试") - time.sleep(1) # 重试前等待 + time.sleep(0.5) # 重试前等待 except Exception as e: self.logger.error(f"设备 {device_id} 第{attempt + 1}次尝试失败: {str(e)}") @@ -1280,7 +1236,7 @@ class ScreenshotPage: 'percent': 0.5 }) - time.sleep(1) + time.sleep(0.2) self.logger.info(f"设备 {device_id} 额外下滑完成") return True @@ -1470,7 +1426,7 @@ class ScreenshotPage: self.logger.error(f"设备 {device_id} 加载线路时间映射字典失败") return False - time.sleep(5) + # time.sleep(5) # 循环检查数据数量是否一致,直到获取到完整数据 retry_count = 0 @@ -1524,98 +1480,90 @@ class ScreenshotPage: }) # 开始循环 + all_success = True for breakpoint_name in global_variable.get_upload_breakpoint_dict().keys(): self.logger.info(f"开始处理要平差截图的断点 {breakpoint_name}") # 把断点名称给find_keyword if not self.find_keyword(breakpoint_name): self.logger.error(f"设备 {device_id} 未找到包含 {breakpoint_name} 的文件名") - return False + all_success = False + continue if not self.handle_measurement_dialog(): self.logger.error(f"设备 {device_id} 处理测量弹窗失败") - return False + all_success = False + continue if not self.check_apply_btn(): self.logger.error(f"设备 {device_id} 检查平差处理按钮失败") - return False + all_success = False + self.click_back_button(device_id) + continue # 根据断点名称在get_upload_breakpoint_dict()中获取线路编码 line_code = global_variable.get_upload_breakpoint_dict().get(breakpoint_name) if not line_code: 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) if not time_str or not date_str: 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): self.logger.error(f"设备 {device_id} 设置设备时间失败") - return False + all_success = False + continue # 滑动列表到底部 if not self.scroll_list_to_bottom(device_id): self.logger.error(f"设备 {device_id} 下滑列表到底部失败") - return False + all_success = False + continue # 2. 点击最后一个spinner if not self.click_last_spinner_with_retry(device_id): self.logger.error(f"设备 {device_id} 点击最后一个spinner失败") - return False + all_success = False + continue # 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 - - # # 检查是否在测量页面,在就重新执行选择断点,滑动列表到底部,点击最后一个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("断点保存到上传列表成功,开始截图") - 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.write_screenshot_status(breakpoint_name, success=False) - return False - - self.write_screenshot_status(breakpoint_name, success=True) + all_success = False + else: + self.write_screenshot_status(breakpoint_name, success=True) # 点击返回按钮并处理弹窗 if not self.execute_back_navigation_steps(device_id): self.logger.error(f"设备 {device_id} 处理返回按钮确认失败") - return False + all_success = False # 启用WiFi # if not self.enable_wifi(device_id): # self.logger.error(f"设备 {device_id} 启用WiFi失败") # 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 except Exception as e: self.logger.error(f"设备 {device_id} 执行截图页面操作时出错: {str(e)}") diff --git a/scheduler.py b/scheduler.py index 6757a12..6c7db76 100644 --- a/scheduler.py +++ b/scheduler.py @@ -73,7 +73,7 @@ def parse_time_config(): for line in f: line = line.strip() # 匹配:用户名 时间 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: username, scheduled_time = match.group(1), match.group(2) time_map[username] = scheduled_time @@ -81,6 +81,37 @@ def parse_time_config(): print(f"❌ 解析 time.txt 失败: {e}") 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(): """ 结合接口(is_ok==1)和本地文件(true)筛选任务 @@ -96,13 +127,14 @@ def get_combined_tasks(): return {} task_list = {} - today = datetime.now().strftime("%Y-%m-%d") + # today = datetime.now().strftime("%Y-%m-%d") 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') ip = account.get('device_ip') port = account.get('device_port') + project_name = account.get('project_name') # 只有在 time.txt 中是 true 的账号才会被加入 if user in local_times and ip and port: @@ -110,21 +142,22 @@ def get_combined_tasks(): # full_time = f"{today} {local_times[user]}" # 确保时间是两位数格式 raw_time = local_times[user] - # 将时间格式化为两位数:9:52:20 -> 09:52:20 - if ':' in raw_time: - parts = raw_time.split(':') - if len(parts[0]) == 1: - raw_time = f"0{raw_time}" # 补齐前导零 + # # 将时间格式化为两位数:9:52:20 -> 09:52:20 + # if ':' in raw_time: + # parts = raw_time.split(':') + # if len(parts[0]) == 1: + # raw_time = f"0{raw_time}" # 补齐前导零 - full_time = f"{today} {raw_time}" - task_list[address] = {"time": full_time, "user": user} + # full_time = f"{today} {raw_time}" + full_time = normalize_datetime(raw_time) + task_list[address] = {"time": full_time, "user": user, "project_name": project_name} return task_list except Exception as e: print(f"❌ 获取任务异常: {e}") 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"): return f"⏭️ {username} 状态已变更,跳过执行。" - print(f"🚀 [任务锁定] 设备: {address} | 用户: {username} | 计划时间: {target_time}") + print(f"🚀 [任务锁定] 设备: {address} | 用户: {username} | 计划时间: {target_time} | 项目: {project_name}") try: # 2. 计算并执行等待逻辑 @@ -146,7 +179,7 @@ def run_task(address, target_time, username): # 3. 调用 main.py 中的自动化逻辑 print(f"▶️ [正在执行] {username} 开始自动化操作...") - automation = DeviceAutomation(address) + automation = DeviceAutomation(address, project_name) result = automation.handle_app_state() # 4. 执行完成后,将状态从 running 改为 done @@ -169,9 +202,9 @@ def monitor_center(): if tasks: print(f"📡 发现 {len(tasks)} 个符合条件且未跑过的任务,准备启动线程池...") with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor: - # 提交任务,将 address, time, username 传入 + # 提交任务,将 address, time, username, project_name 传入 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() } diff --git a/test_results/screenshots/CZSCZQ-13A-二工区-沙马大桥-1#墩身-山区_20260209.png b/test_results/screenshots/CZSCZQ-13A-二工区-沙马大桥-1#墩身-山区_20260209.png deleted file mode 100644 index 1f45d03..0000000 Binary files a/test_results/screenshots/CZSCZQ-13A-二工区-沙马大桥-1#墩身-山区_20260209.png and /dev/null differ diff --git a/test_results/screenshots/CZSCZQ-13A-二工区-沙马大桥-2#墩身-山区_20260209.png b/test_results/screenshots/CZSCZQ-13A-二工区-沙马大桥-2#墩身-山区_20260209.png deleted file mode 100644 index 180c095..0000000 Binary files a/test_results/screenshots/CZSCZQ-13A-二工区-沙马大桥-2#墩身-山区_20260209.png and /dev/null differ diff --git a/test_results/screenshots/CZSCZQ-13A-二工区-沙马大桥-7#墩-山区_20260209.png b/test_results/screenshots/CZSCZQ-13A-二工区-沙马大桥-7#墩-山区_20260209.png deleted file mode 100644 index fcbdd81..0000000 Binary files a/test_results/screenshots/CZSCZQ-13A-二工区-沙马大桥-7#墩-山区_20260209.png and /dev/null differ