| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063 |
- import sys
- import requests
- import json
- import os
- import re
- import uuid
- import hashlib
- import time
- import threading
- from concurrent.futures import ThreadPoolExecutor, as_completed
- from Crypto.Cipher import AES
- from Crypto.Util.Padding import unpad
- from typing import Optional, List, Dict, Any
- from PyQt5.QtWidgets import *
- from PyQt5.QtCore import *
- from PyQt5.QtGui import *
- from requests.adapters import HTTPAdapter
- from urllib3.util.retry import Retry
- # --- 核心功能类 ---
- class OrderManager:
- def __init__(self, access_token, ocr_url):
- self.access_token = access_token
- self.ocr_url = ocr_url
- self.headers = {
- 'Connection': 'keep-alive',
- 'content-type': 'application/json',
- 'Version': '2.50.0',
- 'timestamp': '1784094158706',
- 'Distinct-Id': '1784057137891-1059356-086b42dd38b0b98-24491897',
- 'vaccine': 'aa454c048e8843247cebf378d1a9a0fed759027f329caf68fd900654da4232d0',
- 'U-A': 'JiDaXiaWeApp/23958 (:)',
- 'x-version': 'ITC-77385',
- 'ahs-gate-language': 'zh_CN',
- 'APP-ID': 'jdx135624',
- 'platform': 'weapp',
- 'Register-Flow-Source': 'RECYCLER',
- 'ahs-language': 'zh-CN',
- 'access_token': access_token,
- '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',
- 'Referer': 'https://servicewechat.com/wx7e6573879e91b1ac/28/page-frame.html',
- }
- self.key = b"S9u978T13NLCGc5W"
- self.iv = b"X83yWMD9iKhLxfwX"
-
- # 缓存
- self.order_detail_cache = {}
- self.ocr_cache = {}
- self.cache_lock = threading.Lock()
-
- # 创建会话并配置连接池
- self.session = requests.Session()
- retry_strategy = Retry(
- total=2,
- backoff_factor=0.5,
- status_forcelist=[429, 500, 502, 503, 504],
- )
- adapter = HTTPAdapter(
- pool_connections=20,
- pool_maxsize=20,
- max_retries=retry_strategy
- )
- self.session.mount('http://', adapter)
- self.session.mount('https://', adapter)
- def decrypt_order_data(self, hex_string):
- """解密订单数据"""
- encrypted_data = bytes.fromhex(hex_string)
- cipher = AES.new(self.key, AES.MODE_CBC, self.iv)
- decrypted_data = unpad(cipher.decrypt(encrypted_data), AES.block_size)
- return decrypted_data.decode('utf-8')
- def get_pending_order_list(self, page_size=10):
- """获取待抢订单列表"""
- url = "https://wirelessgate.aihuishou.com/jdx-rc-bff-service/app/order/pendingGrab/list"
-
- payload = {
- "pageSize": page_size,
- "status": -1,
- "isApplyForHandle": None,
- "applyPhotoStatus": None,
- "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],
- "sourceIds": None,
- "keyword": None,
- "timeObj": None,
- "rcOrderResult": None
- }
-
- try:
- response = self.session.post(url, headers=self.headers, json=payload, timeout=30)
- response.raise_for_status()
- result = response.json()
-
- if result.get('code') != 200:
- return None, result.get('resultMessage', '未知错误')
-
- return result.get('data', []), None
- except Exception as e:
- return None, str(e)
- def get_order_detail(self, order_no):
- """获取订单详情(带缓存)"""
- # 检查缓存
- with self.cache_lock:
- if order_no in self.order_detail_cache:
- return self.order_detail_cache[order_no], None
-
- # 获取BiddingNo
- snatch_url = "https://wirelessgate.aihuishou.com/jdx-rc-service/front/bid/snatch"
- snatch_payload = {"orderNo": order_no}
-
- try:
- snatch_response = self.session.post(snatch_url, headers=self.headers, json=snatch_payload, timeout=15)
- snatch_response.raise_for_status()
- snatch_data = snatch_response.json()
-
- if snatch_data.get('code') != 200:
- return None, snatch_data.get('resultMessage', '获取BiddingNo失败')
-
- bidding_no = snatch_data.get('data')
- if not bidding_no:
- return None, "获取BiddingNo为空"
-
- except Exception as e:
- return None, str(e)
-
- # 获取订单详情
- detail_url = f"https://wirelessgate.aihuishou.com/jdx-qa-service/app/ka/v3/order/detail?orderNo={order_no}&biddingNo={bidding_no}"
-
- try:
- detail_response = self.session.get(detail_url, headers=self.headers, timeout=15)
- detail_response.raise_for_status()
- detail_data = detail_response.json()
-
- if detail_data.get('code') != 200:
- return None, detail_data.get('resultMessage', '获取订单详情失败')
-
- encrypted_hex = detail_data.get('data')
- if not encrypted_hex:
- return None, "未获取到加密数据"
-
- decrypted_json_string = self.decrypt_order_data(encrypted_hex)
- order_detail = json.loads(decrypted_json_string)
-
- # 存入缓存
- with self.cache_lock:
- self.order_detail_cache[order_no] = order_detail
-
- return order_detail, None
-
- except Exception as e:
- return None, str(e)
- def find_original_image_by_notice(self, json_data, notice_keyword):
- """优化的图片查找(使用栈迭代)"""
- if isinstance(json_data, str):
- try:
- json_data = json.loads(json_data)
- except json.JSONDecodeError:
- return None
-
- # 使用栈迭代而非递归
- stack = [json_data]
- keyword_lower = notice_keyword.lower()
-
- while stack:
- obj = stack.pop()
-
- if isinstance(obj, dict):
- # 检查当前字典
- if 'notice' in obj and 'originalImage' in obj:
- if keyword_lower in obj['notice'].lower():
- return obj['originalImage']
-
- # 将所有值加入栈
- for value in obj.values():
- stack.append(value)
-
- elif isinstance(obj, list):
- # 将列表元素加入栈
- stack.extend(obj)
-
- return None
- def get_all_image_notices(self, json_data):
- """获取所有图片的notice信息"""
- notices = []
- stack = [json_data]
-
- while stack:
- obj = stack.pop()
-
- if isinstance(obj, dict):
- if 'notice' in obj and 'image' in obj:
- notices.append(obj['notice'])
- for value in obj.values():
- stack.append(value)
- elif isinstance(obj, list):
- stack.extend(obj)
-
- return list(set(notices))
- def ocr_by_url(self, image_url):
- """通过URL进行OCR识别(带缓存)"""
- # 检查缓存
- cache_key = hashlib.md5(image_url.encode()).hexdigest()
- with self.cache_lock:
- if cache_key in self.ocr_cache:
- return self.ocr_cache[cache_key]
-
- try:
- payload = {"url": image_url}
- response = self.session.post(self.ocr_url, json=payload, timeout=30)
-
- if response.status_code == 200:
- ocr_result = response.json()
- if ocr_result.get('success'):
- # 存入缓存
- with self.cache_lock:
- self.ocr_cache[cache_key] = ocr_result
- return ocr_result
- return None
-
- except Exception as e:
- return None
- def extract_text_from_ocr_result(self, ocr_result):
- """提取OCR文本"""
- if not ocr_result or not ocr_result.get('texts'):
- return ''
-
- texts = ''
- for item in ocr_result['texts']:
- rec_texts = item.get('rec_texts', [])
- for text in rec_texts:
- texts = texts + text
-
- return texts
- def extract_charge_count(self, text):
- """提取充电次数"""
- # 匹配(232次循环)格式
- pattern1 = r'[((]\s*(\d+)\s*次\s*循环\s*[))]'
- match = re.search(pattern1, text)
- if match:
- return int(match.group(1))
-
- # 匹配充电次数123次格式
- pattern2 = r'充电次数(\d+)次'
- match = re.search(pattern2, text)
- if match:
- return int(match.group(1))
-
- # 匹配单独的 数字+次 格式(如 71477次)
- pattern3 = r'(\d+)次'
- match = re.search(pattern3, text)
- if match:
- return int(match.group(1))
-
- return None
-
- def find_best_image(self, order_detail):
- """查找最佳验机图片(按优先级)"""
- keywords = [
- "苹果沙漏验机报告",
- "沙漏图",
- "苹果验机软件",
- "苹果沙漏验机基本信息",
- "验机照片",
- "验机报告"
- ]
-
- for keyword in keywords:
- img = self.find_original_image_by_notice(order_detail, keyword)
- if img:
- return img
- return None
- # --- 后台工作线程 ---
- class OrderListWorker(QThread):
- finished = pyqtSignal(list)
- error = pyqtSignal(str)
- def __init__(self, order_manager):
- super().__init__()
- self.order_manager = order_manager
- def run(self):
- try:
- order_list, error = self.order_manager.get_pending_order_list(100)
- if error:
- self.error.emit(error)
- else:
- self.finished.emit(order_list)
- except Exception as e:
- self.error.emit(str(e))
- class ChargeQueryWorker(QThread):
- finished = pyqtSignal(int, object, object, list)
- error = pyqtSignal(int, str)
- def __init__(self, order_manager, order_no, index):
- super().__init__()
- self.order_manager = order_manager
- self.order_no = order_no
- self.index = index
- def run(self):
- try:
- # 获取订单详情
- order_detail, error = self.order_manager.get_order_detail(self.order_no)
- if error:
- self.error.emit(self.index, error)
- return
-
- # 查找所有图片的notice
- image_notices = self.order_manager.get_all_image_notices(order_detail)
-
- # 查找验机图片
- report_img = self.order_manager.find_best_image(order_detail)
-
- if not report_img:
- self.finished.emit(self.index, None, None, image_notices)
- return
-
- # OCR识别
- ocr_result = self.order_manager.ocr_by_url(report_img)
- if not ocr_result:
- self.finished.emit(self.index, None, None, image_notices)
- return
-
- # 提取充电次数
- ocr_text = self.order_manager.extract_text_from_ocr_result(ocr_result)
- charge_count = self.order_manager.extract_charge_count(ocr_text)
-
- self.finished.emit(self.index, charge_count, ocr_result, image_notices)
-
- except Exception as e:
- self.error.emit(self.index, str(e))
- # --- 主窗口 ---
- class MainWindow(QMainWindow):
- def __init__(self):
- super().__init__()
- self.order_manager = None
- self.order_list = []
- self.order_details_cache = {}
- self.charge_counts = {}
- self.is_batch_processing = False
- self.batch_queue = []
- self.current_batch_index = 0
-
- # 并行处理相关
- self.thread_pool = ThreadPoolExecutor(max_workers=5)
- self.future_tasks = {}
- self.processing_lock = threading.Lock()
- self.completed_count = 0
- self.total_tasks = 0
-
- self.init_ui()
- def init_ui(self):
- self.setWindowTitle('爱回收订单查询工具 (优化版)')
- self.setGeometry(100, 100, 1200, 800)
-
- # 设置样式
- self.setStyleSheet("""
- QMainWindow {
- background-color: #f0f0f0;
- }
- QGroupBox {
- font-weight: bold;
- border: 1px solid #ccc;
- border-radius: 5px;
- margin-top: 10px;
- padding-top: 10px;
- }
- QGroupBox::title {
- subcontrol-origin: margin;
- left: 10px;
- padding: 0 5px 0 5px;
- }
- QPushButton {
- background-color: #0078d7;
- color: white;
- border: none;
- padding: 8px 16px;
- border-radius: 4px;
- font-weight: bold;
- }
- QPushButton:hover {
- background-color: #005a9e;
- }
- QPushButton:disabled {
- background-color: #cccccc;
- }
- QPushButton#batch_btn {
- background-color: #28a745;
- }
- QPushButton#batch_btn:hover {
- background-color: #218838;
- }
- QPushButton#batch_btn:disabled {
- background-color: #cccccc;
- }
- QLineEdit, QTextEdit {
- border: 1px solid #ccc;
- border-radius: 4px;
- padding: 5px;
- }
- QTableWidget {
- border: 1px solid #ccc;
- border-radius: 4px;
- gridline-color: #e0e0e0;
- }
- QTableWidget::item {
- padding: 5px;
- }
- QHeaderView::section {
- background-color: #e8e8e8;
- padding: 5px;
- border: 1px solid #ccc;
- font-weight: bold;
- }
- QStatusBar {
- background-color: #f0f0f0;
- color: #333;
- }
- #log_text {
- background-color: #1e1e1e;
- color: #d4d4d4;
- font-family: 'Consolas', 'Courier New', monospace;
- font-size: 11px;
- }
- QProgressBar {
- border: 1px solid #ccc;
- border-radius: 4px;
- text-align: center;
- }
- QProgressBar::chunk {
- background-color: #0078d7;
- border-radius: 4px;
- }
- """)
- central_widget = QWidget()
- self.setCentralWidget(central_widget)
- main_layout = QVBoxLayout(central_widget)
- main_layout.setSpacing(10)
- # --- 顶部配置区域 ---
- config_group = QGroupBox("配置")
- config_layout = QGridLayout()
-
- config_layout.addWidget(QLabel("Access Token:"), 0, 0)
- self.token_input = QLineEdit()
- self.token_input.setText("jdx9ce52336abee4cf78db56567dc259512")
- self.token_input.setEchoMode(QLineEdit.Password)
- config_layout.addWidget(self.token_input, 0, 1)
-
- self.show_token_btn = QPushButton("显示")
- self.show_token_btn.setFixedWidth(60)
- self.show_token_btn.clicked.connect(self.toggle_token_visibility)
- config_layout.addWidget(self.show_token_btn, 0, 2)
-
- config_layout.addWidget(QLabel("OCR地址:"), 1, 0)
- self.ocr_input = QLineEdit()
- self.ocr_input.setText("http://111.229.45.38:19100/ocr/url")
- config_layout.addWidget(self.ocr_input, 1, 1, 1, 2)
-
- # 并发数设置
- config_layout.addWidget(QLabel("并发数:"), 2, 0)
- self.thread_count_combo = QComboBox()
- self.thread_count_combo.addItems(["3", "5", "8", "10", "15"])
- self.thread_count_combo.setCurrentText("5")
- config_layout.addWidget(self.thread_count_combo, 2, 1)
-
- btn_layout = QHBoxLayout()
- self.search_btn = QPushButton("🔍 搜索订单")
- self.search_btn.clicked.connect(self.search_orders)
- self.search_btn.setFixedHeight(35)
- btn_layout.addWidget(self.search_btn)
-
- self.refresh_btn = QPushButton("🔄 刷新")
- self.refresh_btn.clicked.connect(self.refresh_orders)
- self.refresh_btn.setFixedHeight(35)
- btn_layout.addWidget(self.refresh_btn)
-
- self.batch_btn = QPushButton("⚡ 并行查询充电次数")
- self.batch_btn.setObjectName("batch_btn")
- self.batch_btn.clicked.connect(self.batch_check_charge_counts_parallel)
- self.batch_btn.setFixedHeight(35)
- self.batch_btn.setEnabled(False)
- btn_layout.addWidget(self.batch_btn)
-
- btn_layout.addStretch()
- config_layout.addLayout(btn_layout, 3, 0, 1, 3)
-
- config_group.setLayout(config_layout)
- main_layout.addWidget(config_group)
- # --- 订单列表 ---
- list_group = QGroupBox("订单列表")
- list_layout = QVBoxLayout()
-
- self.progress_bar = QProgressBar()
- self.progress_bar.setVisible(False)
- list_layout.addWidget(self.progress_bar)
-
- self.table = QTableWidget()
- self.table.setColumnCount(7)
- self.table.setHorizontalHeaderLabels(["序号", "订单号", "商品信息", "IMEI", "预估价格", "门店", "充电次数"])
- self.table.setColumnWidth(0, 50)
- self.table.setColumnWidth(1, 200)
- self.table.setColumnWidth(2, 250)
- self.table.setColumnWidth(3, 150)
- self.table.setColumnWidth(4, 100)
- self.table.setColumnWidth(5, 200)
- self.table.setColumnWidth(6, 100)
- self.table.horizontalHeader().setStretchLastSection(True)
- self.table.verticalHeader().setVisible(False)
-
- list_layout.addWidget(self.table)
- list_group.setLayout(list_layout)
- main_layout.addWidget(list_group)
- # --- 日志区域 ---
- log_group = QGroupBox("日志")
- log_layout = QVBoxLayout()
-
- self.log_text = QTextEdit()
- self.log_text.setObjectName("log_text")
- self.log_text.setReadOnly(True)
- self.log_text.setMaximumHeight(200)
- log_layout.addWidget(self.log_text)
-
- log_btn_layout = QHBoxLayout()
- self.clear_log_btn = QPushButton("清空日志")
- self.clear_log_btn.clicked.connect(self.clear_log)
- log_btn_layout.addWidget(self.clear_log_btn)
-
- log_btn_layout.addStretch()
- log_layout.addLayout(log_btn_layout)
-
- log_group.setLayout(log_layout)
- main_layout.addWidget(log_group)
- self.status_bar = QStatusBar()
- self.setStatusBar(self.status_bar)
- self.status_bar.showMessage("就绪")
- def toggle_token_visibility(self):
- if self.token_input.echoMode() == QLineEdit.Password:
- self.token_input.setEchoMode(QLineEdit.Normal)
- self.show_token_btn.setText("隐藏")
- else:
- self.token_input.setEchoMode(QLineEdit.Password)
- self.show_token_btn.setText("显示")
- def log_message(self, message, level="INFO"):
- timestamp = QDateTime.currentDateTime().toString("yyyy-MM-dd hh:mm:ss")
- color_map = {
- "INFO": "#d4d4d4",
- "WARNING": "#ffa500",
- "ERROR": "#ff4444",
- "SUCCESS": "#4caf50",
- "PROGRESS": "#00bcd4"
- }
- color = color_map.get(level, "#d4d4d4")
- html = f'<span style="color: #888888;">[{timestamp}]</span> <span style="color: {color};">[{level}]</span> {message}<br>'
- self.log_text.append(html)
- scrollbar = self.log_text.verticalScrollBar()
- scrollbar.setValue(scrollbar.maximum())
- def clear_log(self):
- self.log_text.clear()
- def search_orders(self):
- access_token = self.token_input.text().strip()
- ocr_url = self.ocr_input.text().strip()
-
- if not access_token:
- QMessageBox.warning(self, "提示", "请输入Access Token")
- return
-
- if not ocr_url:
- QMessageBox.warning(self, "提示", "请输入OCR地址")
- return
-
- self.log_message("开始搜索订单...", "INFO")
-
- self.order_manager = OrderManager(access_token, ocr_url)
- self.order_details_cache = {}
- self.charge_counts = {}
- self.batch_btn.setEnabled(False)
-
- self.status_bar.showMessage("正在获取订单列表...")
- self.search_btn.setEnabled(False)
-
- self.worker = OrderListWorker(self.order_manager)
- self.worker.finished.connect(self.on_orders_loaded)
- self.worker.error.connect(self.on_error)
- self.worker.start()
- def refresh_orders(self):
- if self.order_manager:
- self.search_orders()
- else:
- QMessageBox.warning(self, "提示", "请先搜索订单")
- def on_orders_loaded(self, order_list):
- self.order_list = order_list
- self.display_orders()
- self.search_btn.setEnabled(True)
- self.batch_btn.setEnabled(True)
- self.status_bar.showMessage(f"成功加载 {len(order_list)} 条订单")
- self.log_message(f"成功加载 {len(order_list)} 条订单", "SUCCESS")
- def on_error(self, error_msg):
- self.search_btn.setEnabled(True)
- self.batch_btn.setEnabled(False)
- self.status_bar.showMessage("加载失败")
- self.log_message(f"获取订单列表失败: {error_msg}", "ERROR")
- QMessageBox.critical(self, "错误", f"获取订单列表失败:\n{error_msg}")
- def display_orders(self):
- self.table.setRowCount(len(self.order_list))
-
- for i, order in enumerate(self.order_list):
- self.table.setItem(i, 0, QTableWidgetItem(str(i + 1)))
-
- order_no = order.get('orderNo', '')
- self.table.setItem(i, 1, QTableWidgetItem(order_no))
-
- brand_info = order.get('brandInfo', '')
- self.table.setItem(i, 2, QTableWidgetItem(brand_info))
-
- imei = order.get('imei', '')
- self.table.setItem(i, 3, QTableWidgetItem(imei))
-
- price = order.get('price', '')
- price_str = str(price) if price else '待定'
- self.table.setItem(i, 4, QTableWidgetItem(price_str))
-
- title = order.get('title', '')
- self.table.setItem(i, 5, QTableWidgetItem(title))
-
- charge_widget = QWidget()
- charge_layout = QHBoxLayout(charge_widget)
- charge_layout.setContentsMargins(5, 2, 5, 2)
- charge_layout.setSpacing(5)
-
- charge_label = QLabel("未查询")
- charge_label.setObjectName(f"charge_label_{i}")
- charge_layout.addWidget(charge_label)
-
- check_btn = QPushButton("查询")
- check_btn.setFixedSize(50, 25)
- check_btn.setObjectName(f"check_btn_{i}")
- check_btn.clicked.connect(lambda checked, idx=i: self.check_charge_count(idx))
- charge_layout.addWidget(check_btn)
-
- charge_widget.setLayout(charge_layout)
- self.table.setCellWidget(i, 6, charge_widget)
- def check_charge_count(self, index):
- if not self.order_manager:
- return
-
- if index >= len(self.order_list):
- return
-
- order_no = self.order_list[index].get('orderNo')
- if not order_no:
- return
-
- self.log_message(f"开始查询订单 {order_no} 的充电次数", "INFO")
- self.status_bar.showMessage(f"正在查询订单 {order_no} 的充电次数...")
-
- btn = self.findChild(QPushButton, f"check_btn_{index}")
- if btn:
- btn.setEnabled(False)
- btn.setText("查询中")
-
- self.charge_worker = ChargeQueryWorker(self.order_manager, order_no, index)
- self.charge_worker.finished.connect(self.on_charge_queried)
- self.charge_worker.error.connect(self.on_charge_error)
- self.charge_worker.start()
- # ===== 并行批量查询 =====
- def batch_check_charge_counts_parallel(self):
- """并行批量查询充电次数"""
- if not self.order_list:
- QMessageBox.warning(self, "提示", "请先搜索订单")
- return
-
- if self.is_batch_processing:
- return
-
- # 检查哪些订单还没查询
- pending_indices = []
- for i, order in enumerate(self.order_list):
- order_no = order.get('orderNo')
- if order_no not in self.charge_counts:
- pending_indices.append(i)
-
- if not pending_indices:
- self.log_message("所有订单已查询完成", "SUCCESS")
- QMessageBox.information(self, "提示", "所有订单已查询完成")
- return
-
- # 关闭旧的线程池
- if hasattr(self, 'thread_pool'):
- self.thread_pool.shutdown(wait=False)
-
- # 获取并发数
- max_workers = int(self.thread_count_combo.currentText())
- self.thread_pool = ThreadPoolExecutor(max_workers=max_workers)
-
- self.is_batch_processing = True
- self.completed_count = 0
- self.total_tasks = len(pending_indices)
- self.future_tasks = {}
-
- # 禁用按钮
- self.batch_btn.setEnabled(False)
- self.search_btn.setEnabled(False)
- self.refresh_btn.setEnabled(False)
-
- # 显示进度条
- self.progress_bar.setVisible(True)
- self.progress_bar.setMaximum(len(pending_indices))
- self.progress_bar.setValue(0)
-
- self.log_message(f"开始并行查询 {len(pending_indices)} 个订单 (并发数: {max_workers})", "PROGRESS")
-
- # 提交所有任务
- token = self.token_input.text().strip()
- ocr_url = self.ocr_input.text().strip()
-
- for idx in pending_indices:
- order_no = self.order_list[idx].get('orderNo')
-
- # 提交任务到线程池
- future = self.thread_pool.submit(
- self.process_single_order_parallel,
- token,
- ocr_url,
- order_no,
- idx
- )
- self.future_tasks[future] = idx
-
- # 更新UI按钮状态
- btn = self.findChild(QPushButton, f"check_btn_{idx}")
- if btn:
- btn.setEnabled(False)
- btn.setText("查询中")
-
- # 启动结果收集线程
- collector = threading.Thread(target=self.collect_parallel_results)
- collector.daemon = True
- collector.start()
- def process_single_order_parallel(self, token, ocr_url, order_no, index):
- """在独立线程中处理单个订单"""
- try:
- # 每个线程创建独立的OrderManager实例
- order_manager = OrderManager(token, ocr_url)
-
- # 获取订单详情
- order_detail, error = order_manager.get_order_detail(order_no)
- if error:
- return index, None, error, None
-
- # 查找验机图片
- report_img = order_manager.find_best_image(order_detail)
- if not report_img:
- return index, None, "未找到验机图片", None
-
- # OCR识别
- ocr_result = order_manager.ocr_by_url(report_img)
- if not ocr_result:
- return index, None, "OCR识别失败", None
-
- # 提取充电次数
- ocr_text = order_manager.extract_text_from_ocr_result(ocr_result)
- charge_count = order_manager.extract_charge_count(ocr_text)
-
- return index, charge_count, None, ocr_result
-
- except Exception as e:
- return index, None, str(e), None
- def collect_parallel_results(self):
- """收集并行处理的结果"""
- for future in as_completed(self.future_tasks):
- index = self.future_tasks[future]
- try:
- idx, charge_count, error, ocr_result = future.result(timeout=60)
-
- # 更新UI(在主线程)
- QMetaObject.invokeMethod(self, "update_parallel_result",
- Qt.QueuedConnection,
- Q_ARG(int, idx),
- Q_ARG(object, charge_count),
- Q_ARG(str, error or ""),
- Q_ARG(object, ocr_result)
- )
-
- except Exception as e:
- QMetaObject.invokeMethod(self, "update_parallel_result",
- Qt.QueuedConnection,
- Q_ARG(int, index),
- Q_ARG(object, None),
- Q_ARG(str, str(e)),
- Q_ARG(object, None)
- )
-
- self.completed_count += 1
-
- # 更新进度(在主线程)
- QMetaObject.invokeMethod(self, "update_batch_progress",
- Qt.QueuedConnection,
- Q_ARG(int, self.completed_count)
- )
-
- # 全部完成
- QMetaObject.invokeMethod(self, "on_batch_complete_parallel",
- Qt.QueuedConnection
- )
- @pyqtSlot(int, object, str, object)
- def update_parallel_result(self, index, charge_count, error, ocr_result):
- """更新单个结果(在主线程执行)"""
- order_no = self.order_list[index].get('orderNo')
-
- # 缓存结果
- if charge_count is not None:
- self.charge_counts[order_no] = charge_count
- self.update_charge_display(index, charge_count)
- self.log_message(f"订单 {order_no} 充电次数: {charge_count}", "SUCCESS")
- else:
- self.charge_counts[order_no] = None
- self.update_charge_display(index, None)
- if error:
- self.log_message(f"订单 {order_no} 查询失败: {error}", "ERROR")
-
- # 恢复按钮
- btn = self.findChild(QPushButton, f"check_btn_{index}")
- if btn:
- btn.setEnabled(True)
- btn.setText("查询")
- @pyqtSlot(int)
- def update_batch_progress(self, completed):
- """更新进度(在主线程执行)"""
- self.progress_bar.setValue(completed)
- self.status_bar.showMessage(f"并行处理中: {completed}/{self.total_tasks}")
- @pyqtSlot()
- def on_batch_complete_parallel(self):
- """并行批量完成(在主线程执行)"""
- self.is_batch_processing = False
- self.future_tasks.clear()
-
- # 恢复按钮
- self.batch_btn.setEnabled(True)
- self.search_btn.setEnabled(True)
- self.refresh_btn.setEnabled(True)
-
- # 隐藏进度条
- self.progress_bar.setVisible(False)
-
- # 统计结果
- success_count = sum(1 for v in self.charge_counts.values() if v is not None)
- total_count = len(self.charge_counts)
-
- self.status_bar.showMessage(f"批量查询完成,成功 {success_count}/{total_count}")
- self.log_message(f"批量查询完成,成功 {success_count}/{total_count}", "SUCCESS")
-
- QMessageBox.information(self, "完成",
- f"并行批量查询完成!\n\n成功找到充电次数: {success_count}/{total_count}\n总订单: {total_count}")
- # ===== 原有的串行查询(保留作为备选) =====
- def batch_check_charge_counts(self):
- """串行批量查询(保留)"""
- if not self.order_list:
- QMessageBox.warning(self, "提示", "请先搜索订单")
- return
-
- if self.is_batch_processing:
- return
-
- pending_indices = []
- for i, order in enumerate(self.order_list):
- order_no = order.get('orderNo')
- if order_no not in self.charge_counts:
- pending_indices.append(i)
-
- if not pending_indices:
- self.log_message("所有订单已查询完成", "SUCCESS")
- QMessageBox.information(self, "提示", "所有订单已查询完成")
- return
-
- self.is_batch_processing = True
- self.batch_queue = pending_indices
- self.current_batch_index = 0
-
- self.batch_btn.setEnabled(False)
- self.search_btn.setEnabled(False)
- self.refresh_btn.setEnabled(False)
-
- self.progress_bar.setVisible(True)
- self.progress_bar.setMaximum(len(pending_indices))
- self.progress_bar.setValue(0)
-
- self.log_message(f"开始串行查询 {len(pending_indices)} 个订单", "PROGRESS")
- self.process_next_batch()
- def process_next_batch(self):
- if self.current_batch_index >= len(self.batch_queue):
- self.on_batch_complete()
- return
-
- index = self.batch_queue[self.current_batch_index]
- order_no = self.order_list[index].get('orderNo')
-
- self.progress_bar.setValue(self.current_batch_index + 1)
- self.status_bar.showMessage(f"串行处理中: {self.current_batch_index + 1}/{len(self.batch_queue)}")
-
- btn = self.findChild(QPushButton, f"check_btn_{index}")
- if btn:
- btn.setEnabled(False)
- btn.setText("查询中")
-
- self.batch_worker = ChargeQueryWorker(self.order_manager, order_no, index)
- self.batch_worker.finished.connect(self.on_batch_item_complete)
- self.batch_worker.error.connect(self.on_batch_item_error)
- self.batch_worker.start()
- def on_batch_item_complete(self, index, charge_count, ocr_result, image_notices):
- order_no = self.order_list[index].get('orderNo')
-
- self.charge_counts[order_no] = charge_count
- self.update_charge_display(index, charge_count)
-
- btn = self.findChild(QPushButton, f"check_btn_{index}")
- if btn:
- btn.setEnabled(True)
- btn.setText("查询")
-
- if charge_count is not None:
- self.log_message(f"[{self.current_batch_index + 1}/{len(self.batch_queue)}] 订单 {order_no} 充电次数: {charge_count}", "SUCCESS")
- else:
- self.log_message(f"[{self.current_batch_index + 1}/{len(self.batch_queue)}] 订单 {order_no} 未找到充电次数", "WARNING")
-
- self.current_batch_index += 1
- QTimer.singleShot(300, self.process_next_batch)
- def on_batch_item_error(self, index, error_msg):
- order_no = self.order_list[index].get('orderNo')
- self.log_message(f"[{self.current_batch_index + 1}/{len(self.batch_queue)}] 订单 {order_no} 查询失败: {error_msg}", "ERROR")
-
- btn = self.findChild(QPushButton, f"check_btn_{index}")
- if btn:
- btn.setEnabled(True)
- btn.setText("查询")
-
- self.current_batch_index += 1
- QTimer.singleShot(300, self.process_next_batch)
- def on_batch_complete(self):
- self.is_batch_processing = False
- self.batch_queue = []
-
- self.batch_btn.setEnabled(True)
- self.search_btn.setEnabled(True)
- self.refresh_btn.setEnabled(True)
-
- self.progress_bar.setVisible(False)
-
- success_count = sum(1 for v in self.charge_counts.values() if v is not None)
- total_count = len(self.charge_counts)
-
- self.status_bar.showMessage(f"串行查询完成,成功 {success_count}/{total_count}")
- self.log_message(f"串行查询完成,成功 {success_count}/{total_count}", "SUCCESS")
-
- QMessageBox.information(self, "完成", f"串行查询完成!\n\n成功找到充电次数: {success_count}/{total_count}")
- # ===== 通用方法 =====
- def on_charge_queried(self, index, charge_count, ocr_result, image_notices):
- order_no = self.order_list[index].get('orderNo')
-
- if charge_count is None:
- if ocr_result:
- self.log_message(f"订单 {order_no} 未找到充电次数,OCR识别结果:", "WARNING")
- ocr_text = self.order_manager.extract_text_from_ocr_result(ocr_result)
- if ocr_text:
- self.log_message(f"OCR文本: {ocr_text}", "INFO")
- else:
- self.log_message(f"OCR文本为空", "WARNING")
-
- if not image_notices:
- self.log_message(f"订单 {order_no} 未找到验机图片", "ERROR")
- else:
- self.log_message(f"订单 {order_no} 充电次数: {charge_count}", "SUCCESS")
-
- self.charge_counts[order_no] = charge_count
- self.update_charge_display(index, charge_count)
-
- btn = self.findChild(QPushButton, f"check_btn_{index}")
- if btn:
- btn.setEnabled(True)
- btn.setText("查询")
-
- if charge_count is not None:
- self.status_bar.showMessage(f"订单 {order_no} 充电次数: {charge_count}")
- else:
- self.status_bar.showMessage(f"订单 {order_no} 未找到充电次数信息")
- def on_charge_error(self, index, error_msg):
- order_no = self.order_list[index].get('orderNo')
- self.log_message(f"订单 {order_no} 查询失败: {error_msg}", "ERROR")
-
- btn = self.findChild(QPushButton, f"check_btn_{index}")
- if btn:
- btn.setEnabled(True)
- btn.setText("查询")
-
- self.status_bar.showMessage(f"查询失败: {error_msg}")
- QMessageBox.warning(self, "查询失败", f"获取订单详情失败:\n{error_msg}")
- def update_charge_display(self, index, charge_count):
- widget = self.table.cellWidget(index, 6)
- if widget:
- label = widget.findChild(QLabel)
- if label:
- if charge_count is not None:
- label.setText(f"{charge_count} 次")
- label.setStyleSheet("color: #28a745; font-weight: bold;")
- else:
- label.setText("未找到")
- label.setStyleSheet("color: #dc3545;")
-
- def closeEvent(self, event):
- """关闭窗口时清理线程池"""
- if hasattr(self, 'thread_pool'):
- self.thread_pool.shutdown(wait=False)
- event.accept()
- # --- 主程序入口 ---
- def main():
- app = QApplication(sys.argv)
- app.setStyle('Fusion')
-
- window = MainWindow()
- window.show()
-
- sys.exit(app.exec_())
- if __name__ == "__main__":
- main()
|