解密机大侠.py 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140
  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 ImageSelectionDialog(QDialog):
  21. """图片选择对话框 - 纯主线程,无定时器,无子线程"""
  22. def __init__(self, image_urls_with_notice, parent=None):
  23. super().__init__(parent)
  24. self.image_urls_with_notice = image_urls_with_notice
  25. self.selected_url = None
  26. self.selected_index = -1
  27. self.skip = False
  28. self.image_cache = {}
  29. self.init_ui()
  30. # 直接加载,不用定时器
  31. self.load_images()
  32. def init_ui(self):
  33. self.setWindowTitle("选择验机图片")
  34. self.setGeometry(200, 200, 900, 600)
  35. main_layout = QVBoxLayout(self)
  36. hint_label = QLabel("请选择要识别的图片(双击预览大图):")
  37. hint_label.setStyleSheet("font-weight: bold; font-size: 12px; padding: 5px;")
  38. main_layout.addWidget(hint_label)
  39. self.list_widget = QListWidget()
  40. self.list_widget.setIconSize(QSize(150, 100))
  41. self.list_widget.setViewMode(QListWidget.IconMode)
  42. self.list_widget.setResizeMode(QListWidget.Adjust)
  43. self.list_widget.setGridSize(QSize(180, 140))
  44. self.list_widget.setSpacing(10)
  45. main_layout.addWidget(self.list_widget)
  46. btn_layout = QHBoxLayout()
  47. self.preview_btn = QPushButton("🔍 预览大图")
  48. self.preview_btn.clicked.connect(self.preview_selected)
  49. btn_layout.addWidget(self.preview_btn)
  50. self.select_btn = QPushButton("✅ 选择此图片识别")
  51. self.select_btn.clicked.connect(self.select_image)
  52. self.select_btn.setStyleSheet("background-color: #28a745; color: white;")
  53. btn_layout.addWidget(self.select_btn)
  54. self.skip_btn = QPushButton("⏭️ 跳过此订单")
  55. self.skip_btn.clicked.connect(self.skip_order)
  56. self.skip_btn.setStyleSheet("background-color: #dc3545; color: white;")
  57. btn_layout.addWidget(self.skip_btn)
  58. self.cancel_btn = QPushButton("❌ 取消全部")
  59. self.cancel_btn.clicked.connect(self.reject)
  60. self.cancel_btn.setStyleSheet("background-color: #6c757d; color: white;")
  61. btn_layout.addWidget(self.cancel_btn)
  62. main_layout.addLayout(btn_layout)
  63. self.status_label = QLabel("正在加载图片...")
  64. self.status_label.setStyleSheet("color: #ffa500; padding: 5px;")
  65. main_layout.addWidget(self.status_label)
  66. # 直接连接信号,不用QueuedConnection
  67. self.list_widget.itemSelectionChanged.connect(self.on_selection_changed)
  68. self.list_widget.itemDoubleClicked.connect(self.preview_selected)
  69. def load_images(self):
  70. """直接在主线程加载图片"""
  71. if not self.image_urls_with_notice:
  72. self.status_label.setText("没有找到图片")
  73. self.status_label.setStyleSheet("color: #dc3545; padding: 5px;")
  74. return
  75. self.status_label.setText(f"正在加载图片... 共 {len(self.image_urls_with_notice)} 张")
  76. QApplication.processEvents()
  77. # 先创建所有列表项
  78. for i, (notice, url) in enumerate(self.image_urls_with_notice):
  79. display_text = notice[:25] + "..." if len(notice) > 25 else notice
  80. item = QListWidgetItem()
  81. item.setText(display_text)
  82. item.setToolTip(f"完整信息: {notice}\nURL: {url}")
  83. item.setData(Qt.UserRole, url)
  84. item.setData(Qt.UserRole + 1, i)
  85. item.setData(Qt.UserRole + 2, notice)
  86. item.setIcon(self.style().standardIcon(QStyle.SP_FileIcon))
  87. self.list_widget.addItem(item)
  88. # 直接在主线程下载图片
  89. loaded_count = 0
  90. for i, (notice, url) in enumerate(self.image_urls_with_notice):
  91. try:
  92. response = requests.get(url, timeout=10)
  93. if response.status_code == 200:
  94. img_data = response.content
  95. pixmap = QPixmap()
  96. pixmap.loadFromData(img_data)
  97. if not pixmap.isNull():
  98. scaled = pixmap.scaled(150, 100, Qt.KeepAspectRatio, Qt.SmoothTransformation)
  99. item = self.list_widget.item(i)
  100. if item:
  101. item.setIcon(QIcon(scaled))
  102. self.image_cache[url] = pixmap
  103. loaded_count += 1
  104. self.status_label.setText(f"加载图片 {i+1}/{len(self.image_urls_with_notice)}")
  105. QApplication.processEvents()
  106. except Exception as e:
  107. print(f"加载图片失败: {e}")
  108. self.status_label.setText(f"加载完成,共加载 {loaded_count}/{len(self.image_urls_with_notice)} 张图片")
  109. self.status_label.setStyleSheet("color: #28a745; padding: 5px;")
  110. if self.list_widget.count() > 0:
  111. self.list_widget.setCurrentRow(0)
  112. def on_selection_changed(self):
  113. selected = self.list_widget.currentItem()
  114. if selected:
  115. notice = selected.data(Qt.UserRole + 2)
  116. self.status_label.setText(f"已选择: {notice[:50]}...")
  117. self.status_label.setStyleSheet("color: #0078d7; padding: 5px;")
  118. else:
  119. self.status_label.setText("请选择一个图片")
  120. self.status_label.setStyleSheet("color: #666; padding: 5px;")
  121. def preview_selected(self):
  122. selected = self.list_widget.currentItem()
  123. if not selected:
  124. QMessageBox.warning(self, "提示", "请先选择一个图片")
  125. return
  126. url = selected.data(Qt.UserRole)
  127. notice = selected.data(Qt.UserRole + 2)
  128. preview_dialog = QDialog(self)
  129. preview_dialog.setWindowTitle(f"图片预览: {notice[:30]}...")
  130. preview_dialog.setGeometry(300, 300, 800, 700)
  131. layout = QVBoxLayout(preview_dialog)
  132. scroll_area = QScrollArea()
  133. scroll_area.setWidgetResizable(True)
  134. label = QLabel()
  135. label.setAlignment(Qt.AlignCenter)
  136. label.setStyleSheet("background-color: #f0f0f0; padding: 10px;")
  137. if url in self.image_cache:
  138. pixmap = self.image_cache[url]
  139. if pixmap and not pixmap.isNull():
  140. scaled = pixmap.scaled(700, 600, Qt.KeepAspectRatio, Qt.SmoothTransformation)
  141. label.setPixmap(scaled)
  142. else:
  143. label.setText("图片加载失败")
  144. else:
  145. try:
  146. response = requests.get(url, timeout=10)
  147. if response.status_code == 200:
  148. pixmap = QPixmap()
  149. pixmap.loadFromData(response.content)
  150. if not pixmap.isNull():
  151. self.image_cache[url] = pixmap
  152. scaled = pixmap.scaled(700, 600, Qt.KeepAspectRatio, Qt.SmoothTransformation)
  153. label.setPixmap(scaled)
  154. else:
  155. label.setText("图片加载失败")
  156. else:
  157. label.setText("图片加载失败")
  158. except Exception as e:
  159. label.setText(f"加载失败")
  160. scroll_area.setWidget(label)
  161. layout.addWidget(scroll_area)
  162. close_btn = QPushButton("关闭")
  163. close_btn.clicked.connect(preview_dialog.accept)
  164. close_btn.setFixedHeight(35)
  165. layout.addWidget(close_btn)
  166. preview_dialog.exec_()
  167. def select_image(self):
  168. selected = self.list_widget.currentItem()
  169. if not selected:
  170. QMessageBox.warning(self, "提示", "请先选择一个图片")
  171. return
  172. self.selected_url = selected.data(Qt.UserRole)
  173. self.selected_index = selected.data(Qt.UserRole + 1)
  174. self.accept()
  175. def skip_order(self):
  176. reply = QMessageBox.question(self, "确认跳过",
  177. "确定要跳过此订单吗?",
  178. QMessageBox.Yes | QMessageBox.No,
  179. QMessageBox.No)
  180. if reply == QMessageBox.Yes:
  181. self.skip = True
  182. self.accept()
  183. def get_selected_url(self):
  184. return self.selected_url
  185. def is_skipped(self):
  186. return self.skip
  187. # --- 核心功能类 ---
  188. class OrderManager:
  189. def __init__(self, access_token, ocr_url):
  190. self.access_token = access_token
  191. self.ocr_url = ocr_url
  192. self.headers = {
  193. 'Connection': 'keep-alive',
  194. 'content-type': 'application/json',
  195. 'Version': '2.50.0',
  196. 'timestamp': '1784094158706',
  197. 'Distinct-Id': '1784057137891-1059356-086b42dd38b0b98-24491897',
  198. 'vaccine': 'aa454c048e8843247cebf378d1a9a0fed759027f329caf68fd900654da4232d0',
  199. 'U-A': 'JiDaXiaWeApp/23958 (:)',
  200. 'x-version': 'ITC-77385',
  201. 'ahs-gate-language': 'zh_CN',
  202. 'APP-ID': 'jdx135624',
  203. 'platform': 'weapp',
  204. 'Register-Flow-Source': 'RECYCLER',
  205. 'ahs-language': 'zh-CN',
  206. 'access_token': access_token,
  207. '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',
  208. 'Referer': 'https://servicewechat.com/wx7e6573879e91b1ac/28/page-frame.html',
  209. }
  210. self.key = b"S9u978T13NLCGc5W"
  211. self.iv = b"X83yWMD9iKhLxfwX"
  212. self.order_detail_cache = {}
  213. self.ocr_cache = {}
  214. self.cache_lock = threading.Lock()
  215. self.session = requests.Session()
  216. retry_strategy = Retry(
  217. total=2,
  218. backoff_factor=0.5,
  219. status_forcelist=[429, 500, 502, 503, 504],
  220. )
  221. adapter = HTTPAdapter(
  222. pool_connections=20,
  223. pool_maxsize=20,
  224. max_retries=retry_strategy
  225. )
  226. self.session.mount('http://', adapter)
  227. self.session.mount('https://', adapter)
  228. def decrypt_order_data(self, hex_string):
  229. encrypted_data = bytes.fromhex(hex_string)
  230. cipher = AES.new(self.key, AES.MODE_CBC, self.iv)
  231. decrypted_data = unpad(cipher.decrypt(encrypted_data), AES.block_size)
  232. return decrypted_data.decode('utf-8')
  233. def get_pending_order_list(self, page_size=10):
  234. url = "https://wirelessgate.aihuishou.com/jdx-rc-bff-service/app/order/pendingGrab/list"
  235. payload = {
  236. "pageSize": page_size,
  237. "status": -1,
  238. "isApplyForHandle": None,
  239. "applyPhotoStatus": None,
  240. "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],
  241. "sourceIds": None,
  242. "keyword": None,
  243. "timeObj": None,
  244. "rcOrderResult": None
  245. }
  246. try:
  247. response = self.session.post(url, headers=self.headers, json=payload, timeout=30)
  248. response.raise_for_status()
  249. result = response.json()
  250. if result.get('code') != 200:
  251. return None, result.get('resultMessage', '未知错误')
  252. return result.get('data', []), None
  253. except Exception as e:
  254. return None, str(e)
  255. def get_order_detail(self, order_no):
  256. with self.cache_lock:
  257. if order_no in self.order_detail_cache:
  258. return self.order_detail_cache[order_no], None
  259. snatch_url = "https://wirelessgate.aihuishou.com/jdx-rc-service/front/bid/snatch"
  260. snatch_payload = {"orderNo": order_no}
  261. try:
  262. snatch_response = self.session.post(snatch_url, headers=self.headers, json=snatch_payload, timeout=15)
  263. snatch_response.raise_for_status()
  264. snatch_data = snatch_response.json()
  265. if snatch_data.get('code') != 200:
  266. return None, snatch_data.get('resultMessage', '获取BiddingNo失败')
  267. bidding_no = snatch_data.get('data')
  268. if not bidding_no:
  269. return None, "获取BiddingNo为空"
  270. except Exception as e:
  271. return None, str(e)
  272. detail_url = f"https://wirelessgate.aihuishou.com/jdx-qa-service/app/ka/v3/order/detail?orderNo={order_no}&biddingNo={bidding_no}"
  273. try:
  274. detail_response = self.session.get(detail_url, headers=self.headers, timeout=15)
  275. detail_response.raise_for_status()
  276. detail_data = detail_response.json()
  277. if detail_data.get('code') != 200:
  278. return None, detail_data.get('resultMessage', '获取订单详情失败')
  279. encrypted_hex = detail_data.get('data')
  280. if not encrypted_hex:
  281. return None, "未获取到加密数据"
  282. decrypted_json_string = self.decrypt_order_data(encrypted_hex)
  283. order_detail = json.loads(decrypted_json_string)
  284. with self.cache_lock:
  285. self.order_detail_cache[order_no] = order_detail
  286. return order_detail, None
  287. except Exception as e:
  288. return None, str(e)
  289. def find_original_image_by_notice(self, json_data, notice_keyword):
  290. if isinstance(json_data, str):
  291. try:
  292. json_data = json.loads(json_data)
  293. except json.JSONDecodeError:
  294. return None
  295. stack = [json_data]
  296. keyword_lower = notice_keyword.lower()
  297. while stack:
  298. obj = stack.pop()
  299. if isinstance(obj, dict):
  300. if 'notice' in obj and 'originalImage' in obj:
  301. if keyword_lower in obj['notice'].lower():
  302. return obj['originalImage']
  303. for value in obj.values():
  304. stack.append(value)
  305. elif isinstance(obj, list):
  306. stack.extend(obj)
  307. return None
  308. def get_all_images_with_notice(self, json_data):
  309. images = []
  310. stack = [json_data]
  311. while stack:
  312. obj = stack.pop()
  313. if isinstance(obj, dict):
  314. if 'notice' in obj and 'originalImage' in obj:
  315. images.append((obj['notice'], obj['originalImage']))
  316. for value in obj.values():
  317. stack.append(value)
  318. elif isinstance(obj, list):
  319. stack.extend(obj)
  320. return images
  321. def get_all_image_notices(self, json_data):
  322. notices = []
  323. stack = [json_data]
  324. while stack:
  325. obj = stack.pop()
  326. if isinstance(obj, dict):
  327. if 'notice' in obj and 'image' in obj:
  328. notices.append(obj['notice'])
  329. for value in obj.values():
  330. stack.append(value)
  331. elif isinstance(obj, list):
  332. stack.extend(obj)
  333. return list(set(notices))
  334. def ocr_by_url(self, image_url):
  335. cache_key = hashlib.md5(image_url.encode()).hexdigest()
  336. with self.cache_lock:
  337. if cache_key in self.ocr_cache:
  338. return self.ocr_cache[cache_key]
  339. try:
  340. payload = {"url": image_url}
  341. response = self.session.post(self.ocr_url, json=payload, timeout=30)
  342. if response.status_code == 200:
  343. ocr_result = response.json()
  344. if ocr_result.get('success'):
  345. with self.cache_lock:
  346. self.ocr_cache[cache_key] = ocr_result
  347. return ocr_result
  348. return None
  349. except Exception as e:
  350. return None
  351. def extract_text_from_ocr_result(self, ocr_result):
  352. if not ocr_result or not ocr_result.get('texts'):
  353. return ''
  354. texts = ''
  355. for item in ocr_result['texts']:
  356. rec_texts = item.get('rec_texts', [])
  357. for text in rec_texts:
  358. texts = texts + text
  359. return texts
  360. def extract_charge_count(self, text):
  361. if not text:
  362. return None
  363. pattern1 = r'[((]\s*(\d+)\s*次\s*循环\s*[))]'
  364. match = re.search(pattern1, text)
  365. if match:
  366. return int(match.group(1))
  367. pattern2 = r'充电次数(\d+)次'
  368. match = re.search(pattern2, text)
  369. if match:
  370. return int(match.group(1))
  371. pattern3 = r'(\d+)次'
  372. match = re.search(pattern3, text)
  373. if match:
  374. return int(match.group(1))
  375. return None
  376. def find_best_image(self, order_detail, parent_widget=None):
  377. """查找最佳图片,如果找不到则让用户选择"""
  378. keywords = [
  379. "苹果沙漏验机报告",
  380. "沙漏图",
  381. "苹果验机软件",
  382. "苹果沙漏验机基本信息",
  383. "验机照片",
  384. "验机报告"
  385. ]
  386. # 先尝试用关键词自动匹配
  387. for keyword in keywords:
  388. img = self.find_original_image_by_notice(order_detail, keyword)
  389. if img:
  390. return img, "auto"
  391. # 自动匹配失败,获取所有图片让用户选择
  392. all_images = self.get_all_images_with_notice(order_detail)
  393. if not all_images:
  394. return None, "no_images"
  395. # 如果只有一张图片,直接返回
  396. if len(all_images) == 1:
  397. return all_images[0][1], "auto_single"
  398. # 多张图片,让用户选择
  399. if parent_widget:
  400. # 使用信号槽方式在主线程显示对话框
  401. result_container = {"action": None, "url": None}
  402. event = threading.Event()
  403. def show_dialog():
  404. try:
  405. dialog = ImageSelectionDialog(all_images, parent_widget)
  406. if dialog.exec_() == QDialog.Accepted:
  407. if dialog.is_skipped():
  408. result_container["action"] = "skip"
  409. else:
  410. selected_url = dialog.get_selected_url()
  411. if selected_url:
  412. result_container["action"] = "selected"
  413. result_container["url"] = selected_url
  414. else:
  415. result_container["action"] = "cancelled"
  416. else:
  417. result_container["action"] = "cancelled"
  418. except Exception as e:
  419. print(f"显示对话框出错: {e}")
  420. result_container["action"] = "error"
  421. finally:
  422. event.set()
  423. # 在主线程执行
  424. if QThread.currentThread() != QCoreApplication.instance().thread():
  425. # 在子线程中,使用invokeMethod
  426. QMetaObject.invokeMethod(parent_widget, "show_image_selection_dialog",
  427. Qt.BlockingQueuedConnection,
  428. Q_ARG(list, all_images),
  429. Q_ARG(dict, result_container),
  430. Q_ARG(threading.Event, event))
  431. else:
  432. # 已经在主线程,直接调用
  433. show_dialog()
  434. # 等待对话框完成
  435. event.wait()
  436. action = result_container.get("action")
  437. if action == "selected":
  438. return result_container.get("url"), "user_selected"
  439. elif action == "skip":
  440. return None, "skipped"
  441. else:
  442. return None, "cancelled"
  443. # 没有父窗口,返回第一张图片
  444. return all_images[0][1], "auto_first"
  445. # --- 后台工作线程 ---
  446. class OrderListWorker(QThread):
  447. finished = pyqtSignal(list)
  448. error = pyqtSignal(str)
  449. def __init__(self, order_manager):
  450. super().__init__()
  451. self.order_manager = order_manager
  452. def run(self):
  453. try:
  454. order_list, error = self.order_manager.get_pending_order_list(100)
  455. if error:
  456. self.error.emit(error)
  457. else:
  458. self.finished.emit(order_list)
  459. except Exception as e:
  460. self.error.emit(str(e))
  461. class ChargeQueryWorker(QThread):
  462. finished = pyqtSignal(int, object, object, list)
  463. error = pyqtSignal(int, str)
  464. def __init__(self, order_manager, order_no, index, parent_widget=None):
  465. super().__init__()
  466. self.order_manager = order_manager
  467. self.order_no = order_no
  468. self.index = index
  469. self.parent_widget = parent_widget
  470. def run(self):
  471. try:
  472. order_detail, error = self.order_manager.get_order_detail(self.order_no)
  473. if error:
  474. self.error.emit(self.index, error)
  475. return
  476. image_notices = self.order_manager.get_all_image_notices(order_detail)
  477. # 传入 parent_widget 以便显示对话框
  478. report_img, source = self.order_manager.find_best_image(order_detail, self.parent_widget)
  479. # 处理用户取消或跳过的情况
  480. if source in ["cancelled", "skipped"]:
  481. self.error.emit(self.index, "用户取消选择" if source == "cancelled" else "用户跳过订单")
  482. return
  483. if not report_img:
  484. self.finished.emit(self.index, None, None, image_notices)
  485. return
  486. ocr_result = self.order_manager.ocr_by_url(report_img)
  487. if not ocr_result:
  488. self.finished.emit(self.index, None, None, image_notices)
  489. return
  490. ocr_text = self.order_manager.extract_text_from_ocr_result(ocr_result)
  491. charge_count = self.order_manager.extract_charge_count(ocr_text)
  492. self.finished.emit(self.index, charge_count, ocr_result, image_notices)
  493. except Exception as e:
  494. self.error.emit(self.index, str(e))
  495. # --- 主窗口 ---
  496. class MainWindow(QMainWindow):
  497. def __init__(self):
  498. super().__init__()
  499. self.order_manager = None
  500. self.order_list = []
  501. self.order_details_cache = {}
  502. self.charge_counts = {}
  503. self.is_batch_processing = False
  504. self.batch_queue = []
  505. self.current_batch_index = 0
  506. self.init_ui()
  507. def init_ui(self):
  508. self.setWindowTitle('爱回收订单查询工具')
  509. self.setGeometry(100, 100, 1200, 800)
  510. self.setStyleSheet("""
  511. QMainWindow {
  512. background-color: #f0f0f0;
  513. }
  514. QGroupBox {
  515. font-weight: bold;
  516. border: 1px solid #ccc;
  517. border-radius: 5px;
  518. margin-top: 10px;
  519. padding-top: 10px;
  520. }
  521. QGroupBox::title {
  522. subcontrol-origin: margin;
  523. left: 10px;
  524. padding: 0 5px 0 5px;
  525. }
  526. QPushButton {
  527. background-color: #0078d7;
  528. color: white;
  529. border: none;
  530. padding: 8px 16px;
  531. border-radius: 4px;
  532. font-weight: bold;
  533. }
  534. QPushButton:hover {
  535. background-color: #005a9e;
  536. }
  537. QPushButton:disabled {
  538. background-color: #cccccc;
  539. }
  540. QPushButton#batch_btn {
  541. background-color: #28a745;
  542. }
  543. QPushButton#batch_btn:hover {
  544. background-color: #218838;
  545. }
  546. QPushButton#batch_btn:disabled {
  547. background-color: #cccccc;
  548. }
  549. QLineEdit, QTextEdit {
  550. border: 1px solid #ccc;
  551. border-radius: 4px;
  552. padding: 5px;
  553. }
  554. QTableWidget {
  555. border: 1px solid #ccc;
  556. border-radius: 4px;
  557. gridline-color: #e0e0e0;
  558. }
  559. QTableWidget::item {
  560. padding: 5px;
  561. }
  562. QHeaderView::section {
  563. background-color: #e8e8e8;
  564. padding: 5px;
  565. border: 1px solid #ccc;
  566. font-weight: bold;
  567. }
  568. QStatusBar {
  569. background-color: #f0f0f0;
  570. color: #333;
  571. }
  572. #log_text {
  573. background-color: #1e1e1e;
  574. color: #d4d4d4;
  575. font-family: 'Consolas', 'Courier New', monospace;
  576. font-size: 11px;
  577. }
  578. QProgressBar {
  579. border: 1px solid #ccc;
  580. border-radius: 4px;
  581. text-align: center;
  582. }
  583. QProgressBar::chunk {
  584. background-color: #0078d7;
  585. border-radius: 4px;
  586. }
  587. """)
  588. central_widget = QWidget()
  589. self.setCentralWidget(central_widget)
  590. main_layout = QVBoxLayout(central_widget)
  591. main_layout.setSpacing(10)
  592. config_group = QGroupBox("配置")
  593. config_layout = QGridLayout()
  594. config_layout.addWidget(QLabel("Access Token:"), 0, 0)
  595. self.token_input = QLineEdit()
  596. self.token_input.setText("jdx9ce52336abee4cf78db56567dc259512")
  597. self.token_input.setEchoMode(QLineEdit.Password)
  598. config_layout.addWidget(self.token_input, 0, 1)
  599. self.show_token_btn = QPushButton("显示")
  600. self.show_token_btn.setFixedWidth(60)
  601. self.show_token_btn.clicked.connect(self.toggle_token_visibility)
  602. config_layout.addWidget(self.show_token_btn, 0, 2)
  603. config_layout.addWidget(QLabel("OCR地址:"), 1, 0)
  604. self.ocr_input = QLineEdit()
  605. self.ocr_input.setText("http://111.229.45.38:19100/ocr/url")
  606. config_layout.addWidget(self.ocr_input, 1, 1, 1, 2)
  607. btn_layout = QHBoxLayout()
  608. self.search_btn = QPushButton("🔍 搜索订单")
  609. self.search_btn.clicked.connect(self.search_orders)
  610. self.search_btn.setFixedHeight(35)
  611. btn_layout.addWidget(self.search_btn)
  612. self.refresh_btn = QPushButton("🔄 刷新")
  613. self.refresh_btn.clicked.connect(self.refresh_orders)
  614. self.refresh_btn.setFixedHeight(35)
  615. btn_layout.addWidget(self.refresh_btn)
  616. self.batch_btn = QPushButton("📋 批量查询充电次数")
  617. self.batch_btn.setObjectName("batch_btn")
  618. self.batch_btn.clicked.connect(self.batch_check_charge_counts)
  619. self.batch_btn.setFixedHeight(35)
  620. self.batch_btn.setEnabled(False)
  621. btn_layout.addWidget(self.batch_btn)
  622. btn_layout.addStretch()
  623. config_layout.addLayout(btn_layout, 3, 0, 1, 3)
  624. config_group.setLayout(config_layout)
  625. main_layout.addWidget(config_group)
  626. list_group = QGroupBox("订单列表")
  627. list_layout = QVBoxLayout()
  628. self.progress_bar = QProgressBar()
  629. self.progress_bar.setVisible(False)
  630. list_layout.addWidget(self.progress_bar)
  631. self.table = QTableWidget()
  632. self.table.setColumnCount(7)
  633. self.table.setHorizontalHeaderLabels(["序号", "订单号", "商品信息", "IMEI", "预估价格", "门店", "充电次数"])
  634. self.table.setColumnWidth(0, 50)
  635. self.table.setColumnWidth(1, 200)
  636. self.table.setColumnWidth(2, 250)
  637. self.table.setColumnWidth(3, 150)
  638. self.table.setColumnWidth(4, 100)
  639. self.table.setColumnWidth(5, 200)
  640. self.table.setColumnWidth(6, 120)
  641. self.table.horizontalHeader().setStretchLastSection(True)
  642. self.table.verticalHeader().setVisible(False)
  643. list_layout.addWidget(self.table)
  644. list_group.setLayout(list_layout)
  645. main_layout.addWidget(list_group)
  646. log_group = QGroupBox("日志")
  647. log_layout = QVBoxLayout()
  648. self.log_text = QTextEdit()
  649. self.log_text.setObjectName("log_text")
  650. self.log_text.setReadOnly(True)
  651. self.log_text.setMaximumHeight(200)
  652. log_layout.addWidget(self.log_text)
  653. log_btn_layout = QHBoxLayout()
  654. self.clear_log_btn = QPushButton("清空日志")
  655. self.clear_log_btn.clicked.connect(self.clear_log)
  656. log_btn_layout.addWidget(self.clear_log_btn)
  657. log_btn_layout.addStretch()
  658. log_layout.addLayout(log_btn_layout)
  659. log_group.setLayout(log_layout)
  660. main_layout.addWidget(log_group)
  661. self.status_bar = QStatusBar()
  662. self.setStatusBar(self.status_bar)
  663. self.status_bar.showMessage("就绪")
  664. @pyqtSlot(list, dict, threading.Event)
  665. def show_image_selection_dialog(self, all_images, result_container, event):
  666. """在主线程显示图片选择对话框(供invokeMethod调用)"""
  667. try:
  668. dialog = ImageSelectionDialog(all_images, self)
  669. if dialog.exec_() == QDialog.Accepted:
  670. if dialog.is_skipped():
  671. result_container["action"] = "skip"
  672. else:
  673. selected_url = dialog.get_selected_url()
  674. if selected_url:
  675. result_container["action"] = "selected"
  676. result_container["url"] = selected_url
  677. else:
  678. result_container["action"] = "cancelled"
  679. else:
  680. result_container["action"] = "cancelled"
  681. except Exception as e:
  682. print(f"显示图片选择对话框出错: {e}")
  683. result_container["action"] = "error"
  684. finally:
  685. event.set()
  686. def toggle_token_visibility(self):
  687. if self.token_input.echoMode() == QLineEdit.Password:
  688. self.token_input.setEchoMode(QLineEdit.Normal)
  689. self.show_token_btn.setText("隐藏")
  690. else:
  691. self.token_input.setEchoMode(QLineEdit.Password)
  692. self.show_token_btn.setText("显示")
  693. def log_message(self, message, level="INFO"):
  694. timestamp = QDateTime.currentDateTime().toString("yyyy-MM-dd hh:mm:ss")
  695. color_map = {
  696. "INFO": "#d4d4d4",
  697. "WARNING": "#ffa500",
  698. "ERROR": "#ff4444",
  699. "SUCCESS": "#4caf50",
  700. "PROGRESS": "#00bcd4"
  701. }
  702. color = color_map.get(level, "#d4d4d4")
  703. html = f'<span style="color: #888888;">[{timestamp}]</span> <span style="color: {color};">[{level}]</span> {message}<br>'
  704. self.log_text.append(html)
  705. scrollbar = self.log_text.verticalScrollBar()
  706. scrollbar.setValue(scrollbar.maximum())
  707. def clear_log(self):
  708. self.log_text.clear()
  709. def search_orders(self):
  710. access_token = self.token_input.text().strip()
  711. ocr_url = self.ocr_input.text().strip()
  712. if not access_token:
  713. QMessageBox.warning(self, "提示", "请输入Access Token")
  714. return
  715. if not ocr_url:
  716. QMessageBox.warning(self, "提示", "请输入OCR地址")
  717. return
  718. self.log_message("开始搜索订单...", "INFO")
  719. self.order_manager = OrderManager(access_token, ocr_url)
  720. self.order_details_cache = {}
  721. self.charge_counts = {}
  722. self.batch_btn.setEnabled(False)
  723. self.status_bar.showMessage("正在获取订单列表...")
  724. self.search_btn.setEnabled(False)
  725. self.worker = OrderListWorker(self.order_manager)
  726. self.worker.finished.connect(self.on_orders_loaded)
  727. self.worker.error.connect(self.on_error)
  728. self.worker.start()
  729. def refresh_orders(self):
  730. if self.order_manager:
  731. self.search_orders()
  732. else:
  733. QMessageBox.warning(self, "提示", "请先搜索订单")
  734. def on_orders_loaded(self, order_list):
  735. self.order_list = order_list
  736. self.display_orders()
  737. self.search_btn.setEnabled(True)
  738. self.batch_btn.setEnabled(True)
  739. self.status_bar.showMessage(f"成功加载 {len(order_list)} 条订单")
  740. self.log_message(f"成功加载 {len(order_list)} 条订单", "SUCCESS")
  741. def on_error(self, error_msg):
  742. self.search_btn.setEnabled(True)
  743. self.batch_btn.setEnabled(False)
  744. self.status_bar.showMessage("加载失败")
  745. self.log_message(f"获取订单列表失败: {error_msg}", "ERROR")
  746. QMessageBox.critical(self, "错误", f"获取订单列表失败:\n{error_msg}")
  747. def display_orders(self):
  748. self.table.setRowCount(len(self.order_list))
  749. for i, order in enumerate(self.order_list):
  750. self.table.setItem(i, 0, QTableWidgetItem(str(i + 1)))
  751. order_no = order.get('orderNo', '')
  752. self.table.setItem(i, 1, QTableWidgetItem(order_no))
  753. brand_info = order.get('brandInfo', '')
  754. self.table.setItem(i, 2, QTableWidgetItem(brand_info))
  755. imei = order.get('imei', '')
  756. self.table.setItem(i, 3, QTableWidgetItem(imei))
  757. price = order.get('price', '')
  758. price_str = str(price) if price else '待定'
  759. self.table.setItem(i, 4, QTableWidgetItem(price_str))
  760. title = order.get('title', '')
  761. self.table.setItem(i, 5, QTableWidgetItem(title))
  762. charge_widget = QWidget()
  763. charge_layout = QHBoxLayout(charge_widget)
  764. charge_layout.setContentsMargins(5, 2, 5, 2)
  765. charge_layout.setSpacing(5)
  766. charge_label = QLabel("未查询")
  767. charge_label.setObjectName(f"charge_label_{i}")
  768. charge_layout.addWidget(charge_label)
  769. check_btn = QPushButton("查询")
  770. check_btn.setFixedSize(50, 25)
  771. check_btn.setObjectName(f"check_btn_{i}")
  772. # 关键:让按钮不能获得焦点
  773. check_btn.setFocusPolicy(Qt.NoFocus)
  774. check_btn.clicked.connect(lambda checked, idx=i: self.check_charge_count(idx))
  775. charge_layout.addWidget(check_btn)
  776. charge_widget.setLayout(charge_layout)
  777. self.table.setCellWidget(i, 6, charge_widget)
  778. def check_charge_count(self, index):
  779. if not self.order_manager:
  780. return
  781. if index >= len(self.order_list):
  782. return
  783. order_no = self.order_list[index].get('orderNo')
  784. if not order_no:
  785. return
  786. self.log_message(f"开始查询订单 {order_no} 的充电次数", "INFO")
  787. self.status_bar.showMessage(f"正在查询订单 {order_no} 的充电次数...")
  788. btn = self.findChild(QPushButton, f"check_btn_{index}")
  789. if btn:
  790. btn.setEnabled(False)
  791. btn.setText("稍等")
  792. # 传入 self 作为 parent_widget
  793. self.charge_worker = ChargeQueryWorker(self.order_manager, order_no, index, self)
  794. self.charge_worker.finished.connect(self.on_charge_queried)
  795. self.charge_worker.error.connect(self.on_charge_error)
  796. self.charge_worker.start()
  797. def batch_check_charge_counts(self):
  798. """串行批量查询充电次数(保留弹窗功能)"""
  799. if not self.order_list:
  800. QMessageBox.warning(self, "提示", "请先搜索订单")
  801. return
  802. if self.is_batch_processing:
  803. return
  804. pending_indices = []
  805. for i, order in enumerate(self.order_list):
  806. order_no = order.get('orderNo')
  807. if order_no not in self.charge_counts:
  808. pending_indices.append(i)
  809. if not pending_indices:
  810. self.log_message("所有订单已查询完成", "SUCCESS")
  811. QMessageBox.information(self, "提示", "所有订单已查询完成")
  812. return
  813. self.is_batch_processing = True
  814. self.batch_queue = pending_indices
  815. self.current_batch_index = 0
  816. self.batch_btn.setEnabled(False)
  817. self.search_btn.setEnabled(False)
  818. self.refresh_btn.setEnabled(False)
  819. self.progress_bar.setVisible(True)
  820. self.progress_bar.setMaximum(len(pending_indices))
  821. self.progress_bar.setValue(0)
  822. self.log_message(f"开始批量查询 {len(pending_indices)} 个订单", "PROGRESS")
  823. self.process_next_batch()
  824. def process_next_batch(self):
  825. if self.current_batch_index >= len(self.batch_queue):
  826. self.on_batch_complete()
  827. return
  828. index = self.batch_queue[self.current_batch_index]
  829. order_no = self.order_list[index].get('orderNo')
  830. self.progress_bar.setValue(self.current_batch_index + 1)
  831. self.status_bar.showMessage(f"处理中: {self.current_batch_index + 1}/{len(self.batch_queue)}")
  832. btn = self.findChild(QPushButton, f"check_btn_{index}")
  833. if btn:
  834. btn.setEnabled(False)
  835. btn.setText("稍等")
  836. # 传入 self 作为 parent_widget
  837. self.batch_worker = ChargeQueryWorker(self.order_manager, order_no, index, self)
  838. self.batch_worker.finished.connect(self.on_batch_item_complete)
  839. self.batch_worker.error.connect(self.on_batch_item_error)
  840. self.batch_worker.start()
  841. def on_batch_item_complete(self, index, charge_count, ocr_result, image_notices):
  842. order_no = self.order_list[index].get('orderNo')
  843. self.charge_counts[order_no] = charge_count
  844. self.update_charge_display(index, charge_count)
  845. btn = self.findChild(QPushButton, f"check_btn_{index}")
  846. if btn:
  847. btn.setEnabled(True)
  848. btn.setText("查询")
  849. if charge_count is not None:
  850. self.log_message(f"[{self.current_batch_index + 1}/{len(self.batch_queue)}] 订单 {order_no} 充电次数: {charge_count}", "SUCCESS")
  851. else:
  852. self.log_message(f"[{self.current_batch_index + 1}/{len(self.batch_queue)}] 订单 {order_no} 未找到充电次数", "WARNING")
  853. self.current_batch_index += 1
  854. QTimer.singleShot(300, self.process_next_batch)
  855. def on_batch_item_error(self, index, error_msg):
  856. order_no = self.order_list[index].get('orderNo')
  857. self.log_message(f"[{self.current_batch_index + 1}/{len(self.batch_queue)}] 订单 {order_no} 查询失败: {error_msg}", "ERROR")
  858. btn = self.findChild(QPushButton, f"check_btn_{index}")
  859. if btn:
  860. btn.setEnabled(True)
  861. btn.setText("查询")
  862. self.current_batch_index += 1
  863. QTimer.singleShot(300, self.process_next_batch)
  864. def on_batch_complete(self):
  865. self.is_batch_processing = False
  866. self.batch_queue = []
  867. self.batch_btn.setEnabled(True)
  868. self.search_btn.setEnabled(True)
  869. self.refresh_btn.setEnabled(True)
  870. self.progress_bar.setVisible(False)
  871. success_count = sum(1 for v in self.charge_counts.values() if v is not None)
  872. total_count = len(self.charge_counts)
  873. self.status_bar.showMessage(f"批量查询完成,成功 {success_count}/{total_count}")
  874. self.log_message(f"批量查询完成,成功 {success_count}/{total_count}", "SUCCESS")
  875. QMessageBox.information(self, "完成", f"批量查询完成!\n\n成功找到充电次数: {success_count}/{total_count}")
  876. def on_charge_queried(self, index, charge_count, ocr_result, image_notices):
  877. order_no = self.order_list[index].get('orderNo')
  878. if charge_count is None:
  879. if ocr_result:
  880. self.log_message(f"订单 {order_no} 未找到充电次数,OCR识别结果:", "WARNING")
  881. ocr_text = self.order_manager.extract_text_from_ocr_result(ocr_result)
  882. if ocr_text:
  883. self.log_message(f"OCR文本: {ocr_text}", "INFO")
  884. else:
  885. self.log_message(f"OCR文本为空", "WARNING")
  886. if not image_notices:
  887. self.log_message(f"订单 {order_no} 未找到验机图片", "ERROR")
  888. else:
  889. self.log_message(f"订单 {order_no} 充电次数: {charge_count}", "SUCCESS")
  890. self.charge_counts[order_no] = charge_count
  891. self.update_charge_display(index, charge_count)
  892. btn = self.findChild(QPushButton, f"check_btn_{index}")
  893. if btn:
  894. btn.setEnabled(True)
  895. btn.setText("查询")
  896. if charge_count is not None:
  897. self.status_bar.showMessage(f"订单 {order_no} 充电次数: {charge_count}")
  898. else:
  899. self.status_bar.showMessage(f"订单 {order_no} 未找到充电次数信息")
  900. def on_charge_error(self, index, error_msg):
  901. order_no = self.order_list[index].get('orderNo')
  902. self.log_message(f"订单 {order_no} 查询失败: {error_msg}", "ERROR")
  903. btn = self.findChild(QPushButton, f"check_btn_{index}")
  904. if btn:
  905. btn.setEnabled(True)
  906. btn.setText("查询")
  907. self.status_bar.showMessage(f"查询失败: {error_msg}")
  908. if error_msg != "用户取消选择" and error_msg != "用户跳过订单":
  909. QMessageBox.warning(self, "查询失败", f"获取订单详情失败:\n{error_msg}")
  910. def update_charge_display(self, index, charge_count):
  911. widget = self.table.cellWidget(index, 6)
  912. if widget:
  913. label = widget.findChild(QLabel)
  914. if label:
  915. if charge_count is not None:
  916. label.setText(f"{charge_count} 次")
  917. label.setStyleSheet("color: #28a745; font-weight: bold;")
  918. else:
  919. label.setText("未找到")
  920. label.setStyleSheet("color: #dc3545;")
  921. def closeEvent(self, event):
  922. event.accept()
  923. # --- 主程序入口 ---
  924. def main():
  925. app = QApplication(sys.argv)
  926. app.setStyle('Fusion')
  927. window = MainWindow()
  928. window.show()
  929. sys.exit(app.exec_())
  930. if __name__ == "__main__":
  931. main()