diff --git a/src/auth/__init__.py b/src/auth/__init__.py new file mode 100644 index 0000000..ec2a2d9 --- /dev/null +++ b/src/auth/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +""" +授权认证模块 +""" \ No newline at end of file diff --git a/src/auth/keygen_gui.py b/src/auth/keygen_gui.py new file mode 100644 index 0000000..5105236 --- /dev/null +++ b/src/auth/keygen_gui.py @@ -0,0 +1,205 @@ +# -*- coding: utf-8 -*- +""" +Mega Water - 离线授权发卡器 (开发者专用) +生成绑定特定机器码的 .lic 授权文件 +""" + +import os +import sys + +# 确保 src.auth 在 path 中 +_current_dir = os.path.dirname(os.path.abspath(__file__)) +_project_root = os.path.abspath(os.path.join(_current_dir, "..", "..")) +if _project_root not in sys.path: + sys.path.insert(0, _project_root) + +from PyQt5.QtWidgets import ( + QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, + QPushButton, QFileDialog, QMessageBox, QApplication, QDateEdit +) +from PyQt5.QtCore import Qt, QDate + +from src.auth.license_manager import generate_license + + +class LicenseKeygenWindow(QWidget): + """授权发卡器主窗口""" + + def __init__(self): + super().__init__() + self.setWindowTitle("Mega Water - 离线授权发卡器 (开发者专用)") + self.setMinimumSize(640, 340) + self.move(400, 280) + + self._default_save_path = os.path.join(_project_root, "license.lic") + self._setup_ui() + + def _setup_ui(self): + # ── 全局字体:无衬线,清晰 ── + font_family = "Microsoft YaHei" if sys.platform == "win32" else "Segoe UI" + base_font = f"'{font_family}', 'Segoe UI', sans-serif" + self.setStyleSheet(f""" + * {{ + font-family: {font_family}, 'Segoe UI', sans-serif; + font-size: 11pt; + }} + QLabel#titleLabel {{ + font-size: 16pt; + font-weight: bold; + color: #2c3e50; + }} + QLabel#tipLabel {{ + font-size: 10pt; + color: #95a5a6; + }} + """) + + main_layout = QVBoxLayout() + main_layout.setContentsMargins(45, 40, 45, 40) + main_layout.setSpacing(18) + + # ── 标题 ── + title_label = QLabel("离线授权发卡器 (开发者专用)") + title_label.setObjectName("titleLabel") + title_label.setAlignment(Qt.AlignCenter) + main_layout.addWidget(title_label) + + # ── 机器码输入行 ── + mc_layout = QHBoxLayout() + mc_layout.setSpacing(12) + mc_label = QLabel("机器码:") + mc_label.setFixedWidth(90) + self.mc_input = QLineEdit() + self.mc_input.setPlaceholderText("粘贴用户发来的 32 位机器码") + self.mc_input.setMinimumHeight(36) + self.mc_input.setMinimumWidth(400) + mc_layout.addWidget(mc_label, 0) + mc_layout.addWidget(self.mc_input, 1) + main_layout.addLayout(mc_layout) + + # ── 到期时间选择行 ── + exp_layout = QHBoxLayout() + exp_layout.setSpacing(12) + exp_label = QLabel("到期时间:") + exp_label.setFixedWidth(90) + self.exp_edit = QDateEdit() + self.exp_edit.setCalendarPopup(True) + self.exp_edit.setMinimumHeight(36) + self.exp_edit.setMinimumWidth(160) + # 默认:当前日期 + 1 年 + self.exp_edit.setDate(QDate.currentDate().addYears(1)) + exp_layout.addWidget(exp_label, 0) + exp_layout.addWidget(self.exp_edit, 0) + exp_layout.addStretch(1) + main_layout.addLayout(exp_layout) + + # ── 保存路径行 ── + path_layout = QHBoxLayout() + path_layout.setSpacing(12) + path_label = QLabel("保存路径:") + path_label.setFixedWidth(90) + self.path_input = QLineEdit() + self.path_input.setReadOnly(True) + self.path_input.setMinimumHeight(36) + self.browse_btn = QPushButton("浏览...") + self.browse_btn.setMinimumHeight(36) + self.browse_btn.setFixedWidth(80) + self.browse_btn.clicked.connect(self._on_browse) + path_layout.addWidget(path_label, 0) + path_layout.addWidget(self.path_input, 1) + path_layout.addWidget(self.browse_btn, 0) + main_layout.addLayout(path_layout) + + # ── 弹性空间 ── + main_layout.addSpacing(10) + + # ── 生成按钮 ── + self.gen_btn = QPushButton("生成授权文件 (.lic)") + self.gen_btn.setMinimumHeight(48) + self.gen_btn.setStyleSheet(""" + QPushButton { + background-color: #27ae60; + color: white; + font-size: 13pt; + font-weight: bold; + border: none; + border-radius: 8px; + } + QPushButton:hover { + background-color: #2ecc71; + } + QPushButton:pressed { + background-color: #1e8449; + } + """) + self.gen_btn.clicked.connect(self._on_generate) + main_layout.addWidget(self.gen_btn) + + # ── 底部提示 ── + tip_label = QLabel("生成后请将 license.lic 文件发给用户,放置到软件安装目录下即可。") + tip_label.setObjectName("tipLabel") + tip_label.setAlignment(Qt.AlignCenter) + main_layout.addWidget(tip_label) + + self.setLayout(main_layout) + + def _on_browse(self): + """打开文件对话框选择保存路径""" + path, _ = QFileDialog.getSaveFileName( + self, + "选择授权文件保存位置", + self._default_save_path, + "授权文件 (*.lic)" + ) + if path: + if not path.lower().endswith(".lic"): + path += ".lic" + self.path_input.setText(path) + + def _on_generate(self): + """点击生成按钮,调用授权管理器""" + machine_code = self.mc_input.text().strip() + if not machine_code: + QMessageBox.warning(self, "输入错误", "请输入机器码") + return + + expiry_date = self.exp_edit.date().toString("yyyy-MM-dd") + output_path = self.path_input.text().strip() + + if not output_path: + QMessageBox.warning(self, "输入错误", "请设置保存路径") + return + + ok, msg = generate_license( + machine_code=machine_code, + output_path=output_path, + expiry_date=expiry_date + ) + + if ok: + QMessageBox.information( + self, + "生成成功", + f"✅ 授权文件已成功生成!\n\n保存路径:\n{output_path}\n\n请将此文件发给用户即可。", + QMessageBox.Ok + ) + else: + QMessageBox.critical( + self, + "生成失败", + f"❌ {msg}", + QMessageBox.Ok + ) + + +if __name__ == "__main__": + # ── 高 DPI 自适应(必须放在 QApplication 实例化之前)── + from PyQt5.QtCore import Qt + QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True) + QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True) + + app = QApplication(sys.argv) + app.setApplicationName("LicenseKeygen") + window = LicenseKeygenWindow() + window.show() + sys.exit(app.exec_()) \ No newline at end of file diff --git a/src/auth/license_dialog.py b/src/auth/license_dialog.py new file mode 100644 index 0000000..c030b6b --- /dev/null +++ b/src/auth/license_dialog.py @@ -0,0 +1,255 @@ +# -*- coding: utf-8 -*- +""" +LicenseDialog - PyQt5 授权拦截弹窗 +当授权验证失败时弹出,提示用户导入授权文件。 +""" + +import os +import sys +import shutil +from pathlib import Path + +from PyQt5.QtWidgets import ( + QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, + QTextEdit, QFileDialog, QMessageBox, QApplication +) +from PyQt5.QtCore import Qt, QTimer +from PyQt5.QtGui import QFont, QIcon, QGuiApplication + +# 导入授权管理器 +from src.auth.license_manager import get_machine_code, verify_license, get_license_path + + +class LicenseDialog(QDialog): + """ + 授权验证弹窗 + - 显示本机机器码(只读文本框) + - 提供"一键复制"功能 + - 提供"导入授权文件"按钮 + - 导入成功后提示重启 + """ + + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle("授权验证") + self.setWindowFlags( + Qt.Dialog | + Qt.WindowTitleHint | + Qt.WindowCloseButtonHint + ) + self.setModal(True) + self.setMinimumWidth(540) + + self._init_ui() + self._load_machine_code() + + # 窗口居中 + QTimer.singleShot(0, self._center_on_screen) + + def _center_on_screen(self): + """将窗口居中到屏幕""" + screen = QGuiApplication.primaryScreen() + if screen: + geo = screen.geometry() + self.move( + (geo.width() - self.width()) // 2, + (geo.height() - self.height()) // 2 + ) + + def _init_ui(self): + main_layout = QVBoxLayout(self) + main_layout.setContentsMargins(30, 30, 30, 20) + main_layout.setSpacing(16) + + # ── 标题区 ── + title_font = QFont("Microsoft YaHei", 14, QFont.Bold) + title_label = QLabel("本软件需要授权方可运行") + title_label.setFont(title_font) + title_label.setAlignment(Qt.AlignCenter) + title_label.setStyleSheet("color: #2c3e50;") + main_layout.addWidget(title_label) + + # ── 说明文字 ── + hint_label = QLabel( + "请获取授权文件(license.lic)后导入," + "或联系技术支持获取授权。" + ) + hint_label.setAlignment(Qt.AlignCenter) + hint_label.setStyleSheet("color: #7f8c8d; font-size: 12px;") + main_layout.addWidget(hint_label) + + # ── 机器码标签 ── + code_label = QLabel("本机机器码(用于申请授权):") + code_label.setStyleSheet("font-weight: bold; color: #34495e;") + main_layout.addWidget(code_label) + + # ── 机器码文本框 + 复制按钮 ── + code_layout = QHBoxLayout() + code_layout.setSpacing(8) + + self.code_edit = QTextEdit() + self.code_edit.setReadOnly(True) + self.code_edit.setMaximumHeight(72) + self.code_edit.setFont(QFont("Consolas", 13)) + self.code_edit.setStyleSheet( + "QTextEdit {" + " background-color: #ecf0f1;" + " border: 1px solid #bdc3c7;" + " border-radius: 4px;" + " padding: 8px;" + " color: #2c3e50;" + "}" + ) + code_layout.addWidget(self.code_edit, 1) + + copy_btn = QPushButton("复制") + copy_btn.setFixedWidth(72) + copy_btn.setCursor(Qt.PointingHandCursor) + copy_btn.setStyleSheet( + "QPushButton {" + " background-color: #3498db;" + " color: white;" + " border: none;" + " border-radius: 4px;" + " padding: 8px 4px;" + " font-weight: bold;" + "}" + "QPushButton:hover { background-color: #2980b9; }" + "QPushButton:pressed { background-color: #21618c; }" + ) + copy_btn.clicked.connect(self._copy_code) + code_layout.addWidget(copy_btn) + + main_layout.addLayout(code_layout) + + # ── 导入授权文件按钮 ── + import_btn = QPushButton("导入授权文件 (.lic)") + import_btn.setCursor(Qt.PointingHandCursor) + import_btn.setStyleSheet( + "QPushButton {" + " background-color: #27ae60;" + " color: white;" + " border: none;" + " border-radius: 6px;" + " padding: 12px;" + " font-size: 14px;" + " font-weight: bold;" + "}" + "QPushButton:hover { background-color: #229954; }" + "QPushButton:pressed { background-color: #1e8449; }" + ) + import_btn.clicked.connect(self._import_license) + main_layout.addWidget(import_btn) + + # ── 提示文字 ── + tip_label = QLabel( + "导入后软件将自动重启生效。" + ) + tip_label.setAlignment(Qt.AlignCenter) + tip_label.setStyleSheet("color: #95a5a6; font-size: 11px;") + main_layout.addWidget(tip_label) + + # ── 取消按钮(退出程序)── + cancel_btn = QPushButton("退出") + cancel_btn.setCursor(Qt.PointingHandCursor) + cancel_btn.setStyleSheet( + "QPushButton {" + " background-color: #95a5a6;" + " color: white;" + " border: none;" + " border-radius: 4px;" + " padding: 8px 20px;" + "}" + "QPushButton:hover { background-color: #7f8c8d; }" + ) + cancel_btn.clicked.connect(self._quit_app) + main_layout.addWidget(cancel_btn, 0, Qt.AlignRight) + + main_layout.addStretch() + + def _load_machine_code(self): + """读取并显示本机机器码""" + try: + code = get_machine_code(32) + self.code_edit.setPlainText(code) + except Exception as e: + self.code_edit.setPlainText(f"读取失败: {e}") + + def _copy_code(self): + """复制机器码到剪贴板""" + clipboard = QApplication.clipboard() + clipboard.setText(self.code_edit.toPlainText().strip()) + + # 显示反馈 + QMessageBox.information(self, "已复制", "机器码已复制到剪贴板。") + + def _import_license(self): + """打开文件选择对话框,导入 .lic 授权文件""" + file_path, _ = QFileDialog.getOpenFileName( + self, + "选择授权文件", + "", + "授权文件 (*.lic);;所有文件 (*.*)" + ) + + if not file_path: + return + + # 验证授权文件 + ok, msg = verify_license(file_path) + if not ok: + QMessageBox.warning( + self, + "授权文件无效", + f"验证失败: {msg}\n\n请确认选择了正确的授权文件。" + ) + return + + # 复制授权文件到标准路径 + dest_path = get_license_path() + try: + dest_dir = os.path.dirname(dest_path) + if dest_dir: + os.makedirs(dest_dir, exist_ok=True) + shutil.copy2(file_path, dest_path) + except OSError as e: + QMessageBox.critical( + self, + "保存失败", + f"无法保存授权文件: {e}" + ) + return + + # 成功提示,重启程序 + reply = QMessageBox.information( + self, + "导入成功", + "授权文件已成功导入。\n\n软件将自动重启以应用授权。" + if False else # 占位,维持下面的逻辑 + "授权文件已成功导入。\n软件将自动重启以应用授权。", + QMessageBox.Ok + ) + + self.accept() + self._restart_app() + + def _quit_app(self): + """退出程序""" + self.reject() + sys.exit(0) + + def _restart_app(self): + """重启程序""" + self.close() + QApplication.quit() + + # 延迟重启(确保 QApplication 完全退出) + import subprocess + import sys as _sys + executable = _sys.executable + if getattr(_sys, 'frozen', False): + # PyInstaller 打包环境下 + subprocess.Popen([executable] + _sys.argv[1:]) + else: + # 开发环境 + subprocess.Popen([executable, __file__]) \ No newline at end of file diff --git a/src/auth/license_manager.py b/src/auth/license_manager.py new file mode 100644 index 0000000..4de3bf0 --- /dev/null +++ b/src/auth/license_manager.py @@ -0,0 +1,322 @@ +# -*- coding: utf-8 -*- +""" +License Manager - 离线授权管理模块 +使用 HMAC-SHA256 + 盐值签名防止篡改 +""" + +import os +import json +import hmac +import hashlib +import base64 +import uuid +import hashlib as _hashlib +import subprocess +import re +import sys +from datetime import datetime, timedelta +from pathlib import Path +from typing import Optional, Tuple + +# ============================================================ +# 第一部分:硬件指纹提取(内嵌 get_machine_code) +# ============================================================ + +def get_cpu_id() -> Optional[str]: + """读取 CPU 序列号(Processor ID)""" + try: + if sys.platform == "win32": + result = subprocess.run( + ["wmic", "cpu", "get", "ProcessorId"], + capture_output=True, + text=True, + timeout=5, + creationflags=subprocess.CREATE_NO_WINDOW + ) + cpu_id = result.stdout.strip().split("\n")[-1].strip() + if cpu_id: + return cpu_id + else: + with open("/proc/cpuinfo", "r") as f: + for line in f: + if "Serial" in line or "processor" in line: + cpu_id = line.split(":")[-1].strip() + if cpu_id: + return cpu_id + except Exception: + pass + return None + + +def get_motherboard_uuid() -> Optional[str]: + """读取主板 UUID(BaseBoard Serial Number)""" + try: + if sys.platform == "win32": + result = subprocess.run( + ["wmic", "baseboard", "get", "SerialNumber"], + capture_output=True, + text=True, + timeout=5, + creationflags=subprocess.CREATE_NO_WINDOW + ) + board_uuid = result.stdout.strip().split("\n")[-1].strip() + board_uuid = re.sub(r'[^a-zA-Z0-9\-]', '', board_uuid) + if board_uuid and board_uuid not in ("To be filled", "None"): + return board_uuid + else: + result = subprocess.run( + ["cat", "/sys/class/dmi/id/product_uuid"], + capture_output=True, + text=True, + timeout=5 + ) + if result.returncode == 0: + return result.stdout.strip() + except Exception: + pass + return None + + +def get_machine_code(code_length: int = 32) -> str: + """ + 生成唯一的机器码(硬件指纹) + 参数: + code_length: 机器码长度,支持 16/24/32/48/64 位,默认 32 位 + 返回: + 全大写字母+数字的机器码字符串 + """ + cpu_id = get_cpu_id() or "" + board_uuid = get_motherboard_uuid() or "" + raw_hardware = f"{cpu_id}-{board_uuid}" + + if not raw_hardware.strip("-") or len(raw_hardware) < 8: + try: + machine_name = uuid.gethostname() or "" + mac = ':'.join(re.findall('..', '%012x' % uuid.getnode())) + raw_hardware = f"{machine_name}-{mac}" + except Exception: + raw_hardware = str(uuid.getnode()) + + raw_hardware = re.sub(r'[^a-zA-Z0-9]', '', raw_hardware) + hash_hex = hashlib.sha256(raw_hardware.encode('utf-8')).hexdigest().upper() + hash_hex = hash_hex.replace('O', 'X').replace('L', 'Y').replace('I', 'Z') + + valid_lengths = [16, 24, 32, 48, 64] + if code_length not in valid_lengths: + code_length = 32 + + return hash_hex[:code_length] + + +# ============================================================ +# 第二部分:授权文件格式与签名机制 +# ============================================================ + +# 开发者密钥(硬编码在软件中,用于验证授权文件) +# 注意:实际部署时建议对密钥进行简单混淆或从外部文件加载 +DEVELOPER_SECRET = b"WaterQuality_v1_2025_SecretKey" +LICENSE_VERSION = "1.0" + + +def _compute_signature(payload_json: str) -> str: + """ + 计算 HMAC-SHA256 签名 + payload_json: JSON 序列化后的字符串(不含 signature 字段) + """ + sig = hmac.new( + DEVELOPER_SECRET, + payload_json.encode('utf-8'), + hashlib.sha256 + ).hexdigest().upper() + return sig + + +def _clean_hash(s: str) -> str: + """清洗哈希字符串,避免混淆字符""" + return s.replace('O', 'X').replace('L', 'Y').replace('I', 'Z') + + +# ============================================================ +# 第三部分:核心 API +# ============================================================ + +def get_license_path() -> str: + """获取授权文件的标准存放路径(程序根目录)""" + if getattr(sys, 'frozen', False): + base_dir = os.path.dirname(sys.executable) + else: + base_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + return os.path.join(base_dir, "license.lic") + + +def verify_license(license_path: Optional[str] = None) -> Tuple[bool, str]: + """ + 校验授权文件是否匹配本机硬件指纹。 + + 参数: + license_path: 授权文件路径,默认使用标准路径 + + 返回: + (is_valid, message) + - is_valid=True 表示授权有效 + - is_valid=False 表示授权无效,message 为具体原因 + """ + if license_path is None: + license_path = get_license_path() + + # Step 1: 文件是否存在 + if not os.path.isfile(license_path): + return False, "授权文件不存在" + + # Step 2: 读取并解析 JSON + try: + with open(license_path, 'r', encoding='utf-8') as f: + content = f.read().strip() + lic_data = json.loads(content) + except (json.JSONDecodeError, OSError) as e: + return False, f"授权文件格式错误: {e}" + + # Step 3: 校验版本号 + version = lic_data.get("version", "") + if version != LICENSE_VERSION: + return False, f"授权文件版本不匹配 (期望 {LICENSE_VERSION})" + + # Step 4: 校验过期时间 + expiry_str = lic_data.get("expiry", "") + if expiry_str: + try: + expiry_dt = datetime.strptime(expiry_str, "%Y-%m-%d") + if datetime.now() > expiry_dt: + return False, "授权已过期" + except ValueError: + return False, "授权文件日期格式错误" + + # Step 5: 提取 payload(不含 signature) + payload_for_verify = {k: v for k, v in lic_data.items() if k != "signature"} + payload_json = json.dumps(payload_for_verify, sort_keys=True, ensure_ascii=False) + + # Step 6: 校验签名完整性(防篡改) + expected_sig = _compute_signature(payload_json) + stored_sig = lic_data.get("signature", "").upper() + if not hmac.compare_digest(expected_sig, stored_sig): + return False, "授权文件签名校验失败(可能被篡改)" + + # Step 7: 校验机器码绑定 + bound_machine = lic_data.get("machine_code", "") + current_machine = get_machine_code(32) + if not hmac.compare_digest(bound_machine, current_machine): + return False, "机器码不匹配(授权文件与本机不兼容)" + + return True, "授权验证通过" + + +def generate_license( + machine_code: str, + output_path: str, + expiry_date: Optional[str] = None, + product_name: str = "WaterQualityInversion", + max_uses: Optional[int] = None +) -> Tuple[bool, str]: + """ + 为指定机器码生成合法的授权文件(供开发者使用)。 + + 参数: + machine_code: 目标机器的机器码(32位) + output_path: 授权文件输出路径(含文件名,如 "D:/license.lic") + expiry_date: 有效期截止日期,格式 "YYYY-MM-DD",默认永久 + product_name: 产品名称 + max_uses: 最大使用次数(可选,默认不限制) + + 返回: + (success, message) + """ + if len(machine_code) not in (16, 24, 32, 48, 64): + return False, f"机器码长度无效(期望 16/24/32/48/64,实际 {len(machine_code)})" + + # 构建 payload + payload = { + "version": LICENSE_VERSION, + "product": product_name, + "machine_code": machine_code.upper(), + "generated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + "expiry": expiry_date or "", + } + if max_uses is not None: + payload["max_uses"] = max_uses + + # 计算签名 + payload_json = json.dumps(payload, sort_keys=True, ensure_ascii=False) + signature = _compute_signature(payload_json) + + # 完整授权文件内容 + lic_content = json.dumps({**payload, "signature": signature}, indent=2, ensure_ascii=False) + + # 写入文件 + try: + os.makedirs(os.path.dirname(output_path) or ".", exist_ok=True) + with open(output_path, 'w', encoding='utf-8') as f: + f.write(lic_content) + return True, f"授权文件已生成: {output_path}" + except OSError as e: + return False, f"写入授权文件失败: {e}" + + +def get_machine_info() -> dict: + """获取完整机器信息(调试用)""" + return { + "cpu_id": get_cpu_id(), + "motherboard_uuid": get_motherboard_uuid(), + "machine_code_16": get_machine_code(16), + "machine_code_32": get_machine_code(32), + "license_path": get_license_path(), + } + + +# ============================================================ +# 第四部分:便捷入口(支持直接运行) +# ============================================================ + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser(description="WaterQuality 授权管理工具") + subparsers = parser.add_subparsers(dest="command", help="子命令") + + # 子命令:verify + p_verify = subparsers.add_parser("verify", help="验证本机授权") + p_verify.add_argument("-f", "--file", default=None, help="授权文件路径") + + # 子命令:gen / generate + p_gen = subparsers.add_parser("generate", help="为指定机器码生成授权文件") + p_gen.add_argument("-m", "--machine", required=True, help="目标机器的机器码") + p_gen.add_argument("-o", "--output", required=True, help="输出文件路径") + p_gen.add_argument("-e", "--expiry", default=None, help="有效期截止日期 YYYY-MM-DD") + p_gen.add_argument("-n", "--name", default="WaterQualityInversion", help="产品名称") + + # 子命令:info + subparsers.add_parser("info", help="显示本机机器信息") + + args = parser.parse_args() + + if args.command == "verify": + ok, msg = verify_license(args.file) + print(f"[{'OK' if ok else 'FAIL'}] {msg}") + + elif args.command == "generate": + ok, msg = generate_license(args.machine, args.output, args.expiry, args.name) + print(f"[{'OK' if ok else 'FAIL'}] {msg}") + + elif args.command == "info": + info = get_machine_info() + print("=" * 50) + print("硬件指纹信息") + print("=" * 50) + for k, v in info.items(): + print(f" {k}: {v or '(读取失败)'}") + print("=" * 50) + # 同时演示验证 + ok, msg = verify_license() + print(f"\n授权验证: [{'OK' if ok else 'FAIL'}] {msg}") + + else: + parser.print_help() \ No newline at end of file diff --git a/src/gui/water_quality_gui.py b/src/gui/water_quality_gui.py index 54e3a96..462414c 100644 --- a/src/gui/water_quality_gui.py +++ b/src/gui/water_quality_gui.py @@ -2996,23 +2996,49 @@ class WaterQualityGUI(QMainWindow): item.setForeground(QColor(ModernStylesheet.COLORS.get('text_secondary', '#666666'))) # 原始颜色 +# ============================================================ +# 主启动逻辑 +# ============================================================ def main(): """主函数""" + import sys + + # 离线授权验证拦截(必须在业务窗口创建前执行) + try: + from src.auth.license_manager import verify_license + from src.auth.license_dialog import LicenseDialog + except ImportError: + import os + _current_dir = os.path.dirname(os.path.abspath(__file__)) + _project_root = os.path.abspath(os.path.join(_current_dir, '..', '..')) + if _project_root not in sys.path: + sys.path.insert(0, _project_root) + from src.auth.license_manager import verify_license + from src.auth.license_dialog import LicenseDialog + + _is_license_valid, _license_msg = verify_license() + if not _is_license_valid: + _license_app = QApplication(sys.argv) + _license_app.setApplicationName("WaterQuality") + _dialog = LicenseDialog() + _dialog.exec_() + _license_app.quit() + sys.exit(0) + + # 授权通过,正常载入主程序 app = QApplication(sys.argv) - - # 设置应用信息 app.setApplicationName("Mega Water") app.setOrganizationName("WaterQuality") - - # 创建主窗口 + window = WaterQualityGUI() window.show() - + sys.exit(app.exec_()) if __name__ == "__main__": - #冻结,只显示1个exe - # multiprocessing.freeze_support() + # 必须紧跟在 if __name__ == "__main__": 下面第一行 + import multiprocessing + multiprocessing.freeze_support() main()