python-qdairlines-helper 0.0.6__py3-none-any.whl → 0.2.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. {python_qdairlines_helper-0.0.6.dist-info → python_qdairlines_helper-0.2.6.dist-info}/METADATA +2 -1
  2. python_qdairlines_helper-0.2.6.dist-info/RECORD +35 -0
  3. qdairlines_helper/config/url_const.py +2 -1
  4. qdairlines_helper/controller/add_passenger.py +29 -26
  5. qdairlines_helper/controller/book_payment.py +73 -41
  6. qdairlines_helper/controller/book_search.py +105 -52
  7. qdairlines_helper/controller/cash_pax_info.py +11 -8
  8. qdairlines_helper/controller/home.py +4 -3
  9. qdairlines_helper/controller/nhlms_cashdesk.py +32 -18
  10. qdairlines_helper/controller/order_detail.py +43 -34
  11. qdairlines_helper/controller/order_query.py +9 -6
  12. qdairlines_helper/controller/order_verify.py +9 -4
  13. qdairlines_helper/controller/pay_success.py +66 -0
  14. qdairlines_helper/controller/user_login.py +14 -9
  15. qdairlines_helper/po/add_passenger_page.py +3 -2
  16. qdairlines_helper/po/book_search_page.py +46 -28
  17. qdairlines_helper/po/cash_pax_info_page.py +10 -10
  18. qdairlines_helper/po/nhlms_cash_desk_page.py +2 -1
  19. qdairlines_helper/po/order_verify_page.py +0 -2
  20. qdairlines_helper/po/pay_success_page.py +33 -0
  21. qdairlines_helper/utils/exception_utils.py +91 -3
  22. python_qdairlines_helper-0.0.6.dist-info/RECORD +0 -34
  23. qdairlines_helper/utils/log_utils.py +0 -14
  24. {python_qdairlines_helper-0.0.6.dist-info → python_qdairlines_helper-0.2.6.dist-info}/WHEEL +0 -0
  25. {python_qdairlines_helper-0.0.6.dist-info → python_qdairlines_helper-0.2.6.dist-info}/licenses/LICENSE +0 -0
  26. {python_qdairlines_helper-0.0.6.dist-info → python_qdairlines_helper-0.2.6.dist-info}/top_level.txt +0 -0
@@ -10,17 +10,21 @@
10
10
  # ---------------------------------------------------------------------------------------------------------
11
11
  """
12
12
  import inspect
13
- from typing import Tuple, Callable
13
+ from logging import Logger
14
+ from typing import Callable
14
15
  from playwright.async_api import BrowserContext
15
- from qdairlines_helper.utils.log_utils import logger
16
16
  import qdairlines_helper.config.url_const as url_const
17
17
  from qdairlines_helper.po.nhlms_cash_desk_page import NhlmsCashDeskPage
18
18
  from playwright_helper.utils.browser_utils import switch_for_table_window
19
19
  from qdairlines_helper.utils.exception_utils import DuplicatePaymentError
20
+ from flight_helper.models.dto.payment import HFPaidAccountPaymentInputDTO, PaymentResultDTO
20
21
 
21
22
 
22
- async def load_nhlms_cash_desk_po(*, context: BrowserContext, timeout: float = 60.0) -> NhlmsCashDeskPage:
23
- nhlms_cashdesk_url = url_const.nhlms_cashdesk_domain + url_const.nhlms_cashdesk_url
23
+ async def load_nhlms_cash_desk_po(
24
+ *, context: BrowserContext, logger: Logger, domain: str, protocol: str, timeout: float = 60.0
25
+ ) -> NhlmsCashDeskPage:
26
+ url_prefix = f"{protocol}://{domain}"
27
+ nhlms_cashdesk_url = url_prefix + url_const.nhlms_cashdesk_url
24
28
 
25
29
  current_page = await switch_for_table_window(
26
30
  browser=context, url_keyword=url_const.nhlms_cashdesk_url, wait_time=int(timeout)
@@ -32,10 +36,10 @@ async def load_nhlms_cash_desk_po(*, context: BrowserContext, timeout: float = 6
32
36
  return nhlms_cashdesk_po
33
37
 
34
38
 
35
- async def pay_account_payment(
36
- *, page: NhlmsCashDeskPage, operator_account: str, pay_password: str, order_id: int,
37
- is_pay_completed_callback: Callable, payment_type: str = "付款账户支付", timeout: float = 60.0
38
- ) -> Tuple[str, float]:
39
+ async def hf_paid_account_payment(
40
+ *, page: NhlmsCashDeskPage, logger: Logger, order_no: str, is_pay_completed_callback: Callable,
41
+ hf_paid_account_payment_dto: HFPaidAccountPaymentInputDTO, timeout: float = 60.0
42
+ ) -> PaymentResultDTO:
39
43
  # 1. 获取收银台支付流水
40
44
  pay_transaction = await page.get_order_transaction(timeout=timeout)
41
45
  logger.info(f"汇付天下收银台页面,支付流水<{pay_transaction}>获取完成")
@@ -45,30 +49,40 @@ async def pay_account_payment(
45
49
  logger.info(f"汇付天下收银台页面,支付金额<{actual_payment_amount}>获取完成")
46
50
 
47
51
  # 3. 获取付款方式tab
48
- payment_type_tab = await page.get_payment_type_tab(payment_type=payment_type, timeout=timeout)
52
+ payment_type_tab = await page.get_payment_type_tab(
53
+ payment_type=hf_paid_account_payment_dto.payment_type, timeout=timeout
54
+ )
49
55
  await payment_type_tab.click(button="left")
50
- logger.info(f"汇付天下收银台页面,【{payment_type}】Tab点击完成")
56
+ logger.info(f"汇付天下收银台页面,【{hf_paid_account_payment_dto.payment_type}】Tab点击完成")
51
57
 
52
58
  # 4. 输入操作员号
53
59
  username_input = await page.get_username_input(timeout=timeout)
54
- await username_input.fill(value=operator_account)
55
- logger.info(f"汇付天下收银台页面,操作员号<{operator_account}>输入完成")
60
+ await username_input.fill(value=hf_paid_account_payment_dto.account)
61
+ logger.info(f"汇付天下收银台页面,操作员号<{hf_paid_account_payment_dto.account}>输入完成")
56
62
 
57
63
  # 5. 输入交易密码
58
64
  password_input = await page.get_password_input(timeout=timeout)
59
- await password_input.fill(value=pay_password)
60
- logger.info(f"汇付天下收银台页面,交易密码<{pay_password}>输入完成")
65
+ await password_input.fill(value=hf_paid_account_payment_dto.password)
66
+ logger.info(f"汇付天下收银台页面,交易密码<{hf_paid_account_payment_dto.password}>输入完成")
61
67
 
62
68
  # 6. 校验订单是否已经被支付
63
69
  if inspect.iscoroutinefunction(is_pay_completed_callback):
64
- is_pay: bool = await is_pay_completed_callback(order_id=order_id)
70
+ is_pay: bool = await is_pay_completed_callback(order_id=order_no)
65
71
  else:
66
- is_pay: bool = is_pay_completed_callback(order_id=order_id)
72
+ is_pay: bool = is_pay_completed_callback(order_id=order_no)
67
73
  if is_pay is True:
68
- raise DuplicatePaymentError(order_id=order_id)
74
+ raise DuplicatePaymentError(order_no=order_no)
69
75
 
70
76
  # 6. 点击【确认支付】
71
77
  confirm_payment_btn = await page.get_confirm_payment_btn(timeout=timeout)
72
78
  await confirm_payment_btn.click(button="left")
73
79
  logger.info("汇付天下收银台页面,【确认支付】按钮点击完成")
74
- return pay_transaction, actual_payment_amount
80
+ return PaymentResultDTO(
81
+ channel_name=hf_paid_account_payment_dto.channel_name,
82
+ payment_type=hf_paid_account_payment_dto.payment_type,
83
+ account=hf_paid_account_payment_dto.account,
84
+ password=hf_paid_account_payment_dto.password,
85
+ order_no=order_no,
86
+ pay_amount=actual_payment_amount,
87
+ pay_transaction=pay_transaction
88
+ )
@@ -9,45 +9,54 @@
9
9
  # Copyright ©2011-2026. Hunan xxxxxxx Company limited. All rights reserved.
10
10
  # ---------------------------------------------------------------------------------------------------------
11
11
  """
12
+ import re
12
13
  from aiohttp import CookieJar
13
14
  from typing import Dict, Any, Optional, List
14
- from qdairlines_helper.utils.log_utils import logger
15
15
  from qdairlines_helper.http.flight_order import FlightOrder
16
+ from flight_helper.models.dto.itinerary import QueryItineraryResponseDTO, QueryItineraryRequestDTO
16
17
 
17
18
 
18
- async def get_order_itinerary(
19
- *, playwright_state: Dict[str, Any], token: Optional[str], pre_order_no: str, user_id: str,
20
- proxy: Dict[str, Any], domain: Optional[str] = None, protocol: Optional[str] = None,
21
- timeout: Optional[int] = None, retry: Optional[int] = None, enable_log: Optional[bool] = None,
22
- cookie_jar: Optional[CookieJar] = None, headers: Dict[str, str] = None
19
+ async def get_order_detail(
20
+ *, query_dto: QueryItineraryRequestDTO, timeout: Optional[int] = None, retry: Optional[int] = None,
21
+ cookie_jar: Optional[CookieJar] = None, enable_log: Optional[bool] = None
23
22
  ) -> Dict[str, Any]:
24
23
  flight_order = FlightOrder(
25
- playwright_state=playwright_state, token=token, domain=domain, protocol=protocol, timeout=timeout, retry=retry,
26
- enable_log=enable_log, cookie_jar=cookie_jar
24
+ playwright_state=query_dto.storage_state, token=query_dto.token, domain=query_dto.payment_domain,
25
+ protocol=query_dto.payment_protocol, timeout=timeout, retry=retry, enable_log=enable_log,
26
+ cookie_jar=cookie_jar, proxy=query_dto.proxy
27
+ )
28
+ order_info = await flight_order.get_order_details(
29
+ pre_order_no=query_dto.pre_order_no, user_id=query_dto.user_id, is_end=True, proxy=query_dto.proxy,
30
+ headers=query_dto.headers
31
+ )
32
+ if isinstance(order_info, dict) and order_info.get("result") and order_info.get("code") == 1:
33
+ return order_info.get("result")
34
+ else:
35
+ raise RuntimeError(f"订单<{query_dto.pre_order_no}>,获取详情数据异常,返回值:{order_info}")
36
+
37
+
38
+ async def get_order_itinerary(
39
+ *, query_dto: QueryItineraryRequestDTO, timeout: Optional[int] = None, retry: Optional[int] = None,
40
+ cookie_jar: Optional[CookieJar] = None, enable_log: Optional[bool] = None
41
+ ) -> Optional[QueryItineraryResponseDTO]:
42
+ _order_info: Dict[str, Any] = dict(pre_order_no=query_dto.pre_order_no)
43
+ result = await get_order_detail(
44
+ query_dto=query_dto, timeout=timeout, retry=retry, cookie_jar=cookie_jar, enable_log=enable_log
27
45
  )
28
- _order_info: Dict[str, Any] = dict(pre_order_no=pre_order_no)
29
- try:
30
- order_info = await flight_order.get_order_details(
31
- pre_order_no=pre_order_no, user_id=user_id, is_end=True, proxy=proxy, headers=headers
32
- )
33
- if isinstance(order_info, dict) and order_info.get("result"):
34
- result = order_info.get("result") or dict()
35
- _order_info["cash_unit"] = result.get("cashUnit")
36
- _order_info["order_status"] = result.get("orderStatus")
37
- _order_info["actual_payment_amount"] = result.get("totalPrice")
38
- passenger_infos: List[Dict[str, Any]] = result.get("passengerInfoVoList")
39
- order_itineraries = list()
40
- for passenger_info in passenger_infos:
41
- id_no = passenger_info.get("idNo")
42
- order_itinerary = passenger_info.get("tktNo")
43
- passenger = passenger_info.get("passName")
44
- order_itineraries.append(dict(
45
- passenger=passenger, id_no=id_no, order_itinerary=order_itinerary, pre_order_no=pre_order_no
46
- ))
47
- if order_itineraries:
48
- _order_info["order_itineraries"] = order_itineraries
49
- else:
50
- raise RuntimeError(f"订单<{pre_order_no}>,获取订单详情数据异常,返回值:{order_info}")
51
- except Exception as e:
52
- logger.error(e)
53
- return _order_info
46
+ _order_info["cash_unit"] = result.get("cashUnit")
47
+ _order_info["order_status"] = result.get("orderStatus")
48
+ _order_info["order_amount"] = result.get("totalPrice")
49
+ passenger_infos: List[Dict[str, Any]] = result.get("passengerInfoVoList")
50
+ order_itineraries = list()
51
+ for passenger_info in passenger_infos:
52
+ id_no = passenger_info.get("idNo")
53
+ order_itinerary = passenger_info.get("tktNo")
54
+ passenger = passenger_info.get("passName")
55
+ if bool(re.fullmatch(r'\d+-\d+', order_itinerary)):
56
+ order_itineraries.append(dict(
57
+ passenger_name=passenger, id_no=id_no, order_itinerary=order_itinerary,
58
+ pre_order_no=query_dto.pre_order_no
59
+ ))
60
+ if order_itineraries:
61
+ _order_info["itinerary_info"] = order_itineraries
62
+ return QueryItineraryResponseDTO(**_order_info)
@@ -10,14 +10,17 @@
10
10
  # ---------------------------------------------------------------------------------------------------------
11
11
  """
12
12
  from typing import Any
13
+ from logging import Logger
13
14
  from playwright.async_api import Page
14
- from qdairlines_helper.utils.log_utils import logger
15
15
  import qdairlines_helper.config.url_const as url_const
16
16
  from qdairlines_helper.po.air_order_page import AirOrderPage
17
+ from qdairlines_helper.utils.exception_utils import IPBlockError
17
18
  from qdairlines_helper.utils.po_utils import get_ip_access_blocked_msg
18
19
 
19
20
 
20
- async def open_air_page(*, page: Page, protocol: str, domain: str, timeout: float = 60.0) -> AirOrderPage:
21
+ async def open_air_order_page(
22
+ *, page: Page, logger: Logger, protocol: str, domain: str, timeout: float = 60.0
23
+ ) -> AirOrderPage:
21
24
  url_prefix = f"{protocol}://{domain}"
22
25
  air_order_url = url_prefix + url_const.air_order_url
23
26
  await page.goto(air_order_url)
@@ -27,15 +30,15 @@ async def open_air_page(*, page: Page, protocol: str, domain: str, timeout: floa
27
30
  logger.info(f"即将进入青岛航空官网航班查询页面,页面URL<{air_order_url}>")
28
31
  ip_access_blocked_msg = await get_ip_access_blocked_msg(page=page, timeout=3)
29
32
  if ip_access_blocked_msg:
30
- raise EnvironmentError(ip_access_blocked_msg)
33
+ raise IPBlockError(ip_access_blocked_msg)
31
34
  return air_order_po
32
35
 
33
36
 
34
- async def is_open_air_page_callback(
35
- *, page: Page, protocol: str, domain: str, timeout: float = 60.0, **kwargs: Any
37
+ async def is_open_air_order_page_callback(
38
+ *, page: Page, protocol: str, logger: Logger, domain: str, timeout: float = 60.0, **kwargs: Any
36
39
  ) -> bool:
37
40
  try:
38
- await open_air_page(page=page, protocol=protocol, domain=domain, timeout=timeout)
41
+ await open_air_order_page(page=page, logger=logger, protocol=protocol, domain=domain, timeout=timeout)
39
42
  return True
40
43
  except (Exception,):
41
44
  return False
@@ -9,14 +9,17 @@
9
9
  # Copyright ©2011-2026. Hunan xxxxxxx Company limited. All rights reserved.
10
10
  # ---------------------------------------------------------------------------------------------------------
11
11
  """
12
+ from logging import Logger
12
13
  from playwright.async_api import Page
13
- from qdairlines_helper.utils.log_utils import logger
14
14
  import qdairlines_helper.config.url_const as url_const
15
+ from qdairlines_helper.utils.exception_utils import IPBlockError
15
16
  from qdairlines_helper.po.order_verify_page import OrderVerifyPage
16
17
  from qdairlines_helper.utils.po_utils import get_ip_access_blocked_msg
17
18
 
18
19
 
19
- async def load_order_verify_po(*, page: Page, protocol: str, domain: str, timeout: float = 60.0) -> OrderVerifyPage:
20
+ async def load_order_verify_po(
21
+ *, page: Page, logger: Logger, protocol: str, domain: str, timeout: float = 60.0
22
+ ) -> OrderVerifyPage:
20
23
  url_prefix = f"{protocol}://{domain}"
21
24
  order_verify_url = url_prefix + url_const.order_verify_url
22
25
  order_verify_po = OrderVerifyPage(page=page, url=order_verify_url)
@@ -24,11 +27,13 @@ async def load_order_verify_po(*, page: Page, protocol: str, domain: str, timeou
24
27
  logger.info(f"即将进入青岛航空官网订单校验页面,页面URL<{order_verify_url}>")
25
28
  ip_access_blocked_msg = await get_ip_access_blocked_msg(page=page, timeout=3)
26
29
  if ip_access_blocked_msg:
27
- raise EnvironmentError(ip_access_blocked_msg)
30
+ raise IPBlockError(ip_access_blocked_msg)
28
31
  return order_verify_po
29
32
 
30
33
 
31
- async def order_verify(*, page: OrderVerifyPage, refresh_attempt: int = 3, timeout: float = 60.0) -> None:
34
+ async def order_verify(
35
+ *, page: OrderVerifyPage, logger: Logger, refresh_attempt: int = 3, timeout: float = 60.0
36
+ ) -> None:
32
37
  # 1. 先确认订单数据是否加载出来
33
38
  await page.get_order_info_plane(timeout=timeout, refresh_attempt=refresh_attempt)
34
39
 
@@ -0,0 +1,66 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ # ---------------------------------------------------------------------------------------------------------
4
+ # ProjectName: python-qdairlines-helper
5
+ # FileName: pay_success.py
6
+ # Description: 支付成功控制器
7
+ # Author: ASUS
8
+ # CreateDate: 2026/01/09
9
+ # Copyright ©2011-2026. Hunan xxxxxxx Company limited. All rights reserved.
10
+ # ---------------------------------------------------------------------------------------------------------
11
+ """
12
+ import inspect
13
+ from logging import Logger
14
+ from aiohttp import CookieJar
15
+ from playwright.async_api import Page
16
+ from typing import Dict, Any, Optional, Callable
17
+ import qdairlines_helper.config.url_const as url_const
18
+ from qdairlines_helper.po.pay_success_page import PaySuccessPage
19
+ from qdairlines_helper.utils.po_utils import get_ip_access_blocked_msg
20
+ from qdairlines_helper.controller.order_detail import get_order_detail
21
+ from flight_helper.models.dto.itinerary import QueryItineraryRequestDTO
22
+ from qdairlines_helper.utils.exception_utils import IPBlockError, PaymentFailedError
23
+
24
+
25
+ async def load_pay_success_page(
26
+ *, page: Page, logger: Logger, protocol: str, domain: str, timeout: float = 60.0
27
+ ) -> PaySuccessPage:
28
+ url_prefix = f"{protocol}://{domain}"
29
+ pay_success_url = url_prefix + url_const.pay_success_url
30
+ pay_success_po = PaySuccessPage(page=page, url=pay_success_url)
31
+ await pay_success_po.url_wait_for(url=pay_success_url, timeout=timeout)
32
+ logger.info(f"即将进入青岛航空官网支付成功页面,页面URL<{pay_success_url}>")
33
+ ip_access_blocked_msg = await get_ip_access_blocked_msg(page=page, timeout=3)
34
+ if ip_access_blocked_msg:
35
+ raise IPBlockError(ip_access_blocked_msg)
36
+ return pay_success_po
37
+
38
+
39
+ async def two_check_pay_success(
40
+ *, page: Page, logger: Logger, query_dto: QueryItineraryRequestDTO, callback_get_proxy: Callable,
41
+ cookie_jar: Optional[CookieJar] = None, timeout: float = 60.0, retry: int = 0, enable_log: bool = True
42
+ ) -> bool:
43
+ try:
44
+ # 1. 看支付成功页面是否加载完成
45
+ pay_success_po = await load_pay_success_page(
46
+ page=page, logger=logger, protocol=query_dto.payment_protocol, domain=query_dto.payment_domain,
47
+ timeout=timeout
48
+ )
49
+ # 2. 检测页面的支付成功image是否存在
50
+ await pay_success_po.get_pay_success_image(timeout=timeout)
51
+ return True
52
+ except Exception as e:
53
+ logger.error(e)
54
+
55
+ if inspect.iscoroutinefunction(callback_get_proxy):
56
+ query_dto.proxy = await callback_get_proxy(logger=logger)
57
+ else:
58
+ query_dto.proxy = callback_get_proxy(logger=logger)
59
+
60
+ # 2. 尝试取查询票号,看订单状态,是 BOOKED,还是 TICKETED
61
+ order_detail: Dict[str, Any] = await get_order_detail(
62
+ query_dto=query_dto, timeout=int(timeout), retry=retry, enable_log=enable_log, cookie_jar=cookie_jar,
63
+ )
64
+ order_status = (order_detail.get("orderStatus", "")).upper()
65
+ if order_status not in ("TICKED",):
66
+ raise PaymentFailedError(pre_order_no=query_dto.pre_order_no, order_status=order_status)
@@ -9,17 +9,20 @@
9
9
  # Copyright ©2011-2026. Hunan xxxxxxx Company limited. All rights reserved.
10
10
  # ---------------------------------------------------------------------------------------------------------
11
11
  """
12
+ from logging import Logger
12
13
  from typing import Dict, Any
13
- from qdairlines_helper.utils.log_utils import logger
14
14
  from playwright.async_api import Page, BrowserContext
15
15
  from qdairlines_helper.po.login_page import LoginPage
16
16
  import qdairlines_helper.config.url_const as url_const
17
17
  from qdairlines_helper.controller.home import load_home_po
18
+ from qdairlines_helper.utils.exception_utils import IPBlockError
18
19
  from playwright.async_api import Error as PlaywrightError, TimeoutError as PlaywrightTimeoutError
19
20
  from qdairlines_helper.utils.po_utils import get_ip_access_blocked_msg, parse_user_info_from_page_response
20
21
 
21
22
 
22
- async def open_login_page(*, page: Page, protocol: str, domain: str, timeout: float = 60.0) -> LoginPage:
23
+ async def open_login_page(
24
+ *, page: Page, logger: Logger, protocol: str, domain: str, timeout: float = 60.0
25
+ ) -> LoginPage:
23
26
  url_prefix = f"{protocol}://{domain}"
24
27
  login_url = url_prefix + url_const.login_url
25
28
  await page.goto(login_url)
@@ -29,12 +32,13 @@ async def open_login_page(*, page: Page, protocol: str, domain: str, timeout: fl
29
32
  logger.info(f"即将进入青岛航空官网登录页面,页面URL<{login_url}>")
30
33
  ip_access_blocked_msg = await get_ip_access_blocked_msg(page=page, timeout=3)
31
34
  if ip_access_blocked_msg:
32
- raise EnvironmentError(ip_access_blocked_msg)
35
+ raise IPBlockError(ip_access_blocked_msg)
33
36
  return login_po
34
37
 
35
38
 
36
39
  async def _user_login(
37
- *, page: LoginPage, protocol: str, domain: str, username: str, password: str, timeout: float = 60.0
40
+ *, page: LoginPage, logger: Logger, protocol: str, domain: str, username: str, password: str,
41
+ timeout: float = 60.0
38
42
  ) -> None:
39
43
  # 1. 输入用户名
40
44
  username_input = await page.get_login_username_input(timeout=timeout)
@@ -62,18 +66,18 @@ async def _user_login(
62
66
 
63
67
  # 5. 加载首页页面对象,加载成功说明进入了首页,登录成功
64
68
  try:
65
- await load_home_po(page=page.get_page(), protocol=protocol, domain=domain, timeout=timeout)
69
+ await load_home_po(page=page.get_page(), logger=logger, protocol=protocol, domain=domain, timeout=timeout)
66
70
  logger.info(f"青岛航空官网登录页面,用户:{username}, 密码:{password}登录成功")
67
71
  except (PlaywrightError, PlaywrightTimeoutError, EnvironmentError, RuntimeError, Exception) as e:
68
72
  raise RuntimeError(f"青岛航空官网登录页面,用户:{username}, 密码:{password}登录失败,原因:{e}")
69
73
 
70
74
 
71
75
  async def user_login_callback(
72
- *, page: Page, context: BrowserContext, protocol: str, domain: str, username: str, password: str,
73
- timeout: float = 60.0, **kwargs: Any
76
+ *, page: Page, context: BrowserContext, logger: Logger, protocol: str, domain: str, username: str,
77
+ password: str, timeout: float = 60.0, **kwargs: Any
74
78
  ) -> Dict[str, Any]:
75
79
  # 1. 打开登录页面
76
- login_po = await open_login_page(page=page, protocol=protocol, domain=domain, timeout=timeout)
80
+ login_po = await open_login_page(page=page, logger=logger, protocol=protocol, domain=domain, timeout=timeout)
77
81
 
78
82
  # 2. 开启网络监听
79
83
  keywords = [url_const.auth_form_api_url, url_const.login_after_api_url]
@@ -84,7 +88,8 @@ async def user_login_callback(
84
88
 
85
89
  # 2. 执行登录操作
86
90
  await _user_login(
87
- page=login_po, protocol=protocol, domain=domain, username=username, password=password, timeout=timeout
91
+ page=login_po, protocol=protocol, logger=logger, domain=domain, username=username, password=password,
92
+ timeout=timeout
88
93
  )
89
94
  user_info = parse_user_info_from_page_response(network_logs=network_logs)
90
95
  user_info.update(dict(storage_state=await context.storage_state()))
@@ -13,6 +13,7 @@ from typing import Optional
13
13
  from playwright.async_api import Page, Locator
14
14
  from playwright_helper.libs.base_po import BasePo
15
15
  import qdairlines_helper.config.url_const as url_const
16
+ from qdairlines_helper.utils.exception_utils import PassengerTypeError
16
17
 
17
18
 
18
19
  class AddPassengerPage(BasePo):
@@ -37,7 +38,7 @@ class AddPassengerPage(BasePo):
37
38
  elif passenger_type == "婴儿":
38
39
  btn_text = "添加婴儿"
39
40
  else:
40
- raise EnvironmentError(f"乘客类型<{passenger_type}>暂不支持")
41
+ raise PassengerTypeError(passenger_type=passenger_type)
41
42
  selector: str = f'//div[@class="el-row"]//button[@class="search_btn" and contains(text(), "{btn_text}")]'
42
43
  return await self.get_locator(selector=selector, timeout=timeout)
43
44
 
@@ -68,7 +69,7 @@ class AddPassengerPage(BasePo):
68
69
  elif passenger_type == "婴儿":
69
70
  passenger_class = f"passengerBaby{position_index}"
70
71
  else:
71
- raise EnvironmentError(f"乘客类型<{passenger_type}>暂不支持")
72
+ raise PassengerTypeError(passenger_type=passenger_type)
72
73
  selector: str = f'//label[@for="{passenger_class}"]/span[@class="checkbox"]'
73
74
  return await self.get_locator(selector=selector, timeout=timeout)
74
75
 
@@ -10,11 +10,12 @@
10
10
  # ---------------------------------------------------------------------------------------------------------
11
11
  """
12
12
  import re
13
+ from logging import Logger
13
14
  from typing import Optional, Dict, Any, List
14
15
  from playwright.async_api import Page, Locator
15
16
  from playwright_helper.libs.base_po import BasePo
16
- from qdairlines_helper.utils.log_utils import logger
17
17
  import qdairlines_helper.config.url_const as url_const
18
+ from qdairlines_helper.utils.exception_utils import ProductTypeError
18
19
  from playwright_helper.utils.type_utils import convert_order_amount_text, safe_convert_advanced
19
20
  from playwright.async_api import Error as PlaywrightError, TimeoutError as PlaywrightTimeoutError
20
21
 
@@ -27,6 +28,15 @@ class BookSearchPage(BasePo):
27
28
  super().__init__(page, url or url_const.book_search_url)
28
29
  self.__page = page
29
30
 
31
+ async def get_empyt_data_page(self, timeout: float = 5.0) -> Locator:
32
+ """
33
+ 打开搜索页后,看看当前默认状态是不是空数据页,非空数据页,会有弹框
34
+ :param timeout: 超时时间(秒)
35
+ :return: (是否存在, 错误信息 | 元素对象)
36
+ """
37
+ selector: str = '//strong[contains(text(), "没有在飞航班")]'
38
+ return await self.get_locator(selector=selector, timeout=timeout)
39
+
30
40
  async def get_reminder_dialog_continue_book_btn(self, timeout: float = 5.0) -> Locator:
31
41
  """
32
42
  获取预订搜索页的温馨提醒弹框中的【继续购票】按钮
@@ -72,25 +82,18 @@ class BookSearchPage(BasePo):
72
82
  selector: str = '//div[contains(@class, "flight_search_form")]//button[@class="search_btn"]'
73
83
  return await self.get_locator(selector=selector, timeout=timeout)
74
84
 
75
- async def get_flight_info_plane(self, timeout: float = 5.0) -> Locator:
85
+ async def get_flight_info_plane(self, flight_no: str, timeout: float = 5.0) -> Locator:
76
86
  """
77
87
  获取预订搜索页航班内容栏航班基本信息
88
+ :param flight_no: 航班编号
78
89
  :param timeout: 超时时间(秒)
79
90
  :return: (是否存在, 错误信息|元素对象)
80
91
  """
81
- selector: str = '//div[@class="airlines_cont"]//div[@class="text-center el-row"]'
82
- return await self.get_locator(selector=selector, timeout=timeout)
83
-
84
- async def get_flight_no(self, locator: Locator, timeout: float = 5.0) -> str:
85
- """
86
- 获取预订搜索页航班编号
87
- :param timeout: 超时时间(秒)
88
- :param locator: flight_info_plane Locator 对象
89
- :return: (是否存在, 错误信息|元素对象)
90
- """
91
- selector: str = 'xpath=(.//div[contains(@class, "flight_qda")]/span)[2]'
92
- sub_locator: Locator = await self.get_sub_locator(locator=locator, selector=selector, timeout=timeout)
93
- return (await sub_locator.inner_text(timeout=timeout)).strip()
92
+ try:
93
+ selector: str = f'//div[contains(@class, "flight_qda")]/span[contains(text(), "{flight_no}")]/../../../..'
94
+ return await self.get_locator(selector=selector, timeout=timeout)
95
+ except (Exception,):
96
+ raise RuntimeError(f"航班预订查询页面,没有搜索到航班<{flight_no}>数据")
94
97
 
95
98
  async def get_flight_product_nav(self, locator: Locator, product_type: str, timeout: float = 5.0) -> Locator:
96
99
  """
@@ -107,31 +110,45 @@ class BookSearchPage(BasePo):
107
110
  elif product_type == "公务舱":
108
111
  index = 3
109
112
  else:
110
- raise EnvironmentError(f"产品类型参数值<{product_type}>无效,目前仅支持<经济舱,超级经济舱,公务舱>")
113
+ raise ProductTypeError(product_type=product_type)
111
114
  selector: str = f'xpath=(.//div[contains(@class, "nav_item el-col el-col-24")])[{index}]'
112
115
  return await self.get_sub_locator(locator=locator, selector=selector, timeout=timeout)
113
116
 
114
- async def get_flight_products(self, timeout: float = 5.0) -> Dict[str, Any]:
117
+ async def get_more_product_btn(self, locator: Locator, timeout: float = 5.0) -> Locator:
115
118
  """
116
- 获取预订搜索页航班内容栏所有航班产品
119
+ 获取预订搜索页航班列表更多产品
117
120
  :param timeout: 超时时间(秒)
121
+ :param locator: flight_info_plane Locator 对象
122
+ :return: (是否存在, 错误信息|元素对象)
123
+ """
124
+ selector: str = f'xpath=.//span[@class="fa el-icon-caret-bottom"]'
125
+ return await self.get_sub_locator(locator=locator, selector=selector, timeout=timeout)
126
+
127
+ async def get_flight_products(self, locator: Locator, logger: Logger, flight_no: str) -> List[Dict[str, Any]]:
128
+ """
129
+ 获取预订搜索页航班内容栏所有航班产品
130
+ :param locator: flight_info_plane Locator 对象
131
+ :param logger: Logger 对象,日志记录
132
+ :param flight_no: 航班编号
118
133
  :return: (是否存在, 错误信息|元素对象)
119
134
  """
120
- selector: str = '//div[@class="airlines_cont"]//div[@class="text-center cabinClasses_info el-row" and not(@style="display: none;")]/table/tbody/tr'
121
- products_locator: Locator = await self.get_locator(selector=selector, timeout=timeout)
135
+ timeout: float = 1.0
136
+ selector: str = 'xpath=.//div[@class="text-center cabinClasses_info el-row" and not(@style="display: none;")]//table/tbody/tr[not(@class)]/td[@class]/..'
137
+ products_locator: Locator = await self.get_sub_locator(locator=locator, selector=selector, timeout=timeout)
122
138
  locators: List[Locator] = await products_locator.all()
123
- flight_products = dict()
139
+ flight_products = list()
140
+ logger.info(f"当前找到了关于航班<{flight_no}>的<{len(locators)}>条产品数据")
124
141
  for locator in locators:
125
142
  try:
126
143
  booking_btn: Locator = await self._get_flight_product_booking_btn(locator=locator, timeout=timeout)
127
144
  amounts: Dict[str, Any] = await self._get_flight_product_price(locator=locator, timeout=timeout)
128
145
  seats_status: int = await self._get_flight_product_seats_status(locator=locator, timeout=timeout)
129
146
  cabin = await self._get_flight_product_cabin(locator=locator, timeout=timeout)
130
- flight_products[cabin] = dict(
147
+ flight_products.append(dict(
131
148
  amounts=amounts, cabin=cabin, seats_status=seats_status, booking_btn=booking_btn
132
- )
149
+ ))
133
150
  except (PlaywrightError, PlaywrightTimeoutError, EnvironmentError, RuntimeError, Exception) as e:
134
- logger.warning(e)
151
+ logger.error(e)
135
152
  continue
136
153
  return flight_products
137
154
 
@@ -157,7 +174,7 @@ class BookSearchPage(BasePo):
157
174
  :param locator: flight_product Locator 对象
158
175
  :return: (是否存在, 错误信息|元素对象)
159
176
  """
160
- selector: str = 'xpath=.//div[@class="price_flex_top"]/span'
177
+ selector: str = 'xpath=.//div[@class="price_flex_top"]'
161
178
  locator: Locator = await self.get_sub_locator(locator=locator, selector=selector, timeout=timeout)
162
179
  amount_text: str = (await locator.inner_text(timeout=timeout)).strip()
163
180
  return convert_order_amount_text(amount_text=amount_text)
@@ -169,7 +186,7 @@ class BookSearchPage(BasePo):
169
186
  :param locator: flight_product Locator 对象
170
187
  :return: (是否存在, 错误信息|元素对象)
171
188
  """
172
- selector: str = 'xpath=(.//td[@class="ticket_num"]/div/span)[1]'
189
+ selector: str = 'xpath=(.//td[@class="ticket_num"]/div/span)[1]'
173
190
  return await self.get_sub_locator(locator=locator, selector=selector, timeout=timeout)
174
191
 
175
192
  async def _get_flight_product_seats_status(self, locator: Locator, timeout: float = 5.0) -> int:
@@ -179,11 +196,12 @@ class BookSearchPage(BasePo):
179
196
  :param locator: flight_product Locator 对象
180
197
  :return: (是否存在, 错误信息|元素对象)
181
198
  """
182
- selector: str = 'xpath=(.//td[@class="ticket_num"]/div/span)[2]'
199
+ selector: str = 'xpath=(.//td[@class="ticket_num"]/div/span)[2]'
183
200
  locator: Locator = await self.get_sub_locator(locator=locator, selector=selector, timeout=timeout)
184
201
  more_seats_text: str = (await locator.inner_text(timeout=timeout)).strip()
185
202
  match = re.search(r'\d+', more_seats_text)
186
203
  if match:
187
- return safe_convert_advanced(value=match.group(1).strip())
204
+ # ⚠️ 正则 \d+ 没有捕获组,只能用 group(0) 或 group()
205
+ return safe_convert_advanced(value=match.group().strip())
188
206
  else:
189
207
  return 999999
@@ -13,8 +13,8 @@ from typing import Optional
13
13
  from urllib.parse import urlparse, parse_qs
14
14
  from playwright.async_api import Page, Locator
15
15
  from playwright_helper.libs.base_po import BasePo
16
- from qdairlines_helper.utils.log_utils import logger
17
16
  import qdairlines_helper.config.url_const as url_const
17
+ from qdairlines_helper.utils.exception_utils import PaymentChannelError
18
18
 
19
19
 
20
20
  class CashPaxInfoPage(BasePo):
@@ -25,23 +25,23 @@ class CashPaxInfoPage(BasePo):
25
25
  super().__init__(page, url or url_const.cash_pax_info_url)
26
26
  self.__page = page
27
27
 
28
- async def get_payment_type_checkbox(self, payment_type: str, timeout: float = 5.0) -> Locator:
28
+ async def get_payment_channel_checkbox(self, channel_name: str, timeout: float = 5.0) -> Locator:
29
29
  """
30
- 获取支付方式页面中的支付类型【支付宝|微信|汇付天下|易宝支付】单选框
31
- :param payment_type: 支付类型,支付宝|微信|汇付天下|易宝支付
30
+ 获取支付方式页面中的支付渠道【支付宝|微信|汇付天下|易宝支付】单选框
31
+ :param channel_name: 支付渠道,支付宝|微信|汇付天下|易宝支付
32
32
  :param timeout: 超时时间(秒)
33
33
  :return: (是否存在, 错误信息|元素对象)
34
34
  """
35
- if payment_type == '支付宝':
35
+ if channel_name == '支付宝':
36
36
  for_class = 0
37
- elif payment_type == "微信":
37
+ elif channel_name == "微信":
38
38
  for_class = 1
39
- elif payment_type == "汇付天下":
39
+ elif channel_name == "汇付天下":
40
40
  for_class = 2
41
- elif payment_type == "易宝支付":
41
+ elif channel_name == "易宝支付":
42
42
  for_class = 3
43
43
  else:
44
- raise EnvironmentError(f"支付类型<{payment_type}>暂不支持")
44
+ raise PaymentChannelError(channel_name=channel_name)
45
45
  selector: str = f'xpath=//label[@for="passengerAdult{for_class}"]//span[@class="checkbox"]'
46
46
  return await self.get_locator(selector=selector, timeout=timeout)
47
47
 
@@ -68,7 +68,7 @@ class CashPaxInfoPage(BasePo):
68
68
  if pre_order_no and len(pre_order_no) == 18 and pre_order_no.startswith("OW") is True:
69
69
  return pre_order_no
70
70
  except (Exception,):
71
- logger.error(f"支付方式页面获取到的url={self.__page.url},信息异常")
71
+ pass
72
72
  selector: str = '//div[@class="order-form"]//span[contains(text(), "订单编号")]/../span[@class="font_red"]'
73
73
  locator = await self.get_locator(selector=selector, timeout=timeout)
74
74
  return (await locator.inner_text()).strip()
@@ -14,6 +14,7 @@ from playwright.async_api import Page, Locator
14
14
  from playwright_helper.libs.base_po import BasePo
15
15
  import qdairlines_helper.config.url_const as url_const
16
16
  from playwright_helper.utils.type_utils import safe_convert_advanced
17
+ from qdairlines_helper.utils.exception_utils import HFPaymentTypeError
17
18
 
18
19
 
19
20
  class NhlmsCashDeskPage(BasePo):
@@ -62,7 +63,7 @@ class NhlmsCashDeskPage(BasePo):
62
63
  elif payment_type == "快捷支付":
63
64
  value = "fp"
64
65
  else:
65
- raise EnvironmentError(f"汇付天下收银台暂不支持<{payment_type}>的付款方式")
66
+ raise HFPaymentTypeError(payment_type=payment_type)
66
67
  selector: str = f'//div[@class="content-bottom network-recharge"]//li[@value="{value}"]'
67
68
  return await self.get_locator(selector=selector, timeout=timeout)
68
69