python-qlv-helper 0.2.0__py3-none-any.whl → 0.5.7__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.2.0
3
+ Version: 0.5.7
4
4
  Summary: qlv helper python package
5
5
  Author-email: ckf10000 <ckf10000@sina.com>
6
6
  License: Apache License
@@ -216,7 +216,8 @@ Requires-Dist: ddddocr==1.5.6; python_version >= "3.12" and platform_system == "
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
218
  Requires-Dist: airtest==1.3.6; python_version >= "3.12" and platform_system == "Windows"
219
- Requires-Dist: python_http_helper==0.1.4; python_version >= "3.12"
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
221
  Dynamic: license-file
221
222
 
222
223
  # qlv-helper
@@ -1,21 +1,24 @@
1
- python_qlv_helper-0.2.0.dist-info/licenses/LICENSE,sha256=WtjCEwlcVzkh1ziO35P2qfVEkLjr87Flro7xlHz3CEY,11556
1
+ python_qlv_helper-0.5.7.dist-info/licenses/LICENSE,sha256=WtjCEwlcVzkh1ziO35P2qfVEkLjr87Flro7xlHz3CEY,11556
2
2
  qlv_helper/__init__.py,sha256=5DCc5JhfdsgtIuFWgkxPOW5VVKZ8RPikQLGIuyZX6_Y,465
3
+ qlv_helper/config/__init__.py,sha256=0pKLgui-sC6yMNBBuuTTLkUGhPybiJQSTKTbi66alvg,465
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
3
6
  qlv_helper/controller/__init__.py,sha256=cOJA0xMIytv17oICzPYqWLaSy-Ro2Ceeti0hHhsUj6Y,468
4
7
  qlv_helper/controller/domestic_activity_order.py,sha256=MlmsDVsMBWq2h4Yjh1rhO372Z3p8tu2-4IZGP-nkfr8,1136
5
8
  qlv_helper/controller/main_page.py,sha256=JtkB6BdKYHoNYZ4fkeALpSmAj-NAuiv1x6BvCcBtpic,1252
6
- qlv_helper/controller/order_detail.py,sha256=F6KxZrupitF4EzykHLXxncT1ikmmP89O17E5oCn1XsY,1527
7
- qlv_helper/controller/order_table.py,sha256=rMqdyXrJbbqZmjvOYK1Ot09tR937m-0INU2rgzizjhU,5417
9
+ qlv_helper/controller/order_detail.py,sha256=tOV80nrL8YDVNjvKi8YQP0hWqAcd8M1J0ToRK5YLWZ0,21701
10
+ qlv_helper/controller/order_table.py,sha256=4AgA9EO0_GVuwnW3ltXPoN74U_6YpOqkRJVdeLb6L1o,6082
8
11
  qlv_helper/controller/user_login.py,sha256=iyyDbOREsXtV5bqAFeXGwurvcCmDHmquh6ReWCfnOBE,5281
9
12
  qlv_helper/http/__init__.py,sha256=yDh1xi_o7ohXqDAzLu62qCWIGk4_aD1dhnUaCon3klM,484
10
13
  qlv_helper/http/main_page.py,sha256=LTpwrG8H_NqwCa3185irgcGd6JQkChAk7HQsDM-TNTI,1519
11
- qlv_helper/http/order_page.py,sha256=VcoDoEPz3R0zHmKNRuRv2vDHkC3XS_bXhBE_uW9wEYw,11852
12
- qlv_helper/http/order_table_page.py,sha256=qDmQXkLcuTkLGPSURGzBKnhirjE8M1Ma9ibehg-mAkk,13135
14
+ qlv_helper/http/order_page.py,sha256=zIUZk1UiinoLQ4Y-Qwcv0ZmNAXtYoOGOYDdwdoA0RBI,13157
15
+ qlv_helper/http/order_table_page.py,sha256=IaXn5wjqPi1aRXHz0kucdHEdZswUWAZfECC13y57y8k,14440
13
16
  qlv_helper/po/__init__.py,sha256=eDr06o0eYapBsYpOhA11bbxzs2F0dsuDjOKmxk_2HVE,480
14
- qlv_helper/po/base_po.py,sha256=_Cym-vsrB1kjZeCkKzOj1BYC8pTR-nKlGDj7QaUiLBs,1362
15
- qlv_helper/po/domestic_activity_order_page.py,sha256=ySOrC1Rdc2ApL0FCRwf8xDKQrzTY63OxDC3EHPauvcA,5088
16
- qlv_helper/po/login_page.py,sha256=XcBSvOC_wQ4pInv6w_tDe6iB2UuNG-UUTpUS-mgwUEo,5951
17
- qlv_helper/po/main_page.py,sha256=x4ZJqWdxL6nQdQXDIZB87uDVXfLKs9FIAe0AQqaQO3M,3323
18
- qlv_helper/po/wechat_auth_page.py,sha256=SfZQn_mmfquf7srDs-rPRWKjaDWN7Xtsauk9AnLMMJs,3162
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
21
+ qlv_helper/po/wechat_auth_page.py,sha256=a4YZlM5JOS0l3CNJm_oJFBhZY7AbNOpdIwilSjAO5bY,3171
19
22
  qlv_helper/utils/__init__.py,sha256=rGzBkUf1tslG4WRPQjVWTVuwWG76pkckuKO_6K4sEus,465
20
23
  qlv_helper/utils/browser_utils.py,sha256=mKoqSEz1vFrVemp9cgI4R5UhA4k7i0Cd9cWsIXJZ6E4,986
21
24
  qlv_helper/utils/datetime_utils.py,sha256=BaDJKuH-yqc2NF9KYN66zUYUEJ9ZRHj09AV4-gILf3o,606
@@ -26,7 +29,7 @@ qlv_helper/utils/po_utils.py,sha256=SwQKL58HERGG2Weou_AwY_TQoYSvgi0gvaVCJBput_k,
26
29
  qlv_helper/utils/stealth_browser.py,sha256=srNOYJOboYo30TvW5OP5TaVpg4jgHm9GxqmYnuwcUQU,3140
27
30
  qlv_helper/utils/type_utils.py,sha256=S5FXUje2mbDuq27LU05WymxNu1VGOLBUV3tuqcx51dE,3792
28
31
  qlv_helper/utils/windows_utils.py,sha256=Cvedsk1c2ujgPNVxszz8XWANkvEr8G9kne6povtZRU4,2866
29
- python_qlv_helper-0.2.0.dist-info/METADATA,sha256=irKOAys7QdoUtxIoR1vxZnPT-yiD3zi4k_yF2-CDtf4,14779
30
- python_qlv_helper-0.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
31
- python_qlv_helper-0.2.0.dist-info/top_level.txt,sha256=0pYdhD8SfBcC57LzLYGHY7cwwPqdqAkB1twysCJh5OA,11
32
- python_qlv_helper-0.2.0.dist-info/RECORD,,
32
+ python_qlv_helper-0.5.7.dist-info/METADATA,sha256=QcI93-dpNseb9ie1ciSpmZu2GMC45SVG--lmaK8ZkJ8,14853
33
+ python_qlv_helper-0.5.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
34
+ python_qlv_helper-0.5.7.dist-info/top_level.txt,sha256=0pYdhD8SfBcC57LzLYGHY7cwwPqdqAkB1twysCJh5OA,11
35
+ python_qlv_helper-0.5.7.dist-info/RECORD,,
@@ -0,0 +1,11 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ # ---------------------------------------------------------------------------------------------------------
4
+ # ProjectName: qlv-helper
5
+ # FileName: __init__.py
6
+ # Description: 配置包
7
+ # Author: ASUS
8
+ # CreateDate: 2025/12/16
9
+ # Copyright ©2011-2025. Hunan xxxxxxx Company limited. All rights reserved.
10
+ # ---------------------------------------------------------------------------------------------------------
11
+ """
@@ -0,0 +1,35 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ # ---------------------------------------------------------------------------------------------------------
4
+ # ProjectName: qlv-helper
5
+ # FileName: custom_exception.py
6
+ # Description: 自定义异常模块
7
+ # Author: ASUS
8
+ # CreateDate: 2025/12/17
9
+ # Copyright ©2011-2025. Hunan xxxxxxx Company limited. All rights reserved.
10
+ # ---------------------------------------------------------------------------------------------------------
11
+ """
12
+
13
+
14
+ class FirstPhaseException(Exception):
15
+ pass
16
+
17
+
18
+ class SecondPhaseException(Exception):
19
+ pass
20
+
21
+
22
+ class ThirdPhaseException(Exception):
23
+ pass
24
+
25
+
26
+ class FourthPhaseException(Exception):
27
+ pass
28
+
29
+
30
+ class FifthPhaseException(Exception):
31
+ pass
32
+
33
+
34
+ class sixthPhaseException(Exception):
35
+ pass
@@ -0,0 +1,13 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ # ---------------------------------------------------------------------------------------------------------
4
+ # ProjectName: qlv-helper
5
+ # FileName: url_const.py
6
+ # Description: url配置常量
7
+ # Author: ASUS
8
+ # CreateDate: 2025/12/16
9
+ # Copyright ©2011-2025. Hunan xxxxxxx Company limited. All rights reserved.
10
+ # ---------------------------------------------------------------------------------------------------------
11
+ """
12
+
13
+ order_detail_url = "/OrderProcessing/NewTicket_show/{}?&r={}"
@@ -10,13 +10,19 @@
10
10
  # ---------------------------------------------------------------------------------------------------------
11
11
  """
12
12
  import aiohttp
13
- from typing import Dict, Any, Optional
13
+ import asyncio
14
+ from logging import Logger
15
+ from datetime import datetime
16
+ import qlv_helper.config.url_const as url_const
17
+ from typing import Dict, Any, List, cast, Optional
18
+ from qlv_helper.po.order_detail_page import OrderDetailPage
14
19
  from qlv_helper.http.order_page import parser_order_info, get_order_page_html, parser_order_flight_table
20
+ from playwright.async_api import Page, Locator, Error as PlaywrightError, TimeoutError as PlaywrightTimeoutError
15
21
 
16
22
 
17
23
  async def get_order_info_with_http(
18
- order_id: int, 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
24
+ *, order_id: int, domain: str, protocol: str = "http", retry: int = 1, timeout: int = 5,
25
+ enable_log: bool = True, cookie_jar: Optional[aiohttp.CookieJar] = None, playwright_state: Dict[str, Any] = None
20
26
  ) -> Dict[str, Any]:
21
27
  response = await get_order_page_html(
22
28
  order_id=order_id, domain=domain, protocol=protocol, retry=retry, timeout=timeout, enable_log=enable_log,
@@ -33,3 +39,389 @@ async def get_order_info_with_http(
33
39
  order_info["peoples"] = flight_info
34
40
  response["data"] = order_info
35
41
  return response
42
+
43
+
44
+ async def open_order_detail_page(
45
+ *, page: Page, logger: Logger, protocol: str, domain: str, order_id: int, timeout: float = 20.0
46
+ ) -> OrderDetailPage:
47
+ url_prefix = f"{protocol}://{domain}"
48
+ current_dtstr: str = datetime.now().strftime("%Y%m%d%H%M%S")
49
+ order_detail_url_suffix = url_const.order_detail_url.format(order_id, current_dtstr)
50
+ order_detail_url = url_prefix + order_detail_url_suffix
51
+ await page.goto(order_detail_url)
52
+
53
+ order_detail_po = OrderDetailPage(page=page, url=order_detail_url)
54
+ await order_detail_po.url_wait_for(url=order_detail_url, timeout=timeout)
55
+ logger.info(f"即将劲旅订单详情页面,页面URL<{order_detail_url}>")
56
+
57
+ try:
58
+ confirm_btn = await order_detail_po.get_message_notice_dialog_confirm_btn(timeout=1)
59
+ await confirm_btn.click(button="left")
60
+ logger.info("订单详情页面,消息提醒弹框,【确认】按钮点击完成")
61
+ except (PlaywrightTimeoutError, PlaywrightError, RuntimeError, Exception):
62
+ pass
63
+ return order_detail_po
64
+
65
+
66
+ async def add_custom_remark(*, logger: Logger, page: OrderDetailPage, remark: str, timeout: float = 5.0) -> None:
67
+ # 1. 添加自定义备注
68
+ custom_remark_input = await page.get_custom_remark_input(timeout=timeout)
69
+ await custom_remark_input.fill(value=remark)
70
+ logger.info(f"订单详情页面,日志记录栏,自定义备注:{remark},输入完成")
71
+
72
+ # 2. 点击【保存备注】按钮
73
+ custom_remark_save_btn = await page.get_custom_remark_save_btn(timeout=timeout)
74
+ await custom_remark_save_btn.click(button="left")
75
+ logger.info(f"订单详情页面,日志记录栏,【保存备注】按钮点击完成")
76
+
77
+
78
+ async def update_order_policy(
79
+ *, page: OrderDetailPage, logger: Logger, policy: str, order_id: int, timeout: float = 5.0
80
+ ) -> None:
81
+ # 1. 修改政策代码
82
+ policy_input = await page.get_policy_input(timeout=timeout)
83
+ await policy_input.fill(value=policy)
84
+
85
+ # 2. 执行回车按键
86
+ await policy_input.press("Enter")
87
+ logger.info(f"订单<{order_id}>,政策代码已更新成<{policy}>完成")
88
+
89
+
90
+ async def order_unlock(
91
+ *, logger: Logger, page: OrderDetailPage, remark: str, order_id: int, is_force: bool = False,
92
+ qlv_user_id: Optional[str] = None, timeout: float = 5.0
93
+ ) -> None:
94
+ # 1. 获取订单操作锁的状态
95
+ lock_state, lock_btn = await page.get_order_lock_state_btn(timeout=timeout)
96
+ if "强制解锁" in lock_state:
97
+ if is_force is False:
98
+ raise EnvironmentError(f"订单<{order_id}>,被他人锁定,无需解锁处理")
99
+ await lock_btn.click(button="left")
100
+ logger.info(f"订单详情页面,用户操作【{lock_state}】按钮点击完成")
101
+ try:
102
+ confirm_btn = await page.get_message_notice_dialog_confirm_btn(timeout=1)
103
+ await confirm_btn.click(button="left")
104
+ logger.info("订单详情页面,强制解锁弹框确认,【确认】按钮点击完成")
105
+ await asyncio.sleep(2)
106
+ except (PlaywrightTimeoutError, PlaywrightError, RuntimeError, Exception):
107
+ pass
108
+ logger.warning(f"订单<{order_id}>,已被用户<{qlv_user_id or '无记录'}>强制解锁")
109
+ # 再次获取订单操作锁的状态
110
+ lock_state, lock_btn = await page.get_order_lock_state_btn(timeout=timeout)
111
+ elif "锁定" in lock_state:
112
+ logger.info(f"订单<{order_id}>,处于无锁状态,无需解锁处理")
113
+ return
114
+ elif "操作" in lock_state:
115
+ await lock_btn.click(button="left")
116
+ await asyncio.sleep(2)
117
+ lock_state_temp = lock_state
118
+ lock_state, lock_btn = await page.get_order_lock_state_btn(timeout=timeout)
119
+ logger.info(f"订单详情页面,日志记录栏,【{lock_state_temp}】按钮点击完成")
120
+ if remark:
121
+ # 2. 添加解锁备注
122
+ await add_custom_remark(logger=logger, page=page, remark=remark, timeout=timeout)
123
+
124
+ # 3. 点击【解锁返回】按钮
125
+ await lock_btn.click(button="left")
126
+ await asyncio.sleep(2)
127
+ logger.info(f"订单详情页面,日志记录栏,【{lock_state}】按钮点击完成")
128
+
129
+
130
+ async def order_locked(
131
+ *, logger: Logger, page: OrderDetailPage, order_id: int, is_force: bool = False, timeout: float = 5.0,
132
+ qlv_user_id: Optional[str] = None
133
+ ) -> None:
134
+ # 1. 获取订单操作锁的状态
135
+ lock_state, lock_btn = await page.get_order_lock_state_btn(timeout=timeout)
136
+ if "强制解锁" in lock_state:
137
+ if is_force is False:
138
+ raise EnvironmentError(f"订单<{order_id}>,被他人锁定,不做加锁处理")
139
+ await lock_btn.click(button="left")
140
+ logger.info(f"订单详情页面,用户操作【{lock_state}】按钮点击完成")
141
+ try:
142
+ confirm_btn = await page.get_message_notice_dialog_confirm_btn(timeout=1)
143
+ await confirm_btn.click(button="left")
144
+ logger.info("订单详情页面,强制解锁弹框确认,【确认】按钮点击完成")
145
+ await asyncio.sleep(2)
146
+ except (PlaywrightTimeoutError, PlaywrightError, RuntimeError, Exception):
147
+ pass
148
+ logger.warning(f"订单<{order_id}>,已被用户<{qlv_user_id or '无记录'}>强制解锁")
149
+ return
150
+ elif "解锁返回" in lock_state:
151
+ logger.warning(f"订单<{order_id}>,处于锁定状态,不做加锁处理")
152
+ return
153
+ # 2. 点击【锁定|操作】按钮
154
+ await lock_btn.click(button="left")
155
+ logger.info(f"订单详情页面,日志记录栏,【{lock_state}】按钮点击完成")
156
+
157
+
158
+ async def first_open_page_order_locked(
159
+ *, logger: Logger, page: Page, protocol: str, domain: str, order_id: int, is_force: bool = False,
160
+ qlv_user_id: Optional[str] = None, timeout: float = 5.0, **kwargs
161
+ ) -> OrderDetailPage:
162
+ # 1. 打开页面
163
+ order_detail_po = await open_order_detail_page(
164
+ page=page, logger=logger, protocol=protocol, domain=domain, order_id=order_id, timeout=timeout
165
+ )
166
+
167
+ # 2. 锁定订单
168
+ await order_locked(
169
+ logger=logger, page=order_detail_po, order_id=order_id, timeout=timeout, is_force=is_force,
170
+ qlv_user_id=qlv_user_id
171
+ )
172
+ return order_detail_po
173
+
174
+
175
+ async def first_open_page_order_unlock(
176
+ *, logger: Logger, page: Page, protocol: str, domain: str, order_id: int, is_force: bool = False,
177
+ qlv_user_id: Optional[str] = None, timeout: float = 5.0, remark: Optional[str] = None, **kwargs
178
+ ) -> OrderDetailPage:
179
+ # 1. 打开页面
180
+ order_detail_po = await open_order_detail_page(
181
+ page=page, logger=logger, protocol=protocol, domain=domain, order_id=order_id, timeout=timeout
182
+ )
183
+
184
+ # 2. 解锁订单
185
+ await order_unlock(
186
+ logger=logger, page=order_detail_po, order_id=order_id, timeout=timeout, is_force=is_force,
187
+ qlv_user_id=qlv_user_id, remark=remark
188
+ )
189
+ return order_detail_po
190
+
191
+
192
+ async def fill_procurement_info(
193
+ *, logger: Logger, page: OrderDetailPage, order_id: int, out_ticket_platform_type: str, purchase_amount: float,
194
+ out_ticket_platform: str, out_ticket_account: str, purchase_account_type: str, purchase_account: str,
195
+ ceair_user_id: str, ceair_password: str, payment_id: str, platform_order_id: str, timeout: float = 5.0
196
+ ) -> None:
197
+ # 1. 出票地类型选择【out_ticket_platform_type】
198
+ out_ticket_platform_type_dropdown = await page.get_out_ticket_platform_type_dropdown(timeout=timeout)
199
+ await out_ticket_platform_type_dropdown.select_option(label=out_ticket_platform_type)
200
+ # await out_ticket_platform_type_dropdown.click(button="left")
201
+ # out_ticket_platform_type_select_option = await page.get_out_ticket_platform_type_select_option(
202
+ # select_option=out_ticket_platform_type, timeout=timeout
203
+ # )
204
+ # await out_ticket_platform_type_select_option.click(button="left")
205
+ logger.info(f"订单详情页面,采购信息栏,出票地类型选择<{out_ticket_platform_type}>已完成")
206
+ await asyncio.sleep(1)
207
+
208
+ # 2. 出票平台选择【out_ticket_platform】
209
+ out_ticket_platform_dropdown = await page.get_out_ticket_platform_dropdown(timeout=timeout)
210
+ await out_ticket_platform_dropdown.select_option(label=out_ticket_platform)
211
+ # await out_ticket_platform_dropdown.click(button="left")
212
+ # out_ticket_platform_select_option = await page.get_out_ticket_platform_select_option(
213
+ # select_option=out_ticket_platform, timeout=timeout
214
+ # )
215
+ # await out_ticket_platform_select_option.click(button="left")
216
+ logger.info(f"订单详情页面,采购信息栏,出票平台选择<{out_ticket_platform}>已完成")
217
+ await asyncio.sleep(1)
218
+
219
+ # 3. 出票账号选择【out_ticket_account】
220
+ out_ticket_account_dropdown = await page.get_out_ticket_account_dropdown(timeout=timeout)
221
+ await out_ticket_account_dropdown.select_option(label=out_ticket_account)
222
+ # await out_ticket_account_dropdown.click(button="left")
223
+ # out_ticket_account_select_option = await page.get_out_ticket_account_select_option(
224
+ # select_option=out_ticket_account, timeout=timeout
225
+ # )
226
+ # await out_ticket_account_select_option.click(button="left")
227
+ logger.info(f"订单详情页面,采购信息栏,出票账号选择<{out_ticket_account}>已完成")
228
+ await asyncio.sleep(1)
229
+
230
+ # 4. 采购账号类型选择【purchase_account_type】
231
+ purchase_account_type_dropdown = await page.get_purchase_account_type_dropdown(timeout=timeout)
232
+ await purchase_account_type_dropdown.select_option(label=purchase_account_type)
233
+ # await purchase_account_type_dropdown.click(button="left")
234
+ # purchase_account_type_select_option = await page.get_purchase_account_type_select_option(
235
+ # select_option=purchase_account_type, timeout=timeout
236
+ # )
237
+ # await purchase_account_type_select_option.click(button="left")
238
+ logger.info(f"订单详情页面,采购信息栏,采购账号类型选择<{purchase_account_type}>已完成")
239
+ await asyncio.sleep(1)
240
+
241
+ # 5. 采购账号选择【purchase_account】
242
+ purchase_account_dropdown = await page.get_purchase_account_dropdown(timeout=timeout)
243
+ await purchase_account_dropdown.select_option(label=purchase_account)
244
+ # await purchase_account_dropdown.click(button="left")
245
+ # purchase_account_select_option = await page.get_purchase_account_select_option(
246
+ # select_option=purchase_account, timeout=timeout
247
+ # )
248
+ # await purchase_account_select_option.click(button="left")
249
+ logger.info(f"订单详情页面,采购信息栏,采购账号选择<{purchase_account}>已完成")
250
+ await asyncio.sleep(1)
251
+
252
+ # 6. 填写采购金额
253
+ purchase_amount_input = await page.get_purchase_amount_input(timeout=timeout)
254
+ await purchase_amount_input.fill(value=str(purchase_amount))
255
+ logger.info(f"订单详情页面,采购信息栏,采购金额<{purchase_amount}>输入完成")
256
+
257
+ # 7. 填写备注
258
+ remark_input = await page.get_remark_input(timeout=timeout)
259
+ value = f"{ceair_user_id}/{ceair_password}"
260
+ await remark_input.fill(value=value)
261
+ logger.info(f"订单详情页面,采购信息栏,备注<{value}>输入完成")
262
+
263
+ # 8. 填写对账标识
264
+ main_check_input = await page.get_main_check_input(timeout=timeout)
265
+ await main_check_input.fill(value=payment_id)
266
+ logger.info(f"订单详情页面,采购信息栏,对账标识<{payment_id}>输入完成")
267
+
268
+ # 9. 填写官网订单号
269
+ air_comp_order_id_input = await page.get_air_comp_order_id_input(timeout=timeout)
270
+ await air_comp_order_id_input.fill(value=platform_order_id)
271
+ logger.info(f"订单详情页面,采购信息栏,官网订单号<{platform_order_id}>输入完成")
272
+
273
+ # 10. 点击【保存采购】按钮
274
+ procurement_info_save_btn = await page.get_procurement_info_save_btn(timeout=timeout)
275
+ await procurement_info_save_btn.click(button="left")
276
+ logger.info(f"订单详情页面,采购信息栏,【保存采购】按钮点击完成")
277
+
278
+
279
+ async def first_open_page_fill_procurement_info(
280
+ *, logger: Logger, page: Page, order_id: int, protocol: str, domain: str, out_ticket_platform_type: str,
281
+ purchase_amount: float, out_ticket_platform: str, out_ticket_account: str, purchase_account_type: str,
282
+ purchase_account: str, ceair_user_id: str, ceair_password: str, payment_id: str, platform_order_id: str,
283
+ is_force: bool = False, qlv_user_id: Optional[str] = None, timeout: float = 5.0, **kwargs: Any
284
+ ) -> OrderDetailPage:
285
+ # 1. 打开页面
286
+ order_detail_po = await open_order_detail_page(
287
+ page=page, logger=logger, protocol=protocol, domain=domain, order_id=order_id, timeout=timeout
288
+ )
289
+
290
+ # 2. 锁定订单
291
+ await order_locked(
292
+ logger=logger, page=order_detail_po, order_id=order_id, is_force=is_force, timeout=timeout,
293
+ qlv_user_id=qlv_user_id
294
+ )
295
+
296
+ # 3. 回填采购信息
297
+ await fill_procurement_info(
298
+ out_ticket_platform_type=out_ticket_platform_type, out_ticket_account=out_ticket_account, payment_id=payment_id,
299
+ purchase_amount=purchase_amount, out_ticket_platform=out_ticket_platform, purchase_account=purchase_account,
300
+ purchase_account_type=purchase_account_type, ceair_user_id=ceair_user_id, ceair_password=ceair_password,
301
+ platform_order_id=platform_order_id, timeout=timeout, logger=logger, page=order_detail_po, order_id=order_id
302
+ )
303
+ return order_detail_po
304
+
305
+
306
+ async def fill_itinerary(
307
+ *, logger: Logger, page: OrderDetailPage, order_id: int, passengers_itinerary: Dict[str, Any],
308
+ timeout: float = 5.0
309
+ ) -> None:
310
+ # 1. 获取订单操作锁的状态
311
+ passenger_itinerary_locators: List[Dict[str, Any]] = await page.get_passenger_itinerary_locators(timeout=timeout)
312
+ if not passenger_itinerary_locators:
313
+ logger.warning(f"订单<{order_id}>,没有需要填写票号的乘客,直接跳过")
314
+ return
315
+ current_passengers = set([x.get("username") for x in passenger_itinerary_locators])
316
+ kwargs_passengers = set(passengers_itinerary.keys())
317
+ diff = kwargs_passengers.difference(current_passengers)
318
+ if diff:
319
+ raise RuntimeError(
320
+ f"订单<{order_id}>,传递回填票号的乘客证件信息<{kwargs_passengers}>与订单实际乘客信息<{current_passengers}>不一致"
321
+ )
322
+ passenger_itinerary_locator = cast(Locator, None)
323
+ for passenger_locator in passenger_itinerary_locators:
324
+ current_passenger = passenger_locator.get("username")
325
+ passenger_itinerary = passengers_itinerary.get(current_passenger)
326
+ if passenger_itinerary:
327
+ passenger_itinerary_locator: Locator = passenger_locator.get("locator")
328
+ await passenger_itinerary_locator.fill(value=passenger_itinerary)
329
+ logger.info(f"订单<{order_id}>,乘客<{current_passenger}>的票号<{passenger_itinerary}>填写完成")
330
+ else:
331
+ logger.warning(f"订单<{order_id}>,乘客<{current_passenger}>的票号没有获取到,本次回填跳过,等待下一次")
332
+ if passenger_itinerary_locator:
333
+ await passenger_itinerary_locator.press("Enter")
334
+ logger.info(f"订单<{order_id}>,本次的票号回填完成")
335
+ else:
336
+ raise RuntimeError(f"订单<{order_id}>,回填票号过程异常,回填失败")
337
+
338
+
339
+ async def first_open_page_fill_itinerary(
340
+ *, page: Page, logger: Logger, protocol: str, domain: str, order_id: int,
341
+ passengers_itinerary: Dict[str, Any], timeout: float = 20.0, **kwargs: Any
342
+ ) -> OrderDetailPage:
343
+ # 1. 开发页面
344
+ order_detail_po = await open_order_detail_page(
345
+ page=page, logger=logger, protocol=protocol, domain=domain, order_id=order_id, timeout=timeout
346
+ )
347
+
348
+ # 2. 回填票号
349
+ await fill_itinerary(
350
+ logger=logger, page=order_detail_po, order_id=order_id, timeout=timeout,
351
+ passengers_itinerary=passengers_itinerary
352
+ )
353
+ logger.info(f"订单<{order_id}>,票号回填流程结束")
354
+ await order_unlock(logger=logger, page=order_detail_po, order_id=order_id, timeout=timeout, remark="")
355
+ logger.info(f"订单<{order_id}>,订单解锁成功")
356
+ return order_detail_po
357
+
358
+
359
+ async def first_open_page_update_policy(
360
+ *, page: Page, logger: Logger, protocol: str, domain: str, order_id: int, policy: str, timeout: float = 20.0,
361
+ **kwargs: Any
362
+ ) -> OrderDetailPage:
363
+ # 1. 打开页面
364
+ order_detail_po = await open_order_detail_page(
365
+ page=page, logger=logger, protocol=protocol, domain=domain, order_id=order_id, timeout=timeout
366
+ )
367
+
368
+ # 2. 更新订单政策代码
369
+ await update_order_policy(
370
+ logger=logger, page=order_detail_po, order_id=order_id, timeout=timeout, policy=policy
371
+ )
372
+
373
+ return order_detail_po
374
+
375
+
376
+ async def first_open_page_my_order_unlock(
377
+ *, page: Page, logger: Logger, protocol: str, domain: str, order_id: int, qlv_user_id: str,
378
+ policy: Optional[str] = None, remark: Optional[str] = None, timeout: float = 20.0, is_force: bool = False,
379
+ **kwargs: Any
380
+ ) -> OrderDetailPage:
381
+ # 1. 打开页面
382
+ order_detail_po = await open_order_detail_page(
383
+ page=page, logger=logger, protocol=protocol, domain=domain, order_id=order_id, timeout=timeout
384
+ )
385
+
386
+ if policy:
387
+ # 2. 更新订单政策代码
388
+ await update_order_policy(
389
+ logger=logger, page=order_detail_po, order_id=order_id, timeout=timeout, policy=policy
390
+ )
391
+
392
+ if remark:
393
+ # 3. 添加解锁备注
394
+ await add_custom_remark(logger=logger, page=order_detail_po, remark=remark, timeout=timeout)
395
+
396
+ # 4. 获取订单操作锁的状态
397
+ lock_state, lock_btn = await order_detail_po.get_order_lock_state_btn(timeout=timeout)
398
+ if "强制解锁" in lock_state:
399
+ if is_force is False:
400
+ logger.warning(f"订单<{order_id}>,非本账号<{qlv_user_id}>的锁单,暂不处理,即将跳过")
401
+ return order_detail_po
402
+ await lock_btn.click(button="left")
403
+ logger.info(f"订单详情页面,用户操作【{lock_state}】按钮点击完成")
404
+ try:
405
+ confirm_btn = await order_detail_po.get_message_notice_dialog_confirm_btn(timeout=1)
406
+ await confirm_btn.click(button="left")
407
+ logger.info("订单详情页面,强制解锁弹框确认,【确认】按钮点击完成")
408
+ await asyncio.sleep(2)
409
+ except (PlaywrightTimeoutError, PlaywrightError, RuntimeError, Exception):
410
+ pass
411
+ logger.warning(f"订单<{order_id}>,已被用户<{qlv_user_id or '无记录'}>强制解锁")
412
+ # 再次获取订单操作锁的状态
413
+ lock_state, lock_btn = await order_detail_po.get_order_lock_state_btn(timeout=timeout)
414
+ elif "锁定" in lock_state:
415
+ logger.info(f"订单<{order_id}>,处于无锁状态,无需解锁处理")
416
+ elif "操作" in lock_state:
417
+ await lock_btn.click(button="left")
418
+ await asyncio.sleep(2)
419
+ lock_state_temp = lock_state
420
+ lock_state, lock_btn = await order_detail_po.get_order_lock_state_btn(timeout=timeout)
421
+ logger.info(f"订单详情页面,日志记录栏,【{lock_state_temp}】按钮点击完成")
422
+
423
+ # 5. 点击【解锁返回】按钮
424
+ await lock_btn.click(button="left")
425
+ await asyncio.sleep(2)
426
+ logger.info(f"订单详情页面,日志记录栏,【{lock_state}】按钮点击完成")
427
+ return order_detail_po
@@ -15,7 +15,7 @@ from typing import Optional, Dict, Any, Callable, List
15
15
  from http_helper.client.async_proxy import HttpClientFactory
16
16
  from qlv_helper.utils.html_utils import parse_pagination_info
17
17
  from qlv_helper.http.order_table_page import get_domestic_activity_order_page_html, get_domestic_ticket_outed_page_html, \
18
- parse_order_table
18
+ parse_order_table, get_domestic_unticketed_order_page_html
19
19
 
20
20
 
21
21
  async def _get_paginated_order_table(
@@ -143,3 +143,19 @@ async def get_domestic_ticket_outed_table(
143
143
  table_state="completed",
144
144
  fetch_page_fn=get_domestic_ticket_outed_page_html
145
145
  )
146
+
147
+ async def get_domestic_unticketed_order_table(
148
+ domain: str, protocol: str = "http", retry: int = 1, timeout: int = 5, enable_log: bool = True,
149
+ cookie_jar: Optional[CookieJar] = None, playwright_state: Dict[str, Any] = None
150
+ ) -> Dict[str, Any]:
151
+ return await _get_paginated_order_table(
152
+ domain=domain,
153
+ protocol=protocol,
154
+ retry=retry,
155
+ timeout=timeout,
156
+ enable_log=enable_log,
157
+ cookie_jar=cookie_jar,
158
+ playwright_state=playwright_state,
159
+ table_state="proccessing",
160
+ fetch_page_fn=get_domestic_unticketed_order_page_html
161
+ )
@@ -55,40 +55,73 @@ def order_info_static_headers() -> OrderedDict[str, str]:
55
55
 
56
56
  def parser_order_info(html: str) -> Dict[str, Any]:
57
57
  soup = BeautifulSoup(html, "html.parser")
58
- # 找到目标 table
59
- table = soup.find("table", class_="table no_border")
60
- if not table:
58
+ # 找到目标 table_border
59
+ table_border = soup.find("table", class_="table no_border")
60
+ if not table_border:
61
+ return {}
62
+
63
+ table_flight = soup.find("table", class_="info_flight")
64
+ if not table_border:
61
65
  return {}
62
66
 
63
67
  # 所有 td
64
- tds = table.find_all("td")
68
+ tds_border = table_border.find_all("td")
65
69
  result = {}
66
70
 
67
- for td in tds:
68
- text = td.get_text(strip=True)
69
- # 如果 td 内没有冒号、也不是按钮,跳过
70
- if ":" not in text:
71
- continue
72
- # 按 ":" 进行分割
71
+ for td in tds_border:
73
72
  try:
74
- key, value = text.split(":", 1)
75
- except (Exception, ValueError):
76
- continue
77
- # 去掉换行符、空格
78
- key = key.strip()
79
- value = value.strip()
80
-
81
- # 如果 value 为空,尝试取 <b> 或其他控件内文本
82
- if not value:
83
- b = td.find("b")
84
- if b:
85
- value = b.get_text(strip=True)
86
-
87
- # 去掉尾部不需要的空格
88
- value = value.replace("\u00a0", "").strip()
89
- value = value[1:] if isinstance(value, str) and value.startswith("[") else value
90
- value = value[:-1] if isinstance(value, str) and value.endswith("]") else value
91
- result[key] = safe_convert_advanced(value)
73
+ text = td.get_text(strip=True)
74
+ # 如果 td 内没有冒号、也不是按钮,跳过
75
+ if ":" not in text:
76
+ continue
77
+ # ":" 进行分割
78
+ try:
79
+ key, value = text.split(":", 1)
80
+ except (Exception, ValueError):
81
+ continue
82
+ # 去掉换行符、空格
83
+ key = key.strip()
84
+ value = value.strip()
85
+
86
+ # 如果 value 为空,尝试取 <b> 或其他控件内文本
87
+ if not value:
88
+ b = td.find("b")
89
+ if b:
90
+ value = b.get_text(strip=True)
91
+
92
+ # 去掉尾部不需要的空格
93
+ value = value.replace("\u00a0", "").strip()
94
+ value = value[1:] if isinstance(value, str) and value.startswith("[") else value
95
+ value = value[:-1] if isinstance(value, str) and value.endswith("]") else value
96
+ result[key] = safe_convert_advanced(value)
97
+ except (Exception,):
98
+ pass
99
+
100
+ # 所有 td
101
+ tds_flight = table_flight.find_all("td")
102
+
103
+ for td in tds_flight:
104
+ try:
105
+ dat_dep = td.find("span", class_="DatDep")
106
+ if dat_dep:
107
+ text = dat_dep.get_text(strip=True)
108
+ result["dat_dep"] = text + ":00"
109
+ city_dep = td.find("span", class_="CityDep")
110
+ if city_dep:
111
+ text = city_dep.get_text(strip=True)
112
+ city_dep = text.split("【")[0].strip()
113
+ result["city_dep"] = city_dep
114
+ dat_arr = td.find("p", class_="DatArr")
115
+ if dat_arr:
116
+ text = dat_arr.get_text(strip=True)
117
+ result["dat_arr"] = text + ":00"
118
+ city_arr = td.find("span", class_="CityArr")
119
+ if city_arr:
120
+ text = city_arr.get_text(strip=True)
121
+ city_arr = text.split("【")[0].strip()
122
+ result["city_arr"] = city_arr
123
+ except (Exception,):
124
+ pass
92
125
  return convert_cn_to_en(data=result, header_map=order_info_static_headers())
93
126
 
94
127
 
@@ -81,10 +81,34 @@ async def get_domestic_ticket_outed_page_html(
81
81
  json_data["PageCountFormPage"] = pages
82
82
  return await order_http_client.request(method="post", url=url, is_end=is_end, json_data=json_data)
83
83
 
84
+ async def get_domestic_unticketed_order_page_html(
85
+ domain: str, protocol: str = "http", retry: int = 1, timeout: int = 5, enable_log: bool = True,
86
+ cookie_jar: Optional[aiohttp.CookieJar] = None, playwright_state: Dict[str, Any] = None, current_page: int = 1,
87
+ pages: int = 1, order_http_client: HttpClientFactory = None, is_end: bool = True
88
+ ) -> Dict[str, Any]:
89
+ if not order_http_client:
90
+ order_http_client = HttpClientFactory(
91
+ protocol=protocol if protocol == "http" else "https",
92
+ domain=domain,
93
+ timeout=timeout,
94
+ retry=retry,
95
+ enable_log=enable_log,
96
+ cookie_jar=cookie_jar,
97
+ playwright_state=playwright_state
98
+ )
99
+ url = "/OrderList/GuoNei_TicketOutUndo"
100
+ if current_page == 1:
101
+ return await order_http_client.request(method="get", url=url, is_end=is_end)
102
+ else:
103
+ json_data = deepcopy(kwargs)
104
+ json_data["JumpPageFromPage"] = current_page
105
+ json_data["PageCountFormPage"] = pages
106
+ return await order_http_client.request(method="post", url=url, is_end=is_end, json_data=json_data)
107
+
84
108
 
85
109
  def processing_static_headers() -> OrderedDict[str, str]:
86
110
  return OrderedDict([
87
- ("source_ota", "来源"), # 0
111
+ ("source_name", "来源"), # 0
88
112
  ("id", "订单号"), # 1
89
113
  ("raw_order_no", "平台订单号"), # 2
90
114
  ("adult_pnr", "成人PNR"), # 3
@@ -104,7 +128,7 @@ def processing_static_headers() -> OrderedDict[str, str]:
104
128
 
105
129
  def completed_static_headers() -> OrderedDict[str, str]:
106
130
  return OrderedDict([
107
- ("source_ota", "来源"), # 0
131
+ ("source_name", "来源"), # 0
108
132
  ("id", "订单号"), # 1
109
133
  ("raw_order_no", "平台订单号"), # 2
110
134
  ("adult_pnr", "成人PNR"), # 3
@@ -274,8 +298,10 @@ def parse_order_row(cells: ResultSet[Tag], headers: OrderedDict, extend_headers:
274
298
  total_child_key = get_key_by_index(ordered_dict=extend_headers, index=7)
275
299
  order_data[total_adult_key] = safe_convert_advanced(cell_text_slice[1])
276
300
  order_data[total_child_key] = safe_convert_advanced(cell_text_slice[2])
277
- else:
301
+ elif header_key == "receipted":
278
302
  order_data[header_key] = safe_convert_advanced(cell_text)
303
+ else:
304
+ order_data[header_key] = cell_text
279
305
 
280
306
  # 提取特殊属性
281
307
  extract_special_attributes(cells=cells, order_data=order_data, header=headers)
@@ -317,7 +343,7 @@ def parse_order_table(html: str, table_state: str = "proccessing") -> List[Optio
317
343
  cells = row.find_all(['td', 'th'])
318
344
 
319
345
  order_data = parse_order_row(cells, headers=headers, extend_headers=extend, row_index=i)
320
- if order_data:
346
+ if order_data and order_data.get("id") is not None:
321
347
  orders.append(order_data)
322
348
 
323
349
  return orders
@@ -10,8 +10,8 @@
10
10
  # ---------------------------------------------------------------------------------------------------------
11
11
  """
12
12
  from bs4 import BeautifulSoup
13
- from qlv_helper.po.base_po import BasePo
14
13
  from typing import Tuple, Union, List, Any, Dict
14
+ from playwright_helper.libs.base_po import BasePo
15
15
  from playwright.async_api import Page, TimeoutError as PlaywrightTimeoutError, Locator
16
16
 
17
17
 
@@ -105,13 +105,11 @@ class DomesticActivityOrderPage(BasePo):
105
105
  # 解析当前页的数据
106
106
  rows = self.parse_tbody_rows(tbody_html=tbody_html, headers=headers)
107
107
  all_rows.extend(rows)
108
- print(all_rows)
109
108
 
110
109
  # --- 2. 获取当前页数与总页数 ---
111
110
  current_page: int = int(await self.__page.locator('//label[@id="Lab_PageIndex"][1]').inner_text())
112
111
  pages: int = int(await self.__page.locator('//label[@id="Lab_PageCount"][1]').inner_text())
113
112
 
114
- print(current_page, pages)
115
113
  # --- 3. 如果到最后一页,退出 ---
116
114
  if current_page >= pages:
117
115
  break
@@ -122,7 +120,7 @@ class DomesticActivityOrderPage(BasePo):
122
120
  # 等待页面刷新(简单但稳)
123
121
  await self.__page.wait_for_timeout(timeout=refresh_wait_time)
124
122
  except PlaywrightTimeoutError:
125
- all_rows.append(dict(error_message=f"元素 '{self.__table_selector}' 未在 {timeout} 秒内找到"))
123
+ all_rows.append(dict(error_message=f"元素 '{self.__table_selector}' 未在 {refresh_wait_time} 秒内找到"))
126
124
  except Exception as e:
127
125
  all_rows.append(dict(error_message=f"检查元素时发生错误: {str(e)}"))
128
126
 
@@ -10,7 +10,7 @@
10
10
  # ---------------------------------------------------------------------------------------------------------
11
11
  """
12
12
  from typing import Tuple, Union
13
- from qlv_helper.po.base_po import BasePo
13
+ from playwright_helper.libs.base_po import BasePo
14
14
  from qlv_helper.utils.ocr_helper import get_image_text
15
15
  from playwright.async_api import Page, TimeoutError as PlaywrightTimeoutError, Locator
16
16
 
@@ -10,8 +10,7 @@
10
10
  # ---------------------------------------------------------------------------------------------------------
11
11
  """
12
12
  from typing import Tuple, Union
13
- from qlv_helper.po.base_po import BasePo
14
- from qlv_helper.utils.ocr_helper import get_image_text
13
+ from playwright_helper.libs.base_po import BasePo
15
14
  from playwright.async_api import Page, TimeoutError as PlaywrightTimeoutError, Locator
16
15
 
17
16
 
@@ -42,7 +41,7 @@ class MainPage(BasePo):
42
41
  except Exception as e:
43
42
  return False, f"检查元素时发生错误: {str(e)}"
44
43
 
45
- async def get_level1_menu_order_checkout(self) -> Tuple[bool, Union[Locator, str]]:
44
+ async def get_level1_menu_order_checkout(self, timeout: float = 5.0) -> Tuple[bool, Union[Locator, str]]:
46
45
  selector: str = "//span[contains(normalize-space(), '订单出票')]"
47
46
  try:
48
47
  locator = self.__page.locator(selector)
@@ -56,7 +55,7 @@ class MainPage(BasePo):
56
55
  except Exception as e:
57
56
  return False, f"检查元素时发生错误: {str(e)}"
58
57
 
59
- async def get_level2_menu_order_checkout(self) -> Tuple[bool, Union[Locator, str]]:
58
+ async def get_level2_menu_order_checkout(self, timeout: float = 5.0) -> Tuple[bool, Union[Locator, str]]:
60
59
  selector: str = "//a[@menuname='国内活动订单']"
61
60
  try:
62
61
  locator = self.__page.locator(selector)
@@ -0,0 +1,275 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ # ---------------------------------------------------------------------------------------------------------
4
+ # ProjectName: qlv-helper
5
+ # FileName: order_detail_page.py
6
+ # Description: 订单详情页面对象
7
+ # Author: ASUS
8
+ # CreateDate: 2025/12/16
9
+ # Copyright ©2011-2025. Hunan xxxxxxx Company limited. All rights reserved.
10
+ # ---------------------------------------------------------------------------------------------------------
11
+ """
12
+ from typing import Dict, Any, List, Tuple
13
+ from playwright.async_api import Page, Locator
14
+ from playwright_helper.libs.base_po import BasePo
15
+ from qlv_helper.config.url_const import order_detail_url
16
+
17
+
18
+ class OrderDetailPage(BasePo):
19
+ url: str = order_detail_url
20
+ __page: Page
21
+
22
+ def __init__(self, page: Page, url: str = order_detail_url) -> None:
23
+ super().__init__(page, url)
24
+ self.url = url
25
+ self.__page = page
26
+
27
+ async def get_order_lock_state_btn(self, timeout: float = 5.0) -> Tuple[str, Locator]:
28
+ """
29
+ 订单详情页面,获取订单锁单状态按钮
30
+ :param timeout: 超时时间(秒)
31
+ :return: (是否存在, 错误信息|元素对象)
32
+ """
33
+ selector: str = '//div[@class="order_information "]//a[(@class="view_detail_red" and contains(text(), "强制解锁")) or (@class="view_detail_green" and (contains(text(), "锁定") or contains(text(), "解锁返回") or contains(text(), "操作")))]'
34
+ locator: Locator = await self.get_locator(selector=selector, timeout=timeout)
35
+ text: str = (await locator.inner_text()).strip()
36
+ return text, locator
37
+
38
+ async def get_out_ticket_platform_type_dropdown(self, timeout: float = 5.0) -> Locator:
39
+ """
40
+ 订单详情页面,采购信息栏,获取出票地类型下拉菜单按钮
41
+ :param timeout: 超时时间(秒)
42
+ :return: (是否存在, 错误信息|元素对象)
43
+ """
44
+ selector: str = '//table[@id="PurchaseInfos"]//select[@id="OutTktPFTypeID"]'
45
+ return await self.get_locator(selector=selector, timeout=timeout)
46
+
47
+ async def get_out_ticket_platform_type_select_option(self, select_option: str, timeout: float = 5.0) -> Locator:
48
+ """
49
+ 订单详情页面,采购信息栏,获取出票地类型下拉菜单选项
50
+ :param select_option: 要获取的选项
51
+ :param timeout: 超时时间(秒)
52
+ :return: (是否存在, 错误信息|元素对象)
53
+ """
54
+ selector: str = f'//table[@id="PurchaseInfos"]//select[@id="OutTktPFTypeID"]/option[contains(text(), "{select_option}")]'
55
+ return await self.get_locator(selector=selector, timeout=timeout)
56
+
57
+ async def get_out_ticket_platform_dropdown(self, timeout: float = 5.0) -> Locator:
58
+ """
59
+ 订单详情页面,采购信息栏,获取出票平台下拉菜单按钮
60
+ :param timeout: 超时时间(秒)
61
+ :return: (是否存在, 错误信息|元素对象)
62
+ """
63
+ selector: str = '//table[@id="PurchaseInfos"]//select[@id="OutTktPF"]'
64
+ return await self.get_locator(selector=selector, timeout=timeout)
65
+
66
+ async def get_out_ticket_platform_select_option(self, select_option: str, timeout: float = 5.0) -> Locator:
67
+ """
68
+ 订单详情页面,采购信息栏,获取出票平台下拉菜单选项
69
+ :param select_option: 要获取的选项
70
+ :param timeout: 超时时间(秒)
71
+ :return: (是否存在, 错误信息|元素对象)
72
+ """
73
+ selector: str = f'//table[@id="PurchaseInfos"]//select[@id="OutTktPF"]/option[contains(text(), "{select_option}")]'
74
+ return await self.get_locator(selector=selector, timeout=timeout)
75
+
76
+ async def get_out_ticket_account_dropdown(self, timeout: float = 5.0) -> Locator:
77
+ """
78
+ 订单详情页面,采购信息栏,获取出票账号下拉菜单按钮
79
+ :param timeout: 超时时间(秒)
80
+ :return: (是否存在, 错误信息|元素对象)
81
+ """
82
+ selector: str = '//table[@id="PurchaseInfos"]//select[@id="AccountNumber"]'
83
+ return await self.get_locator(selector=selector, timeout=timeout)
84
+
85
+ async def get_out_ticket_account_select_option(self, select_option: str, timeout: float = 5.0) -> Locator:
86
+ """
87
+ 订单详情页面,采购信息栏,获取出票账号下拉菜单选项
88
+ :param select_option: 要获取的选项
89
+ :param timeout: 超时时间(秒)
90
+ :return: (是否存在, 错误信息|元素对象)
91
+ """
92
+ selector: str = f'//table[@id="PurchaseInfos"]//select[@id="AccountNumber"]/option[contains(text(), "{select_option}")]'
93
+ return await self.get_locator(selector=selector, timeout=timeout)
94
+
95
+ async def get_purchase_account_type_dropdown(self, timeout: float = 5.0) -> Locator:
96
+ """
97
+ 订单详情页面,采购信息栏,获取采购账号类型下拉菜单按钮
98
+ :param timeout: 超时时间(秒)
99
+ :return: (是否存在, 错误信息|元素对象)
100
+ """
101
+ selector: str = '//table[@id="PurchaseInfos"]//select[@id="TypeName"]'
102
+ return await self.get_locator(selector=selector, timeout=timeout)
103
+
104
+ async def get_purchase_account_type_select_option(self, select_option: str, timeout: float = 5.0) -> Locator:
105
+ """
106
+ 订单详情页面,采购信息栏,获取采购账号类型下拉菜单选项
107
+ :param select_option: 要获取的选项
108
+ :param timeout: 超时时间(秒)
109
+ :return: (是否存在, 错误信息|元素对象)
110
+ """
111
+ selector: str = f'//table[@id="PurchaseInfos"]//select[@id="TypeName"]/option[contains(text(), "{select_option}")]'
112
+ return await self.get_locator(selector=selector, timeout=timeout)
113
+
114
+ async def get_purchase_account_dropdown(self, timeout: float = 5.0) -> Locator:
115
+ """
116
+ 订单详情页面,采购信息栏,获取采购账号下拉菜单按钮
117
+ :param timeout: 超时时间(秒)
118
+ :return: (是否存在, 错误信息|元素对象)
119
+ """
120
+ selector: str = '//table[@id="PurchaseInfos"]//select[@id="PurchaseAccount"]'
121
+ return await self.get_locator(selector=selector, timeout=timeout)
122
+
123
+ async def get_purchase_account_select_option(self, select_option: str, timeout: float = 5.0) -> Locator:
124
+ """
125
+ 订单详情页面,采购信息栏,获取采购账号下拉菜单选项
126
+ :param select_option: 要获取的选项
127
+ :param timeout: 超时时间(秒)
128
+ :return: (是否存在, 错误信息|元素对象)
129
+ """
130
+ selector: str = f'//table[@id="PurchaseInfos"]//select[@id="PurchaseAccount"]/option[contains(text(), "{select_option}")]'
131
+ return await self.get_locator(selector=selector, timeout=timeout)
132
+
133
+ async def get_purchase_amount_input(self, timeout: float = 5.0) -> Locator:
134
+ """
135
+ 订单详情页面,采购信息栏,获取采购金额输入框
136
+ :param timeout: 超时时间(秒)
137
+ :return: (是否存在, 错误信息|元素对象)
138
+ """
139
+ selector: str = '//table[@id="PurchaseInfos"]//input[@id="TransactionAmount"]'
140
+ return await self.get_locator(selector=selector, timeout=timeout)
141
+
142
+ async def get_remark_input(self, timeout: float = 5.0) -> Locator:
143
+ """
144
+ 订单详情页面,采购信息栏,获取备注输入框,一般输入登录官网的"账号/密码"
145
+ :param timeout: 超时时间(秒)
146
+ :return: (是否存在, 错误信息|元素对象)
147
+ """
148
+ selector: str = '//table[@id="PurchaseInfos"]//input[@id="Remark"]'
149
+ return await self.get_locator(selector=selector, timeout=timeout)
150
+
151
+ async def get_main_check_input(self, timeout: float = 5.0) -> Locator:
152
+ """
153
+ 订单详情页面,采购信息栏,获取对账标识输入框,一般输入虚拟卡的card_id
154
+ :param timeout: 超时时间(秒)
155
+ :return: (是否存在, 错误信息|元素对象)
156
+ """
157
+ selector: str = '//table[@id="PurchaseInfos"]//input[@id="MainCheckNumber"]'
158
+ return await self.get_locator(selector=selector, timeout=timeout)
159
+
160
+ async def get_air_comp_order_id_input(self, timeout: float = 5.0) -> Locator:
161
+ """
162
+ 订单详情页面,采购信息栏,获取官网订单号输入框
163
+ :param timeout: 超时时间(秒)
164
+ :return: (是否存在, 错误信息|元素对象)
165
+ """
166
+ selector: str = '//table[@id="PurchaseInfos"]//input[@id="AirCoOrderID"]'
167
+ return await self.get_locator(selector=selector, timeout=timeout)
168
+
169
+ async def get_procurement_info_save_btn(self, timeout: float = 5.0) -> Locator:
170
+ """
171
+ 订单详情页面,采购信息栏,获取【保存采购】按钮
172
+ :param timeout: 超时时间(秒)
173
+ :return: (是否存在, 错误信息|元素对象)
174
+ """
175
+ selector: str = '//table[@id="PurchaseInfos"]//input[@id="submit"]'
176
+ return await self.get_locator(selector=selector, timeout=timeout)
177
+
178
+ async def get_passenger_itinerary_locators(self, timeout: float = 5.0) -> List[Dict[str, Any]]:
179
+ """
180
+ 订单详情页面,乘客信息栏,获取票号输入框
181
+ :param timeout:超时时间(秒)
182
+ :return: List[{
183
+ "username": str,
184
+ "id_number": str,
185
+ "locator": Locator # 指向 <input name="ticketNo">
186
+ }]
187
+ """
188
+ results = list()
189
+ tbody_selector: str = '(//div[@class="order_information"]//table[@class="table table_border table_center"]/tbody)[3]'
190
+ tbody_locator: Locator = await self.get_locator(selector=tbody_selector, timeout=timeout)
191
+ # 只选真正的数据行(有 pid / ticketNo 的 tr)
192
+ rows = tbody_locator.locator("tr[pid]")
193
+ row_count = await rows.count()
194
+ for i in range(row_count):
195
+ row = rows.nth(i)
196
+
197
+ out_ticket_state: Locator = await self.get_sub_locator(
198
+ locator=row, selector='xpath=(.//td)[1]', timeout=timeout
199
+ )
200
+ out_ticket_state_text = (await out_ticket_state.inner_text()).strip()
201
+ if "已出票" in out_ticket_state_text:
202
+ continue
203
+
204
+ # 1️⃣ 用户名
205
+ name_locator = await self.get_sub_locator(
206
+ locator=row, selector='xpath=.//span[@name="pname"]', timeout=timeout
207
+ )
208
+ username = (await name_locator.first.inner_text()).strip()
209
+
210
+ # 2️⃣ 身份证号
211
+ # a 标签里:javascript:showIDNo('身份证号', ...)
212
+ id_link = await self.get_sub_locator(locator=row, selector='xpath=.//a[starts-with(@id, "IDNo_")]',
213
+ timeout=timeout)
214
+
215
+ href = await id_link.first.get_attribute("href")
216
+ # href 示例:
217
+ # javascript:showIDNo('320324198511254466','175687',...)
218
+
219
+ id_number = None
220
+ if href:
221
+ # 非正则版本,更安全
222
+ start = href.find("'") + 1
223
+ end = href.find("'", start)
224
+ id_number = href[start:end]
225
+
226
+ # 3️⃣ ticketNo 输入框 locator
227
+ ticket_input = await self.get_sub_locator(locator=row, selector='xpath=.//input[@name="ticketNo"]',
228
+ timeout=timeout)
229
+
230
+ # 防御性校验(可选)
231
+ if await ticket_input.count() == 0:
232
+ continue
233
+
234
+ results.append({
235
+ "username": username,
236
+ "id_number": id_number,
237
+ "locator": ticket_input # Playwright Locator 对象
238
+ })
239
+ return results
240
+
241
+ async def get_custom_remark_input(self, timeout: float = 5.0) -> Locator:
242
+ """
243
+ 订单详情页面,日志记录栏,获取自定义备注输入框
244
+ :param timeout: 超时时间(秒)
245
+ :return: (是否存在, 错误信息|元素对象)
246
+ """
247
+ selector: str = '//input[@id="rmk"]'
248
+ return await self.get_locator(selector=selector, timeout=timeout)
249
+
250
+ async def get_custom_remark_save_btn(self, timeout: float = 5.0) -> Locator:
251
+ """
252
+ 订单详情页面,日志记录栏,获取【保存备注】按钮
253
+ :param timeout: 超时时间(秒)
254
+ :return: (是否存在, 错误信息|元素对象)
255
+ """
256
+ selector: str = '//a[@href="javascript:fnSaveRemark()"]'
257
+ return await self.get_locator(selector=selector, timeout=timeout)
258
+
259
+ async def get_message_notice_dialog_confirm_btn(self, timeout: float = 5.0) -> Locator:
260
+ """
261
+ 订单详情页面,消息提醒弹框,获取【确认】按钮
262
+ :param timeout: 超时时间(秒)
263
+ :return: (是否存在, 错误信息|元素对象)
264
+ """
265
+ selector: str = '//a[@id="alertMsg_btnConfirm"]/cite'
266
+ return await self.get_locator(selector=selector, timeout=timeout)
267
+
268
+ async def get_policy_input(self, timeout: float = 5.0) -> Locator:
269
+ """
270
+ 订单详情页面,获取政策代码输入框
271
+ :param timeout:
272
+ :return:
273
+ """
274
+ selector: str = '//legend//input[@name="PolicyName"]'
275
+ return await self.get_locator(selector=selector, timeout=timeout)
@@ -12,7 +12,7 @@
12
12
  import os
13
13
  import asyncio
14
14
  from typing import Tuple, Union
15
- from qlv_helper.po.base_po import BasePo
15
+ from playwright_helper.libs.base_po import BasePo
16
16
  from qlv_helper.utils.file_handle import get_caller_dir
17
17
  from qlv_helper.utils.windows_utils import gen_allow_btn_image, windows_on_click
18
18
  from playwright.async_api import Page, TimeoutError as PlaywrightTimeoutError, Locator
qlv_helper/po/base_po.py DELETED
@@ -1,40 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- """
3
- # ---------------------------------------------------------------------------------------------------------
4
- # ProjectName: qlv-helper
5
- # FileName: base_po.py
6
- # Description: po对象基础类
7
- # Author: ASUS
8
- # CreateDate: 2025/11/25
9
- # Copyright ©2011-2025. Hunan xxxxxxx Company limited. All rights reserved.
10
- # ---------------------------------------------------------------------------------------------------------
11
- """
12
- from typing import List
13
- from playwright.async_api import Page
14
-
15
-
16
- class BasePo(object):
17
- __page: Page
18
-
19
- def __init__(self, page: Page, url: str):
20
- self.url = url
21
- self.__page = page
22
- if self.is_current_page() is False:
23
- raise ValueError("page参数值无效")
24
-
25
- def get_page(self) -> Page:
26
- return self.__page
27
-
28
- def is_current_page(self) -> bool:
29
- url = self.__page.url.split("?")[0]
30
- if isinstance(self.__page, Page) and url.endswith(self.url):
31
- return True
32
- else:
33
- return False
34
-
35
- def get_url_domain(self) -> str:
36
- if isinstance(self.__page, Page):
37
- page_slice: List[str] = self.__page.url.split("/")
38
- return f"{page_slice[0]}://{page_slice[2]}"
39
- else:
40
- raise AttributeError("PO对象中的page属性未被初始化")