From 842fb434f4ac779ca0b7b00f7cf068084e030730 Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Fri, 28 Nov 2025 15:03:51 +0800 Subject: [PATCH] feat(AutoLib): new feature 'Auto Renew' --- src/gui/ALConfigWidget.py | 19 +- src/gui/ALConfigWidget.ui | 975 +++++++++++++++++++++----------------- src/operators/AutoLib.py | 9 +- src/operators/LibRenew.py | 185 +++++++- 4 files changed, 747 insertions(+), 441 deletions(-) diff --git a/src/gui/ALConfigWidget.py b/src/gui/ALConfigWidget.py index 0b9a0aa..caf2890 100644 --- a/src/gui/ALConfigWidget.py +++ b/src/gui/ALConfigWidget.py @@ -306,6 +306,9 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget): self.MaxEndTimeDiffSpinBox.setValue(30) self.ExpectDurationSpinBox.setValue(self.BeginTimeEdit.time().secsTo(self.EndTimeEdit.time())/3600) self.SatisfyDurationCheckBox.setChecked(False) + self.ExpectRenewDurationSpinBox.setValue(1.0) + self.MaxRenewTimeDiffSpinBox.setValue(30) + self.PreferLateRenewTimeCheckBox.setChecked(False) def collectUserConfigFromUserInfoWidget( @@ -317,7 +320,8 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget): "password": self.PasswordEdit.text(), "reserve_info": { "begin_time":{}, - "end_time": {} + "end_time": {}, + "renew_time": {} } } user_config["reserve_info"]["date"] = self.DateEdit.dateTime().toString("yyyy-MM-dd") @@ -333,6 +337,9 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget): user_config["reserve_info"]["end_time"]["prefer_early"] = not self.PreferLateEndTimeCheckBox.isChecked() user_config["reserve_info"]["expect_duration"] = self.ExpectDurationSpinBox.value() user_config["reserve_info"]["satisfy_duration"] = self.SatisfyDurationCheckBox.isChecked() + user_config["reserve_info"]["renew_time"]["expect_duration"] = self.ExpectRenewDurationSpinBox.value() + user_config["reserve_info"]["renew_time"]["max_diff"] = self.MaxRenewTimeDiffSpinBox.value() + user_config["reserve_info"]["renew_time"]["prefer_early"] = not self.PreferLateRenewTimeCheckBox.isChecked() return user_config @@ -371,6 +378,9 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget): self.PreferLateEndTimeCheckBox.setChecked(not user_config["reserve_info"]["end_time"]["prefer_early"]) self.ExpectDurationSpinBox.setValue(user_config["reserve_info"]["expect_duration"]) self.SatisfyDurationCheckBox.setChecked(user_config["reserve_info"]["satisfy_duration"]) + self.ExpectRenewDurationSpinBox.setValue(user_config["reserve_info"]["renew_time"]["expect_duration"]) + self.MaxRenewTimeDiffSpinBox.setValue(user_config["reserve_info"]["renew_time"]["max_diff"]) + self.PreferLateRenewTimeCheckBox.setChecked(not user_config["reserve_info"]["renew_time"]["prefer_early"]) except: QMessageBox.warning( self, @@ -565,7 +575,12 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget): "prefer_early": True }, "expect_duration": 2.0, - "satisfy_duration": False + "satisfy_duration": False, + "renew_time": { + "expect_duration": 1.0, + "max_diff": 30, + "prefer_early": True + } } } user_item = QListWidgetItem(new_user["username"]) diff --git a/src/gui/ALConfigWidget.ui b/src/gui/ALConfigWidget.ui index de0221e..0947db5 100644 --- a/src/gui/ALConfigWidget.ui +++ b/src/gui/ALConfigWidget.ui @@ -254,7 +254,7 @@ - 5 + 0 @@ -376,151 +376,6 @@ 5 - - - - - 100 - 25 - - - - - 100 - 25 - - - - 日期: - - - - - - - - 100 - 25 - - - - - 100 - 25 - - - - 最大时长偏差: - - - - - - - - 100 - 25 - - - - - 100 - 25 - - - - 地点: - - - - - - - - 0 - 25 - - - - - 16777215 - 25 - - - - <html><head/><body><p><br/></p></body></html> - - - 8.000000000000000 - - - QAbstractSpinBox::StepType::AdaptiveDecimalStepType - - - - - - - - 0 - 25 - - - - - 16777215 - 25 - - - - <html><head/><body><p>勾选此项,如果有多个与目标<span style=" font-weight:700; font-style:italic;">结束时间</span>相近的可选时间,选择时间最晚项</p></body></html> - - - 优先选择最晚 - - - true - - - - - - - - 100 - 30 - - - - - 100 - 30 - - - - 结束时间: - - - - - - - - 100 - 25 - - - - - 100 - 25 - - - - 开始时间: - - - @@ -557,6 +412,511 @@ + + + + + 100 + 25 + + + + + 100 + 25 + + + + 区域: + + + + + + + + 100 + 25 + + + + + 100 + 25 + + + + 楼层: + + + + + + + + 100 + 25 + + + + + 100 + 25 + + + + 期望续约时长: + + + + + + + + 100 + 30 + + + + + 100 + 30 + + + + 结束时间: + + + + + + + 0 + + + + + + 0 + 25 + + + + + 130 + 25 + + + + + + + + + 25 + 25 + + + + + 25 + 25 + + + + <html><head/><body><p>查询座位布局</p></body></html> + + + + + + + + + + + + + + + + 0 + 25 + + + + + 16777215 + 25 + + + + + + + + + 100 + 25 + + + + + 100 + 25 + + + + 座位号: + + + + + + + + 100 + 25 + + + + + 100 + 25 + + + + 期望预约时长: + + + + + + + + 100 + 25 + + + + + 100 + 25 + + + + 日期: + + + + + + + 4.000000000000000 + + + 1.000000000000000 + + + + + + + + 0 + 25 + + + + + 16777215 + 25 + + + + true + + + + 2025 + 11 + 1 + + + + + + + + 0 + + + + + + 55 + 25 + + + + + 16777215 + 25 + + + + <html><head/><body><p>期望的<span style=" font-weight:700; font-style:italic;">结束时间</span>不可用时,按照该偏差范围寻找最近可选时间</p></body></html> + + + 120 + + + QAbstractSpinBox::StepType::AdaptiveDecimalStepType + + + + + + + + 75 + 25 + + + + + 16777215 + 25 + + + + <html><head/><body><p>勾选此项,如果有多个与目标<span style=" font-weight:700; font-style:italic;">结束时间</span>相近的可选时间,选择时间最晚项</p></body></html> + + + 优先最晚 + + + true + + + + + + + + + + 0 + 25 + + + + + 16777215 + 25 + + + + + 2000 + 1 + 1 + + + + + + + + + + H:mm + + + + + + + + 100 + 25 + + + + + 100 + 25 + + + + 地点: + + + + + + + + 0 + 25 + + + + + 200 + 25 + + + + <html><head/><body><p>勾选此项,会优先满足预约时长限制,当座位紧张时可能会导致<span style=" font-weight:700; font-style:italic;">预约失败</span></p></body></html> + + + 优先满足预约时长 + + + true + + + + + + + false + + + + 0 + 25 + + + + + 16777215 + 25 + + + + + 图书馆 + + + + + + + + + 0 + 25 + + + + + 16777215 + 25 + + + + <html><head/><body><p><br/></p></body></html> + + + 0.000000000000000 + + + 8.000000000000000 + + + QAbstractSpinBox::StepType::DefaultStepType + + + 2.000000000000000 + + + + + + + 0 + + + + + + 65 + 25 + + + + + 16777215 + 25 + + + + <html><head/><body><p>期望的<span style=" font-weight:700; font-style:italic;">开始时间</span>不可用时,按照该偏差范围寻找最近可选时间</p></body></html> + + + 120 + + + QAbstractSpinBox::StepType::AdaptiveDecimalStepType + + + + + + + + 75 + 25 + + + + + 16777215 + 25 + + + + <html><head/><body><p>勾选此项,如果有多个与目标<span style=" font-weight:700; font-style:italic;">开始时间</span>相近的可选时间,选择时间最早项</p></body></html> + + + 优先最早 + + + true + + + + + + + + + + 100 + 25 + + + + + 100 + 25 + + + + 最大时长偏差: + + + @@ -609,34 +969,8 @@ - - - - - 0 - 25 - - - - - 16777215 - 25 - - - - true - - - - 2025 - 11 - 1 - - - - - - + + 100 @@ -650,292 +984,61 @@ - 期望时长: + 开始时间: - - - - - 100 - 25 - - - - - 100 - 25 - - - - 区域: - - - - - - - - 0 - 25 - - - - - 16777215 - 25 - - - - - 2000 - 1 - 1 - - - - - - - - - - H:mm - - - - - - - - 0 - 25 - - - - - 16777215 - 25 - - - - <html><head/><body><p>勾选此项,会优先满足预约时长限制,当座位紧张时可能会导致<span style=" font-weight:700; font-style:italic;">预约失败</span></p></body></html> - - - 优先满足时长要求 - - - true - - - - - - - - 100 - 25 - - - - - 16777215 - 25 - - - - <html><head/><body><p>期望的<span style=" font-weight:700; font-style:italic;">开始时间</span>不可用时,按照该偏差范围寻找最近可选时间</p></body></html> - - - 120 - - - QAbstractSpinBox::StepType::AdaptiveDecimalStepType - - - - - - - - 100 - 25 - - - - - 100 - 25 - - - - 楼层: - - - - - - - false - - - - 0 - 25 - - - - - 16777215 - 25 - + + + + 0 - - 图书馆 - + - - - - - - - 100 - 25 - - - - - 16777215 - 25 - - - - <html><head/><body><p>勾选此项,如果有多个与目标<span style=" font-weight:700; font-style:italic;">开始时间</span>相近的可选时间,选择时间最早项</p></body></html> - - - 优先选择最早 - - - true - - - - - - - - 0 - 25 - - - - - 16777215 - 25 - - - - <html><head/><body><p>期望的<span style=" font-weight:700; font-style:italic;">结束时间</span>不可用时,按照该偏差范围寻找最近可选时间</p></body></html> - - - 120 - - - QAbstractSpinBox::StepType::AdaptiveDecimalStepType - - - - - - - - 0 - 25 - - - - - 16777215 - 25 - - - - - - - - - 100 - 25 - - - - - 100 - 25 - - - - 座位号: - - - - - - + - 0 + 75 25 - 130 + 16777215 25 - - - - - - - 25 - 25 - - - - - 25 - 25 - - - - <html><head/><body><p>查询座位布局</p></body></html> - - + 优先最晚 - - + + true + + + + + 100 + 25 + + + + + 100 + 25 + + + + 最大时长偏差 + + + diff --git a/src/operators/AutoLib.py b/src/operators/AutoLib.py index b7ec3cd..43d160a 100644 --- a/src/operators/AutoLib.py +++ b/src/operators/AutoLib.py @@ -22,6 +22,7 @@ from operators.LibLogin import LibLogin from operators.LibLogout import LibLogout from operators.LibReserve import LibReserve from operators.LibCheckin import LibCheckin +from operators.LibRenew import LibRenew from utils.ConfigReader import ConfigReader @@ -114,6 +115,7 @@ class AutoLib(MsgBase): self.__lib_logout = LibLogout(self._input_queue, self._output_queue, self.__driver) self.__lib_reserve = LibReserve(self._input_queue, self._output_queue, self.__driver) self.__lib_checkin = LibCheckin(self._input_queue, self._output_queue, self.__driver) + self.__lib_renew = LibRenew(self._input_queue, self._output_queue, self.__driver) def __waitResponseLoad( @@ -204,8 +206,11 @@ class AutoLib(MsgBase): result = 2 # renewal if run_mode["auto_renewal"] and result == 2: - if self.__lib_checker.canRenew(reserve_info.get("date")): - pass + if record := self.__lib_checker.canRenew(): + if self.__lib_renew.renew(username, record, reserve_info): + result = 0 + else: + result = 1 else: self._showTrace(f"用户 {username} 无法续约,已跳过") result = 2 diff --git a/src/operators/LibRenew.py b/src/operators/LibRenew.py index dbf00d3..be8fb7b 100644 --- a/src/operators/LibRenew.py +++ b/src/operators/LibRenew.py @@ -37,4 +37,187 @@ class LibRenew(LibOperator): self ) -> bool: - pass \ No newline at end of file + try: + WebDriverWait(self.__driver, 2).until( + EC.presence_of_element_located((By.CLASS_NAME, "ui_dialog")) + ) + WebDriverWait(self.__driver, 2).until( + EC.presence_of_element_located((By.CLASS_NAME, "resultMessage")) + ) + WebDriverWait(self.__driver, 2).until( + EC.element_to_be_clickable((By.CLASS_NAME, "btnOK")) + ) + result_message_element = self.__driver.find_element( + By.CLASS_NAME, "resultMessage" + ) + ok_btn = self.__driver.find_element(By.CLASS_NAME, "btnOK") + except: + self._showTrace("续约时发生未知错误 !") + return False + result_message = result_message_element.text + if "续约成功" in result_message: + try: + detail_elements = self.__driver.find_elements( + By.CSS_SELECTOR, ".resultMessage dd" + ) + except: + pass + if detail_elements: + details = [element.text for element in detail_elements if element.text.strip()] + if len(details) >= 5: + self._showTrace(f"\n"\ + f" 续约成功 !\n"\ + f" {details[1]}\n"\ + f" {details[2]}\n"\ + f" {details[3]}\n"\ + f" {details[4]}") + else: + self._showTrace(f"\n"\ + " 续约成功 !\n"\ + " 未获取到续约详情 !") + ok_btn.click() + return True + else: + failure_reason = result_message.replace("续约失败", "").strip() + self._showTrace(f"\n"\ + " 续约失败 !\n"\ + f" {failure_reason}" + ) + ok_btn.click() + return False + + @staticmethod + def __timeToMins( + time_str: str + ) -> int: + + hour, minute = map(int, time_str.split(":")) + return hour*60 + minute + + + @staticmethod + def __minsToTime( + mins: int + ) -> str: + + hour, minute = divmod(mins, 60) + return f"{hour:02d}:{minute:02d}" + + + def __selectNearstRecord( + self, + record: dict, + reserve_info: dict + ) -> bool: + + end_time = record["time"]["end"] + renew_info = reserve_info["renew_time"] + max_diff = renew_info["max_diff"] + prefer_earlier = renew_info["prefer_early"] + target_renew_mins = self.__timeToMins(end_time) + renew_info["expect_duration"]*60 + try: + WebDriverWait(self.__driver, 2).until( + EC.visibility_of_element_located((By.ID, "extendDiv")) + ) + WebDriverWait(self.__driver, 2).until( + EC.presence_of_all_elements_located( + (By.CSS_SELECTOR, "#extendDiv .renewal_List li") + ) + ) + renew_ok_btn = WebDriverWait(self.__driver, 2).until( + EC.presence_of_element_located((By.CSS_SELECTOR, "#extendDiv .btnOK")) + ) + except: + self._showTrace("续约时间选择界面加载失败 !") + return False + try: + renew_time_opts = self.__driver.find_elements( + By.CSS_SELECTOR, "#extendDiv .renewal_List li" + ) + free_times = [] + best_time_diff = max_diff + best_actual_diff = None + best_time_opt = None + + if not renew_time_opts: + self._showTrace("当前未查询到可用续约时间 !") + return False + for time_opt in renew_time_opts: + time_attr = time_opt.get_attribute("id") + if time_attr and time_attr.isdigit(): + time_val = int(time_attr) + free_times.append(time_opt.text.strip()) + else: + continue + actual_diff = time_val - target_renew_mins + abs_diff = abs(actual_diff) + if abs_diff < best_time_diff or ( + abs_diff == best_time_diff and ( + # 优先选择更早的时间 + (prefer_earlier and actual_diff <= 0) or + # 优先选择更晚的时间 + (not prefer_earlier and actual_diff >= 0) + ) + ): + best_time_diff = abs_diff + best_actual_diff = actual_diff + best_time_opt = time_opt + + if best_time_opt is not None: + best_time_opt.click() + abs_time_diff = abs(best_actual_diff) + if best_actual_diff < 0: + time_relation = f"早了 {abs_time_diff} 分钟" + elif best_actual_diff > 0: + time_relation = f"晚了 {abs_time_diff} 分钟" + else: + time_relation = f"正好等于续约时间" + self._showTrace( + f"选择距离期望续约时间最近的 {best_time_opt.text}, "\ + f"与期望续约时间相比 {time_relation}" + ) + renew_ok_btn.click() + return True + self._showTrace( + "无法选择最近的可用续约时间 !" \ + f"所有可选时间与目标时间相差都超过了 {max_diff} 分钟 !" + ) + self._showTrace( + f"当前可供续约的时间有: {free_times}" + ) + return False + except: + self._showTrace("查询可用续约时间时发生未知错误 !") + return False + + + def renew( + self, + username: str, + record: dict, + reserve_info: dict + ) -> bool: + + if self.__driver is None: + self._showTrace("未提供有效 WebDriver 实例 !") + return False + try: + renew_btn = WebDriverWait(self.__driver, 2).until( + EC.element_to_be_clickable((By.ID, "btnExtend")) + ) + except: + self._showTrace(f"用户 {username} 续约界面加载失败 !") + return False + if "disabled" in renew_btn.get_attribute("class"): + self._showTrace(f"用户 {username} 续约按钮不可用, 可能不在场馆内") + return False + renew_btn.click() + if not self.__selectNearstRecord(record, reserve_info): + return False + # renew_ok_btn.click() + if self._waitResponseLoad(): + self._showTrace(f"用户 {username} 续约成功 !") + return True + else: + self._showTrace(f"用户 {username} 续约失败 !") + return False