python-qdairlines-helper 0.0.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 (34) hide show
  1. python_qdairlines_helper-0.0.6.dist-info/METADATA +247 -0
  2. python_qdairlines_helper-0.0.6.dist-info/RECORD +34 -0
  3. python_qdairlines_helper-0.0.6.dist-info/WHEEL +5 -0
  4. python_qdairlines_helper-0.0.6.dist-info/licenses/LICENSE +201 -0
  5. python_qdairlines_helper-0.0.6.dist-info/top_level.txt +1 -0
  6. qdairlines_helper/__init__.py +11 -0
  7. qdairlines_helper/config/__init__.py +11 -0
  8. qdairlines_helper/config/url_const.py +37 -0
  9. qdairlines_helper/controller/__init__.py +11 -0
  10. qdairlines_helper/controller/add_passenger.py +92 -0
  11. qdairlines_helper/controller/book_payment.py +83 -0
  12. qdairlines_helper/controller/book_search.py +114 -0
  13. qdairlines_helper/controller/cash_pax_info.py +47 -0
  14. qdairlines_helper/controller/home.py +28 -0
  15. qdairlines_helper/controller/nhlms_cashdesk.py +74 -0
  16. qdairlines_helper/controller/order_detail.py +53 -0
  17. qdairlines_helper/controller/order_query.py +41 -0
  18. qdairlines_helper/controller/order_verify.py +43 -0
  19. qdairlines_helper/controller/user_login.py +91 -0
  20. qdairlines_helper/http/__init__.py +11 -0
  21. qdairlines_helper/http/flight_order.py +86 -0
  22. qdairlines_helper/po/__init__.py +11 -0
  23. qdairlines_helper/po/add_passenger_page.py +155 -0
  24. qdairlines_helper/po/air_order_page.py +24 -0
  25. qdairlines_helper/po/book_search_page.py +189 -0
  26. qdairlines_helper/po/cash_pax_info_page.py +74 -0
  27. qdairlines_helper/po/home_page.py +24 -0
  28. qdairlines_helper/po/login_page.py +70 -0
  29. qdairlines_helper/po/nhlms_cash_desk_page.py +94 -0
  30. qdairlines_helper/po/order_verify_page.py +62 -0
  31. qdairlines_helper/utils/__init__.py +11 -0
  32. qdairlines_helper/utils/exception_utils.py +17 -0
  33. qdairlines_helper/utils/log_utils.py +14 -0
  34. qdairlines_helper/utils/po_utils.py +50 -0
@@ -0,0 +1,91 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ # ---------------------------------------------------------------------------------------------------------
4
+ # ProjectName: python-qdairlines-helper
5
+ # FileName: user_login.py
6
+ # Description: 用户登录控制器
7
+ # Author: ASUS
8
+ # CreateDate: 2026/01/04
9
+ # Copyright ©2011-2026. Hunan xxxxxxx Company limited. All rights reserved.
10
+ # ---------------------------------------------------------------------------------------------------------
11
+ """
12
+ from typing import Dict, Any
13
+ from qdairlines_helper.utils.log_utils import logger
14
+ from playwright.async_api import Page, BrowserContext
15
+ from qdairlines_helper.po.login_page import LoginPage
16
+ import qdairlines_helper.config.url_const as url_const
17
+ from qdairlines_helper.controller.home import load_home_po
18
+ from playwright.async_api import Error as PlaywrightError, TimeoutError as PlaywrightTimeoutError
19
+ from qdairlines_helper.utils.po_utils import get_ip_access_blocked_msg, parse_user_info_from_page_response
20
+
21
+
22
+ async def open_login_page(*, page: Page, protocol: str, domain: str, timeout: float = 60.0) -> LoginPage:
23
+ url_prefix = f"{protocol}://{domain}"
24
+ login_url = url_prefix + url_const.login_url
25
+ await page.goto(login_url)
26
+
27
+ login_po = LoginPage(page=page, url=login_url)
28
+ await login_po.url_wait_for(url=login_url, timeout=timeout)
29
+ logger.info(f"即将进入青岛航空官网登录页面,页面URL<{login_url}>")
30
+ ip_access_blocked_msg = await get_ip_access_blocked_msg(page=page, timeout=3)
31
+ if ip_access_blocked_msg:
32
+ raise EnvironmentError(ip_access_blocked_msg)
33
+ return login_po
34
+
35
+
36
+ async def _user_login(
37
+ *, page: LoginPage, protocol: str, domain: str, username: str, password: str, timeout: float = 60.0
38
+ ) -> None:
39
+ # 1. 输入用户名
40
+ username_input = await page.get_login_username_input(timeout=timeout)
41
+ await username_input.fill(value=username.strip())
42
+ logger.info(f"青岛航空官网登录页面,用户名<{username}>输入完成")
43
+
44
+ # 2. 输入密码
45
+ password_input = await page.get_login_password_input(timeout=timeout)
46
+ await password_input.fill(value=password.strip())
47
+ logger.info(f"青岛航空官网登录页面,密码<{password}>输入完成")
48
+
49
+ # 3. 点击易盾验证logo
50
+ check_logo_icon = await page.get_check_logo_icon(timeout=timeout)
51
+ await check_logo_icon.click(button="left")
52
+ logger.info("青岛航空官网登录页面,【易盾验证logo】点击完成")
53
+
54
+ # 4. 获取校验成功的结果
55
+ check_pass_text = await page.get_check_pass_text(timeout=timeout)
56
+ logger.info(f"青岛航空官网登录页面,易盾验证logo点击后,校验结果【{check_pass_text}】获取完成")
57
+
58
+ # 4. 点击登录
59
+ login_btn = await page.get_login_btn(timeout=timeout)
60
+ await login_btn.click(button="left")
61
+ logger.info("青岛航空官网登录页面,【登录】按钮点击完成")
62
+
63
+ # 5. 加载首页页面对象,加载成功说明进入了首页,登录成功
64
+ try:
65
+ await load_home_po(page=page.get_page(), protocol=protocol, domain=domain, timeout=timeout)
66
+ logger.info(f"青岛航空官网登录页面,用户:{username}, 密码:{password}登录成功")
67
+ except (PlaywrightError, PlaywrightTimeoutError, EnvironmentError, RuntimeError, Exception) as e:
68
+ raise RuntimeError(f"青岛航空官网登录页面,用户:{username}, 密码:{password}登录失败,原因:{e}")
69
+
70
+
71
+ 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
74
+ ) -> Dict[str, Any]:
75
+ # 1. 打开登录页面
76
+ login_po = await open_login_page(page=page, protocol=protocol, domain=domain, timeout=timeout)
77
+
78
+ # 2. 开启网络监听
79
+ keywords = [url_const.auth_form_api_url, url_const.login_after_api_url]
80
+ network_logs = await login_po.capture_network(
81
+ keywords=keywords, include_post_data=True, include_response_body=True, parse_form_data=True
82
+ )
83
+ # network_logs1 = await login_po.capture_network_by_route(keywords=keywords, parse_form_data=True)
84
+
85
+ # 2. 执行登录操作
86
+ await _user_login(
87
+ page=login_po, protocol=protocol, domain=domain, username=username, password=password, timeout=timeout
88
+ )
89
+ user_info = parse_user_info_from_page_response(network_logs=network_logs)
90
+ user_info.update(dict(storage_state=await context.storage_state()))
91
+ return user_info
@@ -0,0 +1,11 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ # ---------------------------------------------------------------------------------------------------------
4
+ # ProjectName: python-qdairlines-helper
5
+ # FileName: __init__.py
6
+ # Description: http协议处理包
7
+ # Author: ASUS
8
+ # CreateDate: 2026/01/06
9
+ # Copyright ©2011-2026. Hunan xxxxxxx Company limited. All rights reserved.
10
+ # ---------------------------------------------------------------------------------------------------------
11
+ """
@@ -0,0 +1,86 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ # ---------------------------------------------------------------------------------------------------------
4
+ # ProjectName: python-qdairlines-helper
5
+ # FileName: flight_order.py
6
+ # Description: 航班订单模块
7
+ # Author: ASUS
8
+ # CreateDate: 2026/01/06
9
+ # Copyright ©2011-2026. Hunan xxxxxxx Company limited. All rights reserved.
10
+ # ---------------------------------------------------------------------------------------------------------
11
+ """
12
+ from aiohttp import CookieJar
13
+ from typing import Optional, Dict, Any
14
+ import qdairlines_helper.config.url_const as url_const
15
+ from http_helper.client.async_proxy import HttpClientFactory as _HttpClientFactory
16
+
17
+
18
+ class FlightOrder:
19
+
20
+ def __init__(
21
+ self, *, playwright_state: Dict[str, Any], token: Optional[str], proxy: Optional[Dict[str, Any]] = None,
22
+ domain: Optional[str] = None, protocol: Optional[str] = None, timeout: Optional[int] = None,
23
+ retry: Optional[int] = None, enable_log: Optional[bool] = None, cookie_jar: Optional[CookieJar] = None
24
+ ):
25
+ self._domain = domain or "127.0.0.1:18070"
26
+ self._protocol = protocol or "http"
27
+ self._timeout = timeout or 60
28
+ self._retry = retry or 0
29
+ self._token = token
30
+ self._origin = f"{self._protocol}://{self._domain}"
31
+ self._enable_log = enable_log or True
32
+ self._cookie_jar = cookie_jar or CookieJar()
33
+ self._proxy = proxy
34
+ self._playwright_state: Dict[str, Any] = playwright_state
35
+ self.http_client: Optional[_HttpClientFactory] = None
36
+
37
+ def _get_http_client(self) -> _HttpClientFactory:
38
+ """延迟获取 HTTP 客户端"""
39
+ if self.http_client is None:
40
+ self.http_client = _HttpClientFactory(
41
+ protocol=self._protocol,
42
+ domain=self._domain,
43
+ timeout=self._timeout,
44
+ retry=self._retry,
45
+ enable_log=self._enable_log,
46
+ cookie_jar=self._cookie_jar,
47
+ playwright_state=self._playwright_state
48
+ )
49
+ return self.http_client
50
+
51
+ def _get_headers(self) -> Dict[str, str]:
52
+ headers = {
53
+ "content-type": "application/json;charset=utf-8",
54
+ "origin": self._domain,
55
+ "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36",
56
+ "authorization": f"Bearer {self._token}"
57
+ }
58
+ return headers
59
+
60
+ async def get_order_details(
61
+ self, *, pre_order_no: str, user_id: str, proxy: Optional[Dict[str, Any]] = None,
62
+ headers: Dict[str, str] = None, is_end: Optional[bool] = None
63
+ ) -> Dict[str, Any]:
64
+ json_data = {
65
+ "orderNo": pre_order_no,
66
+ "plat": "NB2C",
67
+ "userId": user_id
68
+ }
69
+ _headers = self._get_headers()
70
+ if headers is not None:
71
+ _headers.update(headers)
72
+ _headers["referer"] = f"{self._origin}{url_const.order_detail_url}?orderNo={pre_order_no}&orderType=2"
73
+ if is_end is None:
74
+ is_end = True
75
+ if proxy:
76
+ self._proxy = proxy
77
+ exception_keywords = [r'<h3[^>]*class="font-bold"[^>]*>([^<]+)</h3>']
78
+ return await self._get_http_client().request(
79
+ method="POST",
80
+ url=url_const.order_info_api_url,
81
+ headers=_headers,
82
+ is_end=is_end,
83
+ json_data=json_data,
84
+ proxy_config=self._proxy or None,
85
+ exception_keywords=exception_keywords
86
+ )
@@ -0,0 +1,11 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ # ---------------------------------------------------------------------------------------------------------
4
+ # ProjectName: python-qdairlines-helper
5
+ # FileName: __init__.py
6
+ # Description: po对象包
7
+ # Author: ASUS
8
+ # CreateDate: 2026/01/04
9
+ # Copyright ©2011-2026. Hunan xxxxxxx Company limited. All rights reserved.
10
+ # ---------------------------------------------------------------------------------------------------------
11
+ """
@@ -0,0 +1,155 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ # ---------------------------------------------------------------------------------------------------------
4
+ # ProjectName: python-qdairlines-helper
5
+ # FileName: add_passenger_page.py
6
+ # Description: 添加乘客页面对象
7
+ # Author: ASUS
8
+ # CreateDate: 2026/01/04
9
+ # Copyright ©2011-2026. Hunan xxxxxxx Company limited. All rights reserved.
10
+ # ---------------------------------------------------------------------------------------------------------
11
+ """
12
+ from typing import Optional
13
+ from playwright.async_api import Page, Locator
14
+ from playwright_helper.libs.base_po import BasePo
15
+ import qdairlines_helper.config.url_const as url_const
16
+
17
+
18
+ class AddPassengerPage(BasePo):
19
+ url: str = url_const.add_passenger_url
20
+ __page: Page
21
+
22
+ def __init__(self, page: Page, url: Optional[str] = None) -> None:
23
+ super().__init__(page, url or url_const.add_passenger_url)
24
+ self.__page = page
25
+
26
+ async def get_add_passenger_btn(self, passenger_type: str, timeout: float = 5.0) -> Locator:
27
+ """
28
+ 获取添加乘客页中的【添加成人|添加儿童|添加婴儿】按钮
29
+ :param passenger_type: 乘客类型,成人|儿童|婴儿
30
+ :param timeout: 超时时间(秒)
31
+ :return: (是否存在, 错误信息|元素对象)
32
+ """
33
+ if passenger_type == '成人':
34
+ btn_text = "添加成人"
35
+ elif passenger_type == "儿童":
36
+ btn_text = "添加儿童"
37
+ elif passenger_type == "婴儿":
38
+ btn_text = "添加婴儿"
39
+ else:
40
+ raise EnvironmentError(f"乘客类型<{passenger_type}>暂不支持")
41
+ selector: str = f'//div[@class="el-row"]//button[@class="search_btn" and contains(text(), "{btn_text}")]'
42
+ return await self.get_locator(selector=selector, timeout=timeout)
43
+
44
+ async def get_add_passenger_plane(self, timeout: float = 5.0, position_index: int = 0) -> Locator:
45
+ """
46
+ 获取添加乘客页中的乘机人信息面板,按索引提取
47
+ :param timeout: 超时时间(秒)
48
+ :param position_index: 元素位置索引,该元素在页面中可能存在多组,默认从第0组开始
49
+ :return: (是否存在, 错误信息|元素对象)
50
+ """
51
+ selector: str = f'xpath=//label[@for="passengerAdult{position_index}"]/../../../..'
52
+ return await self.get_locator(selector=selector, timeout=timeout)
53
+
54
+ async def get_passenger_type_checkbox(
55
+ self, passenger_type: str, timeout: float = 5.0, position_index: int = 0
56
+ ) -> Locator:
57
+ """
58
+ 获取添加乘客页中的旅客类型【成人|儿童|婴儿】单选框
59
+ :param passenger_type: 乘客类型,成人|儿童|婴儿
60
+ :param timeout: 超时时间(秒)
61
+ :param position_index: 元素位置索引,该元素在页面中可能存在多组,默认从第0组开始
62
+ :return: (是否存在, 错误信息|元素对象)
63
+ """
64
+ if passenger_type == '成人':
65
+ passenger_class = f"passengerAdult{position_index}"
66
+ elif passenger_type == "儿童":
67
+ passenger_class = f"passengerChild{position_index}"
68
+ elif passenger_type == "婴儿":
69
+ passenger_class = f"passengerBaby{position_index}"
70
+ else:
71
+ raise EnvironmentError(f"乘客类型<{passenger_type}>暂不支持")
72
+ selector: str = f'//label[@for="{passenger_class}"]/span[@class="checkbox"]'
73
+ return await self.get_locator(selector=selector, timeout=timeout)
74
+
75
+ async def get_passenger_username_input(self, locator: Locator, timeout: float = 5.0) -> Locator:
76
+ """
77
+ 获取添加乘客页中的乘客姓名输入框
78
+ :param locator: 乘机人信息面板 passenger_plane的Locator对象
79
+ :param timeout: 超时时间(秒)
80
+ :return: (是否存在, 错误信息|元素对象)
81
+ """
82
+ selector: str = 'xpath=.//input[contains(@placeholder, "乘机人姓名")]'
83
+ return await self.get_sub_locator(locator=locator, selector=selector, timeout=timeout)
84
+
85
+ async def get_gender_checkbox(self, locator: Locator, gender: str, timeout: float = 5.0) -> Locator:
86
+ """
87
+ 获取添加乘客页中的乘客性别【男|女】单选框
88
+ :param locator: 乘机人信息面板 passenger_plane的Locator对象
89
+ :param gender: 性别
90
+ :param timeout: 超时时间(秒)
91
+ :return: (是否存在, 错误信息|元素对象)
92
+ """
93
+ if gender in ['male', '男', '男士', '男生']:
94
+ index = 0
95
+ else:
96
+ index = -1
97
+ selector: str = f'xpath=.//label[@tabindex="{index}"]//span[@class="el-radio__inner"]'
98
+ return await self.get_sub_locator(locator=locator, selector=selector, timeout=timeout)
99
+
100
+ async def get_id_type_dropdown(self, locator: Locator, timeout: float = 5.0) -> Locator:
101
+ """
102
+ 获取添加乘客页中的证件号码类型下拉菜单按钮
103
+ :param locator: 乘机人信息面板 passenger_plane的Locator对象
104
+ :param timeout: 超时时间(秒)
105
+ :return: (是否存在, 错误信息|元素对象)
106
+ """
107
+ selector: str = f'xpath=.//input[contains(@placeholder, "请选择")]'
108
+ return await self.get_sub_locator(locator=locator, selector=selector, timeout=timeout)
109
+
110
+ async def get_id_type_dropdown_selection(self, id_type: str = "身份证", timeout: float = 5.0) -> Locator:
111
+ """
112
+ 获取添加乘客页中的证件号码类型下拉菜单按钮
113
+ :param id_type: 乘机人的证件类型
114
+ :param timeout: 超时时间(秒)
115
+ :return: (是否存在, 错误信息|元素对象)
116
+ """
117
+ selector: str = f'xpath=//div[@x-placement="bottom-start"]//ul[@class="el-scrollbar__view el-select-dropdown__list"]//span[contains(text(), "{id_type}")]'
118
+ return await self.get_locator(selector=selector, timeout=timeout)
119
+
120
+ async def get_id_no_input(self, locator: Locator, timeout: float = 5.0) -> Locator:
121
+ """
122
+ 获取添加乘客页中的乘客证件号码输入框
123
+ :param locator: 乘机人信息面板 passenger_plane的Locator对象
124
+ :param timeout: 超时时间(秒)
125
+ :return: (是否存在, 错误信息|元素对象)
126
+ """
127
+ selector: str = 'xpath=.//input[contains(@placeholder, "请输入证件号")]'
128
+ return await self.get_sub_locator(locator=locator, selector=selector, timeout=timeout)
129
+
130
+ async def get_service_mobile_input(self, timeout: float = 5.0) -> Locator:
131
+ """
132
+ 获取添加乘客页中的联系人手机号码输入框
133
+ :param timeout: 超时时间(秒)
134
+ :return: (是否存在, 错误信息|元素对象)
135
+ """
136
+ selector: str = 'xpath=//input[contains(@placeholder, "请输入联系人手机号码")]'
137
+ return await self.get_locator(selector=selector, timeout=timeout)
138
+
139
+ async def get_agree_checkbox(self, timeout: float = 5.0) -> Locator:
140
+ """
141
+ 获取添加乘客页中的【已阅读并同意】单选框
142
+ :param timeout: 超时时间(秒)
143
+ :return: (是否存在, 错误信息|元素对象)
144
+ """
145
+ selector: str = f'xpath=//label[contains(text(), "已阅读并同意")]/../label/span'
146
+ return await self.get_locator(selector=selector, timeout=timeout)
147
+
148
+ async def get_next_btn(self, timeout: float = 5.0) -> Locator:
149
+ """
150
+ 获取添加乘客页中的【下一步】按钮
151
+ :param timeout: 超时时间(秒)
152
+ :return: (是否存在, 错误信息|元素对象)
153
+ """
154
+ selector: str = 'xpath=//div[@class="el-row"]//button[contains(text(), "下一步")]'
155
+ return await self.get_locator(selector=selector, timeout=timeout)
@@ -0,0 +1,24 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ # ---------------------------------------------------------------------------------------------------------
4
+ # ProjectName: python-qdairlines-helper
5
+ # FileName: profile_page.py
6
+ # Description: 机票查询页面对象
7
+ # Author: ASUS
8
+ # CreateDate: 2026/01/04
9
+ # Copyright ©2011-2026. Hunan xxxxxxx Company limited. All rights reserved.
10
+ # ---------------------------------------------------------------------------------------------------------
11
+ """
12
+ from typing import Optional
13
+ from playwright.async_api import Page
14
+ from playwright_helper.libs.base_po import BasePo
15
+ import qdairlines_helper.config.url_const as url_const
16
+
17
+
18
+ class AirOrderPage(BasePo):
19
+ url: str = url_const.air_order_url
20
+ __page: Page
21
+
22
+ def __init__(self, page: Page, url: Optional[str] = None) -> None:
23
+ super().__init__(page, url or url_const.air_order_url)
24
+ self.__page = page
@@ -0,0 +1,189 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ # ---------------------------------------------------------------------------------------------------------
4
+ # ProjectName: python-qdairlines-helper
5
+ # FileName: book_search_page.py
6
+ # Description: 预订查询页面对象
7
+ # Author: ASUS
8
+ # CreateDate: 2026/01/04
9
+ # Copyright ©2011-2026. Hunan xxxxxxx Company limited. All rights reserved.
10
+ # ---------------------------------------------------------------------------------------------------------
11
+ """
12
+ import re
13
+ from typing import Optional, Dict, Any, List
14
+ from playwright.async_api import Page, Locator
15
+ from playwright_helper.libs.base_po import BasePo
16
+ from qdairlines_helper.utils.log_utils import logger
17
+ import qdairlines_helper.config.url_const as url_const
18
+ from playwright_helper.utils.type_utils import convert_order_amount_text, safe_convert_advanced
19
+ from playwright.async_api import Error as PlaywrightError, TimeoutError as PlaywrightTimeoutError
20
+
21
+
22
+ class BookSearchPage(BasePo):
23
+ url: str = url_const.book_search_url
24
+ __page: Page
25
+
26
+ def __init__(self, page: Page, url: Optional[str] = None) -> None:
27
+ super().__init__(page, url or url_const.book_search_url)
28
+ self.__page = page
29
+
30
+ async def get_reminder_dialog_continue_book_btn(self, timeout: float = 5.0) -> Locator:
31
+ """
32
+ 获取预订搜索页的温馨提醒弹框中的【继续购票】按钮
33
+ :param timeout: 超时时间(秒)
34
+ :return: (是否存在, 错误信息|元素对象)
35
+ """
36
+ selector: str = '//div[@class="el-dialog__body"]//button[contains(@class, "search_btn")]'
37
+ return await self.get_locator(selector=selector, timeout=timeout)
38
+
39
+ async def get_depart_city_input(self, timeout: float = 5.0) -> Locator:
40
+ """
41
+ 获取预订搜索页搜索栏的起飞城市输入框
42
+ :param timeout: 超时时间(秒)
43
+ :return: (是否存在, 错误信息|元素对象)
44
+ """
45
+ selector: str = '//div[contains(@class, "flight_search_form")]//input[@id="orig"]'
46
+ return await self.get_locator(selector=selector, timeout=timeout)
47
+
48
+ async def get_arrive_city_input(self, timeout: float = 5.0) -> Locator:
49
+ """
50
+ 获取预订搜索页搜索栏的抵达城市输入框
51
+ :param timeout: 超时时间(秒)
52
+ :return: (是否存在, 错误信息|元素对象)
53
+ """
54
+ selector: str = '//div[contains(@class, "flight_search_form")]//input[@id="dest"]'
55
+ return await self.get_locator(selector=selector, timeout=timeout)
56
+
57
+ async def get_depart_date_input(self, timeout: float = 5.0) -> Locator:
58
+ """
59
+ 获取预订搜索页搜索栏的起飞日期输入框
60
+ :param timeout: 超时时间(秒)
61
+ :return: (是否存在, 错误信息|元素对象)
62
+ """
63
+ selector: str = '//div[contains(@class, "flight_search_form")]//input[contains(@placeholder, "去程日期")]'
64
+ return await self.get_locator(selector=selector, timeout=timeout)
65
+
66
+ async def get_flight_query_btn(self, timeout: float = 5.0) -> Locator:
67
+ """
68
+ 获取预订搜索页搜索栏的【查询机票】按钮
69
+ :param timeout: 超时时间(秒)
70
+ :return: (是否存在, 错误信息|元素对象)
71
+ """
72
+ selector: str = '//div[contains(@class, "flight_search_form")]//button[@class="search_btn"]'
73
+ return await self.get_locator(selector=selector, timeout=timeout)
74
+
75
+ async def get_flight_info_plane(self, timeout: float = 5.0) -> Locator:
76
+ """
77
+ 获取预订搜索页航班内容栏航班基本信息
78
+ :param timeout: 超时时间(秒)
79
+ :return: (是否存在, 错误信息|元素对象)
80
+ """
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()
94
+
95
+ async def get_flight_product_nav(self, locator: Locator, product_type: str, timeout: float = 5.0) -> Locator:
96
+ """
97
+ 获取预订搜索页航班产品类型nav
98
+ :param timeout: 超时时间(秒)
99
+ :param product_type: 产品类型
100
+ :param locator: flight_info_plane Locator 对象
101
+ :return: (是否存在, 错误信息|元素对象)
102
+ """
103
+ if product_type == "经济舱":
104
+ index = 1
105
+ elif product_type == "超级经济舱":
106
+ index = 2
107
+ elif product_type == "公务舱":
108
+ index = 3
109
+ else:
110
+ raise EnvironmentError(f"产品类型参数值<{product_type}>无效,目前仅支持<经济舱,超级经济舱,公务舱>")
111
+ selector: str = f'xpath=(.//div[contains(@class, "nav_item el-col el-col-24")])[{index}]'
112
+ return await self.get_sub_locator(locator=locator, selector=selector, timeout=timeout)
113
+
114
+ async def get_flight_products(self, timeout: float = 5.0) -> Dict[str, Any]:
115
+ """
116
+ 获取预订搜索页航班内容栏所有航班产品
117
+ :param timeout: 超时时间(秒)
118
+ :return: (是否存在, 错误信息|元素对象)
119
+ """
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)
122
+ locators: List[Locator] = await products_locator.all()
123
+ flight_products = dict()
124
+ for locator in locators:
125
+ try:
126
+ booking_btn: Locator = await self._get_flight_product_booking_btn(locator=locator, timeout=timeout)
127
+ amounts: Dict[str, Any] = await self._get_flight_product_price(locator=locator, timeout=timeout)
128
+ seats_status: int = await self._get_flight_product_seats_status(locator=locator, timeout=timeout)
129
+ cabin = await self._get_flight_product_cabin(locator=locator, timeout=timeout)
130
+ flight_products[cabin] = dict(
131
+ amounts=amounts, cabin=cabin, seats_status=seats_status, booking_btn=booking_btn
132
+ )
133
+ except (PlaywrightError, PlaywrightTimeoutError, EnvironmentError, RuntimeError, Exception) as e:
134
+ logger.warning(e)
135
+ continue
136
+ return flight_products
137
+
138
+ async def _get_flight_product_cabin(self, locator: Locator, timeout: float = 5.0) -> str:
139
+ """
140
+ 获取预订搜索页航班内容栏航班产品下的舱位
141
+ :param timeout: 超时时间(秒)
142
+ :param locator: flight_product Locator 对象
143
+ :return: (是否存在, 错误信息|元素对象)
144
+ """
145
+ selector: str = 'xpath=.//div[@class="ticket_clazz"]/div/span'
146
+ locator: Locator = await self.get_sub_locator(locator=locator, selector=selector, timeout=timeout)
147
+ text: str = (await locator.inner_text(timeout=timeout)).strip()
148
+ match = re.search(r'\((.*?)舱\)', text)
149
+ if match:
150
+ return match.group(1).strip()
151
+ raise RuntimeError(f"获取到的航班舱位信息:{text},提取异常")
152
+
153
+ async def _get_flight_product_price(self, locator: Locator, timeout: float = 5.0) -> Dict[str, Any]:
154
+ """
155
+ 获取预订搜索页航班内容栏航班产品下的销售价格
156
+ :param timeout: 超时时间(秒)
157
+ :param locator: flight_product Locator 对象
158
+ :return: (是否存在, 错误信息|元素对象)
159
+ """
160
+ selector: str = 'xpath=.//div[@class="price_flex_top"]/span'
161
+ locator: Locator = await self.get_sub_locator(locator=locator, selector=selector, timeout=timeout)
162
+ amount_text: str = (await locator.inner_text(timeout=timeout)).strip()
163
+ return convert_order_amount_text(amount_text=amount_text)
164
+
165
+ async def _get_flight_product_booking_btn(self, locator: Locator, timeout: float = 5.0) -> Locator:
166
+ """
167
+ 获取预订搜索页航班内容栏航班产品下的【购票】按钮
168
+ :param timeout: 超时时间(秒)
169
+ :param locator: flight_product Locator 对象
170
+ :return: (是否存在, 错误信息|元素对象)
171
+ """
172
+ selector: str = 'xpath=(.//td[@class="ticket_num"]/div/span)[1]'
173
+ return await self.get_sub_locator(locator=locator, selector=selector, timeout=timeout)
174
+
175
+ async def _get_flight_product_seats_status(self, locator: Locator, timeout: float = 5.0) -> int:
176
+ """
177
+ 获取预订搜索页航班内容栏航班产品下的余票信息
178
+ :param timeout: 超时时间(秒)
179
+ :param locator: flight_product Locator 对象
180
+ :return: (是否存在, 错误信息|元素对象)
181
+ """
182
+ selector: str = 'xpath=(.//td[@class="ticket_num"]/div/span)[2]'
183
+ locator: Locator = await self.get_sub_locator(locator=locator, selector=selector, timeout=timeout)
184
+ more_seats_text: str = (await locator.inner_text(timeout=timeout)).strip()
185
+ match = re.search(r'\d+', more_seats_text)
186
+ if match:
187
+ return safe_convert_advanced(value=match.group(1).strip())
188
+ else:
189
+ return 999999
@@ -0,0 +1,74 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ # ---------------------------------------------------------------------------------------------------------
4
+ # ProjectName: python-qdairlines-helper
5
+ # FileName: cash_pax_info_page.py
6
+ # Description: 支付类型页面对象
7
+ # Author: ASUS
8
+ # CreateDate: 2026/01/05
9
+ # Copyright ©2011-2026. Hunan xxxxxxx Company limited. All rights reserved.
10
+ # ---------------------------------------------------------------------------------------------------------
11
+ """
12
+ from typing import Optional
13
+ from urllib.parse import urlparse, parse_qs
14
+ from playwright.async_api import Page, Locator
15
+ from playwright_helper.libs.base_po import BasePo
16
+ from qdairlines_helper.utils.log_utils import logger
17
+ import qdairlines_helper.config.url_const as url_const
18
+
19
+
20
+ class CashPaxInfoPage(BasePo):
21
+ url: str = url_const.cash_pax_info_url
22
+ __page: Page
23
+
24
+ def __init__(self, page: Page, url: Optional[str] = None) -> None:
25
+ super().__init__(page, url or url_const.cash_pax_info_url)
26
+ self.__page = page
27
+
28
+ async def get_payment_type_checkbox(self, payment_type: str, timeout: float = 5.0) -> Locator:
29
+ """
30
+ 获取支付方式页面中的支付类型【支付宝|微信|汇付天下|易宝支付】单选框
31
+ :param payment_type: 支付类型,支付宝|微信|汇付天下|易宝支付
32
+ :param timeout: 超时时间(秒)
33
+ :return: (是否存在, 错误信息|元素对象)
34
+ """
35
+ if payment_type == '支付宝':
36
+ for_class = 0
37
+ elif payment_type == "微信":
38
+ for_class = 1
39
+ elif payment_type == "汇付天下":
40
+ for_class = 2
41
+ elif payment_type == "易宝支付":
42
+ for_class = 3
43
+ else:
44
+ raise EnvironmentError(f"支付类型<{payment_type}>暂不支持")
45
+ selector: str = f'xpath=//label[@for="passengerAdult{for_class}"]//span[@class="checkbox"]'
46
+ return await self.get_locator(selector=selector, timeout=timeout)
47
+
48
+ async def get_confirm_payment_btn(self, timeout: float = 5.0) -> Locator:
49
+ """
50
+ 获取支付方式页面中的【确认支付】按钮
51
+ :param timeout: 超时时间(秒)
52
+ :return: (是否存在, 错误信息|元素对象)
53
+ """
54
+ selector: str = '//button[@class="search_btn" and contains(text(), "确认支付")]'
55
+ return await self.get_locator(selector=selector, timeout=timeout)
56
+
57
+ async def get_pre_order_no(self, timeout: float = 5.0) -> str:
58
+ """
59
+ 获取青岛航空官网订单号
60
+ :param timeout: 超时时间(秒)
61
+ :return: (是否存在, 错误信息|元素对象)
62
+ """
63
+ try:
64
+ query = urlparse(self.__page.url).query
65
+ params = parse_qs(query)
66
+ # 取第一个值(即使有多个 orderNo)
67
+ pre_order_no = params.get('orderNo', [""])[0]
68
+ if pre_order_no and len(pre_order_no) == 18 and pre_order_no.startswith("OW") is True:
69
+ return pre_order_no
70
+ except (Exception,):
71
+ logger.error(f"支付方式页面获取到的url={self.__page.url},信息异常")
72
+ selector: str = '//div[@class="order-form"]//span[contains(text(), "订单编号")]/../span[@class="font_red"]'
73
+ locator = await self.get_locator(selector=selector, timeout=timeout)
74
+ return (await locator.inner_text()).strip()