mirror of
https://github.com/KenanZhu/AutoLibrary.git
synced 2026-06-18 07:23:03 +08:00
df7ad92f7f
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
235 lines
7.7 KiB
Python
235 lines
7.7 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
Copyright (c) 2026 KenanZhu.
|
|
All rights reserved.
|
|
|
|
This software is provided "as is", without any warranty of any kind.
|
|
You may use, modify, and distribute this file under the terms of the MIT License.
|
|
See the LICENSE file for details.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import TYPE_CHECKING, Callable, Optional
|
|
|
|
from selenium.common.exceptions import (
|
|
ElementNotInteractableException,
|
|
StaleElementReferenceException,
|
|
TimeoutException,
|
|
)
|
|
from selenium.webdriver.common.by import By
|
|
from selenium.webdriver.remote.webdriver import WebDriver
|
|
from selenium.webdriver.remote.webelement import WebElement
|
|
|
|
from pages.components.Dialog import Dialog
|
|
from pages.strategies.TimeSelectMaker import (
|
|
TimeRangeResult,
|
|
TimeSelectionResult,
|
|
TimeSelectMaker,
|
|
minsToTimeStr,
|
|
timeStrToMins,
|
|
)
|
|
|
|
if TYPE_CHECKING:
|
|
from pages.flows.ReserveFlow import ReserveContext
|
|
|
|
|
|
class TimeSelectDialog(Dialog):
|
|
"""
|
|
Time selection panel that appears after selecting a seat.
|
|
|
|
Contains start-time and end-time option lists.
|
|
Does NOT auto-close — the reserve submission handles cleanup.
|
|
"""
|
|
|
|
ROOT = (By.CSS_SELECTOR, "#startTime ul")
|
|
|
|
def __init__(
|
|
self,
|
|
driver: WebDriver,
|
|
tracer: Optional[Callable[[str, int], None]] = None,
|
|
) -> None:
|
|
|
|
super().__init__(driver, self.ROOT, auto_close_on_exit=False)
|
|
self._tracer = tracer
|
|
|
|
def _trace(
|
|
self,
|
|
msg: str,
|
|
level: int = logging.INFO,
|
|
) -> None:
|
|
|
|
if self._tracer is not None:
|
|
self._tracer(msg, level)
|
|
|
|
def _logTimeStep(
|
|
self,
|
|
time_type: str,
|
|
target_mins: int,
|
|
max_diff: int,
|
|
step_result: TimeSelectionResult,
|
|
) -> bool:
|
|
|
|
if step_result.selected_index >= 0:
|
|
abs_diff = abs(step_result.actual_diff)
|
|
if step_result.actual_diff < 0:
|
|
relation = f"早了 {abs_diff} 分钟"
|
|
elif step_result.actual_diff > 0:
|
|
relation = f"晚了 {abs_diff} 分钟"
|
|
else:
|
|
relation = f"正好等于 {time_type}"
|
|
self._trace(
|
|
f"选择距离期望 {time_type} 最近的 {step_result.display_text}, "
|
|
f"与期望 {time_type} 相比 {relation}"
|
|
)
|
|
return True
|
|
if not step_result.free_times:
|
|
self._trace(
|
|
f"{time_type} 选择失败 ! : 当前未查询到可用时间",
|
|
logging.ERROR,
|
|
)
|
|
else:
|
|
target_str = minsToTimeStr(target_mins)
|
|
self._trace(
|
|
f"无法选择最近的 {time_type} {target_str}, "
|
|
f"所有可选时间与目标时间相差都超过 {max_diff} 分钟",
|
|
logging.WARNING,
|
|
)
|
|
self._trace(f"当前可供预约的 {time_type} 有: {step_result.free_times}")
|
|
return False
|
|
|
|
def getTimeOptions(
|
|
self,
|
|
time_id: str,
|
|
) -> list[WebElement]:
|
|
|
|
try:
|
|
self._waitAllPresence(
|
|
(By.CSS_SELECTOR, f"#{time_id} ul li a")
|
|
)
|
|
except TimeoutException:
|
|
return []
|
|
return self._findAll(
|
|
By.CSS_SELECTOR,
|
|
f"#{time_id} ul li a",
|
|
)
|
|
|
|
def selectNearestTime(
|
|
self,
|
|
time_id: str,
|
|
target_time: int,
|
|
max_time_diff: int,
|
|
prefer_earlier: bool,
|
|
) -> TimeSelectionResult:
|
|
|
|
all_time_opts = self.getTimeOptions(time_id)
|
|
if not all_time_opts:
|
|
return TimeSelectionResult()
|
|
result = TimeSelectMaker.forReserve().decide(
|
|
all_time_opts,
|
|
target_time,
|
|
max_time_diff,
|
|
prefer_earlier,
|
|
)
|
|
if result.selected_index >= 0:
|
|
try:
|
|
all_time_opts[result.selected_index].click()
|
|
except (ElementNotInteractableException, StaleElementReferenceException):
|
|
return TimeSelectionResult(free_times=result.free_times)
|
|
return result
|
|
|
|
def selectTimeRange(
|
|
self,
|
|
begin_target: int,
|
|
end_target: int,
|
|
begin_max_diff: int = 30,
|
|
end_max_diff: int = 30,
|
|
begin_prefer_early: bool = True,
|
|
end_prefer_early: bool = False,
|
|
satisfy_duration: bool = True,
|
|
expect_duration: int = 4,
|
|
library_close_mins: int = TimeSelectMaker.LIBRARY_CLOSE_MINS,
|
|
) -> TimeRangeResult:
|
|
|
|
begin_result = self.selectNearestTime(
|
|
"startTime",
|
|
begin_target,
|
|
begin_max_diff,
|
|
begin_prefer_early,
|
|
)
|
|
if begin_result.selected_index < 0:
|
|
return TimeRangeResult(begin_result=begin_result)
|
|
actual_begin = begin_result.selected_value
|
|
if satisfy_duration:
|
|
end_target = TimeSelectMaker.calcEndTime(
|
|
actual_begin,
|
|
expect_duration,
|
|
library_close_mins,
|
|
)
|
|
end_result = self.selectNearestTime(
|
|
"endTime",
|
|
end_target,
|
|
end_max_diff,
|
|
end_prefer_early,
|
|
)
|
|
if end_result.selected_index < 0:
|
|
return TimeRangeResult(
|
|
begin_result=begin_result,
|
|
actual_begin_mins=actual_begin,
|
|
end_result=end_result,
|
|
expect_end_mins=end_target,
|
|
)
|
|
return TimeRangeResult(
|
|
begin_result=begin_result,
|
|
end_result=end_result,
|
|
actual_begin_mins=actual_begin,
|
|
actual_end_mins=end_result.selected_value,
|
|
expect_end_mins=end_target,
|
|
)
|
|
|
|
def selectSeatTime(
|
|
self,
|
|
ctx: ReserveContext,
|
|
library_close_mins: int = TimeSelectMaker.LIBRARY_CLOSE_MINS,
|
|
) -> bool:
|
|
|
|
exp_beg_mins = timeStrToMins(ctx.begin_time)
|
|
exp_end_mins = timeStrToMins(ctx.end_time)
|
|
result = self.selectTimeRange(
|
|
begin_target=exp_beg_mins,
|
|
end_target=exp_end_mins,
|
|
begin_max_diff=ctx.begin_max_diff,
|
|
end_max_diff=ctx.end_max_diff,
|
|
begin_prefer_early=ctx.begin_prefer_early,
|
|
end_prefer_early=ctx.end_prefer_early,
|
|
satisfy_duration=ctx.satisfy_duration,
|
|
expect_duration=ctx.expect_duration,
|
|
library_close_mins=library_close_mins,
|
|
)
|
|
if not self._logTimeStep("开始时间", exp_beg_mins, ctx.begin_max_diff, result.begin_result):
|
|
return False
|
|
if ctx.satisfy_duration:
|
|
unclipped = result.actual_begin_mins + ctx.expect_duration*60
|
|
if unclipped > library_close_mins:
|
|
self._trace(
|
|
f"预约持续时间 {ctx.expect_duration} 小时, 超过最大预约时间 {minsToTimeStr(library_close_mins)}, "
|
|
f"自动调整为 {minsToTimeStr(library_close_mins)}",
|
|
logging.WARNING,
|
|
)
|
|
act_beg_str = minsToTimeStr(result.actual_begin_mins)
|
|
exp_end_str = minsToTimeStr(result.expect_end_mins)
|
|
self._trace(
|
|
f"需要满足期望预约持续时间: {ctx.expect_duration} 小时, "
|
|
f"根据开始时间 {act_beg_str} 计算结束时间: {exp_end_str}"
|
|
)
|
|
if not self._logTimeStep("结束时间", result.expect_end_mins, ctx.end_max_diff, result.end_result):
|
|
return False
|
|
act_beg_str = minsToTimeStr(result.actual_begin_mins)
|
|
act_end_str = minsToTimeStr(result.actual_end_mins)
|
|
exp_end_str = minsToTimeStr(result.expect_end_mins)
|
|
self._trace(
|
|
f"期望预约时间段: {ctx.begin_time} - {exp_end_str}, "
|
|
f"实际预约时间段: {act_beg_str} - {act_end_str}"
|
|
)
|
|
return True
|