python-qdairlines-helper 0.1.4__py3-none-any.whl → 0.4.3__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 (27) hide show
  1. {python_qdairlines_helper-0.1.4.dist-info → python_qdairlines_helper-0.4.3.dist-info}/METADATA +3 -3
  2. python_qdairlines_helper-0.4.3.dist-info/RECORD +36 -0
  3. qdairlines_helper/config/url_const.py +4 -0
  4. qdairlines_helper/controller/add_passenger.py +1 -1
  5. qdairlines_helper/controller/book_payment.py +22 -20
  6. qdairlines_helper/controller/book_search.py +70 -37
  7. qdairlines_helper/controller/cash_pax_info.py +1 -1
  8. qdairlines_helper/controller/home.py +1 -1
  9. qdairlines_helper/controller/nhlms_cashdesk.py +2 -2
  10. qdairlines_helper/controller/order_detail.py +43 -18
  11. qdairlines_helper/controller/order_query.py +1 -1
  12. qdairlines_helper/controller/order_verify.py +1 -1
  13. qdairlines_helper/controller/pay_success.py +9 -8
  14. qdairlines_helper/controller/user_login.py +35 -2
  15. qdairlines_helper/http/flight_order.py +5 -41
  16. qdairlines_helper/http/http_base.py +54 -0
  17. qdairlines_helper/http/user_login.py +43 -0
  18. qdairlines_helper/po/add_passenger_page.py +1 -1
  19. qdairlines_helper/po/book_search_page.py +48 -29
  20. qdairlines_helper/po/cash_pax_info_page.py +1 -1
  21. qdairlines_helper/po/nhlms_cash_desk_page.py +42 -1
  22. qdairlines_helper/po/order_verify_page.py +1 -1
  23. python_qdairlines_helper-0.1.4.dist-info/RECORD +0 -35
  24. qdairlines_helper/utils/exception_utils.py +0 -105
  25. {python_qdairlines_helper-0.1.4.dist-info → python_qdairlines_helper-0.4.3.dist-info}/WHEEL +0 -0
  26. {python_qdairlines_helper-0.1.4.dist-info → python_qdairlines_helper-0.4.3.dist-info}/licenses/LICENSE +0 -0
  27. {python_qdairlines_helper-0.1.4.dist-info → python_qdairlines_helper-0.4.3.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python_qdairlines_helper
3
- Version: 0.1.4
3
+ Version: 0.4.3
4
4
  Summary: qdairlines helper python package
5
5
  Author-email: ckf10000 <ckf10000@sina.com>
6
6
  License: Apache License
@@ -210,9 +210,9 @@ Project-URL: Issues, https://github.com/ckf10000/python-qdairlines-helper/issues
210
210
  Requires-Python: >=3.12
211
211
  Description-Content-Type: text/markdown
212
212
  License-File: LICENSE
213
- Requires-Dist: python_playwright_helper>=0.3.7
213
+ Requires-Dist: python_playwright_helper>=0.4.0
214
214
  Requires-Dist: python_http_helper>=0.2.1
215
- Requires-Dist: flight_helper>=0.1.3
215
+ Requires-Dist: flight_helper>=0.3.3
216
216
  Dynamic: license-file
217
217
 
218
218
  # python-qdairlines-helper
@@ -0,0 +1,36 @@
1
+ python_qdairlines_helper-0.4.3.dist-info/licenses/LICENSE,sha256=WtjCEwlcVzkh1ziO35P2qfVEkLjr87Flro7xlHz3CEY,11556
2
+ qdairlines_helper/__init__.py,sha256=bN1FqEK_jvefO_HU_8HFJpjuOzHTsuhmRe5KAzmraR4,479
3
+ qdairlines_helper/config/__init__.py,sha256=JjIRocePx3gwFNpdauuiwBU6B2Ug565xaHLEMu5Oo0I,479
4
+ qdairlines_helper/config/url_const.py,sha256=KpvvZEPOsJ7FQdJLp79VfUdUSRuItR4OYYCzl8wfKZI,2194
5
+ qdairlines_helper/controller/__init__.py,sha256=oyeciEBlDLNpqHblB0R-nXQG3HsI8L5mmlWulQ7Y38Q,482
6
+ qdairlines_helper/controller/add_passenger.py,sha256=qixajF_MRzmMZ9htAFeGCP3L27iV1Xk-jnfPQEP48s8,5155
7
+ qdairlines_helper/controller/book_payment.py,sha256=9QdaakkIyi8qCjZ7TT687JX8Jqt5mp9SSQ307NYZ3tY,6270
8
+ qdairlines_helper/controller/book_search.py,sha256=XQQuI1xX6rO-MqdJ15_X5wX94Yeqj9UVat6p55beNu8,8977
9
+ qdairlines_helper/controller/cash_pax_info.py,sha256=AZ09ekt-zaYYs3uUSclul014mbCZEF5BVIghOeIGT5s,2507
10
+ qdairlines_helper/controller/home.py,sha256=2r75pe7d16kZ6tkJZ3zM5ycV-DE5iy3njhpE0xf1EZw,1390
11
+ qdairlines_helper/controller/nhlms_cashdesk.py,sha256=1gWURu9VnjjvvalAu-uF7eXVMSsiaBDWHPwG8EgDRh8,4308
12
+ qdairlines_helper/controller/order_detail.py,sha256=hhL37PiozS1jALMk_eB0vg0eigW_Rcl_mf0l-QrW21o,4337
13
+ qdairlines_helper/controller/order_query.py,sha256=7U6S5xlT89UJHFslDbUT5VRgGlAoPQSOi4NRtEKK9xQ,1903
14
+ qdairlines_helper/controller/order_verify.py,sha256=3_bc1R47_vaCIEJaPWrxyYrE49v2J1-KsGCmAEEloKc,2269
15
+ qdairlines_helper/controller/pay_success.py,sha256=G8Vf5MDDBgvDo3MIg-deEvyv4PJfxdzLhpHU9TGuejc,3295
16
+ qdairlines_helper/controller/user_login.py,sha256=ojW6mqZdiURpi5K1gMPdFlqCUieOkBhcXDJELW5Hktw,6210
17
+ qdairlines_helper/http/__init__.py,sha256=96AJPf2zgw_rm3Wv-boIBltWFgZH3Yo-fn5M9SRK280,489
18
+ qdairlines_helper/http/flight_order.py,sha256=7NTUA8xt6gwPBH7gdU3VjUoBa9LpPAvAEzkWguLPrNk,2066
19
+ qdairlines_helper/http/http_base.py,sha256=zWSgJfyMaekHyvV4lJD-KHiCX9BN3GGb8DperyVpRGQ,2492
20
+ qdairlines_helper/http/user_login.py,sha256=S_thG1RLXv5x5MQJmEG-oo38ReIF3_p17hVwGqs61A8,1815
21
+ qdairlines_helper/po/__init__.py,sha256=HoouLJGLu_dI6IAKTVjBRraGHMcAUgntayTph-BUBAE,481
22
+ qdairlines_helper/po/add_passenger_page.py,sha256=no_QC4hwO7xuZXKLzakNlcWj2R06oVZdzKHvFHsBG5Y,7996
23
+ qdairlines_helper/po/air_order_page.py,sha256=0Ux0B2hUGKCoFLRpJqGHS2w690o0A2PDnvRf9-UKfJE,930
24
+ qdairlines_helper/po/book_search_page.py,sha256=T2bxKFuvApY0rMlU7NHso0CwQ9XXOR0xz4bvIgI1x0k,10865
25
+ qdairlines_helper/po/cash_pax_info_page.py,sha256=yH-ZedYweby1x3Sw3JZX6a57-8PdtQHmNKqym7cn-Xo,3397
26
+ qdairlines_helper/po/home_page.py,sha256=KvoOZOYwKjlJjZOT522K_8ic3_2zaYOPWkuklUHVSlM,907
27
+ qdairlines_helper/po/login_page.py,sha256=SdWmkP_47GeosbFRcfvXLeQxxdM7b9UESvmajWq4-nA,2975
28
+ qdairlines_helper/po/nhlms_cash_desk_page.py,sha256=K3GCcmfHbMOhZnVB7hHKJflVy0DbiidDJBfJ3IMbMGc,6671
29
+ qdairlines_helper/po/order_verify_page.py,sha256=7boJVdzoV9IdIGtbjza3za9C26_4LFBjOmp27-0_ick,2924
30
+ qdairlines_helper/po/pay_success_page.py,sha256=XGddo8as8wf1kCCdvemqHIRWOPIPv1JG8iXEmpH4zck,1329
31
+ qdairlines_helper/utils/__init__.py,sha256=sti2S709puM2mQbQisk0KGq_YNjW6T4zz2vyYXonNB0,479
32
+ qdairlines_helper/utils/po_utils.py,sha256=74ZVyyxPmPk1tcGQoMauXhBE3qsZJej8urni1ch27io,2417
33
+ python_qdairlines_helper-0.4.3.dist-info/METADATA,sha256=J8UgJTIFyVdwztik2df4g_eVENNr_WY8wH6Vj7FqcdM,14424
34
+ python_qdairlines_helper-0.4.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
35
+ python_qdairlines_helper-0.4.3.dist-info/top_level.txt,sha256=MRbQkBMdSG4f5mx2RWfu6aXoDSyM-2KwL-YFqNBdbng,18
36
+ python_qdairlines_helper-0.4.3.dist-info/RECORD,,
@@ -26,6 +26,10 @@ order_verify_url: str = "/book/orderVerify"
26
26
  cash_pax_info_url: str = "/book/cashPaxInfoInput"
27
27
  # https://nhlms.cloudpnr.com/nobel/WebEntry.do 汇付天下收银台
28
28
  nhlms_cashdesk_url: str = "/nobel/WebEntry.do"
29
+ # https://nhlms.cloudpnr.com/nobel/n1026/error?RegionId=P
30
+ nhlms_error_url: str = "/nobel/n1026/error"
31
+
32
+
29
33
  # https://excashier.alipay.com/standard/auth.htm?payOrderId=4e3606b1c842414c88f631c35952a88e.85 支付宝收银台
30
34
  alipay_url: str = "/standard/auth.htm"
31
35
  # https://www.qdairlines.com/book/payPreCash?orderNo=OW20260105B1154687 微信支付收银台
@@ -14,8 +14,8 @@ from logging import Logger
14
14
  from playwright.async_api import Page
15
15
  import qdairlines_helper.config.url_const as url_const
16
16
  from flight_helper.models.dto.passenger import PassengerDTO
17
+ from flight_helper.utils.exception_utils import IPBlockError
17
18
  from flight_helper.models.dto.booking import BookingInputDTO
18
- from qdairlines_helper.utils.exception_utils import IPBlockError
19
19
  from qdairlines_helper.po.add_passenger_page import AddPassengerPage
20
20
  from qdairlines_helper.utils.po_utils import get_ip_access_blocked_msg
21
21
 
@@ -15,15 +15,15 @@ from playwright.async_api import Page
15
15
  from typing import Any, List, Callable, Optional, Dict
16
16
  from flight_helper.models.dto.passenger import PassengerDTO
17
17
  from flight_helper.models.dto.itinerary import QueryItineraryRequestDTO
18
- from qdairlines_helper.controller.pay_success import two_check_pay_success
19
- from qdairlines_helper.utils.exception_utils import PaymentChannelMissError
20
18
  from flight_helper.models.dto.booking import BookingInputDTO, OneWayBookingDTO
21
19
  from qdairlines_helper.controller.book_search import open_book_search_page, book_search
22
20
  from qdairlines_helper.controller.order_verify import load_order_verify_po, order_verify
21
+ from flight_helper.utils.exception_utils import PaymentChannelMissError, PaymentTypeError
23
22
  from flight_helper.models.dto.payment import HFPaidAccountPaymentInputDTO, PaymentResultDTO
24
23
  from qdairlines_helper.controller.add_passenger import add_passenger, load_add_passenger_po
25
24
  from qdairlines_helper.controller.cash_pax_info import load_cash_pax_info_po, select_payment_channel
26
25
  from qdairlines_helper.controller.nhlms_cashdesk import load_nhlms_cash_desk_po, hf_paid_account_payment
26
+ from qdairlines_helper.controller.pay_success import two_check_pay_success, check_hf_payment_is_success
27
27
 
28
28
 
29
29
  async def book_payment_callback(
@@ -88,29 +88,31 @@ async def book_payment_callback(
88
88
  domain=hf_paid_account_payment_dto.pay_domain, protocol=hf_paid_account_payment_dto.pay_protocol
89
89
  )
90
90
 
91
+ if hf_paid_account_payment_dto.payment_type != "付款账户支付":
92
+ raise PaymentTypeError(payment_type=hf_paid_account_payment_dto.payment_type)
93
+
91
94
  # 10. 汇付天下操作支付
92
- if hf_paid_account_payment_dto.payment_type == "付款账户支付":
93
- # 10. 汇付天下操作支付
94
- payment_result_dto = await hf_paid_account_payment(
95
- page=nhlms_cash_desk_po, logger=logger, order_no=pre_order_no, timeout=timeout,
96
- is_pay_completed_callback=is_pay_completed_callback,
97
- hf_paid_account_payment_dto=hf_paid_account_payment_dto
98
- )
99
- logger.info(f"订单<{one_way_booking_dto.order_no}>,汇付天下操作支付结束")
100
- payment_result_dto.pre_order_no = pre_order_no
95
+ payment_result_dto = await hf_paid_account_payment(
96
+ page=nhlms_cash_desk_po, logger=logger, order_no=one_way_booking_dto.order_no, timeout=timeout,
97
+ is_pay_completed_callback=is_pay_completed_callback,
98
+ hf_paid_account_payment_dto=hf_paid_account_payment_dto
99
+ )
100
+ logger.info(f"订单<{one_way_booking_dto.order_no}>,汇付天下操作支付结束")
101
+ payment_result_dto.pre_order_no = pre_order_no
101
102
 
102
- query_dto = QueryItineraryRequestDTO(
103
- payment_domain=book_input_dto.book_domain, payment_protocol=book_input_dto.book_protocol,
104
- storage_state=qdair_cookie.get("storage_state"), token=qdair_cookie.get("token"),
105
- pre_order_no=pre_order_no, user_id=qdair_cookie.get("user_id"), headers=qdair_cookie.get("headers")
106
- )
103
+ query_dto = QueryItineraryRequestDTO(
104
+ payment_domain=book_input_dto.book_domain, payment_protocol=book_input_dto.book_protocol,
105
+ storage_state=qdair_cookie.get("storage_state"), token=qdair_cookie.get("token"),
106
+ pre_order_no=pre_order_no, user_id=qdair_cookie.get("user_id"), headers=qdair_cookie.get("headers")
107
+ )
107
108
 
108
- # 11. 检查是否支付成功
109
- is_success = await two_check_pay_success(
109
+ # 11. 检查是否支付成功
110
+ if kwargs.get("暂时不走这个逻辑,改用轻逻辑验证"):
111
+ await two_check_pay_success(
110
112
  page=page, logger=logger, timeout=timeout, query_dto=query_dto, retry=retry, enable_log=enable_log,
111
113
  callback_get_proxy=callback_get_proxy, cookie_jar=cookie_jar
112
114
  )
113
- if is_success:
114
- return payment_result_dto
115
+ await check_hf_payment_is_success(page=nhlms_cash_desk_po, logger=logger)
116
+ return payment_result_dto
115
117
  else:
116
118
  raise PaymentChannelMissError()
@@ -9,23 +9,25 @@
9
9
  # Copyright ©2011-2026. Hunan xxxxxxx Company limited. All rights reserved.
10
10
  # ---------------------------------------------------------------------------------------------------------
11
11
  """
12
+ import time
12
13
  import asyncio
13
14
  from logging import Logger
14
- from typing import Dict, Any
15
+ from typing import Dict, Any, List, Optional
15
16
  from playwright.async_api import Page, Locator
16
17
  import qdairlines_helper.config.url_const as url_const
17
- from qdairlines_helper.utils.exception_utils import IPBlockError
18
18
  from qdairlines_helper.po.book_search_page import BookSearchPage
19
19
  from qdairlines_helper.utils.po_utils import get_ip_access_blocked_msg
20
+ from playwright.async_api import TimeoutError as PlaywrightTimeoutError
20
21
  from flight_helper.models.dto.booking import OneWayBookingDTO, BookingInputDTO
21
- from qdairlines_helper.utils.exception_utils import NotEnoughTicketsError, ExcessiveProfitdError, ExcessiveLossesError
22
+ from flight_helper.utils.exception_utils import NotEnoughTicketsError, ExcessiveProfitdError, ExcessiveLossesError, \
23
+ IPBlockError
22
24
 
23
25
 
24
26
  async def open_book_search_page(
25
27
  *, page: Page, logger: Logger, protocol: str, domain: str, timeout: float = 60.0
26
28
  ) -> BookSearchPage:
27
29
  url_prefix = f"{protocol}://{domain}"
28
- book_search_url = url_prefix + url_const.login_url
30
+ book_search_url = url_prefix + url_const.book_search_url
29
31
  await page.goto(book_search_url)
30
32
 
31
33
  book_search_po = BookSearchPage(page=page, url=book_search_url)
@@ -37,65 +39,96 @@ async def open_book_search_page(
37
39
  return book_search_po
38
40
 
39
41
 
42
+ async def _book_search_dialog_handle(
43
+ *, logger: Logger, page: BookSearchPage, flight_no: str, timeout: float = 60.0
44
+ ) -> Locator:
45
+ end_time = time.time() + timeout
46
+ last_exception: Optional[Exception] = None
47
+ while time.time() < end_time:
48
+ try:
49
+ # 2. 点击继续购票按钮
50
+ continue_book_btn = await page.get_reminder_dialog_continue_book_btn(timeout=1)
51
+ await continue_book_btn.click(button="left")
52
+ logger.info("航班预订查询页面,出现温馨提醒弹框,【继续购票】按钮点击完成")
53
+ except (Exception,):
54
+ pass
55
+ try:
56
+ # 1.获取航班基本信息plane locator
57
+ flight_info_plane: Locator = await page.get_flight_info_plane(flight_no=flight_no, timeout=1)
58
+ return flight_info_plane
59
+ except (Exception,) as e:
60
+ last_exception = e
61
+ raise last_exception
62
+
63
+
40
64
  async def book_search(
41
65
  *, book_search_page: BookSearchPage, logger: Logger, passengers: int, book_input_dto: BookingInputDTO,
42
66
  one_way_booking_dto: OneWayBookingDTO, timeout: float = 60.0
43
67
  ) -> None:
44
- try:
45
- # 1. 判断是否存在温馨提醒弹框
46
- continue_book_btn = await book_search_page.get_reminder_dialog_continue_book_btn(timeout=timeout)
47
- await continue_book_btn.click(button="left")
48
- logger.info("航班预订查询页面,出现温馨提醒弹框,【继续购票】按钮点击完成")
49
- except (Exception,):
50
- pass
51
-
52
- # 2.搜索栏输入起飞城市
68
+ # 1.搜索栏输入起飞城市
53
69
  depart_city_input = await book_search_page.get_depart_city_input(timeout=timeout)
54
- await depart_city_input.fill(value=one_way_booking_dto.dep_city)
55
- await asyncio.sleep(delay=1)
70
+ await depart_city_input.fill(value=one_way_booking_dto.dep_code)
71
+ await asyncio.sleep(delay=2)
56
72
  await depart_city_input.press("Enter")
57
- logger.info(f"航班预订查询页面,搜索栏-起飞城市<{one_way_booking_dto.dep_city}>输入完成")
73
+ logger.info(
74
+ f"航班预订查询页面,搜索栏-起飞城市<{one_way_booking_dto.dep_city} {one_way_booking_dto.dep_code}>输入完成")
58
75
 
59
- # 3.搜索栏输入抵达城市
76
+ # 2.搜索栏输入抵达城市
60
77
  arrive_city_input = await book_search_page.get_arrive_city_input(timeout=timeout)
61
- await arrive_city_input.fill(value=one_way_booking_dto.arr_city)
62
- await asyncio.sleep(delay=1)
78
+ await arrive_city_input.fill(value=one_way_booking_dto.arr_code)
79
+ await asyncio.sleep(delay=2)
63
80
  await arrive_city_input.press("Enter")
64
- logger.info(f"航班预订查询页面,搜索栏-抵达城市<{one_way_booking_dto.arr_city}>输入完成")
81
+ logger.info(
82
+ f"航班预订查询页面,搜索栏-抵达城市<{one_way_booking_dto.arr_city} {one_way_booking_dto.arr_code}>输入完成")
65
83
 
66
- # 4.搜索栏输入起飞时间
84
+ # 3.搜索栏输入起飞时间
67
85
  depart_date_input = await book_search_page.get_depart_date_input(timeout=timeout)
68
86
  await depart_date_input.fill(value=one_way_booking_dto.dep_date)
69
87
  await asyncio.sleep(delay=1)
70
88
  await depart_date_input.press("Enter")
71
89
  logger.info(f"航班预订查询页面,搜索栏-起飞日期<{one_way_booking_dto.dep_date}>输入完成")
72
90
 
73
- # 5.点击【查询机票】按钮
91
+ # 4.点击【查询机票】按钮
74
92
  flight_query_btn = await book_search_page.get_flight_query_btn(timeout=timeout)
75
93
  await flight_query_btn.click(button="left")
76
94
  logger.info(f"航班预订查询页面,【查询机票】按钮点击完成")
77
95
 
78
- # 6. 获取航班基本信息plane locator
79
- flight_info_plane: Locator = await book_search_page.get_flight_info_plane(timeout=timeout)
80
- page_flight_no: str = await book_search_page.get_flight_no(locator=flight_info_plane, timeout=timeout)
81
- if one_way_booking_dto.flight_no not in page_flight_no:
82
- raise RuntimeError(f"航班预订查询页面,没有搜索到航班<{one_way_booking_dto.flight_no}>数据")
96
+ # 5.点击查询后,再次处理是否有弹框,并航班基本信息plane locator
97
+ flight_info_plane: Locator = await _book_search_dialog_handle(
98
+ logger=logger, page=book_search_page, timeout=timeout, flight_no=one_way_booking_dto.flight_no
99
+ )
100
+
101
+ # 6.获取航班基本信息plane locator
102
+ # flight_info_plane: Locator = await book_search_page.get_flight_info_plane(
103
+ # flight_no=one_way_booking_dto.flight_no, timeout=timeout
104
+ # )
83
105
 
84
- # 7. 获取产品类型
106
+ # 8. 获取产品类型
85
107
  flight_product_nav: Locator = await book_search_page.get_flight_product_nav(
86
108
  locator=flight_info_plane, product_type=book_input_dto.product_type, timeout=timeout
87
109
  )
88
110
  await flight_product_nav.click(button="left")
89
111
  logger.info(f"航班预订查询页面,产品类型【{book_input_dto.product_type}】选择完成")
90
112
 
91
- # 8. 获取所有的产品
92
- products: Dict[str, Any] = await book_search_page.get_flight_products(timeout=timeout)
93
- cabin_product: Dict[str, Any] = products.get(one_way_booking_dto.cabin)
94
- # 8.1 判断是否存在该舱位
95
- if not cabin_product:
113
+ # 9. 点击获取更多产品和价格的按钮
114
+ more_product_btn = await book_search_page.get_more_product_btn(locator=flight_info_plane, timeout=timeout)
115
+ await more_product_btn.click(button="left")
116
+ logger.info(f"航班预订查询页面,航班<{one_way_booking_dto.flight_no}>产品列表,【更多舱位及价格】按钮点击完成")
117
+
118
+ # 9. 获取所有的产品
119
+ products: List[Dict[str, Any]] = await book_search_page.get_flight_products(
120
+ locator=flight_info_plane, flight_no=one_way_booking_dto.flight_no, logger=logger
121
+ )
122
+ # 取出需要预订的舱位产品
123
+ products = [x for x in products if x.get("cabin")]
124
+ # 根据价格升序排序(默认)
125
+ products = sorted(products, key=lambda x: x["amounts"]["amount"])
126
+ # 9.1 判断是否存在该舱位
127
+ if len(products) == 0:
96
128
  raise RuntimeError(
97
129
  f"航班预订查询页面,没有搜索到航班<{one_way_booking_dto.flight_no}>的{one_way_booking_dto.cabin}舱数据")
98
- # 8.2 判断余座
130
+ cabin_product = products[0]
131
+ # 9.2 判断余座
99
132
  seats_status = cabin_product.get("seats_status")
100
133
  logger.info(
101
134
  f"航班预订查询页面,航班<{one_way_booking_dto.flight_no}>舱位<{one_way_booking_dto.cabin}>的座位情况:{seats_status}")
@@ -103,10 +136,10 @@ async def book_search(
103
136
  raise NotEnoughTicketsError(
104
137
  flight_no=one_way_booking_dto.flight_no, seats_status=seats_status, passengers=passengers
105
138
  )
106
- # 8.3. 判断货币符号,是否为 ¥(人民币结算)
139
+ # 9.3. 判断货币符号,是否为 ¥(人民币结算)
107
140
  # 目前都为国内航班,暂不考虑货币种类
108
141
 
109
- # 8.4 判断销售价格是否满足预订需要
142
+ # 9.4 判断销售价格是否满足预订需要
110
143
  amounts: Dict[str, Any] = cabin_product.get("amounts")
111
144
  amount: float = amounts.get("amount")
112
145
  if amount > one_way_booking_dto.standard_price + book_input_dto.standard_increase_threshold:
@@ -133,7 +166,7 @@ async def book_search(
133
166
  reduction_threshold=book_input_dto.sale_reduction_threshold, asset="销售价"
134
167
  )
135
168
 
136
- # 9. 点击【购票】按钮
169
+ # 10. 点击【购票】按钮
137
170
  booking_btn: Locator = cabin_product.get("booking_btn")
138
171
  await booking_btn.click(button="left")
139
172
  logger.info(f"航班预订查询页面,【购票】按钮点击完成")
@@ -12,7 +12,7 @@
12
12
  from logging import Logger
13
13
  from playwright.async_api import Page
14
14
  import qdairlines_helper.config.url_const as url_const
15
- from qdairlines_helper.utils.exception_utils import IPBlockError
15
+ from flight_helper.utils.exception_utils import IPBlockError
16
16
  from qdairlines_helper.po.cash_pax_info_page import CashPaxInfoPage
17
17
  from qdairlines_helper.utils.po_utils import get_ip_access_blocked_msg
18
18
 
@@ -13,7 +13,7 @@ from logging import Logger
13
13
  from playwright.async_api import Page
14
14
  from qdairlines_helper.po.home_page import HomePage
15
15
  import qdairlines_helper.config.url_const as url_const
16
- from qdairlines_helper.utils.exception_utils import IPBlockError
16
+ from flight_helper.utils.exception_utils import IPBlockError
17
17
  from qdairlines_helper.utils.po_utils import get_ip_access_blocked_msg
18
18
 
19
19
 
@@ -14,9 +14,9 @@ from logging import Logger
14
14
  from typing import Callable
15
15
  from playwright.async_api import BrowserContext
16
16
  import qdairlines_helper.config.url_const as url_const
17
+ from flight_helper.utils.exception_utils import DuplicatePaymentError
17
18
  from qdairlines_helper.po.nhlms_cash_desk_page import NhlmsCashDeskPage
18
19
  from playwright_helper.utils.browser_utils import switch_for_table_window
19
- from qdairlines_helper.utils.exception_utils import DuplicatePaymentError
20
20
  from flight_helper.models.dto.payment import HFPaidAccountPaymentInputDTO, PaymentResultDTO
21
21
 
22
22
 
@@ -37,7 +37,7 @@ async def load_nhlms_cash_desk_po(
37
37
 
38
38
 
39
39
  async def hf_paid_account_payment(
40
- *, page: NhlmsCashDeskPage, logger: Logger, order_no: str, is_pay_completed_callback: Callable,
40
+ *, page: NhlmsCashDeskPage, logger: Logger, order_no: int, is_pay_completed_callback: Callable,
41
41
  hf_paid_account_payment_dto: HFPaidAccountPaymentInputDTO, timeout: float = 60.0
42
42
  ) -> PaymentResultDTO:
43
43
  # 1. 获取收银台支付流水
@@ -10,38 +10,60 @@
10
10
  # ---------------------------------------------------------------------------------------------------------
11
11
  """
12
12
  import re
13
+ import inspect
14
+ from logging import Logger
13
15
  from aiohttp import CookieJar
14
- from typing import Dict, Any, Optional, List
16
+ from typing import Dict, Any, Optional, List, Callable
15
17
  from qdairlines_helper.http.flight_order import FlightOrder
16
- from flight_helper.models.dto.itinerary import QueryItineraryResponseDTO, QueryItineraryRequestDTO, ItineraryInfoDTO
18
+ from flight_helper.models.dto.http_schema import HTTPRequestDTO
19
+ from flight_helper.models.dto.itinerary import QueryItineraryResponseDTO, QueryItineraryRequestDTO
17
20
 
18
21
 
19
22
  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
+ *, query_dto: QueryItineraryRequestDTO, logger: Logger, callback_get_proxy: Callable,
24
+ timeout: Optional[int] = None, retry: Optional[int] = None, cookie_jar: Optional[CookieJar] = None,
25
+ enable_log: Optional[bool] = None
22
26
  ) -> Dict[str, Any]:
23
- flight_order = FlightOrder(
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
+ http_request_dto = HTTPRequestDTO(
28
+ http_domain=query_dto.payment_domain, http_protocol=query_dto.payment_protocol,
29
+ storage_state=query_dto.storage_state, timeout=timeout, retry=retry, enable_log=enable_log,
30
+ token=query_dto.token
27
31
  )
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")
32
+ flight_order = FlightOrder(http_request_dto=http_request_dto, cookie_jar=cookie_jar)
33
+ last_order_info: Dict[str, Any] = dict()
34
+ for index in range(5):
35
+ proxy = None
36
+ is_end: bool = True if index + 1 == 5 else False
37
+ if index != 0:
38
+ if inspect.iscoroutinefunction(callback_get_proxy):
39
+ proxy = await callback_get_proxy(logger=logger)
40
+ else:
41
+ proxy = callback_get_proxy(logger=logger)
42
+ try:
43
+ last_order_info = await flight_order.get_order_details(
44
+ pre_order_no=query_dto.pre_order_no, user_id=query_dto.user_id, is_end=is_end, proxy=proxy,
45
+ headers=query_dto.headers,
46
+ )
47
+ await flight_order.http_client.close()
48
+ logger.info("查询青岛航空官网的订单详情数据成功")
49
+ break
50
+ except (Exception,):
51
+ pass
52
+ if isinstance(last_order_info, dict) and last_order_info.get("result") and last_order_info.get("code") == 1:
53
+ return last_order_info.get("result")
34
54
  else:
35
- raise RuntimeError(f"订单<{query_dto.pre_order_no}>,获取详情数据异常,返回值:{order_info}")
55
+ raise RuntimeError(f"订单<{query_dto.pre_order_no}>,获取详情数据异常,返回值:{last_order_info}")
36
56
 
37
57
 
38
58
  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
59
+ *, query_dto: QueryItineraryRequestDTO, logger: Logger, callback_get_proxy: Callable,
60
+ timeout: Optional[int] = None, retry: Optional[int] = None, cookie_jar: Optional[CookieJar] = None,
61
+ enable_log: Optional[bool] = None
41
62
  ) -> Optional[QueryItineraryResponseDTO]:
42
63
  _order_info: Dict[str, Any] = dict(pre_order_no=query_dto.pre_order_no)
43
64
  result = await get_order_detail(
44
- query_dto=query_dto, timeout=timeout, retry=retry, cookie_jar=cookie_jar, enable_log=enable_log
65
+ query_dto=query_dto, timeout=timeout, retry=retry, cookie_jar=cookie_jar, enable_log=enable_log,
66
+ callback_get_proxy=callback_get_proxy, logger=logger
45
67
  )
46
68
  _order_info["cash_unit"] = result.get("cashUnit")
47
69
  _order_info["order_status"] = result.get("orderStatus")
@@ -60,3 +82,6 @@ async def get_order_itinerary(
60
82
  if order_itineraries:
61
83
  _order_info["itinerary_info"] = order_itineraries
62
84
  return QueryItineraryResponseDTO(**_order_info)
85
+ else:
86
+ logger.warning(
87
+ f'青岛航空官网订单<{query_dto.pre_order_no}>,当前状态:{result.get("orderStatus")},没有生成行程单信息')
@@ -14,7 +14,7 @@ from logging import Logger
14
14
  from playwright.async_api import Page
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
+ from flight_helper.utils.exception_utils import IPBlockError
18
18
  from qdairlines_helper.utils.po_utils import get_ip_access_blocked_msg
19
19
 
20
20
 
@@ -12,7 +12,7 @@
12
12
  from logging import Logger
13
13
  from playwright.async_api import Page
14
14
  import qdairlines_helper.config.url_const as url_const
15
- from qdairlines_helper.utils.exception_utils import IPBlockError
15
+ from flight_helper.utils.exception_utils import IPBlockError
16
16
  from qdairlines_helper.po.order_verify_page import OrderVerifyPage
17
17
  from qdairlines_helper.utils.po_utils import get_ip_access_blocked_msg
18
18
 
@@ -9,17 +9,17 @@
9
9
  # Copyright ©2011-2026. Hunan xxxxxxx Company limited. All rights reserved.
10
10
  # ---------------------------------------------------------------------------------------------------------
11
11
  """
12
- import inspect
13
12
  from logging import Logger
14
13
  from aiohttp import CookieJar
15
14
  from playwright.async_api import Page
16
15
  from typing import Dict, Any, Optional, Callable
17
16
  import qdairlines_helper.config.url_const as url_const
18
17
  from qdairlines_helper.po.pay_success_page import PaySuccessPage
18
+ from qdairlines_helper.po.nhlms_cash_desk_page import NhlmsCashDeskPage
19
19
  from qdairlines_helper.utils.po_utils import get_ip_access_blocked_msg
20
20
  from qdairlines_helper.controller.order_detail import get_order_detail
21
21
  from flight_helper.models.dto.itinerary import QueryItineraryRequestDTO
22
- from qdairlines_helper.utils.exception_utils import IPBlockError, PaymentFailedError
22
+ from flight_helper.utils.exception_utils import IPBlockError, PaymentFailedError
23
23
 
24
24
 
25
25
  async def load_pay_success_page(
@@ -52,15 +52,16 @@ async def two_check_pay_success(
52
52
  except Exception as e:
53
53
  logger.error(e)
54
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
55
  # 2. 尝试取查询票号,看订单状态,是 BOOKED,还是 TICKETED
61
56
  order_detail: Dict[str, Any] = await get_order_detail(
62
57
  query_dto=query_dto, timeout=int(timeout), retry=retry, enable_log=enable_log, cookie_jar=cookie_jar,
58
+ callback_get_proxy=callback_get_proxy, logger=logger
63
59
  )
64
60
  order_status = (order_detail.get("orderStatus", "")).upper()
65
61
  if order_status not in ("TICKED",):
66
- raise PaymentFailedError(pre_order_no=query_dto.pre_order_no)
62
+ raise PaymentFailedError(pre_order_no=query_dto.pre_order_no, order_status=order_status)
63
+
64
+
65
+ async def check_hf_payment_is_success(*, page: NhlmsCashDeskPage, logger: Logger) -> None:
66
+ # 1. 看支付成功页面是否加载完成
67
+ await page.check_is_pay_success(logger=logger)
@@ -9,13 +9,17 @@
9
9
  # Copyright ©2011-2026. Hunan xxxxxxx Company limited. All rights reserved.
10
10
  # ---------------------------------------------------------------------------------------------------------
11
11
  """
12
+ import inspect
12
13
  from logging import Logger
13
- from typing import Dict, Any
14
+ from aiohttp import CookieJar
15
+ from typing import Dict, Any, Optional, Callable
14
16
  from playwright.async_api import Page, BrowserContext
15
17
  from qdairlines_helper.po.login_page import LoginPage
16
18
  import qdairlines_helper.config.url_const as url_const
19
+ from qdairlines_helper.http.user_login import UserLogin
17
20
  from qdairlines_helper.controller.home import load_home_po
18
- from qdairlines_helper.utils.exception_utils import IPBlockError
21
+ from flight_helper.utils.exception_utils import IPBlockError
22
+ from flight_helper.models.dto.http_schema import HTTPRequestDTO
19
23
  from playwright.async_api import Error as PlaywrightError, TimeoutError as PlaywrightTimeoutError
20
24
  from qdairlines_helper.utils.po_utils import get_ip_access_blocked_msg, parse_user_info_from_page_response
21
25
 
@@ -94,3 +98,32 @@ async def user_login_callback(
94
98
  user_info = parse_user_info_from_page_response(network_logs=network_logs)
95
99
  user_info.update(dict(storage_state=await context.storage_state()))
96
100
  return user_info
101
+
102
+
103
+ async def get_user_info(
104
+ *, logger: Logger, http_request_dto: HTTPRequestDTO, callback_get_proxy: Callable,
105
+ cookie_jar: Optional[CookieJar] = None
106
+ ) -> Dict[str, Any]:
107
+ user_login = UserLogin(http_request_dto=http_request_dto, cookie_jar=cookie_jar)
108
+ last_user_info: Dict[str, Any] = dict()
109
+ for index in range(5):
110
+ proxy = None
111
+ is_end: bool = True if index + 1 == 5 else False
112
+ if index != 0:
113
+ if inspect.iscoroutinefunction(callback_get_proxy):
114
+ proxy = await callback_get_proxy(logger=logger)
115
+ else:
116
+ proxy = callback_get_proxy(logger=logger)
117
+ try:
118
+ last_user_info = await user_login.get_user_info(
119
+ is_end=is_end, proxy=proxy, headers=http_request_dto.headers
120
+ )
121
+ await user_login.http_client.close()
122
+ logger.info("获取青岛航空官网的用户详情数据成功")
123
+ break
124
+ except (Exception,):
125
+ pass
126
+ if isinstance(last_user_info, dict) and last_user_info.get("result") and last_user_info.get("code") == 1:
127
+ return last_user_info.get("result")
128
+ else:
129
+ raise RuntimeError(f"获取用户详情数据异常,返回值:{last_user_info}")
@@ -11,51 +11,15 @@
11
11
  """
12
12
  from aiohttp import CookieJar
13
13
  from typing import Optional, Dict, Any
14
+ from qdairlines_helper.http.http_base import HttpBase
14
15
  import qdairlines_helper.config.url_const as url_const
15
- from http_helper.client.async_proxy import HttpClientFactory as _HttpClientFactory
16
+ from flight_helper.models.dto.http_schema import HTTPRequestDTO
16
17
 
17
18
 
18
- class FlightOrder:
19
+ class FlightOrder(HttpBase):
19
20
 
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
21
+ def __init__(self, *, http_request_dto: HTTPRequestDTO, cookie_jar: Optional[CookieJar] = None):
22
+ super().__init__(http_request_dto=http_request_dto, cookie_jar=cookie_jar)
59
23
 
60
24
  async def get_order_details(
61
25
  self, *, pre_order_no: str, user_id: str, proxy: Optional[Dict[str, Any]] = None,
@@ -0,0 +1,54 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ # ---------------------------------------------------------------------------------------------------------
4
+ # ProjectName: python-qdairlines-helper
5
+ # FileName: http_base.py
6
+ # Description: http基础模块
7
+ # Author: ASUS
8
+ # CreateDate: 2026/01/13
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
+ from flight_helper.models.dto.http_schema import HTTPRequestDTO
15
+ from http_helper.client.async_proxy import HttpClientFactory as _HttpClientFactory
16
+
17
+
18
+ class HttpBase(object):
19
+
20
+ def __init__(self, *, http_request_dto: HTTPRequestDTO, cookie_jar: Optional[CookieJar] = None):
21
+ self._domain = http_request_dto.http_domain or "127.0.0.1:18070"
22
+ self._protocol = http_request_dto.http_protocol or "http"
23
+ self._timeout = http_request_dto.timeout or 60
24
+ self._retry = http_request_dto.retry or 0
25
+ self._token = http_request_dto.token
26
+ self._origin = f"{self._protocol}://{self._domain}"
27
+ self._enable_log = http_request_dto.enable_log if http_request_dto.enable_log is not None else True
28
+ self._cookie_jar = cookie_jar or CookieJar()
29
+ self._proxy = http_request_dto.proxy
30
+ self._playwright_state: Dict[str, Any] = http_request_dto.storage_state
31
+ self.http_client: Optional[_HttpClientFactory] = None
32
+
33
+ def _get_http_client(self) -> _HttpClientFactory:
34
+ """延迟获取 HTTP 客户端"""
35
+ if self.http_client is None:
36
+ self.http_client = _HttpClientFactory(
37
+ protocol=self._protocol,
38
+ domain=self._domain,
39
+ timeout=self._timeout,
40
+ retry=self._retry,
41
+ enable_log=self._enable_log,
42
+ cookie_jar=self._cookie_jar,
43
+ playwright_state=self._playwright_state
44
+ )
45
+ return self.http_client
46
+
47
+ def _get_headers(self) -> Dict[str, str]:
48
+ headers = {
49
+ "content-type": "application/json;charset=utf-8",
50
+ "origin": self._domain,
51
+ "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",
52
+ "authorization": f"Bearer {self._token}"
53
+ }
54
+ return headers
@@ -0,0 +1,43 @@
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/13
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
+ from qdairlines_helper.http.http_base import HttpBase
15
+ import qdairlines_helper.config.url_const as url_const
16
+ from flight_helper.models.dto.http_schema import HTTPRequestDTO
17
+
18
+
19
+ class UserLogin(HttpBase):
20
+ def __init__(self, *, http_request_dto: HTTPRequestDTO, cookie_jar: Optional[CookieJar] = None):
21
+ super().__init__(http_request_dto=http_request_dto, cookie_jar=cookie_jar)
22
+
23
+ async def get_user_info(
24
+ self, *, proxy: Optional[Dict[str, Any]] = None, headers: Dict[str, str] = None,
25
+ is_end: Optional[bool] = None
26
+ ) -> Dict[str, Any]:
27
+ _headers = self._get_headers()
28
+ if headers is not None:
29
+ _headers.update(headers)
30
+ _headers["referer"] = f"{self._origin}{url_const.login_url}"
31
+ if is_end is None:
32
+ is_end = True
33
+ if proxy:
34
+ self._proxy = proxy
35
+ exception_keywords = [r'<h3[^>]*class="font-bold"[^>]*>([^<]+)</h3>']
36
+ return await self._get_http_client().request(
37
+ method="GET",
38
+ url=url_const.login_after_api_url,
39
+ headers=_headers,
40
+ is_end=is_end,
41
+ proxy_config=self._proxy or None,
42
+ exception_keywords=exception_keywords
43
+ )
@@ -13,7 +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
+ from flight_helper.utils.exception_utils import PassengerTypeError
17
17
 
18
18
 
19
19
  class AddPassengerPage(BasePo):
@@ -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
17
  import qdairlines_helper.config.url_const as url_const
17
- from qdairlines_helper.utils.exception_utils import ProductTypeError
18
+ from flight_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
  """
@@ -111,26 +114,41 @@ class BookSearchPage(BasePo):
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:
151
+ logger.error(e)
134
152
  continue
135
153
  return flight_products
136
154
 
@@ -143,7 +161,7 @@ class BookSearchPage(BasePo):
143
161
  """
144
162
  selector: str = 'xpath=.//div[@class="ticket_clazz"]/div/span'
145
163
  locator: Locator = await self.get_sub_locator(locator=locator, selector=selector, timeout=timeout)
146
- text: str = (await locator.inner_text(timeout=timeout)).strip()
164
+ text: str = (await locator.inner_text(timeout=timeout * 1000)).strip()
147
165
  match = re.search(r'\((.*?)舱\)', text)
148
166
  if match:
149
167
  return match.group(1).strip()
@@ -156,9 +174,9 @@ class BookSearchPage(BasePo):
156
174
  :param locator: flight_product Locator 对象
157
175
  :return: (是否存在, 错误信息|元素对象)
158
176
  """
159
- selector: str = 'xpath=.//div[@class="price_flex_top"]/span'
177
+ selector: str = 'xpath=.//div[@class="price_flex_top"]'
160
178
  locator: Locator = await self.get_sub_locator(locator=locator, selector=selector, timeout=timeout)
161
- amount_text: str = (await locator.inner_text(timeout=timeout)).strip()
179
+ amount_text: str = (await locator.inner_text(timeout=timeout * 1000)).strip()
162
180
  return convert_order_amount_text(amount_text=amount_text)
163
181
 
164
182
  async def _get_flight_product_booking_btn(self, locator: Locator, timeout: float = 5.0) -> Locator:
@@ -168,7 +186,7 @@ class BookSearchPage(BasePo):
168
186
  :param locator: flight_product Locator 对象
169
187
  :return: (是否存在, 错误信息|元素对象)
170
188
  """
171
- selector: str = 'xpath=(.//td[@class="ticket_num"]/div/span)[1]'
189
+ selector: str = 'xpath=(.//td[@class="ticket_num"]/div/span)[1]'
172
190
  return await self.get_sub_locator(locator=locator, selector=selector, timeout=timeout)
173
191
 
174
192
  async def _get_flight_product_seats_status(self, locator: Locator, timeout: float = 5.0) -> int:
@@ -178,11 +196,12 @@ class BookSearchPage(BasePo):
178
196
  :param locator: flight_product Locator 对象
179
197
  :return: (是否存在, 错误信息|元素对象)
180
198
  """
181
- selector: str = 'xpath=(.//td[@class="ticket_num"]/div/span)[2]'
199
+ selector: str = 'xpath=(.//td[@class="ticket_num"]/div/span)[2]'
182
200
  locator: Locator = await self.get_sub_locator(locator=locator, selector=selector, timeout=timeout)
183
- more_seats_text: str = (await locator.inner_text(timeout=timeout)).strip()
201
+ more_seats_text: str = (await locator.inner_text(timeout=timeout * 1000)).strip()
184
202
  match = re.search(r'\d+', more_seats_text)
185
203
  if match:
186
- return safe_convert_advanced(value=match.group(1).strip())
204
+ # ⚠️ 正则 \d+ 没有捕获组,只能用 group(0) 或 group()
205
+ return safe_convert_advanced(value=match.group().strip())
187
206
  else:
188
207
  return 999999
@@ -14,7 +14,7 @@ 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
16
  import qdairlines_helper.config.url_const as url_const
17
- from qdairlines_helper.utils.exception_utils import PaymentChannelError
17
+ from flight_helper.utils.exception_utils import PaymentChannelError
18
18
 
19
19
 
20
20
  class CashPaxInfoPage(BasePo):
@@ -9,12 +9,17 @@
9
9
  # Copyright ©2011-2026. Hunan xxxxxxx Company limited. All rights reserved.
10
10
  # ---------------------------------------------------------------------------------------------------------
11
11
  """
12
+ import asyncio
13
+ from logging import Logger
12
14
  from typing import Optional, Any
15
+ from collections import OrderedDict
16
+ from datetime import datetime, timedelta
13
17
  from playwright.async_api import Page, Locator
14
18
  from playwright_helper.libs.base_po import BasePo
15
19
  import qdairlines_helper.config.url_const as url_const
16
20
  from playwright_helper.utils.type_utils import safe_convert_advanced
17
- from qdairlines_helper.utils.exception_utils import HFPaymentTypeError
21
+ from flight_helper.utils.exception_utils import HFPaymentTypeError, PaymentFailError
22
+ from playwright.async_api import Error as PlaywrightError, TimeoutError as PlaywrightTimeoutError
18
23
 
19
24
 
20
25
  class NhlmsCashDeskPage(BasePo):
@@ -93,3 +98,39 @@ class NhlmsCashDeskPage(BasePo):
93
98
  """
94
99
  selector: str = 'xpath=//div[@class="content-bottom network-recharge"]//button[@id="submitBtn"]'
95
100
  return await self.get_locator(selector=selector, timeout=timeout)
101
+
102
+ async def check_is_pay_success(self, logger: Logger) -> None:
103
+ """检查支付是否成功"""
104
+ data = OrderedDict([
105
+ ("支付成功", 'xpath=//img[@alt="支付成功"]'),
106
+ (PaymentFailError("账户余额不足"), 'xpath=//span[contains(text(), "账户余额不足")]'),
107
+ (
108
+ PaymentFailError(
109
+ "尊敬的旅客:您的IP由于频繁访问已受限,客票预定可前往青岛航空微信公众号预定。如您需要办理其他客票业务,请联系官方客服热线:0532-96630。"
110
+ ),
111
+ '//h3[contains(text(), "您的IP")]'
112
+ )
113
+ ])
114
+ timeout = 120
115
+ end_time = datetime.now() + timedelta(seconds=timeout)
116
+
117
+ while datetime.now() < end_time:
118
+ for index, (outcome, selector) in enumerate(data.items()):
119
+ try:
120
+ await self.get_locator(selector=selector, timeout=1)
121
+ if index == 0:
122
+ logger.info(f"支付结果检测:{outcome}")
123
+ return # 成功
124
+ else:
125
+ # 一旦匹配失败,立即抛出!
126
+ raise outcome
127
+ except (PlaywrightTimeoutError, PlaywrightError, RuntimeError):
128
+ # 只捕获“未找到元素”的异常,其他异常往上抛
129
+ continue
130
+ except Exception:
131
+ # 可选:记录未知异常,但不吞掉
132
+ raise # 或 log 后 re-raise
133
+
134
+ await asyncio.sleep(0.5) # 避免 CPU 占用过高
135
+ logger.error(f"支付结果检测:超时,当前页面url: {self.__page.url}")
136
+ raise PlaywrightTimeoutError(f"支付结果检测超时,{timeout}秒内未出现成功或已知失败状态")
@@ -35,7 +35,7 @@ class OrderVerifyPage(BasePo):
35
35
  selector: str = 'xpath=//div[@class="order_info"]'
36
36
  while attempt <= refresh_attempt:
37
37
  try:
38
- await self.__page.reload(timeout=timeout)
38
+ await self.__page.reload(timeout=timeout * 1000)
39
39
  return await self.get_locator(selector=selector, timeout=timeout)
40
40
  except (PlaywrightError, PlaywrightTimeoutError, EnvironmentError, RuntimeError, Exception) as e:
41
41
  attempt += 1
@@ -1,35 +0,0 @@
1
- python_qdairlines_helper-0.1.4.dist-info/licenses/LICENSE,sha256=WtjCEwlcVzkh1ziO35P2qfVEkLjr87Flro7xlHz3CEY,11556
2
- qdairlines_helper/__init__.py,sha256=bN1FqEK_jvefO_HU_8HFJpjuOzHTsuhmRe5KAzmraR4,479
3
- qdairlines_helper/config/__init__.py,sha256=JjIRocePx3gwFNpdauuiwBU6B2Ug565xaHLEMu5Oo0I,479
4
- qdairlines_helper/config/url_const.py,sha256=Ro-8r3dRxuloJC5NIpwUMQ7Pezg2cDyZOqNUfTqCOFQ,2086
5
- qdairlines_helper/controller/__init__.py,sha256=oyeciEBlDLNpqHblB0R-nXQG3HsI8L5mmlWulQ7Y38Q,482
6
- qdairlines_helper/controller/add_passenger.py,sha256=GxaoSoLOH_XXTR79bDtSEbnmM_dSpBko6OzlrQtuT7U,5159
7
- qdairlines_helper/controller/book_payment.py,sha256=ygIHu6LGnLLihDM4PrpNDr3ZkTEZiQb-taqk4DuvANk,6104
8
- qdairlines_helper/controller/book_search.py,sha256=Q1KZKYeRmJh26nK1G9d8sWJF7_kCUS-9mJjTyoWCXpE,7598
9
- qdairlines_helper/controller/cash_pax_info.py,sha256=V5QKlvbWp8eF6gRos818w-9MyuienjgECkKKZkxCys8,2511
10
- qdairlines_helper/controller/home.py,sha256=KzTyl7vvI3_Blc-XQ8ItueGR1hnV6f9gEdYalfXG9Ws,1394
11
- qdairlines_helper/controller/nhlms_cashdesk.py,sha256=Fai8zZiqhUJl5-RWdkaGOdB3GTd5UQ7fpXDt4Emv65w,4312
12
- qdairlines_helper/controller/order_detail.py,sha256=6h6TyS_-yU2AHdFxbQS16TyjKWGWeCu1uVlsz9wANDE,3172
13
- qdairlines_helper/controller/order_query.py,sha256=JzNrJj6HRHSJMQhszucWPf6nWQl4Y5B7oE1N9SpD47k,1907
14
- qdairlines_helper/controller/order_verify.py,sha256=G9CC4a6KZg-VdWH6hzHDN7DkaoW2J9_yc7-mE2fL7Ms,2273
15
- qdairlines_helper/controller/pay_success.py,sha256=UiIn6rMpD4BIsBbbiZH4xuWJd5eeuHp0sb-qmo3MJGg,3153
16
- qdairlines_helper/controller/user_login.py,sha256=7PBbzP7doeUEy4M_Igb32IhjT4GzY_I-U3YimxekR_Q,4735
17
- qdairlines_helper/http/__init__.py,sha256=96AJPf2zgw_rm3Wv-boIBltWFgZH3Yo-fn5M9SRK280,489
18
- qdairlines_helper/http/flight_order.py,sha256=tC25YNk1pDzMmk4Zy6v120PwNuuaDhlHFh5xPBxtKco,3665
19
- qdairlines_helper/po/__init__.py,sha256=HoouLJGLu_dI6IAKTVjBRraGHMcAUgntayTph-BUBAE,481
20
- qdairlines_helper/po/add_passenger_page.py,sha256=XpaU6g7YSLhA8h3bHWJ_6CwnwgUmRtmLulPoRhkQ6_I,8000
21
- qdairlines_helper/po/air_order_page.py,sha256=0Ux0B2hUGKCoFLRpJqGHS2w690o0A2PDnvRf9-UKfJE,930
22
- qdairlines_helper/po/book_search_page.py,sha256=cHcPhHPPE0xuGfi6fnue-lH33COWU_nyLH2tVUQGI7U,9782
23
- qdairlines_helper/po/cash_pax_info_page.py,sha256=iJlmIxD8IKnMyTNcylmoz9jV0k4u-B5ALXZEUcRajY4,3401
24
- qdairlines_helper/po/home_page.py,sha256=KvoOZOYwKjlJjZOT522K_8ic3_2zaYOPWkuklUHVSlM,907
25
- qdairlines_helper/po/login_page.py,sha256=SdWmkP_47GeosbFRcfvXLeQxxdM7b9UESvmajWq4-nA,2975
26
- qdairlines_helper/po/nhlms_cash_desk_page.py,sha256=TwqszVbCc4iRSE684uafdlGJB1VC5wS9ZuGWvfeb8Fo,4542
27
- qdairlines_helper/po/order_verify_page.py,sha256=omgs6CBPTPCGnO8cxRFd2ePFUrJZVD5cD_GBCXsZIes,2917
28
- qdairlines_helper/po/pay_success_page.py,sha256=XGddo8as8wf1kCCdvemqHIRWOPIPv1JG8iXEmpH4zck,1329
29
- qdairlines_helper/utils/__init__.py,sha256=sti2S709puM2mQbQisk0KGq_YNjW6T4zz2vyYXonNB0,479
30
- qdairlines_helper/utils/exception_utils.py,sha256=rXPao6h9sW5uL3lUA36FS3WTlePAm5EZ6MBjWeJJTUs,3934
31
- qdairlines_helper/utils/po_utils.py,sha256=74ZVyyxPmPk1tcGQoMauXhBE3qsZJej8urni1ch27io,2417
32
- python_qdairlines_helper-0.1.4.dist-info/METADATA,sha256=SIlFOHl1g6nCQGSubFYF6rnaXyF1fq-zg8KmLY0RMvA,14424
33
- python_qdairlines_helper-0.1.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
34
- python_qdairlines_helper-0.1.4.dist-info/top_level.txt,sha256=MRbQkBMdSG4f5mx2RWfu6aXoDSyM-2KwL-YFqNBdbng,18
35
- python_qdairlines_helper-0.1.4.dist-info/RECORD,,
@@ -1,105 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- """
3
- # ---------------------------------------------------------------------------------------------------------
4
- # ProjectName: python-qdairlines-helper
5
- # FileName: exception_utils.py
6
- # Description: 异常工具模块
7
- # Author: ASUS
8
- # CreateDate: 2026/01/07
9
- # Copyright ©2011-2026. Hunan xxxxxxx Company limited. All rights reserved.
10
- # ---------------------------------------------------------------------------------------------------------
11
- """
12
- from typing import Literal
13
-
14
-
15
- class DuplicatePaymentError(Exception):
16
- def __init__(self, order_no: str):
17
- self.order_no = order_no
18
- super().__init__(f"订单<{order_no}>重复支付")
19
-
20
-
21
- class NotEnoughTicketsError(Exception):
22
- def __init__(self, flight_no: str, seats_status: int, passengers: int):
23
- self.flight_no = flight_no
24
- self.seats_status = seats_status
25
- self.passengers = passengers
26
- super().__init__(f"青岛航空官网显示航班<{flight_no}>的余票<{seats_status}>少于乘客人数<{passengers}>")
27
-
28
-
29
- class ExcessiveProfitdError(Exception):
30
- def __init__(
31
- self, flight_no: str, query_price: float, order_price: float, reduction_threshold: float,
32
- asset: Literal["票面价", "销售价"] = "票面价"
33
- ):
34
- self.flight_no = flight_no
35
- self.query_price = query_price
36
- self.order_price = order_price
37
- self.reduction_threshold = reduction_threshold
38
- self.asset = asset
39
- super().__init__(
40
- f"航班<{flight_no}>官网价:{query_price} 低于:订单{asset}[{order_price}] - 下降阈值[{reduction_threshold}],收益过高"
41
- )
42
-
43
-
44
- class ExcessiveLossesError(Exception):
45
- def __init__(
46
- self, flight_no: str, query_price: float, order_price: float, increase_threshold: float,
47
- asset: Literal["票面价", "销售价"] = "票面价"
48
- ):
49
- self.flight_no = flight_no
50
- self.query_price = query_price
51
- self.order_price = order_price
52
- self.increase_threshold = increase_threshold
53
- self.asset = asset
54
- super().__init__(
55
- f"航班<{flight_no}>官网价:{query_price} 高于:订单{asset}[{order_price}] + 上浮阈值[{increase_threshold}],亏损太多"
56
- )
57
-
58
-
59
- class PaymentChannelError(Exception):
60
- def __init__(self, channel_name: str):
61
- self.channel_name = channel_name
62
- super().__init__(f"支付渠道<{channel_name}>暂不支持")
63
-
64
-
65
- class PaymentChannelMissError(Exception):
66
- def __init__(self):
67
- super().__init__(f"支付渠道参数丢失")
68
-
69
-
70
- class PaymentTypeError(Exception):
71
- def __init__(self, payment_type: str):
72
- self.payment_type = payment_type
73
- super().__init__(f"付款方式<{payment_type}>暂不支持")
74
-
75
-
76
- class PassengerTypeError(Exception):
77
- def __init__(self, passenger_type: str):
78
- self.passenger_type = passenger_type
79
- super().__init__(f"乘客类型<{passenger_type}>暂不支持")
80
-
81
-
82
- class ProductTypeError(Exception):
83
- def __init__(self, product_type: str):
84
- self.product_type = product_type
85
- super().__init__(f"产品类型<{product_type}>暂不支持")
86
-
87
-
88
- class HFPaymentTypeError(Exception):
89
- def __init__(self, payment_type: str):
90
- self.payment_type = payment_type
91
- super().__init__(f"汇付天下的付款方式<{payment_type}>暂不支持")
92
-
93
-
94
- class PaymentFailedError(Exception):
95
- def __init__(self, pre_order_no: str, order_status: str):
96
- self.pre_order_no = pre_order_no
97
- self.order_status = order_status
98
- super().__init__(f"青岛航空官网订单<{pre_order_no}>支付失败,支付结束后的状态<{order_status}>")
99
-
100
-
101
- class IPBlockError(Exception):
102
-
103
- def __init__(self, message: str):
104
- self.message = message
105
- super().__init__(message)