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.
- {python_qlv_helper-0.6.0.dist-info → python_qlv_helper-0.7.2.dist-info}/METADATA +8 -6
- {python_qlv_helper-0.6.0.dist-info → python_qlv_helper-0.7.2.dist-info}/RECORD +17 -16
- qlv_helper/config/url_const.py +2 -0
- qlv_helper/controller/main_page.py +18 -2
- qlv_helper/controller/order_detail.py +55 -2
- qlv_helper/controller/order_table.py +173 -4
- qlv_helper/controller/user_login.py +106 -102
- qlv_helper/controller/wechat_login.py +50 -0
- qlv_helper/http/order_page.py +82 -3
- qlv_helper/po/domestic_activity_order_page.py +91 -0
- qlv_helper/po/login_page.py +36 -74
- qlv_helper/po/main_page.py +7 -38
- qlv_helper/po/order_detail_page.py +17 -0
- qlv_helper/utils/ocr_helper.py +38 -43
- {python_qlv_helper-0.6.0.dist-info → python_qlv_helper-0.7.2.dist-info}/WHEEL +0 -0
- {python_qlv_helper-0.6.0.dist-info → python_qlv_helper-0.7.2.dist-info}/licenses/LICENSE +0 -0
- {python_qlv_helper-0.6.0.dist-info → python_qlv_helper-0.7.2.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python_qlv_helper
|
|
3
|
-
Version: 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"
|
|
214
|
-
Requires-Dist: playwright-stealth==2.0.0; python_version >= "3.12"
|
|
215
|
-
Requires-Dist: ddddocr==1.5.6; python_version >= "3.12"
|
|
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"
|
|
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.
|
|
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.
|
|
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=
|
|
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=
|
|
9
|
-
qlv_helper/controller/order_detail.py,sha256=
|
|
10
|
-
qlv_helper/controller/order_table.py,sha256=
|
|
11
|
-
qlv_helper/controller/user_login.py,sha256=
|
|
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=
|
|
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=
|
|
18
|
-
qlv_helper/po/login_page.py,sha256=
|
|
19
|
-
qlv_helper/po/main_page.py,sha256=
|
|
20
|
-
qlv_helper/po/order_detail_page.py,sha256=
|
|
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=
|
|
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.
|
|
33
|
-
python_qlv_helper-0.
|
|
34
|
-
python_qlv_helper-0.
|
|
35
|
-
python_qlv_helper-0.
|
|
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,,
|
qlv_helper/config/url_const.py
CHANGED
|
@@ -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
|
|
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],
|
|
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
|
-
|
|
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
|
-
|
|
16
|
-
from
|
|
17
|
-
from qlv_helper.utils.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
await
|
|
101
|
-
|
|
102
|
-
# 7.
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
qlv_helper/http/order_page.py
CHANGED
|
@@ -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 = {
|
|
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: 翻页后等待时间
|
qlv_helper/po/login_page.py
CHANGED
|
@@ -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
|
|
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) ->
|
|
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
|
-
|
|
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) ->
|
|
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
|
-
|
|
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
|
|
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 = '
|
|
73
|
-
|
|
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
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
|
63
|
+
async def get_captcha_image(self, timeout: float = 5.0) -> ElementHandle:
|
|
106
64
|
selector: str = '//div[@id="signup_forms"]//img'
|
|
107
|
-
|
|
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
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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"]'
|
qlv_helper/po/main_page.py
CHANGED
|
@@ -9,9 +9,8 @@
|
|
|
9
9
|
# Copyright ©2011-2025. Hunan xxxxxxx Company limited. All rights reserved.
|
|
10
10
|
# ---------------------------------------------------------------------------------------------------------
|
|
11
11
|
"""
|
|
12
|
-
from
|
|
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) ->
|
|
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
|
-
|
|
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) ->
|
|
33
|
+
async def get_level1_menu_order_checkout(self, timeout: float = 5.0) -> Locator:
|
|
45
34
|
selector: str = "//span[contains(normalize-space(), '订单出票')]"
|
|
46
|
-
|
|
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) ->
|
|
37
|
+
async def get_level2_menu_order_checkout(self, timeout: float = 5.0) -> Locator:
|
|
59
38
|
selector: str = "//a[@menuname='国内活动订单']"
|
|
60
|
-
|
|
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
|
qlv_helper/utils/ocr_helper.py
CHANGED
|
@@ -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
|
|
16
|
+
from typing import Tuple
|
|
15
17
|
from aiohttp import ClientSession
|
|
16
|
-
from
|
|
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
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
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}")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|