python-qlv-helper 0.6.0__py3-none-any.whl → 0.7.2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python_qlv_helper
3
- Version: 0.6.0
3
+ Version: 0.7.2
4
4
  Summary: qlv helper python package
5
5
  Author-email: ckf10000 <ckf10000@sina.com>
6
6
  License: Apache License
@@ -210,14 +210,16 @@ Project-URL: Issues, https://github.com/ckf10000/qlv-helper/issues
210
210
  Requires-Python: >=3.12
211
211
  Description-Content-Type: text/markdown
212
212
  License-File: LICENSE
213
- Requires-Dist: playwright==1.56.0; python_version >= "3.12" and platform_system == "Windows"
214
- Requires-Dist: playwright-stealth==2.0.0; python_version >= "3.12" and platform_system == "Windows"
215
- Requires-Dist: ddddocr==1.5.6; python_version >= "3.12" and platform_system == "Windows"
213
+ Requires-Dist: playwright==1.56.0; python_version >= "3.12"
214
+ Requires-Dist: playwright-stealth==2.0.0; python_version >= "3.12"
215
+ Requires-Dist: ddddocr==1.5.6; python_version >= "3.12"
216
216
  Requires-Dist: aiohttp==3.13.2; python_version >= "3.12"
217
217
  Requires-Dist: beautifulsoup4==4.14.2; python_version >= "3.12"
218
- Requires-Dist: airtest==1.3.6; python_version >= "3.12" and platform_system == "Windows"
218
+ Requires-Dist: airtest==1.3.6; python_version >= "3.12"
219
219
  Requires-Dist: python_http_helper>=0.2.0; python_version >= "3.12"
220
- Requires-Dist: python_playwright_helper>=0.2.9; python_version >= "3.12"
220
+ Requires-Dist: python_playwright_helper>=0.3.0; python_version >= "3.12"
221
+ Requires-Dist: python_ocr_helper>=0.0.1; python_version >= "3.12"
222
+ Requires-Dist: flight_helper>=0.2.9
221
223
  Dynamic: license-file
222
224
 
223
225
  # qlv-helper
@@ -1,35 +1,36 @@
1
- python_qlv_helper-0.6.0.dist-info/licenses/LICENSE,sha256=WtjCEwlcVzkh1ziO35P2qfVEkLjr87Flro7xlHz3CEY,11556
1
+ python_qlv_helper-0.7.2.dist-info/licenses/LICENSE,sha256=WtjCEwlcVzkh1ziO35P2qfVEkLjr87Flro7xlHz3CEY,11556
2
2
  qlv_helper/__init__.py,sha256=5DCc5JhfdsgtIuFWgkxPOW5VVKZ8RPikQLGIuyZX6_Y,465
3
3
  qlv_helper/config/__init__.py,sha256=0pKLgui-sC6yMNBBuuTTLkUGhPybiJQSTKTbi66alvg,465
4
4
  qlv_helper/config/custom_exception.py,sha256=uYme0iseQt_dP-Y6-hZ_H2OA2OQR0Kp5PmmZvhEZlEc,805
5
- qlv_helper/config/url_const.py,sha256=PbKKKH4heqP6SO186MTt-CEpf-Ix5ODlLQ2Aq2ZBZhU,537
5
+ qlv_helper/config/url_const.py,sha256=EoLHOtlO3Ob2WSMMv6TBrl7eJCfJCB844dwmD_k86BM,630
6
6
  qlv_helper/controller/__init__.py,sha256=cOJA0xMIytv17oICzPYqWLaSy-Ro2Ceeti0hHhsUj6Y,468
7
7
  qlv_helper/controller/domestic_activity_order.py,sha256=MlmsDVsMBWq2h4Yjh1rhO372Z3p8tu2-4IZGP-nkfr8,1136
8
- qlv_helper/controller/main_page.py,sha256=JtkB6BdKYHoNYZ4fkeALpSmAj-NAuiv1x6BvCcBtpic,1252
9
- qlv_helper/controller/order_detail.py,sha256=PjBS1YmiRzWaXg0brJef96k0-0uLr6oaEYz5zZzpjoo,22675
10
- qlv_helper/controller/order_table.py,sha256=4AgA9EO0_GVuwnW3ltXPoN74U_6YpOqkRJVdeLb6L1o,6082
11
- qlv_helper/controller/user_login.py,sha256=iyyDbOREsXtV5bqAFeXGwurvcCmDHmquh6ReWCfnOBE,5281
8
+ qlv_helper/controller/main_page.py,sha256=p_-nXAIptNrx1SUgma7oPB-cqFFHYdCNvXCfsesIQFc,1855
9
+ qlv_helper/controller/order_detail.py,sha256=pdxN6ahsoeJq2XqBNRUwLrGG12WSy737fU17fhJMjgg,25621
10
+ qlv_helper/controller/order_table.py,sha256=unLL1xMKK1xvm_iewpdMcUPZc6AcBcLq0hAGXAdjdhU,15373
11
+ qlv_helper/controller/user_login.py,sha256=oXO1otTGuK3_r07fKdncQE3Is2w-kiI-mWmJyFvsVyE,5915
12
+ qlv_helper/controller/wechat_login.py,sha256=0u9H6tJiQO28nT5AG2Ot7EWFmpBqFJD_9dd12snsJIQ,2196
12
13
  qlv_helper/http/__init__.py,sha256=yDh1xi_o7ohXqDAzLu62qCWIGk4_aD1dhnUaCon3klM,484
13
14
  qlv_helper/http/main_page.py,sha256=LTpwrG8H_NqwCa3185irgcGd6JQkChAk7HQsDM-TNTI,1519
14
- qlv_helper/http/order_page.py,sha256=uhCnRPvIvSg5ACMCfpw5_Z62iAAw-Dm-yDRloe5QBP8,16934
15
+ qlv_helper/http/order_page.py,sha256=Sti3dxZIN1U9z0e7NwyJ6_TrNdI-N3mRFnUN8WpN3FM,21674
15
16
  qlv_helper/http/order_table_page.py,sha256=IaXn5wjqPi1aRXHz0kucdHEdZswUWAZfECC13y57y8k,14440
16
17
  qlv_helper/po/__init__.py,sha256=eDr06o0eYapBsYpOhA11bbxzs2F0dsuDjOKmxk_2HVE,480
17
- qlv_helper/po/domestic_activity_order_page.py,sha256=f-XSc2HD6GYisUJZwK2-u0Tr4W3B_vb5JpXvHnjfHOQ,5030
18
- qlv_helper/po/login_page.py,sha256=HKGjS4WrkWnDdnh0JQAYxA3DyEWbInviQTna_KrP2FA,5960
19
- qlv_helper/po/main_page.py,sha256=Hf8Wj7D9GitIj4HIi_zDHHIc5kasFQwraqEMZObYI7w,3320
20
- qlv_helper/po/order_detail_page.py,sha256=zCB-pWyXdZz8vlDhvl1K79-bQ7C7Y12DiDSXlS_woYw,13681
18
+ qlv_helper/po/domestic_activity_order_page.py,sha256=El63U0GI2PU9WGIkGxPgCE1Fp1yCsw2v1H6cZhSaG4c,8509
19
+ qlv_helper/po/login_page.py,sha256=JbapNFwGCei3K3mpfhte1TVNeqzG7yHsCtp5KiKv_6g,4271
20
+ qlv_helper/po/main_page.py,sha256=0_tqZILZnLS6y3chg9ERTHDtg86pW0aIQ1vNuPFLVG8,1752
21
+ qlv_helper/po/order_detail_page.py,sha256=0Xyi9zxnSHv4OfFAlPPz_D0S5S0ZHKLphUUT8R4svzk,14414
21
22
  qlv_helper/po/wechat_auth_page.py,sha256=a4YZlM5JOS0l3CNJm_oJFBhZY7AbNOpdIwilSjAO5bY,3171
22
23
  qlv_helper/utils/__init__.py,sha256=rGzBkUf1tslG4WRPQjVWTVuwWG76pkckuKO_6K4sEus,465
23
24
  qlv_helper/utils/browser_utils.py,sha256=mKoqSEz1vFrVemp9cgI4R5UhA4k7i0Cd9cWsIXJZ6E4,986
24
25
  qlv_helper/utils/datetime_utils.py,sha256=BaDJKuH-yqc2NF9KYN66zUYUEJ9ZRHj09AV4-gILf3o,606
25
26
  qlv_helper/utils/file_handle.py,sha256=_dJ9Yk8esttJYsjdBMZAkjZTDQh5QYVPXjLRRyWUMh0,1087
26
27
  qlv_helper/utils/html_utils.py,sha256=i5oOFYETH3kDS9-rSyGu1SHFTkfZvAPPQ4za76BCdVA,1962
27
- qlv_helper/utils/ocr_helper.py,sha256=IrS4iPTm1SXnVpbHKMowQA2w1bvI0fUVRqEAbmwnjws,2781
28
+ qlv_helper/utils/ocr_helper.py,sha256=vpIokr07Utmpsb78MHF83UcXL90A8BPbLgjMg4kONyA,3343
28
29
  qlv_helper/utils/po_utils.py,sha256=SwQKL58HERGG2Weou_AwY_TQoYSvgi0gvaVCJBput_k,3516
29
30
  qlv_helper/utils/stealth_browser.py,sha256=srNOYJOboYo30TvW5OP5TaVpg4jgHm9GxqmYnuwcUQU,3140
30
31
  qlv_helper/utils/type_utils.py,sha256=S5FXUje2mbDuq27LU05WymxNu1VGOLBUV3tuqcx51dE,3792
31
32
  qlv_helper/utils/windows_utils.py,sha256=Cvedsk1c2ujgPNVxszz8XWANkvEr8G9kne6povtZRU4,2866
32
- python_qlv_helper-0.6.0.dist-info/METADATA,sha256=cG17GIsXVxDMdh92lqvgtJKpMI4Qn1dNjgtTrQbZMjc,14853
33
- python_qlv_helper-0.6.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
34
- python_qlv_helper-0.6.0.dist-info/top_level.txt,sha256=0pYdhD8SfBcC57LzLYGHY7cwwPqdqAkB1twysCJh5OA,11
35
- python_qlv_helper-0.6.0.dist-info/RECORD,,
33
+ python_qlv_helper-0.7.2.dist-info/METADATA,sha256=LuxKTcs6bOcTbrthW-Q3N35aT9nF7sTKDDZS0uae9bQ,14825
34
+ python_qlv_helper-0.7.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
35
+ python_qlv_helper-0.7.2.dist-info/top_level.txt,sha256=0pYdhD8SfBcC57LzLYGHY7cwwPqdqAkB1twysCJh5OA,11
36
+ python_qlv_helper-0.7.2.dist-info/RECORD,,
@@ -11,3 +11,5 @@
11
11
  """
12
12
 
13
13
  order_detail_url = "/OrderProcessing/NewTicket_show/{}?&r={}"
14
+ domestic_activity_order_url = "/OrderList/GuoNei_ActivityOrders"
15
+ login_url = "/Home/Login"
@@ -10,13 +10,29 @@
10
10
  # ---------------------------------------------------------------------------------------------------------
11
11
  """
12
12
  import aiohttp
13
+ from logging import Logger
14
+ from playwright.async_api import Page
13
15
  from typing import Dict, Any, Optional
16
+ from qlv_helper.po.main_page import MainPage
14
17
  from qlv_helper.http.main_page import get_main_page_html, parser_head_title
15
18
 
16
19
 
20
+ async def open_main_page(
21
+ *, page: Page, logger: Logger, qlv_protocol: str, qlv_domain: str, timeout: float = 60.0, **kwargs: Any
22
+ ) -> MainPage:
23
+ url_prefix = f"{qlv_protocol}://{qlv_domain}"
24
+ main_url = url_prefix + "/"
25
+ await page.goto(main_url)
26
+
27
+ main_po = MainPage(page=page, url=main_url)
28
+ await main_po.url_wait_for(url=main_url, timeout=timeout)
29
+ logger.info(f"即将进入首页,页面URL<{main_url}>")
30
+ return main_po
31
+
32
+
17
33
  async def get_main_info_with_http(
18
34
  domain: str, protocol: str = "http", retry: int = 1, timeout: int = 5, enable_log: bool = True,
19
- cookie_jar: Optional[aiohttp.CookieJar] = None, playwright_state: Dict[str, Any] = None
35
+ cookie_jar: Optional[aiohttp.CookieJar] = None, playwright_state: Dict[str, Any] = None, **kwargs: Any
20
36
  ) -> Dict[str, Any]:
21
37
  response = await get_main_page_html(
22
38
  domain=domain, protocol=protocol, retry=retry, timeout=timeout, enable_log=enable_log,
@@ -27,4 +43,4 @@ async def get_main_info_with_http(
27
43
 
28
44
  html = response.get("data")
29
45
  response["message"] = parser_head_title(html=html)
30
- return response
46
+ return response
@@ -16,8 +16,9 @@ from datetime import datetime
16
16
  import qlv_helper.config.url_const as url_const
17
17
  from typing import Dict, Any, List, cast, Optional
18
18
  from qlv_helper.po.order_detail_page import OrderDetailPage
19
+ from flight_helper.models.dto.procurement import FillProcurementInputDTO
19
20
  from qlv_helper.http.order_page import parser_order_info, get_order_page_html, parser_order_flight_table, \
20
- fill_procurement_info_with_http
21
+ fill_procurement_info_with_http, fill_itinerary_info_with_http, fill_procurement_dto_with_http
21
22
  from playwright.async_api import Page, Locator, Error as PlaywrightError, TimeoutError as PlaywrightTimeoutError
22
23
 
23
24
 
@@ -433,7 +434,7 @@ async def fill_procurement_with_http(
433
434
  *, order_id: int, qlv_domain: str, amount: float, pre_order_id: str, platform_user_id: str, user_password: str,
434
435
  passengers: List[str], fids: str, pids: List[str], transaction_id: str, qlv_protocol: str = "http",
435
436
  retry: int = 1, timeout: int = 5, enable_log: bool = True, cookie_jar: Optional[aiohttp.CookieJar] = None,
436
- playwright_state: Dict[str, Any] = None, data_list: Optional[List[Dict[str, Any]]] = None
437
+ playwright_state: Dict[str, Any] = None, data_list: Optional[List[Dict[str, Any]]] = None, **kwargs: Any
437
438
  ) -> Dict[str, Any]:
438
439
  return await fill_procurement_info_with_http(
439
440
  order_id=order_id, qlv_domain=qlv_domain, amount=amount, pre_order_id=pre_order_id,
@@ -441,3 +442,55 @@ async def fill_procurement_with_http(
441
442
  transaction_id=transaction_id, qlv_protocol=qlv_protocol, retry=retry, timeout=timeout, enable_log=enable_log,
442
443
  cookie_jar=cookie_jar, playwright_state=playwright_state, data_list=data_list
443
444
  )
445
+
446
+
447
+ async def fill_procurement_with_http_callback(
448
+ *, fill_procurement_dto: FillProcurementInputDTO, retry: int = 1, timeout: int = 5, enable_log: bool = True,
449
+ cookie_jar: Optional[aiohttp.CookieJar] = None, playwright_state: Dict[str, Any] = None,
450
+ data_list: Optional[List[Dict[str, Any]]] = None, **kwargs: Any
451
+ ) -> Dict[str, Any]:
452
+ return await fill_procurement_dto_with_http(
453
+ fill_procurement_dto=fill_procurement_dto, retry=retry, timeout=timeout, enable_log=enable_log,
454
+ cookie_jar=cookie_jar, playwright_state=playwright_state, data_list=data_list
455
+ )
456
+
457
+
458
+ async def fill_itinerary_with_http(
459
+ *, page: Page, logger: Logger, qlv_protocol: str, qlv_domain: str, order_id: int, retry: int = 1,
460
+ passengers: List[Dict[str, Any]], timeout: float = 20.0, cookie_jar: Optional[aiohttp.CookieJar] = None,
461
+ playwright_state: Dict[str, Any] = None, enable_log: bool = True, **kwargs: Any
462
+ ) -> bool:
463
+ # 1. 打开页面
464
+ order_detail_po = await open_order_detail_page(
465
+ page=page, logger=logger, protocol=qlv_protocol, domain=qlv_domain, order_id=order_id, timeout=timeout
466
+ )
467
+
468
+ # 2. 获取采购信息的流水id
469
+ purchase_transaction_ids = await order_detail_po.get_purchase_info_transaction_id(timeout=timeout)
470
+ purchase_transaction_ids = [x for x in purchase_transaction_ids if x != "0"]
471
+ if purchase_transaction_ids:
472
+ flag = True
473
+ purchase_transaction_ids.sort()
474
+ purchase_transaction_id = purchase_transaction_ids[0]
475
+ for passenger in passengers:
476
+ pid = passenger.get("pid")
477
+ tid = passenger.get("tid")
478
+ p_name = passenger.get("p_name")
479
+ itinerary_id = passenger.get("itinerary_id")
480
+ try:
481
+ response = await fill_itinerary_info_with_http(
482
+ order_id=order_id, qlv_domain=qlv_domain, pid=pid, tid=tid, transaction_id=purchase_transaction_id,
483
+ itinerary_id=itinerary_id, retry=retry, qlv_protocol=qlv_protocol, timeout=int(timeout),
484
+ enable_log=enable_log, cookie_jar=cookie_jar, playwright_state=playwright_state
485
+ )
486
+ if response == 200 or "OK" in response.get("data"):
487
+ logger.info(f"订单<{order_id}>,乘客<{p_name}>票号<{itinerary_id}>回填成功")
488
+ else:
489
+ logger.warning(
490
+ f'订单<{order_id}>,乘客<{p_name}>票号<{itinerary_id}>回填失败:{response.get("data")}')
491
+ flag = False
492
+ except (Exception,):
493
+ flag = False
494
+ return flag
495
+ else:
496
+ raise EnvironmentError("还未填写采购信息,暂时不能回填票号")
@@ -10,10 +10,17 @@
10
10
  # ---------------------------------------------------------------------------------------------------------
11
11
  """
12
12
  import asyncio
13
+ from copy import deepcopy
14
+ from logging import Logger
13
15
  from aiohttp import CookieJar
14
- from typing import Optional, Dict, Any, Callable, List
16
+ from datetime import datetime, timedelta
17
+ from playwright.async_api import Page, Locator
18
+ import qlv_helper.config.url_const as url_const
15
19
  from http_helper.client.async_proxy import HttpClientFactory
16
20
  from qlv_helper.utils.html_utils import parse_pagination_info
21
+ from qlv_helper.utils.type_utils import safe_convert_advanced
22
+ from typing import Optional, Dict, Any, Callable, List, cast, Tuple
23
+ from qlv_helper.po.domestic_activity_order_page import DomesticActivityOrderPage
17
24
  from qlv_helper.http.order_table_page import get_domestic_activity_order_page_html, get_domestic_ticket_outed_page_html, \
18
25
  parse_order_table, get_domestic_unticketed_order_page_html
19
26
 
@@ -28,7 +35,7 @@ async def _get_paginated_order_table(
28
35
  cookie_jar: Optional[CookieJar],
29
36
  playwright_state: Dict[str, Any],
30
37
  table_state: str,
31
- fetch_page_fn: Callable[..., Any], # 拿到第一页/分页 HTML 的函数
38
+ fetch_page_fn: Callable[..., Any], # 拿到第一页/分页 HTML 的函数
32
39
  ) -> Dict[str, Any]:
33
40
  """通用分页表格抓取(支持并发)"""
34
41
 
@@ -79,7 +86,7 @@ async def _get_paginated_order_table(
79
86
  )
80
87
  if resp.get("code") == 200:
81
88
  return parse_order_table(html=resp["data"], table_state=table_state)
82
- except (Exception, ):
89
+ except (Exception,):
83
90
  return list() # 抓取失败则返回空,不影响整体
84
91
  return list()
85
92
 
@@ -111,6 +118,7 @@ async def _get_paginated_order_table(
111
118
  response["data"] = pagination_info
112
119
  return response
113
120
 
121
+
114
122
  async def get_domestic_activity_order_table(
115
123
  domain: str, protocol: str = "http", retry: int = 1, timeout: int = 5, enable_log: bool = True,
116
124
  cookie_jar: Optional[CookieJar] = None, playwright_state: Dict[str, Any] = None
@@ -144,6 +152,7 @@ async def get_domestic_ticket_outed_table(
144
152
  fetch_page_fn=get_domestic_ticket_outed_page_html
145
153
  )
146
154
 
155
+
147
156
  async def get_domestic_unticketed_order_table(
148
157
  domain: str, protocol: str = "http", retry: int = 1, timeout: int = 5, enable_log: bool = True,
149
158
  cookie_jar: Optional[CookieJar] = None, playwright_state: Dict[str, Any] = None
@@ -158,4 +167,164 @@ async def get_domestic_unticketed_order_table(
158
167
  playwright_state=playwright_state,
159
168
  table_state="proccessing",
160
169
  fetch_page_fn=get_domestic_unticketed_order_page_html
161
- )
170
+ )
171
+
172
+
173
+ async def open_domestic_activity_order_page(
174
+ *, page: Page, logger: Logger, qlv_protocol: str, qlv_domain: str, timeout: float = 20.0
175
+ ) -> DomesticActivityOrderPage:
176
+ url_prefix = f"{qlv_protocol}://{qlv_domain}"
177
+ domestic_activity_order_url = url_prefix + url_const.domestic_activity_order_url
178
+ await page.goto(domestic_activity_order_url)
179
+
180
+ domestic_activity_order_po = DomesticActivityOrderPage(page=page, url=domestic_activity_order_url)
181
+ await domestic_activity_order_po.url_wait_for(url=domestic_activity_order_url, timeout=timeout)
182
+ logger.info(f"即将进入国内活动订单页面,页面URL<{domestic_activity_order_url}>")
183
+ return domestic_activity_order_po
184
+
185
+
186
+ async def pop_will_expire_domestic_activity_order(
187
+ *, page: Page, logger: Logger, qlv_protocol: str, qlv_domain: str, last_minute_threshold: int,
188
+ timeout: float = 20.0, **kwargs: Any
189
+ ) -> Tuple[List[Dict[str, Any]], bool]:
190
+ # 1. 打开国内活动订单页面
191
+ domestic_activity_order_po = await open_domestic_activity_order_page(
192
+ page=page, logger=logger, qlv_protocol=qlv_protocol, qlv_domain=qlv_domain, timeout=timeout
193
+ )
194
+
195
+ # TODO 暂时不考虑分页的情况
196
+ # 2. 获取table所有tr的Locator对象
197
+ trs_locator = await domestic_activity_order_po.get_flight_table_trs_locator(timeout=timeout)
198
+ trs_locator = await trs_locator.all()
199
+ table_data = list()
200
+ feilds = {
201
+ "to_from": "", "urgant_state": "", "order_id": 0, "pre_order_id": "", "aduit_pnr": "", "child_pnr": "",
202
+ "payment_time": "", "last_time_ticket": "", "dat_dep": "", "code_dep": "", "code_arr": "", "flight_no": "",
203
+ "cabin": "", "policy": "", "total_people": 0, "total_adult": 0, "total_child": 0, "receipted": 0.00,
204
+ "stat_opration": "", "more_seats": "", "operation_info": "", "substitute_btn_locator": ""
205
+ }
206
+ pre_pop_orders = list()
207
+ is_pop = False
208
+ for tr_locator in trs_locator[1:]:
209
+ row_locator = await domestic_activity_order_po.get_flight_table_trs_td(locator=tr_locator, timeout=timeout)
210
+ tds_locators = await row_locator.all()
211
+ sub_feilds = list()
212
+ copy_feilds = deepcopy(feilds)
213
+ for index, td_locator in enumerate(tds_locators):
214
+ try:
215
+ text = (await td_locator.inner_text()).strip()
216
+ if index == 0:
217
+ copy_feilds["to_from"] = text
218
+ sub_feilds.append("to_from")
219
+ elif index == 1:
220
+ order_id = await domestic_activity_order_po.get_flight_table_td_order_id(
221
+ locator=td_locator, timeout=timeout
222
+ )
223
+ urgant_state = await domestic_activity_order_po.get_flight_table_td_urgant(
224
+ locator=td_locator, timeout=timeout
225
+ )
226
+ copy_feilds["order_id"] = safe_convert_advanced(value=order_id)
227
+ copy_feilds["urgant_state"] = urgant_state
228
+ sub_feilds.extend(["order_id", "urgant_state"])
229
+ elif index == 2:
230
+ copy_feilds["pre_order_id"] = text
231
+ sub_feilds.append("pre_order_id")
232
+ elif index == 3:
233
+ text = text.replace("\xa0", "")
234
+ text_slice = text.split("|")
235
+ copy_feilds["aduit_pnr"] = text_slice[0].strip()
236
+ copy_feilds["child_pnr"] = text_slice[1].strip()
237
+ sub_feilds.extend(["aduit_pnr", "child_pnr"])
238
+ elif index == 4:
239
+ copy_feilds["payment_time"] = text
240
+ sub_feilds.append("payment_time")
241
+ elif index == 5:
242
+ continue
243
+ elif index == 6:
244
+ copy_feilds["last_time_ticket"] = text
245
+ sub_feilds.append("last_time_ticket")
246
+ elif index == 7:
247
+ continue
248
+ elif index == 8:
249
+ text = text.replace("\xa0", "|")
250
+ text_slice = [i for i in text.split("|") if i.strip()]
251
+ ctrip = text_slice[1].split("-")
252
+ copy_feilds["dat_dep"] = text_slice[0].strip()
253
+ copy_feilds["code_dep"] = ctrip[0].strip()
254
+ copy_feilds["code_arr"] = ctrip[1].strip()
255
+ copy_feilds["flight_no"] = text_slice[2].strip()
256
+ copy_feilds["cabin"] = text_slice[3].strip()
257
+ sub_feilds.extend(["dat_dep", "code_dep", "code_arr", "flight_no", "cabin"])
258
+ elif index == 9:
259
+ text = text.replace("\xa0", "")
260
+ text = text.replace(">", "")
261
+ text = text.replace("<", "")
262
+ text = text.replace("&", "")
263
+ text = text.replace("<br>", "\n")
264
+ copy_feilds["policy"] = text
265
+ sub_feilds.append("policy")
266
+ elif index == 10:
267
+ text = text.replace("【 ", "|")
268
+ text = text.replace("/", "|")
269
+ text = text.replace("】", "")
270
+ text_slice = text.split("|")
271
+ copy_feilds["total_people"] = safe_convert_advanced(value=text_slice[0].strip())
272
+ copy_feilds["total_adult"] = safe_convert_advanced(value=text_slice[1].strip())
273
+ copy_feilds["total_child"] = safe_convert_advanced(value=text_slice[2].strip())
274
+ sub_feilds.extend(["total_people", "total_adult", "total_child"])
275
+ elif index == 11:
276
+ copy_feilds["receipted"] = safe_convert_advanced(value=text)
277
+ sub_feilds.append("receipted")
278
+ elif index == 12:
279
+ copy_feilds["stat_opration"] = text
280
+ sub_feilds.append("stat_opration")
281
+ elif index == 13:
282
+ copy_feilds["more_seats"] = safe_convert_advanced(value=text)
283
+ sub_feilds.append("more_seats")
284
+ elif index == 14:
285
+ text_slice = text.split(" ")
286
+ operation_info = dict(
287
+ lock_btn_locator=cast(Locator, None), pop_btn_locator=cast(Locator, None), locked=""
288
+ )
289
+ if "锁定" in text_slice[0]:
290
+ lock_btn_locator = await domestic_activity_order_po.get_flight_table_td_operation_lock_btn(
291
+ locator=td_locator, timeout=timeout
292
+ )
293
+ operation_info["lock_btn_locator"] = lock_btn_locator
294
+ else:
295
+ operation_info["locked"] = text_slice[0].strip()
296
+ if "踢出" in text:
297
+ pop_btn_locator = await domestic_activity_order_po.get_flight_table_td_operation_pop_btn(
298
+ locator=td_locator, timeout=timeout
299
+ )
300
+ operation_info["pop_btn_locator"] = pop_btn_locator
301
+ copy_feilds["operation_info"] = operation_info
302
+ sub_feilds.append("operation_info")
303
+ elif index == 15:
304
+ copy_feilds[
305
+ "substitute_btn_locator"
306
+ ] = await domestic_activity_order_po.get_flight_table_td_operation_substitute_btn(
307
+ locator=td_locator, timeout=timeout)
308
+ sub_feilds.append("substitute_btn_locator")
309
+ except (Exception,) as e:
310
+ logger.error(f"第<{index + 1}>列数据处理异常,原因:{e}")
311
+ if len(sub_feilds) == 22:
312
+ table_data.append(copy_feilds)
313
+ if datetime.strptime(
314
+ copy_feilds.get("last_time_ticket"), "%Y-%m-%d %H:%M:%S"
315
+ ) < datetime.now() + timedelta(minutes=last_minute_threshold):
316
+ pre_pop_orders.append(copy_feilds)
317
+ for pre_pop_order in pre_pop_orders:
318
+ order_id = pre_pop_order.get("order_id")
319
+ operation_info = pre_pop_order.get("operation_info")
320
+ pop_btn_locator = operation_info.get("pop_btn_locator")
321
+ last_time_ticket = pre_pop_order.get("last_time_ticket")
322
+ if pop_btn_locator and isinstance(pop_btn_locator, Locator):
323
+ minute = (datetime.strptime(last_time_ticket, "%Y-%m-%d %H:%M:%S") - datetime.now()).total_seconds() / 60
324
+ await pop_btn_locator.click()
325
+ if is_pop is False:
326
+ is_pop = True
327
+ logger.info(
328
+ f"订单<{order_id}>,距离最晚出票时限: {last_time_ticket},仅剩<{minute}>分钟,已将工单剔出活动订单"
329
+ )
330
+ return table_data, is_pop
@@ -9,111 +9,115 @@
9
9
  # Copyright ©2011-2025. Hunan xxxxxxx Company limited. All rights reserved.
10
10
  # ---------------------------------------------------------------------------------------------------------
11
11
  """
12
+ import os
12
13
  import asyncio
13
- from typing import Tuple
14
+ import traceback
15
+ from logging import Logger
16
+ from datetime import datetime
17
+ from typing import Dict, Any, Optional
14
18
  from qlv_helper.po.login_page import LoginPage
15
- from playwright.async_api import BrowserContext
16
- from qlv_helper.po.wechat_auth_page import WechatAuthPage
17
- from qlv_helper.utils.browser_utils import switch_for_table_window
18
- from qlv_helper.utils.po_utils import on_click_locator, locator_input_element
19
-
20
-
21
- async def _username_login(login_po: LoginPage, username: str, password: str, timeout: float = 5.0) -> Tuple[bool, str]:
22
- # 1. 输入用户名
23
- is_success, username_input = await login_po.get_login_username_input(timeout=timeout)
24
- if is_success is False:
25
- return is_success, username_input
26
- await locator_input_element(locator=username_input, text=username.strip())
27
-
28
- # 2. 输入密码
29
- is_success, password_input = await login_po.get_login_password_input(timeout=timeout)
30
- if is_success is False:
31
- return is_success, username_input
32
- await locator_input_element(locator=password_input, text=password.strip())
33
-
34
- # 3. 获取一层验证码
35
- is_success, code_str = await login_po.get_number_code(timeout=timeout)
36
- if is_success is False:
37
- return is_success, code_str
38
-
39
- # 4. 输入一层验证码
40
- is_success, code_input = await login_po.get_login_number_code_input(timeout=timeout)
41
- if is_success is False:
42
- return is_success, code_input
43
- await locator_input_element(locator=code_input, text=code_str.lower())
44
-
45
- # 5. 点击登录
46
- is_success, login_btn = await login_po.get_login_btn(timeout=timeout)
47
- if is_success is False:
48
- return is_success, login_btn
49
- await on_click_locator(locator=login_btn)
50
-
51
-
52
- async def _wechat_login(browser: BrowserContext, login_po: LoginPage, timeout: float = 5.0) -> Tuple[bool, str]:
53
- # 1. 点击微信登录快捷入口
54
- is_success, wechat_entrance = await login_po.get_wechat_entrance(timeout=timeout)
55
- if is_success is False:
56
- return is_success, wechat_entrance
57
- await on_click_locator(locator=wechat_entrance)
58
-
59
- page_new = await switch_for_table_window(browser=browser, url_keyword="open.weixin.qq.com", wait_time=int(timeout))
60
- wachat_po = WechatAuthPage(page=page_new)
61
-
62
- # 2. 点击【微信快捷登录】按钮
63
- is_success, wechat_quick_login_btn = await wachat_po.get_wechat_quick_login_btn(timeout=timeout)
64
- if is_success is False:
65
- return is_success, wechat_quick_login_btn
66
- await on_click_locator(locator=wechat_quick_login_btn)
67
-
68
- # 3. 点击微信弹框的中【允许】按钮
69
- return await wachat_po.on_click_allow_btn(timeout=int(timeout) * 3)
70
-
71
-
72
- async def username_login(
73
- login_po: LoginPage, username: str, password: str, timeout: float = 5.0, retry: int = 3
74
- ) -> Tuple[bool, str]:
75
- # 1. 第一次全流程的登录
76
- await _username_login(login_po=login_po, username=username, password=password, timeout=timeout)
77
- for _ in range(retry):
78
- # 2. 判断是否为当前页
79
- if login_po.is_current_page() is False:
80
- return True, f"账号:{username} 登录成功"
81
-
82
- # 3. 判断是否存在登录警告,存在的话,继续输入验证码,再次登录
83
- is_warn: bool = await login_po.is_exist_login_warn(timeout=timeout)
84
- if is_warn is True:
85
- # 4. 获取一层验证码
86
- is_success, code_str = await login_po.get_number_code(timeout=timeout)
87
- if is_success is False:
88
- return is_success, code_str
89
-
90
- # 5. 输入一层验证码
91
- is_success, code_input = await login_po.get_login_number_code_input(timeout=timeout)
92
- if is_success is False:
93
- return is_success, code_input
94
- await locator_input_element(locator=code_input, text=code_str.lower())
19
+ import qlv_helper.config.url_const as url_const
20
+ from playwright.async_api import Page, ElementHandle
21
+ from qlv_helper.utils.ocr_helper import get_image_text
22
+
23
+ async def _username_login(
24
+ *, login_po: LoginPage, logger: Logger, username: str, password: str, screenshot_dir: str, api_key: str,
25
+ secret_key: str, timeout: float = 5.0, attempt: int = 10
26
+ ) -> Optional[Dict[str, Any]]:
27
+ for index in range(1, attempt + 1):
28
+ try:
29
+ # 1. 输入用户名
30
+ username_input = await login_po.get_login_username_input(timeout=timeout)
31
+ await username_input.fill(value=username)
32
+ logger.info(f"登录页面,用户名<{username}>输入完成")
33
+ except (Exception,):
34
+ pass
35
+ try:
36
+ # 2. 输入密码
37
+ password_input = await login_po.get_login_password_input(timeout=timeout)
38
+ await password_input.fill(value=password)
39
+ logger.info(f"登录页面,用户密码<{password}>输入完成")
40
+ except (Exception,):
41
+ pass
42
+ try:
43
+ # 3. 首次获取验证码,并点击
44
+ # captcha_1 = await login_po.get_captcha(timeout=timeout)
45
+ # await captcha_1.click(button="left")
46
+ # await asyncio.sleep(delay=3)
47
+
48
+ # 4. 再次获取验证码
49
+ captcha_2 = await login_po.get_captcha(timeout=timeout)
50
+ # 4.1 获取验证码类型
51
+ captcha_type: int = await login_po.get_captcha_type(locator=captcha_2, timeout=timeout)
52
+ logger.info(f"登录页面,验证码类型<{captcha_type}>获取成功")
53
+ # 4.2 获取验证码图片,直接截图获取原始图片字节,不刷新图片
54
+ image: ElementHandle = await login_po.get_captcha_image(timeout=timeout)
55
+ dt_str: str = datetime.now().strftime("%Y%m%d%H%M%S")
56
+ fn: str = os.path.join(screenshot_dir, f"captcha_{username}_{captcha_type}_{dt_str}.png")
57
+ await image.screenshot(path=fn, timeout=timeout * 1000)
58
+ logger.info(f"登录页面,验证码图片已经生成,图片路径:{fn}")
59
+ # 4.3 获取验证码内容
60
+ capthcha_text = await get_image_text(
61
+ image_path=fn, captcha_type=captcha_type, api_key=api_key, secret_key=secret_key
62
+ )
63
+ logger.info(f"登录页面,验证码内容:<{capthcha_text}>识别成功")
64
+
65
+ # 5. 获取验证码输入框
66
+ captcha_input = await login_po.get_login_captcha_input(timeout=timeout)
67
+ await captcha_input.fill(value=capthcha_text)
68
+ logger.info(f"登录页面,验证码<{capthcha_text}>输入完成")
95
69
 
96
70
  # 6. 点击登录
97
- is_success, login_btn = await login_po.get_login_btn(timeout=timeout)
98
- if is_success is False:
99
- return is_success, login_btn
100
- await on_click_locator(locator=login_btn)
101
- else:
102
- # 7. 重复一次全流程的登录
103
- await _username_login(login_po=login_po, username=username, password=password, timeout=timeout)
104
-
105
- await asyncio.sleep(delay=timeout)
106
-
107
- return True, f"账号:{username} 一次登录流程结束"
71
+ login_btn = await login_po.get_login_btn(timeout=timeout)
72
+ await login_btn.click(button="left")
73
+ logger.info(f"登录页面,【登录】按钮点击完成")
74
+ await asyncio.sleep(delay=3)
75
+
76
+ # 7. 验证登录是否成功
77
+ result = login_po.is_current_page()
78
+ if result is False:
79
+ logger.info(f"用户<{username}>登录成功,登录流程结束")
80
+
81
+ # 9. 获取当前cookie,不指定 path,Playwright 会返回 JSON 字符串
82
+ return await login_po.get_page().context.storage_state()
83
+ else:
84
+ raise RuntimeError("登录失败")
85
+ except (RuntimeError,):
86
+ if index == attempt:
87
+ logger.error(f"尝试登录<{attempt}>次,均失败,登录结束")
88
+ else:
89
+ logger.error(f"第<{index}>次登录失败,等待下一次登录")
90
+ except (Exception,):
91
+ logger.error(traceback.format_exc())
92
+ if index == attempt:
93
+ logger.error(f"尝试登录<{attempt}>次,均失败,登录结束")
94
+ else:
95
+ logger.error(f"第<{index}>次登录失败,等待下一次登录")
96
+
97
+
98
+ async def open_login_page(
99
+ *, page: Page, logger: Logger, qlv_protocol: str, qlv_domain: str, timeout: float = 60.0
100
+ ) -> LoginPage:
101
+ url_prefix = f"{qlv_protocol}://{qlv_domain}"
102
+ login_url = url_prefix + url_const.login_url
103
+ await page.goto(login_url)
104
+
105
+ login_po = LoginPage(page=page, url=login_url)
106
+ await login_po.url_wait_for(url=login_url, timeout=timeout)
107
+ logger.info(f"即将进入登录页,页面URL<{login_url}>")
108
+ return login_po
108
109
 
109
110
 
110
- async def wechat_login(
111
- browser: BrowserContext, login_po: LoginPage, timeout: float = 5.0, retry: int = 3
112
- ) -> Tuple[bool, str]:
113
- for index in range(retry):
114
- # 全流程的登录
115
- is_success, message = await _wechat_login(browser=browser, login_po=login_po, timeout=timeout)
116
-
117
- # 判断是否为当前页
118
- if is_success is True or index == retry - 1:
119
- return is_success, message
111
+ async def username_login(
112
+ *, page: Page, logger: Logger, qlv_protocol: str, qlv_domain: str, username: str, screenshot_dir: str,
113
+ password: str, api_key: str, secret_key: str, timeout: float = 60.0, attempt: int = 10, **kwargs: Any
114
+ ) -> Dict[str, Any]:
115
+ # 1. 打开登录页面
116
+ login_po = await open_login_page(
117
+ page=page, logger=logger, qlv_domain=qlv_domain, qlv_protocol=qlv_protocol, timeout=timeout
118
+ )
119
+ # 2. 一次全流程的登录
120
+ return await _username_login(
121
+ login_po=login_po, logger=logger, username=username, password=password, screenshot_dir=screenshot_dir,
122
+ timeout=timeout, api_key=api_key, secret_key=secret_key, attempt=attempt
123
+ )
@@ -0,0 +1,50 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ # ---------------------------------------------------------------------------------------------------------
4
+ # ProjectName: qlv-helper
5
+ # FileName: wechat_login.py
6
+ # Description: 微信登录模块
7
+ # Author: ASUS
8
+ # CreateDate: 2025/12/31
9
+ # Copyright ©2011-2025. Hunan xxxxxxx Company limited. All rights reserved.
10
+ # ---------------------------------------------------------------------------------------------------------
11
+ """
12
+ from typing import Tuple
13
+ from qlv_helper.po.login_page import LoginPage
14
+ from playwright.async_api import BrowserContext
15
+
16
+ from qlv_helper.utils.po_utils import on_click_locator
17
+ from qlv_helper.po.wechat_auth_page import WechatAuthPage
18
+ from qlv_helper.utils.browser_utils import switch_for_table_window
19
+
20
+
21
+ async def _wechat_login(browser: BrowserContext, login_po: LoginPage, timeout: float = 5.0) -> Tuple[bool, str]:
22
+ # 1. 点击微信登录快捷入口
23
+ is_success, wechat_entrance = await login_po.get_wechat_entrance(timeout=timeout)
24
+ if is_success is False:
25
+ return is_success, wechat_entrance
26
+ await on_click_locator(locator=wechat_entrance)
27
+
28
+ page_new = await switch_for_table_window(browser=browser, url_keyword="open.weixin.qq.com", wait_time=int(timeout))
29
+ wachat_po = WechatAuthPage(page=page_new)
30
+
31
+ # 2. 点击【微信快捷登录】按钮
32
+ is_success, wechat_quick_login_btn = await wachat_po.get_wechat_quick_login_btn(timeout=timeout)
33
+ if is_success is False:
34
+ return is_success, wechat_quick_login_btn
35
+ await on_click_locator(locator=wechat_quick_login_btn)
36
+
37
+ # 3. 点击微信弹框的中【允许】按钮
38
+ return await wachat_po.on_click_allow_btn(timeout=int(timeout) * 3)
39
+
40
+
41
+ async def wechat_login(
42
+ *, browser: BrowserContext, login_po: LoginPage, timeout: float = 5.0, retry: int = 3
43
+ ) -> Tuple[bool, str]:
44
+ for index in range(1, retry + 1):
45
+ # 全流程的登录
46
+ is_success, message = await _wechat_login(browser=browser, login_po=login_po, timeout=timeout)
47
+
48
+ # 判断是否为当前页
49
+ if is_success is True or index == retry:
50
+ return is_success, message
@@ -20,6 +20,7 @@ from typing import Dict, Any, Optional, List
20
20
  from qlv_helper.utils.type_utils import convert_cn_to_en
21
21
  from http_helper.client.async_proxy import HttpClientFactory
22
22
  from qlv_helper.utils.datetime_utils import get_current_dtstr
23
+ from flight_helper.models.dto.procurement import FillProcurementInputDTO
23
24
  from qlv_helper.utils.type_utils import get_key_by_index, get_value_by_index, safe_convert_advanced
24
25
 
25
26
 
@@ -51,8 +52,7 @@ async def fill_procurement_info_with_http(
51
52
  ) -> Dict[str, Any]:
52
53
  client = HttpClientFactory(
53
54
  protocol=qlv_protocol, domain=qlv_domain, timeout=timeout, enable_log=enable_log, retry=retry,
54
- cookie_jar=cookie_jar or aiohttp.CookieJar(),
55
- playwright_state=playwright_state
55
+ cookie_jar=cookie_jar or aiohttp.CookieJar(), playwright_state=playwright_state
56
56
  )
57
57
 
58
58
  headers = {
@@ -84,6 +84,81 @@ async def fill_procurement_info_with_http(
84
84
  )
85
85
 
86
86
 
87
+ async def fill_procurement_dto_with_http(
88
+ *, fill_procurement_dto: FillProcurementInputDTO, retry: int = 1, timeout: int = 5, enable_log: bool = True,
89
+ cookie_jar: Optional[aiohttp.CookieJar] = None, playwright_state: Dict[str, Any] = None,
90
+ data_list: Optional[List[Dict[str, Any]]] = None
91
+ ) -> Dict[str, Any]:
92
+ client = HttpClientFactory(
93
+ protocol=fill_procurement_dto.pl_protocol, domain=fill_procurement_dto.pl_domain, timeout=timeout, retry=retry,
94
+ enable_log=enable_log, cookie_jar=cookie_jar or aiohttp.CookieJar(), playwright_state=playwright_state
95
+ )
96
+
97
+ headers = {
98
+ "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
99
+ "Referer": f"{fill_procurement_dto.pl_protocol}://{fill_procurement_dto.pl_domain}/OrderProcessing/NewTicket/{fill_procurement_dto.order_no}?&r={datetime.now().strftime("%Y%m%d%H%M%S")}",
100
+ }
101
+ if data_list:
102
+ data = data_list
103
+ else:
104
+ pName = "," + ",".join(
105
+ fill_procurement_dto.passenger_names) + "," if fill_procurement_dto.passenger_names else ''
106
+ pids = ",".join(fill_procurement_dto.passenger_ids) if fill_procurement_dto.passenger_ids else ''
107
+ data = [{
108
+ "tradingDat": datetime.now().strftime("%Y-%m-%d %H:%M"),
109
+ "outTktPF": f"{fill_procurement_dto.out_ticket_platform or ''}", "outTktLoginCode": "",
110
+ "typeName": f"{fill_procurement_dto.type_name or ''}",
111
+ "accountID": f"{fill_procurement_dto.purchase_account_id or ''}",
112
+ "accountName": f"{fill_procurement_dto.purchase_account or ''}",
113
+ "transactionAmount": f"{fill_procurement_dto.transaction_amount}",
114
+ "mainCheckNumber": "",
115
+ "airCoOrderID": f"{fill_procurement_dto.air_co_order_id}", "QuotaResultAmount": "0.00",
116
+ "remark": f"{quote(fill_procurement_dto.remark) or ''}",
117
+ "flightIdx": f",{fill_procurement_dto.segment_index or '1'},", "pName": f"{pName}",
118
+ "orderID": f"{fill_procurement_dto.order_no}",
119
+ "businessTypeName": "机票", "tradingItems": "机票支出", "actualAmount": 0,
120
+ "pType": f"{fill_procurement_dto.passenger_type}",
121
+ "fids": f"{fill_procurement_dto.flight_ids or ''}",
122
+ "pids": f"{pids or ''}",
123
+ "iscandel": "true", "isbatch": "false",
124
+ "MainCheckNumberValus": f"{fill_procurement_dto.pay_transaction}",
125
+ "OfficeNo": "", "PriceStdActual": "0.00", "ReturnAmount": "0.0000", "OffsetReturnAmount": "0.00",
126
+ "profitRemark": "", "preSaleType": "", "ErrorType": "",
127
+ "OutTktPFTypeID": f"{fill_procurement_dto.out_ticket_platform_type_id or ''}",
128
+ "OutTicketAccount": f"{fill_procurement_dto.out_ticket_account or ''}",
129
+ "OutTicketAccountID": f"{fill_procurement_dto.out_ticket_account_id or ''}",
130
+ "OutTicketPWD": f"{fill_procurement_dto.out_ticket_account_password or ''}",
131
+ "OutTicketTel": f"{fill_procurement_dto.out_ticket_mobile or ''}",
132
+ "OutTicketPNR": ""}
133
+ ]
134
+ data = f"list={json.dumps(data)}&isPayAll=true&delTransactionids=&OutTicketLossType&OutTicketLossRemark="
135
+ return await client.request(
136
+ method="POST", url="/OrderProcessing/PurchaseInfoSave",
137
+ headers=headers, is_end=True, data=data.encode("utf-8")
138
+ )
139
+
140
+
141
+ async def fill_itinerary_info_with_http(
142
+ *, order_id: int, qlv_domain: str, pid: str, tid: str, transaction_id: str, itinerary_id: str, retry: int = 1,
143
+ qlv_protocol: str = "http", timeout: int = 5, enable_log: bool = True,
144
+ cookie_jar: Optional[aiohttp.CookieJar] = None, playwright_state: Dict[str, Any] = None
145
+ ) -> Dict[str, Any]:
146
+ client = HttpClientFactory(
147
+ protocol=qlv_protocol, domain=qlv_domain, timeout=timeout, enable_log=enable_log, retry=retry,
148
+ cookie_jar=cookie_jar or aiohttp.CookieJar(), playwright_state=playwright_state
149
+ )
150
+
151
+ headers = {
152
+ "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
153
+ "Referer": f"{qlv_protocol}://{qlv_domain}/OrderProcessing/NewTicket_show/{order_id}?&r={datetime.now().strftime("%Y%m%d%H%M%S")}",
154
+ }
155
+ data = f"OrderID={order_id}&OrderPID={pid}&OrderTID={tid}&TicketNo={itinerary_id}&ZJTransactionID={transaction_id}"
156
+ return await client.request(
157
+ method="POST", url="/OrderProcessing/TicketNoSave",
158
+ headers=headers, is_end=True, data=data.encode("utf-8")
159
+ )
160
+
161
+
87
162
  def order_info_static_headers() -> OrderedDict[str, str]:
88
163
  return OrderedDict([
89
164
  ("receipted_ota", "OTA实收"), # 0
@@ -199,6 +274,7 @@ def flight_extend_headers() -> OrderedDict[str, str]:
199
274
  ("code_arr", " 抵达机场"), # 11
200
275
  ("pid", "乘客ID"), # 12
201
276
  ("fid", "航段ID"), # 13
277
+ ("tid", "乘客表ID"), # 14
202
278
  ])
203
279
 
204
280
 
@@ -350,7 +426,10 @@ def parse_order_flight_table_row(
350
426
  ) -> Dict[str, Any]:
351
427
  """解析航班表每一行的数据"""
352
428
  tds = tr.find_all("td", recursive=False)
353
- values = {get_key_by_index(index=12, ordered_dict=extend_headers): tr["pid"]}
429
+ values = {
430
+ get_key_by_index(index=12, ordered_dict=extend_headers): tr["pid"],
431
+ get_key_by_index(index=14, ordered_dict=extend_headers): tr["tid"]
432
+ }
354
433
  for idx, td in enumerate(tds):
355
434
  if idx >= len(headers):
356
435
  continue
@@ -23,6 +23,96 @@ class DomesticActivityOrderPage(BasePo):
23
23
  super().__init__(page, url)
24
24
  self.__page = page
25
25
 
26
+ async def get_flight_table_locator(self, timeout: float = 5.0) -> Locator:
27
+ """
28
+ 获取table
29
+ :param timeout:
30
+ :return:
31
+ """
32
+ selecor: str = 'xpath=//table[@class="table table_hover table_border table_center"]//tbody'
33
+ return await self.get_locator(selector=selecor, timeout=timeout)
34
+
35
+ async def get_flight_table_trs_locator(self, timeout: float = 5.0) -> Locator:
36
+ """
37
+ 获取table所有tr locator对象
38
+ :param timeout:
39
+ :return:
40
+ """
41
+ selecor: str = 'xpath=//table[@class="table table_hover table_border table_center"]/tbody/tr'
42
+ return await self.get_locator(selector=selecor, timeout=timeout)
43
+
44
+ async def get_flight_table_tds_th(self, locator: Locator, timeout: float = 5.0) -> Locator:
45
+ """
46
+ 获取table所有tr下的th locator对象
47
+ :param locator:
48
+ :param timeout:
49
+ :return:
50
+ """
51
+ selecor: str = 'xpath=./th'
52
+ return await self.get_sub_locator(locator=locator, selector=selecor, timeout=timeout)
53
+
54
+ async def get_flight_table_trs_td(self, locator: Locator, timeout: float = 5.0) -> Locator:
55
+ """
56
+ 获取table所有tr下的td locator对象
57
+ :param locator:
58
+ :param timeout:
59
+ :return:
60
+ """
61
+ selecor: str = 'xpath=./td'
62
+ return await self.get_sub_locator(locator=locator, selector=selecor, timeout=timeout)
63
+
64
+ async def get_flight_table_td_order_id(self, locator: Locator, timeout: float = 5.0) -> str:
65
+ """
66
+ 获取table 行中的订单id
67
+ :param locator:
68
+ :param timeout:
69
+ :return:
70
+ """
71
+ selecor: str = 'xpath=./a'
72
+ locator = await self.get_sub_locator(locator=locator, selector=selecor, timeout=timeout)
73
+ return (await locator.inner_text()).strip()
74
+
75
+ async def get_flight_table_td_urgant(self, locator: Locator, timeout: float = 5.0) -> str:
76
+ """
77
+ 获取table 行中的紧急状态
78
+ :param locator:
79
+ :param timeout:
80
+ :return:
81
+ """
82
+ selecor: str = 'xpath=./font'
83
+ locator = await self.get_sub_locator(locator=locator, selector=selecor, timeout=timeout)
84
+ return (await locator.inner_text()).strip()
85
+
86
+ async def get_flight_table_td_operation_lock_btn(self, locator: Locator, timeout: float = 5.0) -> Locator:
87
+ """
88
+ 获取table 行中的锁单按钮
89
+ :param locator:
90
+ :param timeout:
91
+ :return:
92
+ """
93
+ selecor: str = 'xpath=./a'
94
+ return await self.get_sub_locator(locator=locator, selector=selecor, timeout=timeout)
95
+
96
+ async def get_flight_table_td_operation_pop_btn(self, locator: Locator, timeout: float = 5.0) -> Locator:
97
+ """
98
+ 获取table 行中的剔出按钮
99
+ :param locator:
100
+ :param timeout:
101
+ :return:
102
+ """
103
+ selecor: str = 'xpath=./button'
104
+ return await self.get_sub_locator(locator=locator, selector=selecor, timeout=timeout)
105
+
106
+ async def get_flight_table_td_operation_substitute_btn(self, locator: Locator, timeout: float = 5.0) -> Locator:
107
+ """
108
+ 获取table 行中的补位按钮
109
+ :param locator:
110
+ :param timeout:
111
+ :return:
112
+ """
113
+ selecor: str = 'xpath=./a'
114
+ return await self.get_sub_locator(locator=locator, selector=selecor, timeout=timeout)
115
+
26
116
  async def get_flight_table(self, timeout: float = 5.0) -> Tuple[bool, Union[Locator, str]]:
27
117
  try:
28
118
  locator = self.__page.locator(self.__table_selector)
@@ -84,6 +174,7 @@ class DomesticActivityOrderPage(BasePo):
84
174
  3. 主流程:分页 + 每页解析 tbody
85
175
  ------------------------------------------------------------
86
176
  """
177
+
87
178
  async def parse_table_with_pagination(self, refresh_wait_time: float = 10.0) -> List[Dict[str, Any]]:
88
179
  """
89
180
  refresh_wait_time: 翻页后等待时间
@@ -11,8 +11,7 @@
11
11
  """
12
12
  from typing import Tuple, Union
13
13
  from playwright_helper.libs.base_po import BasePo
14
- from qlv_helper.utils.ocr_helper import get_image_text
15
- from playwright.async_api import Page, TimeoutError as PlaywrightTimeoutError, Locator
14
+ from playwright.async_api import Page, TimeoutError as PlaywrightTimeoutError, Locator, ElementHandle
16
15
 
17
16
 
18
17
  class LoginPage(BasePo):
@@ -22,104 +21,67 @@ class LoginPage(BasePo):
22
21
  super().__init__(page, url)
23
22
  self.__page = page
24
23
 
25
- async def get_login_username_input(self, timeout: float = 5.0) -> Tuple[bool, Union[Locator, str]]:
24
+ async def get_login_username_input(self, timeout: float = 5.0) -> Locator:
26
25
  """
27
26
  获取登录页面的用户名输入框
28
27
  :param timeout: 超时时间(秒)
29
28
  :return: (是否存在, 错误信息|元素对象)
30
- :return:
31
29
  """
32
30
  selector: str = '//input[@id="UserName"]'
33
- try:
34
- locator = self.__page.locator(selector)
35
- if locator:
36
- await locator.wait_for(state='visible', timeout=timeout * 1000)
37
- return True, locator
38
- else:
39
- return False, '没有找到登录页面中的【用户名】输入框'
40
- except PlaywrightTimeoutError:
41
- return False, f"元素 '{selector}' 未在 {timeout} 秒内找到"
42
- except Exception as e:
43
- return False, f"检查元素时发生错误: {str(e)}"
31
+ return await self.get_locator(selector=selector, timeout=timeout)
44
32
 
45
- async def get_login_password_input(self, timeout: float = 5.0) -> Tuple[bool, Union[Locator, str]]:
33
+ async def get_login_password_input(self, timeout: float = 5.0) -> Locator:
46
34
  """
47
35
  获取登录页面的密码输入框
48
36
  :param timeout: 超时时间(秒)
49
37
  :return: (是否存在, 错误信息|元素对象)
50
- :return:
51
38
  """
52
39
  selector: str = '//input[@id="Password"]'
53
- try:
54
- locator = self.__page.locator(selector)
55
- if locator:
56
- await locator.wait_for(state='visible', timeout=timeout * 1000)
57
- return True, locator
58
- else:
59
- return False, '没有找到登录页面中的【密码】输入框'
60
- except PlaywrightTimeoutError:
61
- return False, f"元素 '{selector}' 未在 {timeout} 秒内找到"
62
- except Exception as e:
63
- return False, f"检查元素时发生错误: {str(e)}"
40
+ return await self.get_locator(selector=selector, timeout=timeout)
64
41
 
65
- async def get_login_number_code_input(self, timeout: float = 5.0) -> Tuple[bool, Union[Locator, str]]:
42
+ async def get_captcha(self, timeout: float = 5.0) -> Locator:
66
43
  """
67
- 获取登录页面的数字验证码输入框,第一层验证码
44
+ 获取验证码Locator对象
68
45
  :param timeout: 超时时间(秒)
69
46
  :return: (是否存在, 错误信息|元素对象)
70
- :return:
71
47
  """
72
- selector: str = '//input[@id="Code"]'
73
- try:
74
- locator = self.__page.locator(selector)
75
- if locator:
76
- await locator.wait_for(state='visible', timeout=timeout * 1000)
77
- return True, locator
78
- else:
79
- return False, '没有找到登录页面中的【数字验证码】输入框'
80
- except PlaywrightTimeoutError:
81
- return False, f"元素 '{selector}' 未在 {timeout} 秒内找到"
82
- except Exception as e:
83
- return False, f"检查元素时发生错误: {str(e)}"
48
+ selector: str = 'xpath=//div[@class="form_row"]/img[@onclick="this.src=this.src+\'?\'"]'
49
+ return await self.get_locator(selector=selector, timeout=timeout)
84
50
 
85
- async def get_login_btn(self, timeout: float = 5.0) -> Tuple[bool, Union[Locator, str]]:
51
+ @staticmethod
52
+ async def get_captcha_type(locator: Locator, timeout: float = 5.0) -> int:
86
53
  """
87
- 获取登录页面的登录按钮
54
+ 获取验证码的类型
55
+ :param locator: 验证码Locator对象
88
56
  :param timeout: 超时时间(秒)
89
57
  :return: (是否存在, 错误信息|元素对象)
90
- :return:
91
58
  """
92
- selector: str = '//input[@class="login-btn"]'
93
- try:
94
- locator = self.__page.locator(selector)
95
- if locator:
96
- await locator.wait_for(state='visible', timeout=timeout * 1000)
97
- return True, locator
98
- else:
99
- return False, '没有找到登录页面中的【登录】按钮'
100
- except PlaywrightTimeoutError:
101
- return False, f"元素 '{selector}' 未在 {timeout} 秒内找到"
102
- except Exception as e:
103
- return False, f"检查元素时发生错误: {str(e)}"
59
+ img_src: str = await locator.get_attribute(name="src", timeout=timeout)
60
+ img_src_slice = img_src.split("=")
61
+ return int(img_src_slice[-1][0])
104
62
 
105
- async def get_number_code(self, timeout: float = 5.0) -> Tuple[bool, str]:
63
+ async def get_captcha_image(self, timeout: float = 5.0) -> ElementHandle:
106
64
  selector: str = '//div[@id="signup_forms"]//img'
107
- return await get_image_text(page=self.__page, selector=selector, timeout=timeout)
65
+ locator: Locator = await self.get_locator(selector=selector, timeout=timeout)
66
+ return await locator.element_handle(timeout=timeout * 1000)
108
67
 
109
- async def is_exist_login_warn(self, timeout: float = 5.0) -> bool:
110
- selector: str = '//p[@class="login_warn"]'
111
- try:
112
- locator = self.__page.locator(selector)
113
- if locator:
114
- text: str = await locator.text_content(timeout=timeout * 1000)
115
- if text.strip() != "":
116
- return True
117
- else:
118
- return False
119
- else:
120
- return False
121
- except (PlaywrightTimeoutError, Exception):
122
- return False
68
+ async def get_login_captcha_input(self, timeout: float = 5.0) -> Locator:
69
+ """
70
+ 获取登录页面的验证码输入框
71
+ :param timeout: 超时时间(秒)
72
+ :return: (是否存在, 错误信息|元素对象)
73
+ """
74
+ selector: str = '//input[@id="Code"]'
75
+ return await self.get_locator(selector=selector, timeout=timeout)
76
+
77
+ async def get_login_btn(self, timeout: float = 5.0) -> Locator:
78
+ """
79
+ 获取登录页面的登录按钮
80
+ :param timeout: 超时时间(秒)
81
+ :return: (是否存在, 错误信息|元素对象)
82
+ """
83
+ selector: str = '//input[@class="login-btn"]'
84
+ return await self.get_locator(selector=selector, timeout=timeout)
123
85
 
124
86
  async def get_wechat_entrance(self, timeout: float = 5.0) -> Tuple[bool, Union[Locator, str]]:
125
87
  selector: str = '//img[@src="/images/weixin.png"]'
@@ -9,9 +9,8 @@
9
9
  # Copyright ©2011-2025. Hunan xxxxxxx Company limited. All rights reserved.
10
10
  # ---------------------------------------------------------------------------------------------------------
11
11
  """
12
- from typing import Tuple, Union
12
+ from playwright.async_api import Page, Locator
13
13
  from playwright_helper.libs.base_po import BasePo
14
- from playwright.async_api import Page, TimeoutError as PlaywrightTimeoutError, Locator
15
14
 
16
15
 
17
16
  class MainPage(BasePo):
@@ -23,48 +22,18 @@ class MainPage(BasePo):
23
22
  self.url = url
24
23
  self.__page = page
25
24
 
26
- async def get_confirm_btn_with_system_notice_dialog(self, timeout: float = 5.0) -> Tuple[bool, Union[Locator, str]]:
25
+ async def get_confirm_btn_with_system_notice_dialog(self, timeout: float = 5.0) -> Locator:
27
26
  """
28
27
  获取系统通知弹框中的确认按钮,注意这个地方,存在多个叠加的弹框,因此用last()方法,只需定位到最上面的那个弹框就行
29
28
  :return:
30
29
  """
31
30
  selector: str = "//div[@class='CommonAlert'][last()]//a[@class='CommonAlertBtnConfirm']"
32
- try:
33
- locator = self.__page.locator(selector)
34
- if locator:
35
- await locator.wait_for(state='visible', timeout=timeout * 1000)
36
- return True, locator
37
- else:
38
- return False, '没有找到首页中的【系统提醒-确定】按钮'
39
- except PlaywrightTimeoutError:
40
- return False, f"元素 '{selector}' 未在 {timeout} 秒内找到"
41
- except Exception as e:
42
- return False, f"检查元素时发生错误: {str(e)}"
31
+ return await self.get_locator(selector=selector, timeout=timeout)
43
32
 
44
- async def get_level1_menu_order_checkout(self, timeout: float = 5.0) -> Tuple[bool, Union[Locator, str]]:
33
+ async def get_level1_menu_order_checkout(self, timeout: float = 5.0) -> Locator:
45
34
  selector: str = "//span[contains(normalize-space(), '订单出票')]"
46
- try:
47
- locator = self.__page.locator(selector)
48
- if locator:
49
- await locator.wait_for(state='visible', timeout=timeout * 1000)
50
- return True, locator
51
- else:
52
- return False, '没有找到首页中的【订单出票】左侧一级导航菜单'
53
- except PlaywrightTimeoutError:
54
- return False, f"元素 '{selector}' 未在 {timeout} 秒内找到"
55
- except Exception as e:
56
- return False, f"检查元素时发生错误: {str(e)}"
35
+ return await self.get_locator(selector=selector, timeout=timeout)
57
36
 
58
- async def get_level2_menu_order_checkout(self, timeout: float = 5.0) -> Tuple[bool, Union[Locator, str]]:
37
+ async def get_level2_menu_order_checkout(self, timeout: float = 5.0) -> Locator:
59
38
  selector: str = "//a[@menuname='国内活动订单']"
60
- try:
61
- locator = self.__page.locator(selector)
62
- if locator:
63
- await locator.wait_for(state='visible', timeout=timeout * 1000)
64
- return True, locator
65
- else:
66
- return False, '没有找到首页中的【国内活动订单】左侧二级导航菜单'
67
- except PlaywrightTimeoutError:
68
- return False, f"元素 '{selector}' 未在 {timeout} 秒内找到"
69
- except Exception as e:
70
- return False, f"检查元素时发生错误: {str(e)}"
39
+ return await self.get_locator(selector=selector, timeout=timeout)
@@ -273,3 +273,20 @@ class OrderDetailPage(BasePo):
273
273
  """
274
274
  selector: str = '//legend//input[@name="PolicyName"]'
275
275
  return await self.get_locator(selector=selector, timeout=timeout)
276
+
277
+ async def get_purchase_info_transaction_id(self, timeout: float = 5.0) -> List[str]:
278
+ """
279
+ 获取订单详情页面的采购信息流水
280
+ :param timeout:
281
+ :return:
282
+ """
283
+ # selector: str = '//tr[@class="PurchaseInfoClass"]'
284
+ selector: str = '//table[@id="PurchaseInfos"]/tbody/tr'
285
+ loc: Locator = await self.get_locator(selector=selector, timeout=timeout)
286
+ locators = await loc.all()
287
+ transaction_ids = list()
288
+ for locator in locators:
289
+ transaction_id = await locator.get_attribute("transactionid")
290
+ if transaction_id:
291
+ transaction_ids.append(transaction_id.strip())
292
+ return transaction_ids
@@ -9,11 +9,13 @@
9
9
  # Copyright ©2011-2025. Hunan xxxxxxx Company limited. All rights reserved.
10
10
  # ---------------------------------------------------------------------------------------------------------
11
11
  """
12
+ import json
13
+ import asyncio
12
14
  import ddddocr
13
15
  import requests
14
- from typing import Union, Tuple
16
+ from typing import Tuple
15
17
  from aiohttp import ClientSession
16
- from playwright.async_api import Page, TimeoutError as PlaywrightTimeoutError
18
+ from ocr_helper.core.baidu import ImageContentOCR
17
19
 
18
20
  # 复用 OCR 实例,不用每次都重新加载模型(更快)
19
21
  _ocr = ddddocr.DdddOcr(show_ad=False)
@@ -39,45 +41,38 @@ async def async_fetch_and_ocr_captcha(url: str) -> Tuple[str, bytes]:
39
41
  return result, img_bytes
40
42
 
41
43
 
42
- def recognize_captcha(image: Union[str, bytes]) -> str:
43
- """
44
- 识别验证码图片,返回识别文本。
45
- 参数:
46
- image: 图片路径 str,或图片的二进制 bytes
47
- 返回:
48
- 识别出的验证码字符串
49
- """
50
- try:
51
- # 如果是路径,读取文件
52
- if isinstance(image, str):
53
- with open(image, "rb") as f:
54
- img_bytes = f.read()
44
+ async def get_image_text(image_path: str, captcha_type: int, api_key, secret_key: str) -> str:
45
+ if captcha_type == 0:
46
+ with open(image_path, "rb") as f:
47
+ img_bytes = f.read()
48
+ for _ in range(100):
49
+ text = _ocr.classification(img_bytes).strip()
50
+ if len(text) == 4:
51
+ return text
52
+ raise RuntimeError("ddddocr识别验证码失败")
53
+ else:
54
+ api = ImageContentOCR(api_key=api_key, secret_key=secret_key)
55
+ response = await api.get_access_token(is_end=False)
56
+ if not response.get("access_token"):
57
+ raise RuntimeError(f"获取百度API的认证Token失败,原因:{response}")
58
+ token = response.get("access_token")
59
+ response = await api.submit_request(
60
+ question='图片中的文字是什么,如果含有运算信息,请将运算结果返回。注意给我返回一个json格式数据包,例如:{"content":"xxxx", result: xxx}, 如果无运算信息,设置为空串就行',
61
+ image_path=image_path,
62
+ token=token,
63
+ is_end=False
64
+ )
65
+ task_id: str = response.get("result", dict()).get("task_id")
66
+ if not task_id:
67
+ raise RuntimeError(f"提交图片至百度API接口失败,原因:{response}")
68
+ await asyncio.sleep(delay=10)
69
+ response = await api.get_result(task_id=task_id, token=token, is_end=True)
70
+ if response.get("result").get("ret_code") == 0 and response.get("result").get("ret_msg") == "success":
71
+ description = response.get("result").get("description")
72
+ description = json.loads(description[description.find("{"):description.find("}") + 1])
73
+ if description.get("result"):
74
+ return str(description.get("result")).strip()
75
+ else:
76
+ return description.get("content").strip()
55
77
  else:
56
- img_bytes = image
57
-
58
- result = _ocr.classification(img_bytes)
59
- return result
60
-
61
- except Exception as e:
62
- raise RuntimeError(f"OCR 识别失败: {e}")
63
-
64
-
65
- async def get_image_text(page: Page, selector: str, timeout: float = 5.0) -> Tuple[bool, str]:
66
- try:
67
- # 找到 img
68
- locator = page.locator(selector)
69
- if locator:
70
- img = await locator.element_handle(timeout=timeout * 1000)
71
-
72
- # 直接截图获取原始图片字节,不刷新图片
73
- img_bytes = await img.screenshot(timeout=timeout * 1000)
74
-
75
- # OCR 识别
76
- text = _ocr.classification(img_bytes)
77
- return True, text.strip()
78
- else:
79
- return False, f'没有找到当前页面中的【{selector}】图片'
80
- except PlaywrightTimeoutError:
81
- return False, f"元素 '{selector}' 未在 {timeout} 秒内找到"
82
- except Exception as e:
83
- return False, f"检查元素时发生错误: {str(e)}"
78
+ raise RuntimeError(f"调用百度API,获取图片识别结果失败,原因{response}")