PUGE 3 долоо хоног өмнө
parent
commit
0c7e481a32
3 өөрчлөгдсөн 506 нэмэгдсэн , 281 устгасан
  1. 10 7
      控制vm虚拟机.py
  2. 344 267
      解密机大侠.py
  3. 152 7
      飞书提交数据.py

+ 10 - 7
控制vm虚拟机.py

@@ -622,7 +622,7 @@ class VmControlGUI:
                 self.log(f"停止序列 {i+1}/2")
                 
                 # 按 = 等待2-2.5秒
-                wait_time = random.uniform(2.0, 2.5)
+                wait_time = random.uniform(2.5, 3.0)
                 self.log(f"  按 = (延迟 {wait_time:.1f}秒)")
                 self.press_key(0xBB, False)
                 time.sleep(wait_time)
@@ -664,8 +664,8 @@ class VmControlGUI:
                 time.sleep(wait_time)
                 
                 # 按 ESC
-                self.log(f"  按 ESC")
-                self.press_key(0x1B, False)
+                # self.log(f"  按 ESC")
+                # self.press_key(0x1B, False)
                 
                 # 每次之间等待200-800ms(最后一次不等待)
                 if i < 1:
@@ -772,11 +772,11 @@ class VmControlGUI:
                         
                         
             # 执行3次停止序列
-            for i in range(3):
+            for i in range(1):
                 self.log(f"停止序列 {i+1}/3")
                 
                 # 按 = 等待2-2.5秒
-                wait_time = random.uniform(2.0, 2.5)
+                wait_time = random.uniform(2.5, 3.0)
                 self.log(f"  按 = (延迟 {wait_time:.1f}秒)")
                 self.press_key(0xBB, False)
                 time.sleep(wait_time)
@@ -818,8 +818,8 @@ class VmControlGUI:
                 time.sleep(wait_time)
                 
                 # 按 ESC
-                self.log(f"  按 ESC")
-                self.press_key(0x1B, False)
+                # self.log(f"  按 ESC")
+                # self.press_key(0x1B, False)
                 
                 # 每次之间等待200-800ms(最后一次不等待)
                 if i < 2:
@@ -975,6 +975,9 @@ class VmControlGUI:
                 0xBB: b"=",         # = 号
                 0xBD: b"-",         # - 号
                 0x32: b"2",         # 主键盘2(如果确实需要主键盘的)
+                0x33: b"num3",         # 数字3  ← 新增
+                0x34: b"num4",         # 数字4  ← 新增
+                0x35: b"num5",         # 数字5  ← 新增
             }
             
             if self.ghost_available:

+ 344 - 267
解密机大侠.py

@@ -17,6 +17,210 @@ 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):
@@ -43,12 +247,10 @@ class OrderManager:
         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,
@@ -64,14 +266,12 @@ class OrderManager:
         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 = {
@@ -99,13 +299,10 @@ class OrderManager:
             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}
         
@@ -124,7 +321,6 @@ class OrderManager:
         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:
@@ -142,7 +338,6 @@ class OrderManager:
             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
             
@@ -152,14 +347,12 @@ class OrderManager:
             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()
         
@@ -167,23 +360,36 @@ class OrderManager:
             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):
-        """获取所有图片的notice信息"""
         notices = []
         stack = [json_data]
         
@@ -201,8 +407,6 @@ class OrderManager:
         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:
@@ -215,7 +419,6 @@ class OrderManager:
             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
@@ -225,7 +428,6 @@ class OrderManager:
             return None
 
     def extract_text_from_ocr_result(self, ocr_result):
-        """提取OCR文本"""
         if not ocr_result or not ocr_result.get('texts'):
             return ''
         
@@ -238,20 +440,19 @@ class OrderManager:
         return texts
 
     def extract_charge_count(self, text):
-        """提取充电次数"""
-        # 匹配(232次循环)格式
+        if not text:
+            return None
+        
         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:
@@ -259,8 +460,8 @@ class OrderManager:
         
         return None
     
-    def find_best_image(self, order_detail):
-        """查找最佳验机图片(按优先级)"""
+    def find_best_image(self, order_detail, parent_widget=None):
+        """查找最佳图片,如果找不到则让用户选择"""
         keywords = [
             "苹果沙漏验机报告",
             "沙漏图", 
@@ -270,11 +471,75 @@ class OrderManager:
             "验机报告"
         ]
         
+        # 先尝试用关键词自动匹配
         for keyword in keywords:
             img = self.find_original_image_by_notice(order_detail, keyword)
             if img:
-                return img
-        return None
+                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):
@@ -299,37 +564,39 @@ class ChargeQueryWorker(QThread):
     finished = pyqtSignal(int, object, object, list)
     error = pyqtSignal(int, str)
 
-    def __init__(self, order_manager, order_no, index):
+    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
             
-            # 查找所有图片的notice
             image_notices = self.order_manager.get_all_image_notices(order_detail)
             
-            # 查找验机图片
-            report_img = self.order_manager.find_best_image(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识别
             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)
             
@@ -350,20 +617,12 @@ class MainWindow(QMainWindow):
         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.setWindowTitle('爱回收订单查询工具')
         self.setGeometry(100, 100, 1200, 800)
         
-        # 设置样式
         self.setStyleSheet("""
             QMainWindow {
                 background-color: #f0f0f0;
@@ -448,7 +707,6 @@ class MainWindow(QMainWindow):
         main_layout = QVBoxLayout(central_widget)
         main_layout.setSpacing(10)
 
-        # --- 顶部配置区域 ---
         config_group = QGroupBox("配置")
         config_layout = QGridLayout()
         
@@ -468,13 +726,6 @@ class MainWindow(QMainWindow):
         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)
@@ -486,9 +737,9 @@ class MainWindow(QMainWindow):
         self.refresh_btn.setFixedHeight(35)
         btn_layout.addWidget(self.refresh_btn)
         
-        self.batch_btn = QPushButton("⚡ 并行查询充电次数")
+        self.batch_btn = QPushButton("📋 批量查询充电次数")
         self.batch_btn.setObjectName("batch_btn")
-        self.batch_btn.clicked.connect(self.batch_check_charge_counts_parallel)
+        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)
@@ -499,7 +750,6 @@ class MainWindow(QMainWindow):
         config_group.setLayout(config_layout)
         main_layout.addWidget(config_group)
 
-        # --- 订单列表 ---
         list_group = QGroupBox("订单列表")
         list_layout = QVBoxLayout()
         
@@ -516,7 +766,7 @@ class MainWindow(QMainWindow):
         self.table.setColumnWidth(3, 150)
         self.table.setColumnWidth(4, 100)
         self.table.setColumnWidth(5, 200)
-        self.table.setColumnWidth(6, 100)
+        self.table.setColumnWidth(6, 120)
         self.table.horizontalHeader().setStretchLastSection(True)
         self.table.verticalHeader().setVisible(False)
         
@@ -524,7 +774,6 @@ class MainWindow(QMainWindow):
         list_group.setLayout(list_layout)
         main_layout.addWidget(list_group)
 
-        # --- 日志区域 ---
         log_group = QGroupBox("日志")
         log_layout = QVBoxLayout()
         
@@ -549,6 +798,29 @@ class MainWindow(QMainWindow):
         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)
@@ -657,6 +929,8 @@ class MainWindow(QMainWindow):
             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)
             
@@ -680,211 +954,16 @@ class MainWindow(QMainWindow):
         btn = self.findChild(QPushButton, f"check_btn_{index}")
         if btn:
             btn.setEnabled(False)
-            btn.setText("查询中")
+            btn.setText("稍等")
         
-        self.charge_worker = ChargeQueryWorker(self.order_manager, order_no, index)
+        # 传入 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_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
@@ -915,7 +994,7 @@ class MainWindow(QMainWindow):
         self.progress_bar.setMaximum(len(pending_indices))
         self.progress_bar.setValue(0)
         
-        self.log_message(f"开始串行查询 {len(pending_indices)} 个订单", "PROGRESS")
+        self.log_message(f"开始批量查询 {len(pending_indices)} 个订单", "PROGRESS")
         self.process_next_batch()
 
     def process_next_batch(self):
@@ -927,14 +1006,15 @@ class MainWindow(QMainWindow):
         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)}")
+        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("查询中")
+            btn.setText("稍等")
         
-        self.batch_worker = ChargeQueryWorker(self.order_manager, order_no, index)
+        # 传入 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()
@@ -983,12 +1063,11 @@ class MainWindow(QMainWindow):
         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")
+        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}")
+        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')
         
@@ -1029,7 +1108,8 @@ class MainWindow(QMainWindow):
             btn.setText("查询")
         
         self.status_bar.showMessage(f"查询失败: {error_msg}")
-        QMessageBox.warning(self, "查询失败", f"获取订单详情失败:\n{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)
@@ -1044,9 +1124,6 @@ class MainWindow(QMainWindow):
                     label.setStyleSheet("color: #dc3545;")
     
     def closeEvent(self, event):
-        """关闭窗口时清理线程池"""
-        if hasattr(self, 'thread_pool'):
-            self.thread_pool.shutdown(wait=False)
         event.accept()
 
 # --- 主程序入口 ---

+ 152 - 7
飞书提交数据.py

@@ -572,13 +572,155 @@ class FeishuSubmitApp:
             phone = str(row_dict.get("手机号码", "")) if pd.notna(row_dict.get("手机号码")) else ""
             remark = str(row_dict.get("备注", "")) if pd.notna(row_dict.get("备注")) else ""
             institution = str(row_dict.get("机构", "")) if pd.notna(row_dict.get("机构")) else ""
-            target_school = str(row_dict.get("意向学校", "")) if pd.notna(row_dict.get("意向学校")) else ""
-            consulting = str(row_dict.get("咨询项目", "")) if pd.notna(row_dict.get("咨询项目")) else ""
-            product = str(row_dict.get("产品", "")) if pd.notna(row_dict.get("产品")) else ""
+            target_school_text = str(row_dict.get("意向学校", "")) if pd.notna(row_dict.get("意向学校")) else ""
+            consulting_text = str(row_dict.get("咨询项目", "")) if pd.notna(row_dict.get("咨询项目")) else ""
+            product_text = str(row_dict.get("产品", "")) if pd.notna(row_dict.get("产品")) else ""
             source = str(row_dict.get("线索来源", "")) if pd.notna(row_dict.get("线索来源")) else ""
             market_person = str(row_dict.get("市场人员", "")) if pd.notna(row_dict.get("市场人员")) else ""
             
-            self.log(f"  📝 提取数据: 姓名={name}, 手机={phone}", "debug")
+            self.log(f"  📝 提取数据: 姓名={name}, 手机={phone}, 意向学校={target_school_text}, 咨询项目={consulting_text}, 产品={product_text}", "debug")
+            
+            # 意向校区映射字典
+            school_mapping = {
+                "上海": "optqw2jaJ5",
+                "徐汇": "optTD4UQsM",
+                "人广": "opt7hQjSZQ",
+                "长宁": "opt7A8WBtE",
+                "浦东": "optnyoxnTK",
+                "杨浦": "optgArj17r",
+                "闵行": "optCIwo1J1",
+                "松江": "optuZzXsYm",
+                "锦秋个人学校": "optLQYDW3S",
+                "封闭学院": "optitJpd5G",
+                "大学生-市区": "optHlvKsKy",
+                "大学生-曹崇杨": "optdcoQXBt",
+                "大学生-临港": "opt3i4fOKJ",
+                "大学生-松江": "optG7eGFUg",
+                "大学生-奉贤": "optJJqnLgY",
+                "常州": "opt5wuiYjW"
+            }
+            
+            # 咨询项目映射字典
+            consulting_mapping = {
+                "语培": "opt96MRele",
+                "留学": "optQFQsXIe",
+                "考研": "opti3wbRF7",
+                "AF艺术": "optaZC2VCL",
+                "美行音乐": "optzSqwgLp",
+                "锦秋个人学校": "opt9N4AGbJ",
+                "研学考团": "optdYBLknN",
+                "尚思": "optf8lTDcV"
+            }
+            
+            # 语培产品映射字典(完整版)
+            product_mapping = {
+                "ACT": "optlGkVm5n",
+                "AEAS": "optqX3b1rJ",
+                "ALEVEL": "optgMNcV8D",
+                "AP": "optWKWYQgo",
+                "BC": "opt8huX4Nv",
+                "DSE": "optxFBYmUs",
+                "ESL": "optaYj9RZM",
+                "GMAT": "optgwu03Nf",
+                "GRE": "optEOyf5TR",
+                "IB": "optBhWIw7x",
+                "IGCSE": "optIuoUFpo",
+                "KET": "optjcLdrsB",
+                "PET": "optY42DNFo",
+                "PTE": "optOrHJvTX",
+                "SAT": "optTxT8NEu",
+                "SSAT": "optJ2a0tkB",
+                "Toefl Junior": "optEGInE0d",
+                "国际备考": "optgwn1azI",
+                "国际研学及考团": "optAmxtqGN",
+                "国家地理": "opttXZvoj3",
+                "剑桥英语": "optQkkhzD5",
+                "锦秋竞赛": "optUgXXmpF",
+                "考研": "optgQrGHTS",
+                "留学预备": "optWxtOU8b",
+                "美高": "opt36E5u0t",
+                "硕果": "optNKeSnho",
+                "硕果竞赛": "optMzk3TpL",
+                "腾飞计划": "optYTD9h04",
+                "团企培": "optW5UDF6G",
+                "托福": "optKuZ2zCq",
+                "雅思": "optWsal1MB",
+                "用英语讲中国故事": "optinda27s",
+                "在线": "opt2X07PY4",
+                "浙大国际本科": "optZFmVTTC",
+                "语培待定/未知": "optTNitLA5",
+                "大学四级": "optAqYPFmN",
+                "大学六级": "optpdfKgdl",
+                "菁英树": "optBFhBHFX",
+                "其它": "opt698ww1H"
+            }
+            
+            # 处理意向学校:根据文本查找对应的ID
+            target_school_id = None
+            if target_school_text:
+                # 精确匹配
+                if target_school_text in school_mapping:
+                    target_school_id = school_mapping[target_school_text]
+                    self.log(f"  🏫 意向学校: {target_school_text} -> {target_school_id}", "debug")
+                else:
+                    # 尝试模糊匹配(包含关系)
+                    matched = False
+                    for school_name, school_id in school_mapping.items():
+                        if school_name in target_school_text or target_school_text in school_name:
+                            target_school_id = school_id
+                            matched = True
+                            self.log(f"  🏫 意向学校模糊匹配: {target_school_text} -> {school_name} ({target_school_id})", "debug")
+                            break
+                    
+                    if not matched:
+                        self.log(f"  ⚠️ 未找到匹配的意向学校: {target_school_text}", "warning")
+            
+            # 处理咨询项目:根据文本查找对应的ID(支持多选)
+            consulting_ids = []
+            if consulting_text:
+                # 如果是多个项目,用分隔符分割(如逗号、顿号等)
+                consulting_items = re.split(r'[,,、/ ]+', consulting_text)
+                for item in consulting_items:
+                    item = item.strip()
+                    if item:
+                        if item in consulting_mapping:
+                            consulting_ids.append(consulting_mapping[item])
+                            self.log(f"  📚 咨询项目: {item} -> {consulting_mapping[item]}", "debug")
+                        else:
+                            # 尝试模糊匹配
+                            matched = False
+                            for consulting_name, consulting_id in consulting_mapping.items():
+                                if consulting_name in item or item in consulting_name:
+                                    consulting_ids.append(consulting_id)
+                                    matched = True
+                                    self.log(f"  📚 咨询项目模糊匹配: {item} -> {consulting_name} ({consulting_id})", "debug")
+                                    break
+                            if not matched:
+                                self.log(f"  ⚠️ 未找到匹配的咨询项目: {item}", "warning")
+            
+            # 处理产品:根据文本查找对应的ID(支持多选)
+            product_ids = []
+            if product_text:
+                product_items = re.split(r'[,,、/ ]+', product_text)
+                for item in product_items:
+                    item = item.strip()
+                    if item:
+                        if item in product_mapping:
+                            product_ids.append(product_mapping[item])
+                            self.log(f"  📦 产品: {item} -> {product_mapping[item]}", "debug")
+                        else:
+                            # 尝试模糊匹配(支持大小写不敏感)
+                            matched = False
+                            item_lower = item.lower()
+                            for product_name, product_id in product_mapping.items():
+                                # 支持大小写不敏感的匹配
+                                if product_name.lower() == item_lower or product_name in item or item in product_name:
+                                    product_ids.append(product_id)
+                                    matched = True
+                                    self.log(f"  📦 产品模糊匹配: {item} -> {product_name} ({product_id})", "debug")
+                                    break
+                            if not matched:
+                                self.log(f"  ⚠️ 未找到匹配的产品: {item}", "warning")
             
             # 构建备注文本
             remark_text = f"姓名:{name},性别:{gender},手机号码:{phone}"
@@ -592,9 +734,12 @@ class FeishuSubmitApp:
                     "value": [{"type": "text", "text": remark_text}]
                 },
                 "fldexDPwcE": {"type": 3, "value": "optmlZt00z"} if institution else {"type": 3},
-                "fldWt6JrgK": {"type": 3, "value": "optqw2jaJ5"} if target_school else {"type": 3},
-                "fldirDDoLV": {"type": 4, "value": ["opt96MRele"]} if consulting else {"type": 4},
-                "fldudfcNVO": {"type": 4, "value": ["optgwn1azI"]} if product else {"type": 4},
+                # 意向学校 - 使用映射后的ID
+                "fldWt6JrgK": {"type": 3, "value": target_school_id} if target_school_id else {"type": 3},
+                # 咨询项目 - 使用映射后的ID列表(多选)
+                "fldirDDoLV": {"type": 4, "value": consulting_ids} if consulting_ids else {"type": 4},
+                # 语培产品 - 使用映射后的ID列表(多选)
+                "fldudfcNVO": {"type": 4, "value": product_ids} if product_ids else {"type": 4},
                 "fldS90QCXo": {"type": 1, "value": [{"type": "text", "text": name}]} if name else {"type": 1},
                 "fldsA4YdS6": {"type": 3},
                 "fldSSElgkA": {"type": 13, "value": {"fullPhoneNum": phone}} if phone else {"type": 13},