python-qlv-helper 0.2.0__py3-none-any.whl → 0.6.0__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,8 +10,10 @@
10
10
  # ---------------------------------------------------------------------------------------------------------
11
11
  """
12
12
  import re
13
+ import json
13
14
  import aiohttp
14
15
  from datetime import datetime
16
+ from urllib.parse import quote
15
17
  from bs4 import BeautifulSoup, Tag
16
18
  from collections import OrderedDict
17
19
  from typing import Dict, Any, Optional, List
@@ -22,8 +24,8 @@ from qlv_helper.utils.type_utils import get_key_by_index, get_value_by_index, sa
22
24
 
23
25
 
24
26
  async def get_order_page_html(
25
- order_id: int, domain: str, protocol: str = "http", retry: int = 1, timeout: int = 5, enable_log: bool = True,
26
- cookie_jar: Optional[aiohttp.CookieJar] = None, playwright_state: Dict[str, Any] = None
27
+ *, order_id: int, domain: str, protocol: str = "http", retry: int = 1, timeout: int = 5,
28
+ enable_log: bool = True, cookie_jar: Optional[aiohttp.CookieJar] = None, playwright_state: Dict[str, Any] = None
27
29
  ) -> Dict[str, Any]:
28
30
  order_http_client = HttpClientFactory(
29
31
  protocol=protocol if protocol == "http" else "https",
@@ -41,6 +43,47 @@ async def get_order_page_html(
41
43
  )
42
44
 
43
45
 
46
+ async def fill_procurement_info_with_http(
47
+ *, order_id: int, qlv_domain: str, amount: float, pre_order_id: str, platform_user_id: str, user_password: str,
48
+ passengers: List[str], fids: str, pids: List[str], transaction_id: str, qlv_protocol: str = "http",
49
+ retry: int = 1, timeout: int = 5, enable_log: bool = True, cookie_jar: Optional[aiohttp.CookieJar] = None,
50
+ playwright_state: Dict[str, Any] = None, data_list: Optional[List[Dict[str, Any]]] = None
51
+ ) -> Dict[str, Any]:
52
+ client = HttpClientFactory(
53
+ 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
56
+ )
57
+
58
+ headers = {
59
+ "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
60
+ "Referer": f"{qlv_protocol}://{qlv_domain}/OrderProcessing/NewTicket/{order_id}?&r={datetime.now().strftime("%Y%m%d%H%M%S")}",
61
+ }
62
+ if data_list:
63
+ data = data_list
64
+ else:
65
+ remark = f"{platform_user_id}/{user_password}"
66
+ pName = "," + ",".join(passengers) + ","
67
+ pids = ",".join(pids)
68
+ data = [
69
+ {"tradingDat": datetime.now().strftime("%Y-%m-%d %H:%M"), "outTktPF": "G航司官网", "outTktLoginCode": "",
70
+ "typeName": "VCC", "accountID": "8", "accountName": "VCC", "transactionAmount": f"{amount}",
71
+ "mainCheckNumber": "", "airCoOrderID": f"{pre_order_id}", "QuotaResultAmount": "0.00",
72
+ "remark": f"{quote(remark)}", "flightIdx": ",1,", "pName": f"{pName}", "orderID": f"{order_id}",
73
+ "businessTypeName": "机票", "tradingItems": "机票支出", "actualAmount": 0, "pType": "成人",
74
+ "fids": f"{fids}", "pids": f"{pids}", "iscandel": "true", "isbatch": "false",
75
+ "MainCheckNumberValus": f"{transaction_id}",
76
+ "OfficeNo": "", "PriceStdActual": "0.00", "ReturnAmount": "0.0000", "OffsetReturnAmount": "0.00",
77
+ "profitRemark": "", "preSaleType": "", "ErrorType": "", "OutTktPFTypeID": "34", "OutTicketAccount": "",
78
+ "OutTicketAccountID": "", "OutTicketPWD": "", "OutTicketTel": "", "OutTicketPNR": ""}
79
+ ]
80
+ data = f"list={json.dumps(data)}&isPayAll=true&delTransactionids=&OutTicketLossType&OutTicketLossRemark="
81
+ return await client.request(
82
+ method="POST", url="/OrderProcessing/PurchaseInfoSave",
83
+ headers=headers, is_end=True, data=data.encode("utf-8")
84
+ )
85
+
86
+
44
87
  def order_info_static_headers() -> OrderedDict[str, str]:
45
88
  return OrderedDict([
46
89
  ("receipted_ota", "OTA实收"), # 0
@@ -55,40 +98,73 @@ def order_info_static_headers() -> OrderedDict[str, str]:
55
98
 
56
99
  def parser_order_info(html: str) -> Dict[str, Any]:
57
100
  soup = BeautifulSoup(html, "html.parser")
58
- # 找到目标 table
59
- table = soup.find("table", class_="table no_border")
60
- if not table:
101
+ # 找到目标 table_border
102
+ table_border = soup.find("table", class_="table no_border")
103
+ if not table_border:
104
+ return {}
105
+
106
+ table_flight = soup.find("table", class_="info_flight")
107
+ if not table_border:
61
108
  return {}
62
109
 
63
110
  # 所有 td
64
- tds = table.find_all("td")
111
+ tds_border = table_border.find_all("td")
65
112
  result = {}
66
113
 
67
- for td in tds:
68
- text = td.get_text(strip=True)
69
- # 如果 td 内没有冒号、也不是按钮,跳过
70
- if ":" not in text:
71
- continue
72
- # 按 ":" 进行分割
114
+ for td in tds_border:
73
115
  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)
116
+ text = td.get_text(strip=True)
117
+ # 如果 td 内没有冒号、也不是按钮,跳过
118
+ if ":" not in text:
119
+ continue
120
+ # ":" 进行分割
121
+ try:
122
+ key, value = text.split(":", 1)
123
+ except (Exception, ValueError):
124
+ continue
125
+ # 去掉换行符、空格
126
+ key = key.strip()
127
+ value = value.strip()
128
+
129
+ # 如果 value 为空,尝试取 <b> 或其他控件内文本
130
+ if not value:
131
+ b = td.find("b")
132
+ if b:
133
+ value = b.get_text(strip=True)
134
+
135
+ # 去掉尾部不需要的空格
136
+ value = value.replace("\u00a0", "").strip()
137
+ value = value[1:] if isinstance(value, str) and value.startswith("[") else value
138
+ value = value[:-1] if isinstance(value, str) and value.endswith("]") else value
139
+ result[key] = safe_convert_advanced(value)
140
+ except (Exception,):
141
+ pass
142
+
143
+ # 所有 td
144
+ tds_flight = table_flight.find_all("td")
145
+
146
+ for td in tds_flight:
147
+ try:
148
+ dat_dep = td.find("span", class_="DatDep")
149
+ if dat_dep:
150
+ text = dat_dep.get_text(strip=True)
151
+ result["dat_dep"] = text + ":00"
152
+ city_dep = td.find("span", class_="CityDep")
153
+ if city_dep:
154
+ text = city_dep.get_text(strip=True)
155
+ city_dep = text.split("【")[0].strip()
156
+ result["city_dep"] = city_dep
157
+ dat_arr = td.find("p", class_="DatArr")
158
+ if dat_arr:
159
+ text = dat_arr.get_text(strip=True)
160
+ result["dat_arr"] = text + ":00"
161
+ city_arr = td.find("span", class_="CityArr")
162
+ if city_arr:
163
+ text = city_arr.get_text(strip=True)
164
+ city_arr = text.split("【")[0].strip()
165
+ result["city_arr"] = city_arr
166
+ except (Exception,):
167
+ pass
92
168
  return convert_cn_to_en(data=result, header_map=order_info_static_headers())
93
169
 
94
170
 
@@ -121,6 +197,8 @@ def flight_extend_headers() -> OrderedDict[str, str]:
121
197
  ("id_valid_dat", " 证件有效期"), # 9
122
198
  ("code_dep", " 起飞机场"), # 10
123
199
  ("code_arr", " 抵达机场"), # 11
200
+ ("pid", "乘客ID"), # 12
201
+ ("fid", "航段ID"), # 13
124
202
  ])
125
203
 
126
204
 
@@ -236,7 +314,20 @@ def parse_order_flight_table_passenger_info(raw: Tag, headers: OrderedDict[str,
236
314
  nationality = guobies[0].get_text(strip=True) if len(guobies) > 0 else ""
237
315
  issue_country = guobies[1].get_text(strip=True) if len(guobies) > 1 else ""
238
316
 
239
- return {
317
+ a1 = raw.find("a", id=lambda x: x and x.startswith("IDNo_"))
318
+ a2 = raw.find("a", id=lambda x: x and x.startswith("detrni_"))
319
+ a3 = raw.find("a", id=lambda x: x and x.startswith("detrnif_"))
320
+ pid = None
321
+ if a1:
322
+ full_id = a1["id"] # IDNo_279778
323
+ pid = full_id.split("_")[-1]
324
+ elif a2:
325
+ full_id = a2["id"] # detrni_279778
326
+ pid = full_id.split("_")[-1]
327
+ elif a3:
328
+ full_id = a3["id"] # detrnif_279778
329
+ pid = full_id.split("_")[-1]
330
+ result = {
240
331
  get_key_by_index(index=0, ordered_dict=headers): name, # 姓名
241
332
  get_key_by_index(index=1, ordered_dict=headers): ptype, # 类型: 成人/儿童
242
333
  get_key_by_index(index=2, ordered_dict=headers): id_type, # 证件类型
@@ -246,8 +337,12 @@ def parse_order_flight_table_passenger_info(raw: Tag, headers: OrderedDict[str,
246
337
  get_key_by_index(index=6, ordered_dict=headers): sex, # 性别
247
338
  get_key_by_index(index=7, ordered_dict=headers): nationality, # 国籍
248
339
  get_key_by_index(index=8, ordered_dict=headers): issue_country, # 签发国
249
- get_key_by_index(index=9, ordered_dict=headers): id_valid # 证件有效期
340
+ get_key_by_index(index=9, ordered_dict=headers): id_valid, # 证件有效期
250
341
  }
342
+ if pid is not None:
343
+ # 乘客ID
344
+ result[get_key_by_index(index=12, ordered_dict=headers)] = pid
345
+ return result
251
346
 
252
347
 
253
348
  def parse_order_flight_table_row(
@@ -255,8 +350,7 @@ def parse_order_flight_table_row(
255
350
  ) -> Dict[str, Any]:
256
351
  """解析航班表每一行的数据"""
257
352
  tds = tr.find_all("td", recursive=False)
258
- values = {}
259
-
353
+ values = {get_key_by_index(index=12, ordered_dict=extend_headers): tr["pid"]}
260
354
  for idx, td in enumerate(tds):
261
355
  if idx >= len(headers):
262
356
  continue
@@ -270,6 +364,14 @@ def parse_order_flight_table_row(
270
364
  else:
271
365
  raw = clean_order_flight_table(html=td)
272
366
  if "行程" in value:
367
+ fid = ""
368
+ input_tag = td.find('input', {'name': 'fid'})
369
+ fid_key = get_key_by_index(index=13, ordered_dict=extend_headers)
370
+ if input_tag:
371
+ match = re.search(r'\d+', input_tag.get('value'))
372
+ if match:
373
+ fid = match.group()
374
+ values[fid_key] = fid
273
375
  code_dep_key = get_key_by_index(index=10, ordered_dict=extend_headers)
274
376
  code_arr_key = get_key_by_index(index=11, ordered_dict=extend_headers)
275
377
  raw_slice = raw.split("-")
@@ -280,7 +382,6 @@ def parse_order_flight_table_row(
280
382
  values[key] = raw
281
383
  else:
282
384
  values[key] = safe_convert_advanced(raw)
283
-
284
385
  return values
285
386
 
286
387
 
@@ -293,6 +394,8 @@ def extract_structured_table_data(table: Tag) -> List[Optional[Dict[str, Any]]]:
293
394
  for tr in table.find_all("tr")[1:]: # 跳过表头
294
395
  rows.append(parse_order_flight_table_row(tr=tr, headers=headers, extend_headers=extend))
295
396
 
397
+ if rows:
398
+ rows = list({i["id_no"]: i for i in sorted(rows, key=lambda x: bool(x.get("fid")))}.values())
296
399
  return rows
297
400
 
298
401
 
@@ -300,14 +403,12 @@ def parser_order_flight_table(html: str) -> List[Optional[Dict[str, Any]]]:
300
403
  """解析航班表"""
301
404
  soup = BeautifulSoup(html, 'html.parser')
302
405
  # 三个主要的order_sort div
303
- order_sections = soup.find_all('div', class_='order_sort')
304
- section = order_sections[3] if len(order_sections) > 3 else Tag(name="")
406
+ table_sections = soup.find_all('table', class_='table table_border table_center')
407
+ table = table_sections[2] if len(table_sections) > 2 else Tag(name="")
305
408
  results = list()
306
409
 
307
- tables = section.find_all('table', class_='table table_border table_center')
308
- for table in tables:
309
- table_data = extract_structured_table_data(table)
310
- if table_data:
311
- results.extend(table_data)
410
+ table_data = extract_structured_table_data(table)
411
+ if table_data:
412
+ results.extend(table_data)
312
413
 
313
414
  return results
@@ -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)