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 ImageSelectionDialog(QDialog):
"""图片选择对话框 - 纯主线程,无定时器,无子线程"""
def __init__(self, image_urls_with_notice, parent=None):
super().__init__(parent)
self.image_urls_with_notice = image_urls_with_notice
self.selected_url = None
self.selected_index = -1
self.skip = False
self.image_cache = {}
self.init_ui()
# 直接加载,不用定时器
self.load_images()
def init_ui(self):
self.setWindowTitle("选择验机图片")
self.setGeometry(200, 200, 900, 600)
main_layout = QVBoxLayout(self)
hint_label = QLabel("请选择要识别的图片(双击预览大图):")
hint_label.setStyleSheet("font-weight: bold; font-size: 12px; padding: 5px;")
main_layout.addWidget(hint_label)
self.list_widget = QListWidget()
self.list_widget.setIconSize(QSize(150, 100))
self.list_widget.setViewMode(QListWidget.IconMode)
self.list_widget.setResizeMode(QListWidget.Adjust)
self.list_widget.setGridSize(QSize(180, 140))
self.list_widget.setSpacing(10)
main_layout.addWidget(self.list_widget)
btn_layout = QHBoxLayout()
self.preview_btn = QPushButton("🔍 预览大图")
self.preview_btn.clicked.connect(self.preview_selected)
btn_layout.addWidget(self.preview_btn)
self.select_btn = QPushButton("✅ 选择此图片识别")
self.select_btn.clicked.connect(self.select_image)
self.select_btn.setStyleSheet("background-color: #28a745; color: white;")
btn_layout.addWidget(self.select_btn)
self.skip_btn = QPushButton("⏭️ 跳过此订单")
self.skip_btn.clicked.connect(self.skip_order)
self.skip_btn.setStyleSheet("background-color: #dc3545; color: white;")
btn_layout.addWidget(self.skip_btn)
self.cancel_btn = QPushButton("❌ 取消全部")
self.cancel_btn.clicked.connect(self.reject)
self.cancel_btn.setStyleSheet("background-color: #6c757d; color: white;")
btn_layout.addWidget(self.cancel_btn)
main_layout.addLayout(btn_layout)
self.status_label = QLabel("正在加载图片...")
self.status_label.setStyleSheet("color: #ffa500; padding: 5px;")
main_layout.addWidget(self.status_label)
# 直接连接信号,不用QueuedConnection
self.list_widget.itemSelectionChanged.connect(self.on_selection_changed)
self.list_widget.itemDoubleClicked.connect(self.preview_selected)
def load_images(self):
"""直接在主线程加载图片"""
if not self.image_urls_with_notice:
self.status_label.setText("没有找到图片")
self.status_label.setStyleSheet("color: #dc3545; padding: 5px;")
return
self.status_label.setText(f"正在加载图片... 共 {len(self.image_urls_with_notice)} 张")
QApplication.processEvents()
# 先创建所有列表项
for i, (notice, url) in enumerate(self.image_urls_with_notice):
display_text = notice[:25] + "..." if len(notice) > 25 else notice
item = QListWidgetItem()
item.setText(display_text)
item.setToolTip(f"完整信息: {notice}\nURL: {url}")
item.setData(Qt.UserRole, url)
item.setData(Qt.UserRole + 1, i)
item.setData(Qt.UserRole + 2, notice)
item.setIcon(self.style().standardIcon(QStyle.SP_FileIcon))
self.list_widget.addItem(item)
# 直接在主线程下载图片
loaded_count = 0
for i, (notice, url) in enumerate(self.image_urls_with_notice):
try:
response = requests.get(url, timeout=10)
if response.status_code == 200:
img_data = response.content
pixmap = QPixmap()
pixmap.loadFromData(img_data)
if not pixmap.isNull():
scaled = pixmap.scaled(150, 100, Qt.KeepAspectRatio, Qt.SmoothTransformation)
item = self.list_widget.item(i)
if item:
item.setIcon(QIcon(scaled))
self.image_cache[url] = pixmap
loaded_count += 1
self.status_label.setText(f"加载图片 {i+1}/{len(self.image_urls_with_notice)}")
QApplication.processEvents()
except Exception as e:
print(f"加载图片失败: {e}")
self.status_label.setText(f"加载完成,共加载 {loaded_count}/{len(self.image_urls_with_notice)} 张图片")
self.status_label.setStyleSheet("color: #28a745; padding: 5px;")
if self.list_widget.count() > 0:
self.list_widget.setCurrentRow(0)
def on_selection_changed(self):
selected = self.list_widget.currentItem()
if selected:
notice = selected.data(Qt.UserRole + 2)
self.status_label.setText(f"已选择: {notice[:50]}...")
self.status_label.setStyleSheet("color: #0078d7; padding: 5px;")
else:
self.status_label.setText("请选择一个图片")
self.status_label.setStyleSheet("color: #666; padding: 5px;")
def preview_selected(self):
selected = self.list_widget.currentItem()
if not selected:
QMessageBox.warning(self, "提示", "请先选择一个图片")
return
url = selected.data(Qt.UserRole)
notice = selected.data(Qt.UserRole + 2)
preview_dialog = QDialog(self)
preview_dialog.setWindowTitle(f"图片预览: {notice[:30]}...")
preview_dialog.setGeometry(300, 300, 800, 700)
layout = QVBoxLayout(preview_dialog)
scroll_area = QScrollArea()
scroll_area.setWidgetResizable(True)
label = QLabel()
label.setAlignment(Qt.AlignCenter)
label.setStyleSheet("background-color: #f0f0f0; padding: 10px;")
if url in self.image_cache:
pixmap = self.image_cache[url]
if pixmap and not pixmap.isNull():
scaled = pixmap.scaled(700, 600, Qt.KeepAspectRatio, Qt.SmoothTransformation)
label.setPixmap(scaled)
else:
label.setText("图片加载失败")
else:
try:
response = requests.get(url, timeout=10)
if response.status_code == 200:
pixmap = QPixmap()
pixmap.loadFromData(response.content)
if not pixmap.isNull():
self.image_cache[url] = pixmap
scaled = pixmap.scaled(700, 600, Qt.KeepAspectRatio, Qt.SmoothTransformation)
label.setPixmap(scaled)
else:
label.setText("图片加载失败")
else:
label.setText("图片加载失败")
except Exception as e:
label.setText(f"加载失败")
scroll_area.setWidget(label)
layout.addWidget(scroll_area)
close_btn = QPushButton("关闭")
close_btn.clicked.connect(preview_dialog.accept)
close_btn.setFixedHeight(35)
layout.addWidget(close_btn)
preview_dialog.exec_()
def select_image(self):
selected = self.list_widget.currentItem()
if not selected:
QMessageBox.warning(self, "提示", "请先选择一个图片")
return
self.selected_url = selected.data(Qt.UserRole)
self.selected_index = selected.data(Qt.UserRole + 1)
self.accept()
def skip_order(self):
reply = QMessageBox.question(self, "确认跳过",
"确定要跳过此订单吗?",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No)
if reply == QMessageBox.Yes:
self.skip = True
self.accept()
def get_selected_url(self):
return self.selected_url
def is_skipped(self):
return self.skip
# --- 核心功能类 ---
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
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_images_with_notice(self, json_data):
images = []
stack = [json_data]
while stack:
obj = stack.pop()
if isinstance(obj, dict):
if 'notice' in obj and 'originalImage' in obj:
images.append((obj['notice'], obj['originalImage']))
for value in obj.values():
stack.append(value)
elif isinstance(obj, list):
stack.extend(obj)
return images
def get_all_image_notices(self, json_data):
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):
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):
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):
if not text:
return None
pattern1 = r'[((]\s*(\d+)\s*次\s*循环\s*[))]'
match = re.search(pattern1, text)
if match:
return int(match.group(1))
pattern2 = r'充电次数(\d+)次'
match = re.search(pattern2, text)
if match:
return int(match.group(1))
pattern3 = r'(\d+)次'
match = re.search(pattern3, text)
if match:
return int(match.group(1))
return None
def find_best_image(self, order_detail, parent_widget=None):
"""查找最佳图片,如果找不到则让用户选择"""
keywords = [
"苹果沙漏验机报告",
"沙漏图",
"苹果验机软件",
"苹果沙漏验机基本信息",
"验机照片",
"验机报告"
]
# 先尝试用关键词自动匹配
for keyword in keywords:
img = self.find_original_image_by_notice(order_detail, keyword)
if img:
return img, "auto"
# 自动匹配失败,获取所有图片让用户选择
all_images = self.get_all_images_with_notice(order_detail)
if not all_images:
return None, "no_images"
# 如果只有一张图片,直接返回
if len(all_images) == 1:
return all_images[0][1], "auto_single"
# 多张图片,让用户选择
if parent_widget:
# 使用信号槽方式在主线程显示对话框
result_container = {"action": None, "url": None}
event = threading.Event()
def show_dialog():
try:
dialog = ImageSelectionDialog(all_images, parent_widget)
if dialog.exec_() == QDialog.Accepted:
if dialog.is_skipped():
result_container["action"] = "skip"
else:
selected_url = dialog.get_selected_url()
if selected_url:
result_container["action"] = "selected"
result_container["url"] = selected_url
else:
result_container["action"] = "cancelled"
else:
result_container["action"] = "cancelled"
except Exception as e:
print(f"显示对话框出错: {e}")
result_container["action"] = "error"
finally:
event.set()
# 在主线程执行
if QThread.currentThread() != QCoreApplication.instance().thread():
# 在子线程中,使用invokeMethod
QMetaObject.invokeMethod(parent_widget, "show_image_selection_dialog",
Qt.BlockingQueuedConnection,
Q_ARG(list, all_images),
Q_ARG(dict, result_container),
Q_ARG(threading.Event, event))
else:
# 已经在主线程,直接调用
show_dialog()
# 等待对话框完成
event.wait()
action = result_container.get("action")
if action == "selected":
return result_container.get("url"), "user_selected"
elif action == "skip":
return None, "skipped"
else:
return None, "cancelled"
# 没有父窗口,返回第一张图片
return all_images[0][1], "auto_first"
# --- 后台工作线程 ---
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, parent_widget=None):
super().__init__()
self.order_manager = order_manager
self.order_no = order_no
self.index = index
self.parent_widget = parent_widget
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
image_notices = self.order_manager.get_all_image_notices(order_detail)
# 传入 parent_widget 以便显示对话框
report_img, source = self.order_manager.find_best_image(order_detail, self.parent_widget)
# 处理用户取消或跳过的情况
if source in ["cancelled", "skipped"]:
self.error.emit(self.index, "用户取消选择" if source == "cancelled" else "用户跳过订单")
return
if not report_img:
self.finished.emit(self.index, None, None, image_notices)
return
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.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)
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)
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, 120)
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("就绪")
@pyqtSlot(list, dict, threading.Event)
def show_image_selection_dialog(self, all_images, result_container, event):
"""在主线程显示图片选择对话框(供invokeMethod调用)"""
try:
dialog = ImageSelectionDialog(all_images, self)
if dialog.exec_() == QDialog.Accepted:
if dialog.is_skipped():
result_container["action"] = "skip"
else:
selected_url = dialog.get_selected_url()
if selected_url:
result_container["action"] = "selected"
result_container["url"] = selected_url
else:
result_container["action"] = "cancelled"
else:
result_container["action"] = "cancelled"
except Exception as e:
print(f"显示图片选择对话框出错: {e}")
result_container["action"] = "error"
finally:
event.set()
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'[{timestamp}] [{level}] {message}
'
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.setFocusPolicy(Qt.NoFocus)
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 作为 parent_widget
self.charge_worker = ChargeQueryWorker(self.order_manager, order_no, index, self)
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(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 作为 parent_widget
self.batch_worker = ChargeQueryWorker(self.order_manager, order_no, index, self)
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}")
if error_msg != "用户取消选择" and 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):
event.accept()
# --- 主程序入口 ---
def main():
app = QApplication(sys.argv)
app.setStyle('Fusion')
window = MainWindow()
window.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()