解密机大侠.py 39 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063
  1. import sys
  2. import requests
  3. import json
  4. import os
  5. import re
  6. import uuid
  7. import hashlib
  8. import time
  9. import threading
  10. from concurrent.futures import ThreadPoolExecutor, as_completed
  11. from Crypto.Cipher import AES
  12. from Crypto.Util.Padding import unpad
  13. from typing import Optional, List, Dict, Any
  14. from PyQt5.QtWidgets import *
  15. from PyQt5.QtCore import *
  16. from PyQt5.QtGui import *
  17. from requests.adapters import HTTPAdapter
  18. from urllib3.util.retry import Retry
  19. # --- 核心功能类 ---
  20. class OrderManager:
  21. def __init__(self, access_token, ocr_url):
  22. self.access_token = access_token
  23. self.ocr_url = ocr_url
  24. self.headers = {
  25. 'Connection': 'keep-alive',
  26. 'content-type': 'application/json',
  27. 'Version': '2.50.0',
  28. 'timestamp': '1784094158706',
  29. 'Distinct-Id': '1784057137891-1059356-086b42dd38b0b98-24491897',
  30. 'vaccine': 'aa454c048e8843247cebf378d1a9a0fed759027f329caf68fd900654da4232d0',
  31. 'U-A': 'JiDaXiaWeApp/23958 (:)',
  32. 'x-version': 'ITC-77385',
  33. 'ahs-gate-language': 'zh_CN',
  34. 'APP-ID': 'jdx135624',
  35. 'platform': 'weapp',
  36. 'Register-Flow-Source': 'RECYCLER',
  37. 'ahs-language': 'zh-CN',
  38. 'access_token': access_token,
  39. 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 26_5_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/8.0.75(0x18004b3c) NetType/WIFI Language/zh_CN',
  40. 'Referer': 'https://servicewechat.com/wx7e6573879e91b1ac/28/page-frame.html',
  41. }
  42. self.key = b"S9u978T13NLCGc5W"
  43. self.iv = b"X83yWMD9iKhLxfwX"
  44. # 缓存
  45. self.order_detail_cache = {}
  46. self.ocr_cache = {}
  47. self.cache_lock = threading.Lock()
  48. # 创建会话并配置连接池
  49. self.session = requests.Session()
  50. retry_strategy = Retry(
  51. total=2,
  52. backoff_factor=0.5,
  53. status_forcelist=[429, 500, 502, 503, 504],
  54. )
  55. adapter = HTTPAdapter(
  56. pool_connections=20,
  57. pool_maxsize=20,
  58. max_retries=retry_strategy
  59. )
  60. self.session.mount('http://', adapter)
  61. self.session.mount('https://', adapter)
  62. def decrypt_order_data(self, hex_string):
  63. """解密订单数据"""
  64. encrypted_data = bytes.fromhex(hex_string)
  65. cipher = AES.new(self.key, AES.MODE_CBC, self.iv)
  66. decrypted_data = unpad(cipher.decrypt(encrypted_data), AES.block_size)
  67. return decrypted_data.decode('utf-8')
  68. def get_pending_order_list(self, page_size=10):
  69. """获取待抢订单列表"""
  70. url = "https://wirelessgate.aihuishou.com/jdx-rc-bff-service/app/order/pendingGrab/list"
  71. payload = {
  72. "pageSize": page_size,
  73. "status": -1,
  74. "isApplyForHandle": None,
  75. "applyPhotoStatus": None,
  76. "productIds": [265024, 225362, 225361, 225358, 225366, 173473, 161415, 161414, 161413, 161412, 121769, 121439, 121436, 121435, 98526, 98528, 98527, 98525, 43513, 43512, 43511, 43510, 36047, 36045, 36044, 36046, 32292, 32291, 32290, 27640, 27639, 27637, 25827, 25680, 25679, 23423, 23422, 66428, 34701, 20079, 17752, 17726, 17462, 17461, 17459, 17458, 17457, 17460, 17455, 2247, 2246],
  77. "sourceIds": None,
  78. "keyword": None,
  79. "timeObj": None,
  80. "rcOrderResult": None
  81. }
  82. try:
  83. response = self.session.post(url, headers=self.headers, json=payload, timeout=30)
  84. response.raise_for_status()
  85. result = response.json()
  86. if result.get('code') != 200:
  87. return None, result.get('resultMessage', '未知错误')
  88. return result.get('data', []), None
  89. except Exception as e:
  90. return None, str(e)
  91. def get_order_detail(self, order_no):
  92. """获取订单详情(带缓存)"""
  93. # 检查缓存
  94. with self.cache_lock:
  95. if order_no in self.order_detail_cache:
  96. return self.order_detail_cache[order_no], None
  97. # 获取BiddingNo
  98. snatch_url = "https://wirelessgate.aihuishou.com/jdx-rc-service/front/bid/snatch"
  99. snatch_payload = {"orderNo": order_no}
  100. try:
  101. snatch_response = self.session.post(snatch_url, headers=self.headers, json=snatch_payload, timeout=15)
  102. snatch_response.raise_for_status()
  103. snatch_data = snatch_response.json()
  104. if snatch_data.get('code') != 200:
  105. return None, snatch_data.get('resultMessage', '获取BiddingNo失败')
  106. bidding_no = snatch_data.get('data')
  107. if not bidding_no:
  108. return None, "获取BiddingNo为空"
  109. except Exception as e:
  110. return None, str(e)
  111. # 获取订单详情
  112. detail_url = f"https://wirelessgate.aihuishou.com/jdx-qa-service/app/ka/v3/order/detail?orderNo={order_no}&biddingNo={bidding_no}"
  113. try:
  114. detail_response = self.session.get(detail_url, headers=self.headers, timeout=15)
  115. detail_response.raise_for_status()
  116. detail_data = detail_response.json()
  117. if detail_data.get('code') != 200:
  118. return None, detail_data.get('resultMessage', '获取订单详情失败')
  119. encrypted_hex = detail_data.get('data')
  120. if not encrypted_hex:
  121. return None, "未获取到加密数据"
  122. decrypted_json_string = self.decrypt_order_data(encrypted_hex)
  123. order_detail = json.loads(decrypted_json_string)
  124. # 存入缓存
  125. with self.cache_lock:
  126. self.order_detail_cache[order_no] = order_detail
  127. return order_detail, None
  128. except Exception as e:
  129. return None, str(e)
  130. def find_original_image_by_notice(self, json_data, notice_keyword):
  131. """优化的图片查找(使用栈迭代)"""
  132. if isinstance(json_data, str):
  133. try:
  134. json_data = json.loads(json_data)
  135. except json.JSONDecodeError:
  136. return None
  137. # 使用栈迭代而非递归
  138. stack = [json_data]
  139. keyword_lower = notice_keyword.lower()
  140. while stack:
  141. obj = stack.pop()
  142. if isinstance(obj, dict):
  143. # 检查当前字典
  144. if 'notice' in obj and 'originalImage' in obj:
  145. if keyword_lower in obj['notice'].lower():
  146. return obj['originalImage']
  147. # 将所有值加入栈
  148. for value in obj.values():
  149. stack.append(value)
  150. elif isinstance(obj, list):
  151. # 将列表元素加入栈
  152. stack.extend(obj)
  153. return None
  154. def get_all_image_notices(self, json_data):
  155. """获取所有图片的notice信息"""
  156. notices = []
  157. stack = [json_data]
  158. while stack:
  159. obj = stack.pop()
  160. if isinstance(obj, dict):
  161. if 'notice' in obj and 'image' in obj:
  162. notices.append(obj['notice'])
  163. for value in obj.values():
  164. stack.append(value)
  165. elif isinstance(obj, list):
  166. stack.extend(obj)
  167. return list(set(notices))
  168. def ocr_by_url(self, image_url):
  169. """通过URL进行OCR识别(带缓存)"""
  170. # 检查缓存
  171. cache_key = hashlib.md5(image_url.encode()).hexdigest()
  172. with self.cache_lock:
  173. if cache_key in self.ocr_cache:
  174. return self.ocr_cache[cache_key]
  175. try:
  176. payload = {"url": image_url}
  177. response = self.session.post(self.ocr_url, json=payload, timeout=30)
  178. if response.status_code == 200:
  179. ocr_result = response.json()
  180. if ocr_result.get('success'):
  181. # 存入缓存
  182. with self.cache_lock:
  183. self.ocr_cache[cache_key] = ocr_result
  184. return ocr_result
  185. return None
  186. except Exception as e:
  187. return None
  188. def extract_text_from_ocr_result(self, ocr_result):
  189. """提取OCR文本"""
  190. if not ocr_result or not ocr_result.get('texts'):
  191. return ''
  192. texts = ''
  193. for item in ocr_result['texts']:
  194. rec_texts = item.get('rec_texts', [])
  195. for text in rec_texts:
  196. texts = texts + text
  197. return texts
  198. def extract_charge_count(self, text):
  199. """提取充电次数"""
  200. # 匹配(232次循环)格式
  201. pattern1 = r'[((]\s*(\d+)\s*次\s*循环\s*[))]'
  202. match = re.search(pattern1, text)
  203. if match:
  204. return int(match.group(1))
  205. # 匹配充电次数123次格式
  206. pattern2 = r'充电次数(\d+)次'
  207. match = re.search(pattern2, text)
  208. if match:
  209. return int(match.group(1))
  210. # 匹配单独的 数字+次 格式(如 71477次)
  211. pattern3 = r'(\d+)次'
  212. match = re.search(pattern3, text)
  213. if match:
  214. return int(match.group(1))
  215. return None
  216. def find_best_image(self, order_detail):
  217. """查找最佳验机图片(按优先级)"""
  218. keywords = [
  219. "苹果沙漏验机报告",
  220. "沙漏图",
  221. "苹果验机软件",
  222. "苹果沙漏验机基本信息",
  223. "验机照片",
  224. "验机报告"
  225. ]
  226. for keyword in keywords:
  227. img = self.find_original_image_by_notice(order_detail, keyword)
  228. if img:
  229. return img
  230. return None
  231. # --- 后台工作线程 ---
  232. class OrderListWorker(QThread):
  233. finished = pyqtSignal(list)
  234. error = pyqtSignal(str)
  235. def __init__(self, order_manager):
  236. super().__init__()
  237. self.order_manager = order_manager
  238. def run(self):
  239. try:
  240. order_list, error = self.order_manager.get_pending_order_list(100)
  241. if error:
  242. self.error.emit(error)
  243. else:
  244. self.finished.emit(order_list)
  245. except Exception as e:
  246. self.error.emit(str(e))
  247. class ChargeQueryWorker(QThread):
  248. finished = pyqtSignal(int, object, object, list)
  249. error = pyqtSignal(int, str)
  250. def __init__(self, order_manager, order_no, index):
  251. super().__init__()
  252. self.order_manager = order_manager
  253. self.order_no = order_no
  254. self.index = index
  255. def run(self):
  256. try:
  257. # 获取订单详情
  258. order_detail, error = self.order_manager.get_order_detail(self.order_no)
  259. if error:
  260. self.error.emit(self.index, error)
  261. return
  262. # 查找所有图片的notice
  263. image_notices = self.order_manager.get_all_image_notices(order_detail)
  264. # 查找验机图片
  265. report_img = self.order_manager.find_best_image(order_detail)
  266. if not report_img:
  267. self.finished.emit(self.index, None, None, image_notices)
  268. return
  269. # OCR识别
  270. ocr_result = self.order_manager.ocr_by_url(report_img)
  271. if not ocr_result:
  272. self.finished.emit(self.index, None, None, image_notices)
  273. return
  274. # 提取充电次数
  275. ocr_text = self.order_manager.extract_text_from_ocr_result(ocr_result)
  276. charge_count = self.order_manager.extract_charge_count(ocr_text)
  277. self.finished.emit(self.index, charge_count, ocr_result, image_notices)
  278. except Exception as e:
  279. self.error.emit(self.index, str(e))
  280. # --- 主窗口 ---
  281. class MainWindow(QMainWindow):
  282. def __init__(self):
  283. super().__init__()
  284. self.order_manager = None
  285. self.order_list = []
  286. self.order_details_cache = {}
  287. self.charge_counts = {}
  288. self.is_batch_processing = False
  289. self.batch_queue = []
  290. self.current_batch_index = 0
  291. # 并行处理相关
  292. self.thread_pool = ThreadPoolExecutor(max_workers=5)
  293. self.future_tasks = {}
  294. self.processing_lock = threading.Lock()
  295. self.completed_count = 0
  296. self.total_tasks = 0
  297. self.init_ui()
  298. def init_ui(self):
  299. self.setWindowTitle('爱回收订单查询工具 (优化版)')
  300. self.setGeometry(100, 100, 1200, 800)
  301. # 设置样式
  302. self.setStyleSheet("""
  303. QMainWindow {
  304. background-color: #f0f0f0;
  305. }
  306. QGroupBox {
  307. font-weight: bold;
  308. border: 1px solid #ccc;
  309. border-radius: 5px;
  310. margin-top: 10px;
  311. padding-top: 10px;
  312. }
  313. QGroupBox::title {
  314. subcontrol-origin: margin;
  315. left: 10px;
  316. padding: 0 5px 0 5px;
  317. }
  318. QPushButton {
  319. background-color: #0078d7;
  320. color: white;
  321. border: none;
  322. padding: 8px 16px;
  323. border-radius: 4px;
  324. font-weight: bold;
  325. }
  326. QPushButton:hover {
  327. background-color: #005a9e;
  328. }
  329. QPushButton:disabled {
  330. background-color: #cccccc;
  331. }
  332. QPushButton#batch_btn {
  333. background-color: #28a745;
  334. }
  335. QPushButton#batch_btn:hover {
  336. background-color: #218838;
  337. }
  338. QPushButton#batch_btn:disabled {
  339. background-color: #cccccc;
  340. }
  341. QLineEdit, QTextEdit {
  342. border: 1px solid #ccc;
  343. border-radius: 4px;
  344. padding: 5px;
  345. }
  346. QTableWidget {
  347. border: 1px solid #ccc;
  348. border-radius: 4px;
  349. gridline-color: #e0e0e0;
  350. }
  351. QTableWidget::item {
  352. padding: 5px;
  353. }
  354. QHeaderView::section {
  355. background-color: #e8e8e8;
  356. padding: 5px;
  357. border: 1px solid #ccc;
  358. font-weight: bold;
  359. }
  360. QStatusBar {
  361. background-color: #f0f0f0;
  362. color: #333;
  363. }
  364. #log_text {
  365. background-color: #1e1e1e;
  366. color: #d4d4d4;
  367. font-family: 'Consolas', 'Courier New', monospace;
  368. font-size: 11px;
  369. }
  370. QProgressBar {
  371. border: 1px solid #ccc;
  372. border-radius: 4px;
  373. text-align: center;
  374. }
  375. QProgressBar::chunk {
  376. background-color: #0078d7;
  377. border-radius: 4px;
  378. }
  379. """)
  380. central_widget = QWidget()
  381. self.setCentralWidget(central_widget)
  382. main_layout = QVBoxLayout(central_widget)
  383. main_layout.setSpacing(10)
  384. # --- 顶部配置区域 ---
  385. config_group = QGroupBox("配置")
  386. config_layout = QGridLayout()
  387. config_layout.addWidget(QLabel("Access Token:"), 0, 0)
  388. self.token_input = QLineEdit()
  389. self.token_input.setText("jdx9ce52336abee4cf78db56567dc259512")
  390. self.token_input.setEchoMode(QLineEdit.Password)
  391. config_layout.addWidget(self.token_input, 0, 1)
  392. self.show_token_btn = QPushButton("显示")
  393. self.show_token_btn.setFixedWidth(60)
  394. self.show_token_btn.clicked.connect(self.toggle_token_visibility)
  395. config_layout.addWidget(self.show_token_btn, 0, 2)
  396. config_layout.addWidget(QLabel("OCR地址:"), 1, 0)
  397. self.ocr_input = QLineEdit()
  398. self.ocr_input.setText("http://111.229.45.38:19100/ocr/url")
  399. config_layout.addWidget(self.ocr_input, 1, 1, 1, 2)
  400. # 并发数设置
  401. config_layout.addWidget(QLabel("并发数:"), 2, 0)
  402. self.thread_count_combo = QComboBox()
  403. self.thread_count_combo.addItems(["3", "5", "8", "10", "15"])
  404. self.thread_count_combo.setCurrentText("5")
  405. config_layout.addWidget(self.thread_count_combo, 2, 1)
  406. btn_layout = QHBoxLayout()
  407. self.search_btn = QPushButton("🔍 搜索订单")
  408. self.search_btn.clicked.connect(self.search_orders)
  409. self.search_btn.setFixedHeight(35)
  410. btn_layout.addWidget(self.search_btn)
  411. self.refresh_btn = QPushButton("🔄 刷新")
  412. self.refresh_btn.clicked.connect(self.refresh_orders)
  413. self.refresh_btn.setFixedHeight(35)
  414. btn_layout.addWidget(self.refresh_btn)
  415. self.batch_btn = QPushButton("⚡ 并行查询充电次数")
  416. self.batch_btn.setObjectName("batch_btn")
  417. self.batch_btn.clicked.connect(self.batch_check_charge_counts_parallel)
  418. self.batch_btn.setFixedHeight(35)
  419. self.batch_btn.setEnabled(False)
  420. btn_layout.addWidget(self.batch_btn)
  421. btn_layout.addStretch()
  422. config_layout.addLayout(btn_layout, 3, 0, 1, 3)
  423. config_group.setLayout(config_layout)
  424. main_layout.addWidget(config_group)
  425. # --- 订单列表 ---
  426. list_group = QGroupBox("订单列表")
  427. list_layout = QVBoxLayout()
  428. self.progress_bar = QProgressBar()
  429. self.progress_bar.setVisible(False)
  430. list_layout.addWidget(self.progress_bar)
  431. self.table = QTableWidget()
  432. self.table.setColumnCount(7)
  433. self.table.setHorizontalHeaderLabels(["序号", "订单号", "商品信息", "IMEI", "预估价格", "门店", "充电次数"])
  434. self.table.setColumnWidth(0, 50)
  435. self.table.setColumnWidth(1, 200)
  436. self.table.setColumnWidth(2, 250)
  437. self.table.setColumnWidth(3, 150)
  438. self.table.setColumnWidth(4, 100)
  439. self.table.setColumnWidth(5, 200)
  440. self.table.setColumnWidth(6, 100)
  441. self.table.horizontalHeader().setStretchLastSection(True)
  442. self.table.verticalHeader().setVisible(False)
  443. list_layout.addWidget(self.table)
  444. list_group.setLayout(list_layout)
  445. main_layout.addWidget(list_group)
  446. # --- 日志区域 ---
  447. log_group = QGroupBox("日志")
  448. log_layout = QVBoxLayout()
  449. self.log_text = QTextEdit()
  450. self.log_text.setObjectName("log_text")
  451. self.log_text.setReadOnly(True)
  452. self.log_text.setMaximumHeight(200)
  453. log_layout.addWidget(self.log_text)
  454. log_btn_layout = QHBoxLayout()
  455. self.clear_log_btn = QPushButton("清空日志")
  456. self.clear_log_btn.clicked.connect(self.clear_log)
  457. log_btn_layout.addWidget(self.clear_log_btn)
  458. log_btn_layout.addStretch()
  459. log_layout.addLayout(log_btn_layout)
  460. log_group.setLayout(log_layout)
  461. main_layout.addWidget(log_group)
  462. self.status_bar = QStatusBar()
  463. self.setStatusBar(self.status_bar)
  464. self.status_bar.showMessage("就绪")
  465. def toggle_token_visibility(self):
  466. if self.token_input.echoMode() == QLineEdit.Password:
  467. self.token_input.setEchoMode(QLineEdit.Normal)
  468. self.show_token_btn.setText("隐藏")
  469. else:
  470. self.token_input.setEchoMode(QLineEdit.Password)
  471. self.show_token_btn.setText("显示")
  472. def log_message(self, message, level="INFO"):
  473. timestamp = QDateTime.currentDateTime().toString("yyyy-MM-dd hh:mm:ss")
  474. color_map = {
  475. "INFO": "#d4d4d4",
  476. "WARNING": "#ffa500",
  477. "ERROR": "#ff4444",
  478. "SUCCESS": "#4caf50",
  479. "PROGRESS": "#00bcd4"
  480. }
  481. color = color_map.get(level, "#d4d4d4")
  482. html = f'<span style="color: #888888;">[{timestamp}]</span> <span style="color: {color};">[{level}]</span> {message}<br>'
  483. self.log_text.append(html)
  484. scrollbar = self.log_text.verticalScrollBar()
  485. scrollbar.setValue(scrollbar.maximum())
  486. def clear_log(self):
  487. self.log_text.clear()
  488. def search_orders(self):
  489. access_token = self.token_input.text().strip()
  490. ocr_url = self.ocr_input.text().strip()
  491. if not access_token:
  492. QMessageBox.warning(self, "提示", "请输入Access Token")
  493. return
  494. if not ocr_url:
  495. QMessageBox.warning(self, "提示", "请输入OCR地址")
  496. return
  497. self.log_message("开始搜索订单...", "INFO")
  498. self.order_manager = OrderManager(access_token, ocr_url)
  499. self.order_details_cache = {}
  500. self.charge_counts = {}
  501. self.batch_btn.setEnabled(False)
  502. self.status_bar.showMessage("正在获取订单列表...")
  503. self.search_btn.setEnabled(False)
  504. self.worker = OrderListWorker(self.order_manager)
  505. self.worker.finished.connect(self.on_orders_loaded)
  506. self.worker.error.connect(self.on_error)
  507. self.worker.start()
  508. def refresh_orders(self):
  509. if self.order_manager:
  510. self.search_orders()
  511. else:
  512. QMessageBox.warning(self, "提示", "请先搜索订单")
  513. def on_orders_loaded(self, order_list):
  514. self.order_list = order_list
  515. self.display_orders()
  516. self.search_btn.setEnabled(True)
  517. self.batch_btn.setEnabled(True)
  518. self.status_bar.showMessage(f"成功加载 {len(order_list)} 条订单")
  519. self.log_message(f"成功加载 {len(order_list)} 条订单", "SUCCESS")
  520. def on_error(self, error_msg):
  521. self.search_btn.setEnabled(True)
  522. self.batch_btn.setEnabled(False)
  523. self.status_bar.showMessage("加载失败")
  524. self.log_message(f"获取订单列表失败: {error_msg}", "ERROR")
  525. QMessageBox.critical(self, "错误", f"获取订单列表失败:\n{error_msg}")
  526. def display_orders(self):
  527. self.table.setRowCount(len(self.order_list))
  528. for i, order in enumerate(self.order_list):
  529. self.table.setItem(i, 0, QTableWidgetItem(str(i + 1)))
  530. order_no = order.get('orderNo', '')
  531. self.table.setItem(i, 1, QTableWidgetItem(order_no))
  532. brand_info = order.get('brandInfo', '')
  533. self.table.setItem(i, 2, QTableWidgetItem(brand_info))
  534. imei = order.get('imei', '')
  535. self.table.setItem(i, 3, QTableWidgetItem(imei))
  536. price = order.get('price', '')
  537. price_str = str(price) if price else '待定'
  538. self.table.setItem(i, 4, QTableWidgetItem(price_str))
  539. title = order.get('title', '')
  540. self.table.setItem(i, 5, QTableWidgetItem(title))
  541. charge_widget = QWidget()
  542. charge_layout = QHBoxLayout(charge_widget)
  543. charge_layout.setContentsMargins(5, 2, 5, 2)
  544. charge_layout.setSpacing(5)
  545. charge_label = QLabel("未查询")
  546. charge_label.setObjectName(f"charge_label_{i}")
  547. charge_layout.addWidget(charge_label)
  548. check_btn = QPushButton("查询")
  549. check_btn.setFixedSize(50, 25)
  550. check_btn.setObjectName(f"check_btn_{i}")
  551. check_btn.clicked.connect(lambda checked, idx=i: self.check_charge_count(idx))
  552. charge_layout.addWidget(check_btn)
  553. charge_widget.setLayout(charge_layout)
  554. self.table.setCellWidget(i, 6, charge_widget)
  555. def check_charge_count(self, index):
  556. if not self.order_manager:
  557. return
  558. if index >= len(self.order_list):
  559. return
  560. order_no = self.order_list[index].get('orderNo')
  561. if not order_no:
  562. return
  563. self.log_message(f"开始查询订单 {order_no} 的充电次数", "INFO")
  564. self.status_bar.showMessage(f"正在查询订单 {order_no} 的充电次数...")
  565. btn = self.findChild(QPushButton, f"check_btn_{index}")
  566. if btn:
  567. btn.setEnabled(False)
  568. btn.setText("查询中")
  569. self.charge_worker = ChargeQueryWorker(self.order_manager, order_no, index)
  570. self.charge_worker.finished.connect(self.on_charge_queried)
  571. self.charge_worker.error.connect(self.on_charge_error)
  572. self.charge_worker.start()
  573. # ===== 并行批量查询 =====
  574. def batch_check_charge_counts_parallel(self):
  575. """并行批量查询充电次数"""
  576. if not self.order_list:
  577. QMessageBox.warning(self, "提示", "请先搜索订单")
  578. return
  579. if self.is_batch_processing:
  580. return
  581. # 检查哪些订单还没查询
  582. pending_indices = []
  583. for i, order in enumerate(self.order_list):
  584. order_no = order.get('orderNo')
  585. if order_no not in self.charge_counts:
  586. pending_indices.append(i)
  587. if not pending_indices:
  588. self.log_message("所有订单已查询完成", "SUCCESS")
  589. QMessageBox.information(self, "提示", "所有订单已查询完成")
  590. return
  591. # 关闭旧的线程池
  592. if hasattr(self, 'thread_pool'):
  593. self.thread_pool.shutdown(wait=False)
  594. # 获取并发数
  595. max_workers = int(self.thread_count_combo.currentText())
  596. self.thread_pool = ThreadPoolExecutor(max_workers=max_workers)
  597. self.is_batch_processing = True
  598. self.completed_count = 0
  599. self.total_tasks = len(pending_indices)
  600. self.future_tasks = {}
  601. # 禁用按钮
  602. self.batch_btn.setEnabled(False)
  603. self.search_btn.setEnabled(False)
  604. self.refresh_btn.setEnabled(False)
  605. # 显示进度条
  606. self.progress_bar.setVisible(True)
  607. self.progress_bar.setMaximum(len(pending_indices))
  608. self.progress_bar.setValue(0)
  609. self.log_message(f"开始并行查询 {len(pending_indices)} 个订单 (并发数: {max_workers})", "PROGRESS")
  610. # 提交所有任务
  611. token = self.token_input.text().strip()
  612. ocr_url = self.ocr_input.text().strip()
  613. for idx in pending_indices:
  614. order_no = self.order_list[idx].get('orderNo')
  615. # 提交任务到线程池
  616. future = self.thread_pool.submit(
  617. self.process_single_order_parallel,
  618. token,
  619. ocr_url,
  620. order_no,
  621. idx
  622. )
  623. self.future_tasks[future] = idx
  624. # 更新UI按钮状态
  625. btn = self.findChild(QPushButton, f"check_btn_{idx}")
  626. if btn:
  627. btn.setEnabled(False)
  628. btn.setText("查询中")
  629. # 启动结果收集线程
  630. collector = threading.Thread(target=self.collect_parallel_results)
  631. collector.daemon = True
  632. collector.start()
  633. def process_single_order_parallel(self, token, ocr_url, order_no, index):
  634. """在独立线程中处理单个订单"""
  635. try:
  636. # 每个线程创建独立的OrderManager实例
  637. order_manager = OrderManager(token, ocr_url)
  638. # 获取订单详情
  639. order_detail, error = order_manager.get_order_detail(order_no)
  640. if error:
  641. return index, None, error, None
  642. # 查找验机图片
  643. report_img = order_manager.find_best_image(order_detail)
  644. if not report_img:
  645. return index, None, "未找到验机图片", None
  646. # OCR识别
  647. ocr_result = order_manager.ocr_by_url(report_img)
  648. if not ocr_result:
  649. return index, None, "OCR识别失败", None
  650. # 提取充电次数
  651. ocr_text = order_manager.extract_text_from_ocr_result(ocr_result)
  652. charge_count = order_manager.extract_charge_count(ocr_text)
  653. return index, charge_count, None, ocr_result
  654. except Exception as e:
  655. return index, None, str(e), None
  656. def collect_parallel_results(self):
  657. """收集并行处理的结果"""
  658. for future in as_completed(self.future_tasks):
  659. index = self.future_tasks[future]
  660. try:
  661. idx, charge_count, error, ocr_result = future.result(timeout=60)
  662. # 更新UI(在主线程)
  663. QMetaObject.invokeMethod(self, "update_parallel_result",
  664. Qt.QueuedConnection,
  665. Q_ARG(int, idx),
  666. Q_ARG(object, charge_count),
  667. Q_ARG(str, error or ""),
  668. Q_ARG(object, ocr_result)
  669. )
  670. except Exception as e:
  671. QMetaObject.invokeMethod(self, "update_parallel_result",
  672. Qt.QueuedConnection,
  673. Q_ARG(int, index),
  674. Q_ARG(object, None),
  675. Q_ARG(str, str(e)),
  676. Q_ARG(object, None)
  677. )
  678. self.completed_count += 1
  679. # 更新进度(在主线程)
  680. QMetaObject.invokeMethod(self, "update_batch_progress",
  681. Qt.QueuedConnection,
  682. Q_ARG(int, self.completed_count)
  683. )
  684. # 全部完成
  685. QMetaObject.invokeMethod(self, "on_batch_complete_parallel",
  686. Qt.QueuedConnection
  687. )
  688. @pyqtSlot(int, object, str, object)
  689. def update_parallel_result(self, index, charge_count, error, ocr_result):
  690. """更新单个结果(在主线程执行)"""
  691. order_no = self.order_list[index].get('orderNo')
  692. # 缓存结果
  693. if charge_count is not None:
  694. self.charge_counts[order_no] = charge_count
  695. self.update_charge_display(index, charge_count)
  696. self.log_message(f"订单 {order_no} 充电次数: {charge_count}", "SUCCESS")
  697. else:
  698. self.charge_counts[order_no] = None
  699. self.update_charge_display(index, None)
  700. if error:
  701. self.log_message(f"订单 {order_no} 查询失败: {error}", "ERROR")
  702. # 恢复按钮
  703. btn = self.findChild(QPushButton, f"check_btn_{index}")
  704. if btn:
  705. btn.setEnabled(True)
  706. btn.setText("查询")
  707. @pyqtSlot(int)
  708. def update_batch_progress(self, completed):
  709. """更新进度(在主线程执行)"""
  710. self.progress_bar.setValue(completed)
  711. self.status_bar.showMessage(f"并行处理中: {completed}/{self.total_tasks}")
  712. @pyqtSlot()
  713. def on_batch_complete_parallel(self):
  714. """并行批量完成(在主线程执行)"""
  715. self.is_batch_processing = False
  716. self.future_tasks.clear()
  717. # 恢复按钮
  718. self.batch_btn.setEnabled(True)
  719. self.search_btn.setEnabled(True)
  720. self.refresh_btn.setEnabled(True)
  721. # 隐藏进度条
  722. self.progress_bar.setVisible(False)
  723. # 统计结果
  724. success_count = sum(1 for v in self.charge_counts.values() if v is not None)
  725. total_count = len(self.charge_counts)
  726. self.status_bar.showMessage(f"批量查询完成,成功 {success_count}/{total_count}")
  727. self.log_message(f"批量查询完成,成功 {success_count}/{total_count}", "SUCCESS")
  728. QMessageBox.information(self, "完成",
  729. f"并行批量查询完成!\n\n成功找到充电次数: {success_count}/{total_count}\n总订单: {total_count}")
  730. # ===== 原有的串行查询(保留作为备选) =====
  731. def batch_check_charge_counts(self):
  732. """串行批量查询(保留)"""
  733. if not self.order_list:
  734. QMessageBox.warning(self, "提示", "请先搜索订单")
  735. return
  736. if self.is_batch_processing:
  737. return
  738. pending_indices = []
  739. for i, order in enumerate(self.order_list):
  740. order_no = order.get('orderNo')
  741. if order_no not in self.charge_counts:
  742. pending_indices.append(i)
  743. if not pending_indices:
  744. self.log_message("所有订单已查询完成", "SUCCESS")
  745. QMessageBox.information(self, "提示", "所有订单已查询完成")
  746. return
  747. self.is_batch_processing = True
  748. self.batch_queue = pending_indices
  749. self.current_batch_index = 0
  750. self.batch_btn.setEnabled(False)
  751. self.search_btn.setEnabled(False)
  752. self.refresh_btn.setEnabled(False)
  753. self.progress_bar.setVisible(True)
  754. self.progress_bar.setMaximum(len(pending_indices))
  755. self.progress_bar.setValue(0)
  756. self.log_message(f"开始串行查询 {len(pending_indices)} 个订单", "PROGRESS")
  757. self.process_next_batch()
  758. def process_next_batch(self):
  759. if self.current_batch_index >= len(self.batch_queue):
  760. self.on_batch_complete()
  761. return
  762. index = self.batch_queue[self.current_batch_index]
  763. order_no = self.order_list[index].get('orderNo')
  764. self.progress_bar.setValue(self.current_batch_index + 1)
  765. self.status_bar.showMessage(f"串行处理中: {self.current_batch_index + 1}/{len(self.batch_queue)}")
  766. btn = self.findChild(QPushButton, f"check_btn_{index}")
  767. if btn:
  768. btn.setEnabled(False)
  769. btn.setText("查询中")
  770. self.batch_worker = ChargeQueryWorker(self.order_manager, order_no, index)
  771. self.batch_worker.finished.connect(self.on_batch_item_complete)
  772. self.batch_worker.error.connect(self.on_batch_item_error)
  773. self.batch_worker.start()
  774. def on_batch_item_complete(self, index, charge_count, ocr_result, image_notices):
  775. order_no = self.order_list[index].get('orderNo')
  776. self.charge_counts[order_no] = charge_count
  777. self.update_charge_display(index, charge_count)
  778. btn = self.findChild(QPushButton, f"check_btn_{index}")
  779. if btn:
  780. btn.setEnabled(True)
  781. btn.setText("查询")
  782. if charge_count is not None:
  783. self.log_message(f"[{self.current_batch_index + 1}/{len(self.batch_queue)}] 订单 {order_no} 充电次数: {charge_count}", "SUCCESS")
  784. else:
  785. self.log_message(f"[{self.current_batch_index + 1}/{len(self.batch_queue)}] 订单 {order_no} 未找到充电次数", "WARNING")
  786. self.current_batch_index += 1
  787. QTimer.singleShot(300, self.process_next_batch)
  788. def on_batch_item_error(self, index, error_msg):
  789. order_no = self.order_list[index].get('orderNo')
  790. self.log_message(f"[{self.current_batch_index + 1}/{len(self.batch_queue)}] 订单 {order_no} 查询失败: {error_msg}", "ERROR")
  791. btn = self.findChild(QPushButton, f"check_btn_{index}")
  792. if btn:
  793. btn.setEnabled(True)
  794. btn.setText("查询")
  795. self.current_batch_index += 1
  796. QTimer.singleShot(300, self.process_next_batch)
  797. def on_batch_complete(self):
  798. self.is_batch_processing = False
  799. self.batch_queue = []
  800. self.batch_btn.setEnabled(True)
  801. self.search_btn.setEnabled(True)
  802. self.refresh_btn.setEnabled(True)
  803. self.progress_bar.setVisible(False)
  804. success_count = sum(1 for v in self.charge_counts.values() if v is not None)
  805. total_count = len(self.charge_counts)
  806. self.status_bar.showMessage(f"串行查询完成,成功 {success_count}/{total_count}")
  807. self.log_message(f"串行查询完成,成功 {success_count}/{total_count}", "SUCCESS")
  808. QMessageBox.information(self, "完成", f"串行查询完成!\n\n成功找到充电次数: {success_count}/{total_count}")
  809. # ===== 通用方法 =====
  810. def on_charge_queried(self, index, charge_count, ocr_result, image_notices):
  811. order_no = self.order_list[index].get('orderNo')
  812. if charge_count is None:
  813. if ocr_result:
  814. self.log_message(f"订单 {order_no} 未找到充电次数,OCR识别结果:", "WARNING")
  815. ocr_text = self.order_manager.extract_text_from_ocr_result(ocr_result)
  816. if ocr_text:
  817. self.log_message(f"OCR文本: {ocr_text}", "INFO")
  818. else:
  819. self.log_message(f"OCR文本为空", "WARNING")
  820. if not image_notices:
  821. self.log_message(f"订单 {order_no} 未找到验机图片", "ERROR")
  822. else:
  823. self.log_message(f"订单 {order_no} 充电次数: {charge_count}", "SUCCESS")
  824. self.charge_counts[order_no] = charge_count
  825. self.update_charge_display(index, charge_count)
  826. btn = self.findChild(QPushButton, f"check_btn_{index}")
  827. if btn:
  828. btn.setEnabled(True)
  829. btn.setText("查询")
  830. if charge_count is not None:
  831. self.status_bar.showMessage(f"订单 {order_no} 充电次数: {charge_count}")
  832. else:
  833. self.status_bar.showMessage(f"订单 {order_no} 未找到充电次数信息")
  834. def on_charge_error(self, index, error_msg):
  835. order_no = self.order_list[index].get('orderNo')
  836. self.log_message(f"订单 {order_no} 查询失败: {error_msg}", "ERROR")
  837. btn = self.findChild(QPushButton, f"check_btn_{index}")
  838. if btn:
  839. btn.setEnabled(True)
  840. btn.setText("查询")
  841. self.status_bar.showMessage(f"查询失败: {error_msg}")
  842. QMessageBox.warning(self, "查询失败", f"获取订单详情失败:\n{error_msg}")
  843. def update_charge_display(self, index, charge_count):
  844. widget = self.table.cellWidget(index, 6)
  845. if widget:
  846. label = widget.findChild(QLabel)
  847. if label:
  848. if charge_count is not None:
  849. label.setText(f"{charge_count} 次")
  850. label.setStyleSheet("color: #28a745; font-weight: bold;")
  851. else:
  852. label.setText("未找到")
  853. label.setStyleSheet("color: #dc3545;")
  854. def closeEvent(self, event):
  855. """关闭窗口时清理线程池"""
  856. if hasattr(self, 'thread_pool'):
  857. self.thread_pool.shutdown(wait=False)
  858. event.accept()
  859. # --- 主程序入口 ---
  860. def main():
  861. app = QApplication(sys.argv)
  862. app.setStyle('Fusion')
  863. window = MainWindow()
  864. window.show()
  865. sys.exit(app.exec_())
  866. if __name__ == "__main__":
  867. main()