PUGE 1 viikko sitten
vanhempi
commit
cb0151e117

+ 2 - 2
.gitignore

@@ -1,4 +1,4 @@
-/build
-/dist
+build
+dist
 *.spec
 user_scripts_backup.json

BIN
__pycache__/license_manager.cpython-313.pyc


+ 218 - 0
license_manager.py

@@ -0,0 +1,218 @@
+# license_manager.py
+import os
+import json
+import base64
+import hashlib
+import uuid
+import platform
+import subprocess
+import sys
+from datetime import datetime
+
+try:
+    from cryptography.hazmat.primitives.ciphers.aead import AESGCM
+except ImportError:
+    raise ImportError("请先安装依赖: pip install cryptography")
+
+ENCRYPTION_KEY = b"0123456789abcdef0123456789abcdef"
+
+
+def get_app_dir():
+    if getattr(sys, 'frozen', False):
+        return os.path.dirname(sys.executable)
+    return os.path.dirname(os.path.abspath(__file__))
+
+
+LICENSE_FILE = os.path.join(get_app_dir(), "license.lic")
+
+
+def get_machine_guid():
+    system = platform.system()
+    if system == "Windows":
+        try:
+            import winreg
+            with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Microsoft\Cryptography") as key:
+                return winreg.QueryValueEx(key, "MachineGuid")[0]
+        except Exception:
+            pass
+    elif system == "Linux":
+        for path in ["/etc/machine-id", "/var/lib/dbus/machine-id"]:
+            if os.path.exists(path):
+                with open(path, "r", encoding="utf-8") as f:
+                    return f.read().strip()
+    elif system == "Darwin":
+        try:
+            result = subprocess.run(
+                ["ioreg", "-rd1", "-c", "IOPlatformExpertDevice"],
+                capture_output=True, text=True
+            )
+            for line in result.stdout.split("\n"):
+                if "IOPlatformUUID" in line:
+                    return line.split('"')[-2]
+        except Exception:
+            pass
+    return str(uuid.getnode())
+
+
+def get_machine_code():
+    guid = get_machine_guid()
+    h = hashlib.sha256()
+    h.update(b"LicenseSystem")
+    h.update(guid.encode("utf-8"))
+    return h.hexdigest()
+
+
+def decrypt_key(encrypted_key: str):
+    try:
+        data = base64.b64decode(encrypted_key)
+    except Exception:
+        raise ValueError("Base64 解码失败")
+    if len(data) < 12:
+        raise ValueError("密文太短")
+    aesgcm = AESGCM(ENCRYPTION_KEY)
+    nonce = data[:12]
+    ciphertext = data[12:]
+    try:
+        plaintext = aesgcm.decrypt(nonce, ciphertext, None)
+    except Exception:
+        raise ValueError("解密失败,密钥无效或已被篡改")
+    try:
+        return json.loads(plaintext.decode("utf-8"))
+    except Exception:
+        raise ValueError("许可证数据解析失败")
+
+
+def validate_key(key: str):
+    machine_code = get_machine_code()
+    license_info = decrypt_key(key)
+    if license_info.get("machine_code") != machine_code:
+        raise ValueError("机器码不匹配,该密钥无法在本机使用")
+    expire_str = license_info.get("expire_time")
+    if not expire_str:
+        raise ValueError("许可证信息不完整")
+    expire_time = datetime.fromisoformat(expire_str)
+    if datetime.now() > expire_time:
+        raise ValueError("许可证已过期")
+    return license_info
+
+
+def save_license(license_info: dict):
+    with open(LICENSE_FILE, "w", encoding="utf-8") as f:
+        json.dump(license_info, f, indent=2, ensure_ascii=False)
+
+
+def load_license():
+    if not os.path.exists(LICENSE_FILE):
+        return None
+    try:
+        with open(LICENSE_FILE, "r", encoding="utf-8") as f:
+            return json.load(f)
+    except Exception:
+        return None
+
+
+def verify_license(root):
+    """
+    直接在传入的 root 窗口上显示验证界面。
+    验证通过返回 True,关闭窗口返回 False。
+    """
+    import tkinter as tk
+    from tkinter import messagebox
+
+    # 1. 先检查本地缓存,如果有效直接通过
+    cached = load_license()
+    if cached:
+        try:
+            if cached.get("machine_code") != get_machine_code():
+                raise ValueError("机器码不匹配")
+            if datetime.now() > datetime.fromisoformat(cached.get("expire_time")):
+                raise ValueError("许可证已过期")
+            return True
+        except Exception:
+            if os.path.exists(LICENSE_FILE):
+                os.remove(LICENSE_FILE)
+
+    # 2. 没有缓存或缓存无效,显示验证界面
+    machine_code = get_machine_code()
+
+    # 清空 root 里所有内容(防止有残留)
+    for widget in root.winfo_children():
+        widget.destroy()
+
+    # 设置验证窗口样式
+    root.title("软件激活")
+    root.geometry("420x280")
+    root.resizable(False, False)
+
+    # 窗口居中
+    root.update_idletasks()
+    w, h = 420, 280
+    x = (root.winfo_screenwidth() - w) // 2
+    y = (root.winfo_screenheight() - h) // 2
+    root.geometry(f"{w}x{h}+{x}+{y}")
+
+    # 机器码(可复制)
+    tk.Label(root, text="您的机器码(可复制):", font=("微软雅黑", 10)).pack(pady=(15, 5))
+    mc_entry = tk.Entry(root, font=("Consolas", 11), justify="center", width=50)
+    mc_entry.insert(0, machine_code)
+    mc_entry.config(state="readonly")
+    mc_entry.pack(padx=20, pady=5)
+
+    tk.Label(root, text="请将机器码发给管理员获取密钥", font=("微软雅黑", 9), fg="gray").pack(pady=5)
+
+    # 密钥输入
+    tk.Label(root, text="许可证密钥:", font=("微软雅黑", 10)).pack(pady=(10, 5))
+    key_entry = tk.Entry(root, font=("Consolas", 11), show="*", width=50)
+    key_entry.pack(padx=20, pady=5)
+    key_entry.focus()
+
+    error_label = tk.Label(root, text="", font=("微软雅黑", 9), fg="red")
+    error_label.pack(pady=5)
+
+    # 结果标志
+    result = [False]
+
+    def on_verify():
+        key = key_entry.get().strip()
+        if not key:
+            error_label.config(text="密钥不能为空")
+            return
+        try:
+            info = validate_key(key)
+            save_license(info)
+            expire = datetime.fromisoformat(info["expire_time"])
+            days_left = (expire - datetime.now()).days
+            messagebox.showinfo(
+                "激活成功",
+                f"✓ 验证通过!\n有效期至:{expire.strftime('%Y-%m-%d %H:%M:%S')}\n剩余天数:{days_left} 天"
+            )
+            result[0] = True
+            root.quit()  # 退出事件循环,不销毁窗口
+        except Exception as e:
+            error_label.config(text=str(e))
+            key_entry.delete(0, tk.END)
+            key_entry.focus()
+
+    def on_close():
+        result[0] = False
+        root.quit()
+
+    # 按钮
+    btn_frame = tk.Frame(root)
+    btn_frame.pack(pady=15)
+    tk.Button(btn_frame, text="确认激活", command=on_verify, width=12, font=("微软雅黑", 10)).pack(side=tk.LEFT, padx=10)
+    tk.Button(btn_frame, text="关闭", command=on_close, width=12, font=("微软雅黑", 10)).pack(side=tk.LEFT, padx=10)
+
+    # 回车确认,X 按钮关闭
+    root.bind("<Return>", lambda e: on_verify())
+    root.protocol("WM_DELETE_WINDOW", on_close)
+
+    # 显示窗口并阻塞等待用户操作
+    root.deiconify()
+    root.mainloop()
+
+    # 清理验证界面,把 root 交还给主程序
+    for widget in root.winfo_children():
+        widget.destroy()
+
+    return result[0]

+ 229 - 0
license_qt5.py

@@ -0,0 +1,229 @@
+# license_manager.py  (PyQt5 版本)
+import os
+import json
+import base64
+import hashlib
+import uuid
+import platform
+import subprocess
+import sys
+from datetime import datetime, timedelta
+
+try:
+    from cryptography.hazmat.primitives.ciphers.aead import AESGCM
+except ImportError:
+    raise ImportError("请先安装依赖: pip install cryptography")
+
+ENCRYPTION_KEY = b"0123456789abcdef0123456789abcdef"
+
+
+def get_app_dir():
+    if getattr(sys, 'frozen', False):
+        return os.path.dirname(sys.executable)
+    return os.path.dirname(os.path.abspath(__file__))
+
+
+LICENSE_FILE = os.path.join(get_app_dir(), "license.lic")
+
+
+def get_machine_guid():
+    system = platform.system()
+    if system == "Windows":
+        try:
+            import winreg
+            with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Microsoft\Cryptography") as key:
+                return winreg.QueryValueEx(key, "MachineGuid")[0]
+        except Exception:
+            pass
+    elif system == "Linux":
+        for path in ["/etc/machine-id", "/var/lib/dbus/machine-id"]:
+            if os.path.exists(path):
+                with open(path, "r", encoding="utf-8") as f:
+                    return f.read().strip()
+    elif system == "Darwin":
+        try:
+            result = subprocess.run(
+                ["ioreg", "-rd1", "-c", "IOPlatformExpertDevice"],
+                capture_output=True, text=True
+            )
+            for line in result.stdout.split("\n"):
+                if "IOPlatformUUID" in line:
+                    return line.split('"')[-2]
+        except Exception:
+            pass
+    return str(uuid.getnode())
+
+
+def get_machine_code():
+    guid = get_machine_guid()
+    h = hashlib.sha256()
+    h.update(b"LicenseSystem")
+    h.update(guid.encode("utf-8"))
+    return h.hexdigest()
+
+
+def decrypt_key(encrypted_key: str):
+    try:
+        data = base64.b64decode(encrypted_key)
+    except Exception:
+        raise ValueError("Base64 解码失败")
+    if len(data) < 12:
+        raise ValueError("密文太短")
+    aesgcm = AESGCM(ENCRYPTION_KEY)
+    nonce = data[:12]
+    ciphertext = data[12:]
+    try:
+        plaintext = aesgcm.decrypt(nonce, ciphertext, None)
+    except Exception:
+        raise ValueError("解密失败,密钥无效或已被篡改")
+    try:
+        return json.loads(plaintext.decode("utf-8"))
+    except Exception:
+        raise ValueError("许可证数据解析失败")
+
+
+def validate_key(key: str):
+    machine_code = get_machine_code()
+    license_info = decrypt_key(key)
+    if license_info.get("machine_code") != machine_code:
+        raise ValueError("机器码不匹配,该密钥无法在本机使用")
+    expire_str = license_info.get("expire_time")
+    if not expire_str:
+        raise ValueError("许可证信息不完整")
+    expire_time = datetime.fromisoformat(expire_str)
+    if datetime.now() > expire_time:
+        raise ValueError("许可证已过期")
+    return license_info
+
+
+def save_license(license_info: dict):
+    with open(LICENSE_FILE, "w", encoding="utf-8") as f:
+        json.dump(license_info, f, indent=2, ensure_ascii=False)
+
+
+def load_license():
+    if not os.path.exists(LICENSE_FILE):
+        return None
+    try:
+        with open(LICENSE_FILE, "r", encoding="utf-8") as f:
+            return json.load(f)
+    except Exception:
+        return None
+
+
+def verify_license():
+    """
+    PyQt5 弹窗验证许可证。
+    返回 True:验证通过。
+    返回 False:用户关闭窗口,程序应退出。
+    """
+    from PyQt5.QtWidgets import (QApplication, QDialog, QVBoxLayout, QLabel,
+                                  QLineEdit, QPushButton, QHBoxLayout, QMessageBox)
+    from PyQt5.QtCore import Qt
+    from PyQt5.QtGui import QFont
+
+    # 1. 先尝试本地缓存
+    cached = load_license()
+    if cached:
+        try:
+            if cached.get("machine_code") != get_machine_code():
+                raise ValueError("机器码不匹配")
+            if datetime.now() > datetime.fromisoformat(cached.get("expire_time")):
+                raise ValueError("许可证已过期")
+            return True
+        except Exception:
+            if os.path.exists(LICENSE_FILE):
+                os.remove(LICENSE_FILE)
+
+    machine_code = get_machine_code()
+
+    # 确保有 QApplication 实例
+    app = QApplication.instance()
+    if not app:
+        app = QApplication(sys.argv)
+
+    dialog = QDialog()
+    dialog.setWindowTitle("软件激活")
+    dialog.setFixedSize(450, 300)
+    dialog.setWindowFlags(dialog.windowFlags() & ~Qt.WindowContextHelpButtonHint)
+
+    # 窗口居中
+    screen = app.primaryScreen().geometry()
+    dialog.move(
+        (screen.width() - dialog.width()) // 2,
+        (screen.height() - dialog.height()) // 2
+    )
+
+    layout = QVBoxLayout()
+
+    # 机器码(可复制)
+    layout.addWidget(QLabel("您的机器码(可复制):"))
+    mc_edit = QLineEdit(machine_code)
+    mc_edit.setReadOnly(True)
+    mc_edit.setFont(QFont("Consolas", 10))
+    mc_edit.setAlignment(Qt.AlignCenter)
+    layout.addWidget(mc_edit)
+
+    tip = QLabel("请将机器码发给管理员获取密钥")
+    tip.setAlignment(Qt.AlignCenter)
+    tip.setStyleSheet("color: gray;")
+    layout.addWidget(tip)
+
+    # 密钥输入
+    layout.addWidget(QLabel("许可证密钥:"))
+    key_edit = QLineEdit()
+    key_edit.setEchoMode(QLineEdit.Password)
+    key_edit.setFont(QFont("Consolas", 10))
+    key_edit.setAlignment(Qt.AlignCenter)
+    key_edit.setPlaceholderText("在此粘贴密钥")
+    layout.addWidget(key_edit)
+
+    # 错误提示
+    error_label = QLabel("")
+    error_label.setStyleSheet("color: red;")
+    error_label.setAlignment(Qt.AlignCenter)
+    layout.addWidget(error_label)
+
+    # 按钮
+    btn_layout = QHBoxLayout()
+    ok_btn = QPushButton("确认激活")
+    close_btn = QPushButton("关闭")
+    btn_layout.addWidget(ok_btn)
+    btn_layout.addWidget(close_btn)
+    layout.addLayout(btn_layout)
+
+    dialog.setLayout(layout)
+
+    # 结果标志
+    result = [False]
+
+    def on_verify():
+        key = key_edit.text().strip()
+        if not key:
+            error_label.setText("密钥不能为空")
+            return
+        try:
+            info = validate_key(key)
+            save_license(info)
+            expire = datetime.fromisoformat(info["expire_time"])
+            days_left = (expire - datetime.now()).days
+            QMessageBox.information(
+                dialog, "激活成功",
+                f"✓ 验证通过!\n有效期至:{expire.strftime('%Y-%m-%d %H:%M:%S')}\n剩余天数:{days_left} 天"
+            )
+            result[0] = True
+            dialog.accept()
+        except Exception as e:
+            error_label.setText(str(e))
+            key_edit.clear()
+            key_edit.setFocus()
+
+    def on_close():
+        dialog.reject()
+
+    ok_btn.clicked.connect(on_verify)
+    close_btn.clicked.connect(on_close)
+    key_edit.returnPressed.connect(on_verify)
+
+    ret = dialog.exec_()
+    return result[0] and ret == QDialog.Accepted

+ 72 - 51
三一平台.py

@@ -335,9 +335,7 @@ class MainWindow(QMainWindow):
         self.scan_timer = QTimer()  # 定时器用于间隔执行
         self.scan_timer.timeout.connect(self.process_next_scan)
         self.current_scan_plate = None  # 当前等待发送2001的车牌
-        self.wait_timeout_timer = QTimer()  # 等待超时定时器
-        self.wait_timeout_timer.setSingleShot(True)  # 单次触发
-        self.wait_timeout_timer.timeout.connect(self.on_wait_timeout)
+
         self.car_task_state = {}
         # WebSocket相关
         self.ws_client = None
@@ -378,12 +376,10 @@ class MainWindow(QMainWindow):
         login_layout = QHBoxLayout()
         login_layout.addWidget(QLabel("账号:"))
         self.username_input = QLineEdit()
-        self.username_input.setText("深金川")
         login_layout.addWidget(self.username_input)
         login_layout.addWidget(QLabel("密码:"))
         self.password_input = QLineEdit()
         self.password_input.setEchoMode(QLineEdit.Password)
-        self.password_input.setText("123456")
         login_layout.addWidget(self.password_input)
         self.login_btn = QPushButton("登录")
         self.login_btn.clicked.connect(self.handle_login)
@@ -408,12 +404,12 @@ class MainWindow(QMainWindow):
         
         ws_layout.addWidget(QLabel("装车客户端:"))
         self.send_client_combo = QComboBox()
-        self.send_client_combo.setMinimumWidth(120)
+        self.send_client_combo.setMinimumWidth(220)
         ws_layout.addWidget(self.send_client_combo)
         
         ws_layout.addWidget(QLabel("卸车客户端:"))
         self.receive_client_combo = QComboBox()
-        self.receive_client_combo.setMinimumWidth(120)
+        self.receive_client_combo.setMinimumWidth(220)
         ws_layout.addWidget(self.receive_client_combo)
         
         # 测试按钮
@@ -646,8 +642,6 @@ class MainWindow(QMainWindow):
                 self.car_task_state[car_plate] = '已发车'
                 self.update_task_state_column()
                 self.add_log(f"📌 {car_plate} 状态更新为: 已发车")
-                # 停止超时定时器
-                self.wait_timeout_timer.stop()
                 # 发送2001指令
                 self.send_cmd_to_vehicle(car_plate, "2001")
                 # 清空当前车牌
@@ -656,8 +650,54 @@ class MainWindow(QMainWindow):
                 self.process_next_scan()
             else:
                 self.add_log(f"⚠️ 收到开车命令但车牌为空")
+        elif msg_type == 'clear':  # 新增:处理clear事件
+            car_plate = data.get('value', '')
+            if car_plate:
+                self.add_log(f"🧹 收到清除命令,车辆: {car_plate}")
+                self.clear_vehicle_enable(car_plate)
+            else:
+                self.add_log(f"⚠️ 收到清除命令但车牌为空")
+
+    def clear_vehicle_enable(self, car_plate):
+        """取消指定车辆的启用勾选,并自动重启监控"""
+        if not car_plate:
+            return
         
-    
+        # 在表格中查找该车牌
+        found = False
+        for row in range(self.table.rowCount()):
+            plate_item = self.table.item(row, 2)  # 车牌号在第2列
+            if plate_item and plate_item.text() == car_plate:
+                # 获取启用复选框
+                enable_checkbox = self.table.cellWidget(row, 0)
+                if enable_checkbox:
+                    # 取消勾选
+                    enable_checkbox.setChecked(False)
+                    # 更新状态字典
+                    self.car_checked_state[car_plate] = False
+                    # 更新任务状态
+                    self.car_task_state[car_plate] = '空闲'
+                    self.update_task_state_column()
+                    self.add_log(f"✅ 已取消车辆 {car_plate} 的启用勾选")
+                    found = True
+                    break
+        
+        # 如果在表格中没找到该车牌,但在状态字典中有记录
+        if not found:
+            if car_plate in self.car_checked_state:
+                self.car_checked_state[car_plate] = False
+                self.car_task_state[car_plate] = '空闲'
+                self.add_log(f"✅ 已清除车辆 {car_plate} 的启用状态(列表中未找到)")
+            else:
+                self.add_log(f"⚠️ 未找到车辆 {car_plate},无法取消启用")
+                return
+        
+        # 如果监控正在运行,自动重启
+        if self.is_monitoring:
+            self.add_log("🔄 自动重启监控...")
+            self.stop_monitoring()  # 直接调用停止方法
+            QTimer.singleShot(1000, self.start_monitoring)  # 1秒后直接调用启动方法
+
     def start_scan_tasks(self, plate):
         """开始执行扫码任务 - 只处理收到的车牌"""
         if self.is_processing_scan:
@@ -694,7 +734,6 @@ class MainWindow(QMainWindow):
         self.scan_task_queue = [plate]
         self.is_processing_scan = True
         self.current_scan_plate = None
-        self.wait_timeout_timer.stop()
         
         self.add_log(f"🚀 开始扫码任务,车辆: {plate}")
         
@@ -709,7 +748,6 @@ class MainWindow(QMainWindow):
             self.scan_task_queue = []
             self.current_scan_plate = None
             self.scan_timer.stop()
-            self.wait_timeout_timer.stop()
             return
         
         if not self.scan_task_queue:
@@ -718,7 +756,6 @@ class MainWindow(QMainWindow):
             self.statusBar().showMessage("扫码任务全部完成")
             self.scan_timer.stop()
             self.current_scan_plate = None
-            self.wait_timeout_timer.stop()
             return
         
         car_plate = self.scan_task_queue.pop(0)
@@ -728,39 +765,6 @@ class MainWindow(QMainWindow):
         self.on_test_drive(car_plate, from_scan=True)
         
         self.add_log(f"⏳ 等待外部WebSocket通知发送2001指令到: {car_plate},超时时间2分钟")
-        
-        self.wait_timeout_timer.start(120000)
-
-    def on_wait_timeout(self):
-        """等待超时处理"""
-        if not self.current_scan_plate:
-            return
-        
-        car_plate = self.current_scan_plate
-        self.add_log(f"⏰ 等待超时(2分钟),未收到 {car_plate} 的WebSocket通知")
-        
-        # 取消该车辆的启用勾选
-        for row in range(self.table.rowCount()):
-            plate_item = self.table.item(row, 2)
-            if plate_item and plate_item.text() == car_plate:
-                enable_checkbox = self.table.cellWidget(row, 0)
-                if enable_checkbox:
-                    enable_checkbox.setChecked(False)
-                    self.car_checked_state[car_plate] = False
-                    self.add_log(f"❌ 已取消 {car_plate} 的启用勾选(超时)")
-                    break
-        
-        # 重置任务状态为"空闲"
-        self.car_task_state[car_plate] = '空闲'
-        self.update_task_state_column()
-        self.add_log(f"📌 {car_plate} 状态重置为: 空闲")
-        
-        # 清空当前车牌
-        self.current_scan_plate = None
-        
-        # 继续处理下一辆
-        self.add_log(f"⏭️ 继续处理下一辆车...")
-        self.process_next_scan()
 
 
 
@@ -1289,7 +1293,6 @@ class MainWindow(QMainWindow):
         
         # 停止扫码任务
         self.scan_timer.stop()
-        self.wait_timeout_timer.stop()  # 停止超时定时器
         self.is_processing_scan = False
         self.scan_task_queue = []
         self.current_scan_plate = None
@@ -1322,7 +1325,9 @@ class MainWindow(QMainWindow):
         """处理区域检测结果"""
         regions = data.get('list', [])
         current_time = datetime.now()
-        # self.add_log(json.dumps(regions))
+        
+        load_triggered = False  # 添加标记
+        
         for region in regions:
             region_id = region.get('regionId')
             region_name = region.get('regionName', '未知')
@@ -1334,9 +1339,12 @@ class MainWindow(QMainWindow):
             # 检查装车区域
             if load_region_id and region_id == load_region_id:
                 self.check_region_cars(region_name, car_list, 'load', current_time)
+                # 如果装车触发了任务,标记
+                if self.is_processing_scan:
+                    load_triggered = True
             
-            # 检查卸车区域
-            if unload_region_id and region_id == unload_region_id:
+            # 检查卸车区域(只有装车没触发才执行)
+            if not load_triggered and unload_region_id and region_id == unload_region_id:
                 self.check_region_cars(region_name, car_list, 'unload', current_time)
         
         self.update_region_status()
@@ -1454,6 +1462,7 @@ class MainWindow(QMainWindow):
     def save_config(self):
         config = {
             'username': self.username_input.text(),
+            'password': self.password_input.text(),
             'folder_path': self.folder_path_input.text(),
             'last_load_client': self.last_load_client,
             'last_unload_client': self.last_unload_client,
@@ -1471,7 +1480,8 @@ class MainWindow(QMainWindow):
             try:
                 with open('config.json', 'r', encoding='utf-8') as f:
                     config = json.load(f)
-                    self.username_input.setText(config.get('username', '深金川'))
+                    self.username_input.setText(config.get('username', ''))
+                    self.password_input.setText(config.get('password', ''))
                     folder_path = config.get('folder_path', '')
                     if folder_path:
                         self.folder_path_input.setText(folder_path)
@@ -1500,6 +1510,17 @@ class MainWindow(QMainWindow):
 
 def main():
     app = QApplication(sys.argv)
+    
+    # ========== 许可证验证(加在这里) ==========
+    try:
+        from license_qt5 import verify_license
+        if not verify_license():
+            sys.exit(1)
+    except Exception as e:
+        QMessageBox.critical(None, "错误", f"许可证验证出错: {e}")
+        sys.exit(1)
+    # ============================================
+    
     window = MainWindow()
     window.show()
     sys.exit(app.exec_())

+ 30 - 5
控制vm虚拟机.py

@@ -10,6 +10,7 @@ import ctypes
 from ctypes import wintypes
 import platform
 import os
+import string
 import sys
 import json
 import websocket
@@ -270,6 +271,11 @@ class WebSocketClient:
                 self.log_callback(f"发送消息失败: {e}")
             return False
 
+def generate_random_code(length=8):
+    """生成指定长度的随机数字字母组合"""
+    # 数字 + 大小写字母
+    chars = string.ascii_letters + string.digits
+    return ''.join(random.choice(chars) for _ in range(length))
 
 class VmControlGUI:
     def __init__(self, root):
@@ -302,7 +308,7 @@ class VmControlGUI:
         self.pause_duration = tk.StringVar(value="60")
 
         # ===== 脚本B变量 =====
-        self.channel_type = tk.StringVar(value="控制虚拟机_1")
+        self.channel_type = tk.StringVar(value=generate_random_code())
         self.ws_client = None
         self.is_b_running = False
         self.b_stop_flag = False
@@ -852,12 +858,16 @@ class VmControlGUI:
         win32gui.EnumWindows(enum_callback, None)
 
         if self.vm_windows:
+            # 限制最多4个
+            # self.vm_windows = self.vm_windows[:2]
+
             titles = [f"{i+1}. {t[0]}" for i, t in enumerate(self.vm_windows)]
             self.vm_combo["values"] = titles
             self.vm_combo.current(0)
             self.vm_window_title.set(titles[0])
             self.log(f"找到 {len(self.vm_windows)} 个 VMware 窗口")
             self.window_info_label.config(text=f"找到 {len(self.vm_windows)} 个窗口", foreground="green")
+            
             self.select_window(0)
         else:
             self.log("未找到 VMware 窗口")
@@ -1261,6 +1271,23 @@ class VmControlGUI:
 
 
 if __name__ == "__main__":
+    # 先创建 root 但不显示
+    root = tk.Tk()
+    root.withdraw()
+
+    # 许可证验证(直接在 root 上显示)
+    try:
+        from license_manager import verify_license
+        if not verify_license(root):
+            sys.exit(1)
+    except Exception as e:
+        from tkinter import messagebox
+        messagebox.showerror("错误", f"许可证验证出错: {e}")
+        sys.exit(1)
+
+    # 验证通过,恢复主窗口
+    root.deiconify()
+
     # 检查依赖
     if not ghost_available:
         try:
@@ -1268,14 +1295,12 @@ if __name__ == "__main__":
         except ImportError:
             print("需要安装 pywin32: pip install pywin32")
             sys.exit(1)
-    
-    # 检查websocket-client
+
     try:
         import websocket
     except ImportError:
         print("需要安装 websocket-client: pip install websocket-client")
         sys.exit(1)
-    
-    root = tk.Tk()
+
     app = VmControlGUI(root)
     root.mainloop()

+ 312 - 0
条码处理/app.py

@@ -0,0 +1,312 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+PDF 条码合成工具(带缩放与偏移配置)
+"""
+
+import tkinter as tk
+from tkinter import ttk, filedialog, scrolledtext
+import os
+import json
+import re
+import threading
+import fitz  # PyMuPDF
+from PIL import Image
+
+CONFIG_FILE = "pdf_merge_config.json"
+
+
+class PDFMergeApp:
+    def __init__(self, root):
+        self.root = root
+        self.root.title("PDF 条码合成工具")
+        self.root.geometry("780x600")
+        self.root.minsize(700, 500)
+
+        # 路径变量
+        self.input_var = tk.StringVar()
+        self.template_var = tk.StringVar()
+        self.output_var = tk.StringVar()
+
+        # 缩放与偏移变量
+        self.scale_var = tk.StringVar(value="100")   # 相对于模板宽度的百分比
+        self.offset_x_var = tk.StringVar(value="0")  # X 偏移(点)
+        self.offset_y_var = tk.StringVar(value="10")  # Y 偏移(点)
+
+        self.build_ui()
+        self.load_config()
+
+    def build_ui(self):
+        main_frame = ttk.Frame(self.root, padding="12")
+        main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
+
+        self.root.columnconfigure(0, weight=1)
+        self.root.rowconfigure(0, weight=1)
+        main_frame.columnconfigure(1, weight=1)
+
+        # 输入文件夹
+        ttk.Label(main_frame, text="输入文件夹:").grid(row=0, column=0, sticky=tk.W, pady=6)
+        ttk.Entry(main_frame, textvariable=self.input_var).grid(row=0, column=1, sticky=(tk.W, tk.E), padx=6)
+        ttk.Button(main_frame, text="浏览…", width=10, command=self.select_input).grid(row=0, column=2, padx=4)
+
+        # 模板文件
+        ttk.Label(main_frame, text="模板文件:").grid(row=1, column=0, sticky=tk.W, pady=6)
+        ttk.Entry(main_frame, textvariable=self.template_var).grid(row=1, column=1, sticky=(tk.W, tk.E), padx=6)
+        ttk.Button(main_frame, text="浏览…", width=10, command=self.select_template).grid(row=1, column=2, padx=4)
+
+        # 输出文件夹
+        ttk.Label(main_frame, text="输出文件夹:").grid(row=2, column=0, sticky=tk.W, pady=6)
+        ttk.Entry(main_frame, textvariable=self.output_var).grid(row=2, column=1, sticky=(tk.W, tk.E), padx=6)
+        ttk.Button(main_frame, text="浏览…", width=10, command=self.select_output).grid(row=2, column=2, padx=4)
+
+        # 参数配置区域
+        param_frame = ttk.LabelFrame(main_frame, text="合成参数(自动保存)", padding="10")
+        param_frame.grid(row=3, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=10)
+
+        ttk.Label(param_frame, text="缩放比例(% 模板宽):").grid(row=0, column=0, sticky=tk.W, padx=4)
+        scale_entry = ttk.Entry(param_frame, textvariable=self.scale_var, width=10)
+        scale_entry.grid(row=0, column=1, sticky=tk.W, padx=4)
+        ttk.Label(param_frame, text="例:100 = 宽度填满模板").grid(row=0, column=2, sticky=tk.W, padx=4)
+
+        ttk.Label(param_frame, text="X 偏移(点):").grid(row=1, column=0, sticky=tk.W, padx=4, pady=6)
+        ttk.Entry(param_frame, textvariable=self.offset_x_var, width=10).grid(row=1, column=1, sticky=tk.W, padx=4, pady=6)
+        ttk.Label(param_frame, text="左上角水平偏移,0 表示贴左边").grid(row=1, column=2, sticky=tk.W, padx=4, pady=6)
+
+        ttk.Label(param_frame, text="Y 偏移(点):").grid(row=2, column=0, sticky=tk.W, padx=4)
+        ttk.Entry(param_frame, textvariable=self.offset_y_var, width=10).grid(row=2, column=1, sticky=tk.W, padx=4)
+        ttk.Label(param_frame, text="左上角垂直偏移,0 表示贴顶部").grid(row=2, column=2, sticky=tk.W, padx=4)
+
+        # 开始按钮
+        btn_frame = ttk.Frame(main_frame)
+        btn_frame.grid(row=4, column=0, columnspan=3, pady=10)
+        self.start_btn = ttk.Button(btn_frame, text="▶ 开始处理", command=self.start_process)
+        self.start_btn.pack()
+
+        # 日志区域
+        ttk.Label(main_frame, text="处理日志:").grid(row=5, column=0, sticky=tk.W, pady=(8, 0))
+        self.log_text = scrolledtext.ScrolledText(
+            main_frame, height=18, wrap=tk.WORD, state=tk.NORMAL, font=("Consolas", 10)
+        )
+        self.log_text.grid(row=6, column=0, columnspan=3, sticky=(tk.W, tk.E, tk.N, tk.S), pady=4)
+        main_frame.rowconfigure(6, weight=1)
+
+        # 绑定参数变更自动保存
+        for var in (self.scale_var, self.offset_x_var, self.offset_y_var):
+            var.trace_add("write", lambda *args: self.save_config())
+
+    # ---------- 路径选择 ----------
+    def select_input(self):
+        folder = filedialog.askdirectory()
+        if folder:
+            self.input_var.set(folder)
+            self.save_config()
+
+    def select_template(self):
+        file_path = filedialog.askopenfilename(filetypes=[("PDF 文件", "*.pdf")])
+        if file_path:
+            self.template_var.set(file_path)
+            self.save_config()
+
+    def select_output(self):
+        folder = filedialog.askdirectory()
+        if folder:
+            self.output_var.set(folder)
+            self.save_config()
+
+    # ---------- 配置持久化 ----------
+    def save_config(self):
+        config = {
+            "input_dir": self.input_var.get(),
+            "template": self.template_var.get(),
+            "output_dir": self.output_var.get(),
+            "scale_percent": self.scale_var.get(),
+            "offset_x": self.offset_x_var.get(),
+            "offset_y": self.offset_y_var.get(),
+        }
+        try:
+            with open(CONFIG_FILE, "w", encoding="utf-8") as f:
+                json.dump(config, f, ensure_ascii=False, indent=2)
+        except Exception:
+            pass
+
+    def load_config(self):
+        if not os.path.exists(CONFIG_FILE):
+            return
+        try:
+            with open(CONFIG_FILE, "r", encoding="utf-8") as f:
+                config = json.load(f)
+            self.input_var.set(config.get("input_dir", ""))
+            self.template_var.set(config.get("template", ""))
+            self.output_var.set(config.get("output_dir", ""))
+            self.scale_var.set(config.get("scale_percent", "100"))
+            self.offset_x_var.set(config.get("offset_x", "0"))
+            self.offset_y_var.set(config.get("offset_y", "0"))
+        except Exception:
+            pass
+
+    # ---------- 日志 ----------
+    def log(self, msg):
+        self.log_text.insert(tk.END, msg + "\n")
+        self.log_text.see(tk.END)
+        self.root.update_idletasks()
+
+    # ---------- 文字提取 ----------
+    def extract_text(self, page):
+        text = page.get_text().strip()
+        print(f"Extracted text: {text}")  # Debug print
+        if len(text) >= 2:
+            return text
+        try:
+            import pytesseract
+            pix = page.get_pixmap(dpi=300)
+            mode = "RGBA" if pix.alpha else "RGB"
+            img = Image.frombytes(mode, [pix.width, pix.height], pix.samples)
+            if mode == "RGBA":
+                img = img.convert("RGB")
+            text = pytesseract.image_to_string(img, lang="chi_sim+eng").strip()
+            return text
+        except Exception:
+            return ""
+
+    def sanitize_filename(self, text):
+        if not text:
+            return ""
+        lines = [ln.strip() for ln in text.splitlines() if ln.strip()]
+        name = lines[0] if lines else text
+        name = name[:60]
+        name = re.sub(r'[\\/:*?"<>|\s]+', "_", name)
+        return name.strip("._")
+
+    # ---------- 核心处理 ----------
+    def start_process(self):
+        t = threading.Thread(target=self.process, daemon=True)
+        t.start()
+
+    def process(self):
+        self.start_btn.config(state=tk.DISABLED)
+        self.log_text.delete(1.0, tk.END)
+
+        input_dir = self.input_var.get()
+        template_file = self.template_var.get()
+        output_dir = self.output_var.get()
+
+        # 解析参数
+        try:
+            scale_percent = float(self.scale_var.get().strip() or "100")
+            offset_x = float(self.offset_x_var.get().strip() or "0")
+            offset_y = float(self.offset_y_var.get().strip() or "0")
+        except ValueError:
+            self.log("❌ 错误:缩放比例、X 偏移、Y 偏移必须是有效数字")
+            self.start_btn.config(state=tk.NORMAL)
+            return
+
+        if not all([input_dir, template_file, output_dir]):
+            self.log("❌ 错误:请完整填写输入文件夹、模板文件和输出文件夹")
+            self.start_btn.config(state=tk.NORMAL)
+            return
+        if not os.path.isdir(input_dir):
+            self.log(f"❌ 错误:输入文件夹不存在:{input_dir}")
+            self.start_btn.config(state=tk.NORMAL)
+            return
+        if not os.path.isfile(template_file):
+            self.log(f"❌ 错误:模板文件不存在:{template_file}")
+            self.start_btn.config(state=tk.NORMAL)
+            return
+        if not os.path.exists(output_dir):
+            os.makedirs(output_dir, exist_ok=True)
+
+        pdf_files = [f for f in os.listdir(input_dir) if f.lower().endswith(".pdf")]
+        if not pdf_files:
+            self.log("⚠ 输入文件夹中没有找到 PDF 文件")
+            self.start_btn.config(state=tk.NORMAL)
+            return
+
+        self.log(f"📁 共找到 {len(pdf_files)} 个 PDF 文件")
+        self.log(f"⚙ 参数:缩放 {scale_percent}% 宽,偏移 ({offset_x}, {offset_y})\n")
+
+        try:
+            template_doc = fitz.open(template_file)
+            if len(template_doc) == 0:
+                self.log("❌ 错误:模板文件为空")
+                self.start_btn.config(state=tk.NORMAL)
+                return
+
+            template_page = template_doc[0]
+            template_rect = template_page.rect
+            template_w = template_rect.width
+            template_h = template_rect.height
+
+            for pdf_name in pdf_files:
+                pdf_path = os.path.join(input_dir, pdf_name)
+                self.log(f"📄 正在处理:{pdf_name}")
+
+                try:
+                    doc = fitz.open(pdf_path)
+                    odd_indices = list(range(0, len(doc), 2))
+
+                    if not odd_indices:
+                        self.log("   ⚠ 该文件没有奇数页,已跳过")
+                        doc.close()
+                        continue
+
+                    safe_name = self.sanitize_filename(self.extract_text(doc[odd_indices[0]]))
+                    if not safe_name:
+                        safe_name = os.path.splitext(pdf_name)[0]
+
+                    new_doc = fitz.open()
+
+                    for idx in odd_indices:
+                        page = doc[idx]
+                        page_num = idx + 1
+
+                        new_page = new_doc.new_page(width=template_w, height=template_h)
+
+                        # 1) 模板背景
+                        new_page.show_pdf_page(template_rect, template_doc, 0, overlay=False)
+
+                        # 2) 计算输入页目标矩形
+                        input_rect = page.rect
+                        # 按模板宽度的百分比计算目标宽度
+                        target_w = template_w * (scale_percent / 100.0)
+                        # 等比例缩放高度
+                        scale = target_w / input_rect.width
+                        target_h = input_rect.height * scale
+
+                        target_rect = fitz.Rect(offset_x, offset_y, offset_x + target_w, offset_y + target_h)
+
+                        # 3) 叠加输入页(前景)
+                        new_page.show_pdf_page(target_rect, doc, idx, overlay=True)
+
+                        self.log(f"   ✔ 已合成第 {page_num} 页  →  尺寸 {target_w:.1f}×{target_h:.1f}  位置 ({offset_x}, {offset_y})")
+
+                    # 保存
+                    out_name = f"{safe_name}.pdf"
+                    out_path = os.path.join(output_dir, out_name)
+                    counter = 1
+                    while os.path.exists(out_path):
+                        stem, ext = os.path.splitext(out_name)
+                        out_path = os.path.join(output_dir, f"{stem}_{counter}{ext}")
+                        counter += 1
+
+                    new_doc.save(out_path)
+                    new_doc.close()
+                    doc.close()
+
+                    self.log(f"   💾 已保存:{os.path.basename(out_path)}\n")
+
+                except Exception as e:
+                    self.log(f"   ❌ 处理失败:{str(e)}\n")
+
+            template_doc.close()
+            self.log("🎉 全部处理完成!")
+
+        except Exception as e:
+            self.log(f"❌ 发生错误:{str(e)}")
+
+        self.start_btn.config(state=tk.NORMAL)
+
+
+if __name__ == "__main__":
+    root = tk.Tk()
+    app = PDFMergeApp(root)
+    root.mainloop()

+ 8 - 0
条码处理/pdf_merge_config.json

@@ -0,0 +1,8 @@
+{
+  "input_dir": "C:/Users/mail/Downloads/条码测试",
+  "template": "C:/Users/mail/Downloads/SHEIN通用条码模板 (2).pdf",
+  "output_dir": "C:/Users/mail/Downloads",
+  "scale_percent": "100",
+  "offset_x": "0",
+  "offset_y": "10"
+}

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 1732 - 1
飞书提交数据.py


Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä