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.
- python_qdairlines_helper-0.0.6.dist-info/METADATA +247 -0
- python_qdairlines_helper-0.0.6.dist-info/RECORD +34 -0
- python_qdairlines_helper-0.0.6.dist-info/WHEEL +5 -0
- python_qdairlines_helper-0.0.6.dist-info/licenses/LICENSE +201 -0
- python_qdairlines_helper-0.0.6.dist-info/top_level.txt +1 -0
- qdairlines_helper/__init__.py +11 -0
- qdairlines_helper/config/__init__.py +11 -0
- qdairlines_helper/config/url_const.py +37 -0
- qdairlines_helper/controller/__init__.py +11 -0
- qdairlines_helper/controller/add_passenger.py +92 -0
- qdairlines_helper/controller/book_payment.py +83 -0
- qdairlines_helper/controller/book_search.py +114 -0
- qdairlines_helper/controller/cash_pax_info.py +47 -0
- qdairlines_helper/controller/home.py +28 -0
- qdairlines_helper/controller/nhlms_cashdesk.py +74 -0
- qdairlines_helper/controller/order_detail.py +53 -0
- qdairlines_helper/controller/order_query.py +41 -0
- qdairlines_helper/controller/order_verify.py +43 -0
- qdairlines_helper/controller/user_login.py +91 -0
- qdairlines_helper/http/__init__.py +11 -0
- qdairlines_helper/http/flight_order.py +86 -0
- qdairlines_helper/po/__init__.py +11 -0
- qdairlines_helper/po/add_passenger_page.py +155 -0
- qdairlines_helper/po/air_order_page.py +24 -0
- qdairlines_helper/po/book_search_page.py +189 -0
- qdairlines_helper/po/cash_pax_info_page.py +74 -0
- qdairlines_helper/po/home_page.py +24 -0
- qdairlines_helper/po/login_page.py +70 -0
- qdairlines_helper/po/nhlms_cash_desk_page.py +94 -0
- qdairlines_helper/po/order_verify_page.py +62 -0
- qdairlines_helper/utils/__init__.py +11 -0
- qdairlines_helper/utils/exception_utils.py +17 -0
- qdairlines_helper/utils/log_utils.py +14 -0
- 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()
|