python-qlv-helper 0.6.0__py3-none-any.whl → 0.9.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
 
@@ -46,9 +53,10 @@ async def _get_paginated_order_table(
46
53
  response = await fetch_page_fn(
47
54
  domain=domain, protocol=protocol, retry=retry, timeout=timeout,
48
55
  enable_log=enable_log, cookie_jar=cookie_jar, playwright_state=playwright_state,
49
- order_http_client=order_http_client, is_end=True
56
+ order_http_client=order_http_client, is_end=False
50
57
  )
51
58
  if response.get("code") != 200:
59
+ await order_http_client.close()
52
60
  return response
53
61
 
54
62
  html = response["data"]
@@ -66,6 +74,7 @@ async def _get_paginated_order_table(
66
74
  "pages": 1
67
75
  })
68
76
  response["data"] = pagination_info
77
+ await order_http_client.close()
69
78
  return response
70
79
 
71
80
  # --- 3. 多页:并发抓取第 2~pages 页 ---
@@ -75,24 +84,14 @@ async def _get_paginated_order_table(
75
84
  resp = await fetch_page_fn(
76
85
  domain=domain, protocol=protocol, retry=retry, timeout=timeout,
77
86
  enable_log=enable_log, cookie_jar=cookie_jar, playwright_state=playwright_state,
78
- order_http_client=client, current_page=page, pages=pages, is_end=(page == pages)
87
+ order_http_client=client, current_page=page, pages=pages, is_end=False
79
88
  )
80
89
  if resp.get("code") == 200:
81
90
  return parse_order_table(html=resp["data"], table_state=table_state)
82
- except (Exception, ):
91
+ except (Exception,):
83
92
  return list() # 抓取失败则返回空,不影响整体
84
93
  return list()
85
94
 
86
- # 🔥 并发:一口气抓全部分页
87
- order_http_client = HttpClientFactory(
88
- protocol=protocol if protocol == "http" else "https",
89
- domain=domain,
90
- timeout=timeout,
91
- retry=retry,
92
- enable_log=enable_log,
93
- cookie_jar=cookie_jar,
94
- playwright_state=playwright_state
95
- )
96
95
  tasks = [fetch_page(client=order_http_client, page=page) for page in range(2, pages + 1)]
97
96
  results = await asyncio.gather(*tasks)
98
97
 
@@ -109,8 +108,10 @@ async def _get_paginated_order_table(
109
108
  "pages": 1
110
109
  })
111
110
  response["data"] = pagination_info
111
+ await order_http_client.close()
112
112
  return response
113
113
 
114
+
114
115
  async def get_domestic_activity_order_table(
115
116
  domain: str, protocol: str = "http", retry: int = 1, timeout: int = 5, enable_log: bool = True,
116
117
  cookie_jar: Optional[CookieJar] = None, playwright_state: Dict[str, Any] = None
@@ -144,6 +145,7 @@ async def get_domestic_ticket_outed_table(
144
145
  fetch_page_fn=get_domestic_ticket_outed_page_html
145
146
  )
146
147
 
148
+
147
149
  async def get_domestic_unticketed_order_table(
148
150
  domain: str, protocol: str = "http", retry: int = 1, timeout: int = 5, enable_log: bool = True,
149
151
  cookie_jar: Optional[CookieJar] = None, playwright_state: Dict[str, Any] = None
@@ -158,4 +160,164 @@ async def get_domestic_unticketed_order_table(
158
160
  playwright_state=playwright_state,
159
161
  table_state="proccessing",
160
162
  fetch_page_fn=get_domestic_unticketed_order_page_html
161
- )
163
+ )
164
+
165
+
166
+ async def open_domestic_activity_order_page(
167
+ *, page: Page, logger: Logger, qlv_protocol: str, qlv_domain: str, timeout: float = 20.0
168
+ ) -> DomesticActivityOrderPage:
169
+ url_prefix = f"{qlv_protocol}://{qlv_domain}"
170
+ domestic_activity_order_url = url_prefix + url_const.domestic_activity_order_url
171
+ await page.goto(domestic_activity_order_url)
172
+
173
+ domestic_activity_order_po = DomesticActivityOrderPage(page=page, url=domestic_activity_order_url)
174
+ await domestic_activity_order_po.url_wait_for(url=domestic_activity_order_url, timeout=timeout)
175
+ logger.info(f"即将进入国内活动订单页面,页面URL<{domestic_activity_order_url}>")
176
+ return domestic_activity_order_po
177
+
178
+
179
+ async def pop_will_expire_domestic_activity_order(
180
+ *, page: Page, logger: Logger, qlv_protocol: str, qlv_domain: str, last_minute_threshold: int,
181
+ timeout: float = 20.0, **kwargs: Any
182
+ ) -> Tuple[List[Dict[str, Any]], bool]:
183
+ # 1. 打开国内活动订单页面
184
+ domestic_activity_order_po = await open_domestic_activity_order_page(
185
+ page=page, logger=logger, qlv_protocol=qlv_protocol, qlv_domain=qlv_domain, timeout=timeout
186
+ )
187
+
188
+ # TODO 暂时不考虑分页的情况
189
+ # 2. 获取table所有tr的Locator对象
190
+ trs_locator = await domestic_activity_order_po.get_flight_table_trs_locator(timeout=timeout)
191
+ trs_locator = await trs_locator.all()
192
+ table_data = list()
193
+ feilds = {
194
+ "to_from": "", "urgant_state": "", "order_id": 0, "pre_order_id": "", "aduit_pnr": "", "child_pnr": "",
195
+ "payment_time": "", "last_time_ticket": "", "dat_dep": "", "code_dep": "", "code_arr": "", "flight_no": "",
196
+ "cabin": "", "policy": "", "total_people": 0, "total_adult": 0, "total_child": 0, "receipted": 0.00,
197
+ "stat_opration": "", "more_seats": "", "operation_info": "", "substitute_btn_locator": ""
198
+ }
199
+ pre_pop_orders = list()
200
+ is_pop = False
201
+ for tr_locator in trs_locator[1:]:
202
+ row_locator = await domestic_activity_order_po.get_flight_table_trs_td(locator=tr_locator, timeout=timeout)
203
+ tds_locators = await row_locator.all()
204
+ sub_feilds = list()
205
+ copy_feilds = deepcopy(feilds)
206
+ for index, td_locator in enumerate(tds_locators):
207
+ try:
208
+ text = (await td_locator.inner_text()).strip()
209
+ if index == 0:
210
+ copy_feilds["to_from"] = text
211
+ sub_feilds.append("to_from")
212
+ elif index == 1:
213
+ order_id = await domestic_activity_order_po.get_flight_table_td_order_id(
214
+ locator=td_locator, timeout=timeout
215
+ )
216
+ urgant_state = await domestic_activity_order_po.get_flight_table_td_urgant(
217
+ locator=td_locator, timeout=timeout
218
+ )
219
+ copy_feilds["order_id"] = safe_convert_advanced(value=order_id)
220
+ copy_feilds["urgant_state"] = urgant_state
221
+ sub_feilds.extend(["order_id", "urgant_state"])
222
+ elif index == 2:
223
+ copy_feilds["pre_order_id"] = text
224
+ sub_feilds.append("pre_order_id")
225
+ elif index == 3:
226
+ text = text.replace("\xa0", "")
227
+ text_slice = text.split("|")
228
+ copy_feilds["aduit_pnr"] = text_slice[0].strip()
229
+ copy_feilds["child_pnr"] = text_slice[1].strip()
230
+ sub_feilds.extend(["aduit_pnr", "child_pnr"])
231
+ elif index == 4:
232
+ copy_feilds["payment_time"] = text
233
+ sub_feilds.append("payment_time")
234
+ elif index == 5:
235
+ continue
236
+ elif index == 6:
237
+ copy_feilds["last_time_ticket"] = text
238
+ sub_feilds.append("last_time_ticket")
239
+ elif index == 7:
240
+ continue
241
+ elif index == 8:
242
+ text = text.replace("\xa0", "|")
243
+ text_slice = [i for i in text.split("|") if i.strip()]
244
+ ctrip = text_slice[1].split("-")
245
+ copy_feilds["dat_dep"] = text_slice[0].strip()
246
+ copy_feilds["code_dep"] = ctrip[0].strip()
247
+ copy_feilds["code_arr"] = ctrip[1].strip()
248
+ copy_feilds["flight_no"] = text_slice[2].strip()
249
+ copy_feilds["cabin"] = text_slice[3].strip()
250
+ sub_feilds.extend(["dat_dep", "code_dep", "code_arr", "flight_no", "cabin"])
251
+ elif index == 9:
252
+ text = text.replace("\xa0", "")
253
+ text = text.replace(">", "")
254
+ text = text.replace("<", "")
255
+ text = text.replace("&", "")
256
+ text = text.replace("<br>", "\n")
257
+ copy_feilds["policy"] = text
258
+ sub_feilds.append("policy")
259
+ elif index == 10:
260
+ text = text.replace("【 ", "|")
261
+ text = text.replace("/", "|")
262
+ text = text.replace("】", "")
263
+ text_slice = text.split("|")
264
+ copy_feilds["total_people"] = safe_convert_advanced(value=text_slice[0].strip())
265
+ copy_feilds["total_adult"] = safe_convert_advanced(value=text_slice[1].strip())
266
+ copy_feilds["total_child"] = safe_convert_advanced(value=text_slice[2].strip())
267
+ sub_feilds.extend(["total_people", "total_adult", "total_child"])
268
+ elif index == 11:
269
+ copy_feilds["receipted"] = safe_convert_advanced(value=text)
270
+ sub_feilds.append("receipted")
271
+ elif index == 12:
272
+ copy_feilds["stat_opration"] = text
273
+ sub_feilds.append("stat_opration")
274
+ elif index == 13:
275
+ copy_feilds["more_seats"] = safe_convert_advanced(value=text)
276
+ sub_feilds.append("more_seats")
277
+ elif index == 14:
278
+ text_slice = text.split(" ")
279
+ operation_info = dict(
280
+ lock_btn_locator=cast(Locator, None), pop_btn_locator=cast(Locator, None), locked=""
281
+ )
282
+ if "锁定" in text_slice[0]:
283
+ lock_btn_locator = await domestic_activity_order_po.get_flight_table_td_operation_lock_btn(
284
+ locator=td_locator, timeout=timeout
285
+ )
286
+ operation_info["lock_btn_locator"] = lock_btn_locator
287
+ else:
288
+ operation_info["locked"] = text_slice[0].strip()
289
+ if "踢出" in text:
290
+ pop_btn_locator = await domestic_activity_order_po.get_flight_table_td_operation_pop_btn(
291
+ locator=td_locator, timeout=timeout
292
+ )
293
+ operation_info["pop_btn_locator"] = pop_btn_locator
294
+ copy_feilds["operation_info"] = operation_info
295
+ sub_feilds.append("operation_info")
296
+ elif index == 15:
297
+ copy_feilds[
298
+ "substitute_btn_locator"
299
+ ] = await domestic_activity_order_po.get_flight_table_td_operation_substitute_btn(
300
+ locator=td_locator, timeout=timeout)
301
+ sub_feilds.append("substitute_btn_locator")
302
+ except (Exception,) as e:
303
+ logger.error(f"第<{index + 1}>列数据处理异常,原因:{e}")
304
+ if len(sub_feilds) == 22:
305
+ table_data.append(copy_feilds)
306
+ if datetime.strptime(
307
+ copy_feilds.get("last_time_ticket"), "%Y-%m-%d %H:%M:%S"
308
+ ) < datetime.now() + timedelta(minutes=last_minute_threshold):
309
+ pre_pop_orders.append(copy_feilds)
310
+ for pre_pop_order in pre_pop_orders:
311
+ order_id = pre_pop_order.get("order_id")
312
+ operation_info = pre_pop_order.get("operation_info")
313
+ pop_btn_locator = operation_info.get("pop_btn_locator")
314
+ last_time_ticket = pre_pop_order.get("last_time_ticket")
315
+ if pop_btn_locator and isinstance(pop_btn_locator, Locator):
316
+ minute = (datetime.strptime(last_time_ticket, "%Y-%m-%d %H:%M:%S") - datetime.now()).total_seconds() / 60
317
+ await pop_btn_locator.click()
318
+ if is_pop is False:
319
+ is_pop = True
320
+ logger.info(
321
+ f"订单<{order_id}>,距离最晚出票时限: {last_time_ticket},仅剩<{minute}>分钟,已将工单剔出活动订单"
322
+ )
323
+ 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