commit ea4278d1be734279eac761354ef1da425d7a42ec Author: wsb1224 Date: Thu Oct 23 18:27:33 2025 +0800 8.7 diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..359bb53 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml diff --git a/.idea/Exeprotector.iml b/.idea/Exeprotector.iml new file mode 100644 index 0000000..bd20c4b --- /dev/null +++ b/.idea/Exeprotector.iml @@ -0,0 +1,14 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..15ab60a --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,94 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..8a45194 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..940a0f1 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..288b36b --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/config.py b/config.py new file mode 100644 index 0000000..8dfdb73 --- /dev/null +++ b/config.py @@ -0,0 +1,106 @@ +""" +Configuration file for the EXE encryption system +Contains security settings and system parameters +""" +""" +作者:太一 +微信:taiyi1224 +邮箱:shoubo1224@qq.com +""" +import os +from pathlib import Path + +# System configuration +SYSTEM_CONFIG = { + 'app_name': 'EXE Secure Wrapper', + 'version': '2.0.0', + 'company': 'Secure Software Solutions', + 'contact': 'support@securesoft.com' +} + +# Security settings +SECURITY_CONFIG = { + # Encryption settings + 'key_length': 32, # bytes + 'salt_length': 32, # bytes + 'iterations': 100000, # PBKDF2 iterations + 'hash_algorithm': 'sha256', + 'xor_key': os.environ.get('EXE_WRAPPER_KEY', 'EXEWrapper#2024').encode(), + + # File validation + 'min_file_size': 1024, # bytes + 'magic_header': b'ENC_MAGIC', + 'header_size': 512, # 配置头固定长度(字节) + + # License settings + 'trial_days': 7, + 'license_key_length': 20, + 'license_format': 'XXXXX-XXXXX-XXXXX-XXXXX', +} + +# Temporary file settings +TEMP_CONFIG = { + 'temp_prefix': 'sec_wrap_', + 'max_temp_age': 3600, # seconds (1 hour) + 'auto_cleanup': True, +} + +# Database settings (会被嵌入到包装文件中) +DATABASE_CONFIG = { + 'mysql': { + 'host': os.environ.get('DB_HOST', 'localhost'), + 'port': int(os.environ.get('DB_PORT', 3306)), + 'database': os.environ.get('DB_NAME', 'license_system'), + 'user': os.environ.get('DB_USER', 'root'), + 'password': os.environ.get('DB_PASSWORD', ''), + 'charset': 'utf8mb4', + 'connection_timeout': 30, + 'ssl_disabled': True, + }, + 'sqlite': { + 'filename': 'licenses_local.db', + 'check_same_thread': False, + } +} + +# Logging configuration +LOGGING_CONFIG = { + 'level': 'INFO', + 'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s', + 'file_max_size': 10 * 1024 * 1024, # 10MB + 'backup_count': 5, + 'log_dir': 'logs', +} + +# Validation settings +VALIDATION_CONFIG = { + 'check_internet': True, + 'max_retries': 3, + 'retry_delay': 1, # seconds + 'timeout': 10, # seconds + 'heartbeat_interval': 300, # seconds (5 minutes) +} + +# Build paths +BASE_DIR = Path(__file__).parent.absolute() +CONFIG_DIR = BASE_DIR / "config" +LOG_DIR = BASE_DIR / LOGGING_CONFIG['log_dir'] +TEMP_DIR = BASE_DIR / "temp" + +# Ensure directories exist +for directory in [LOG_DIR, TEMP_DIR, CONFIG_DIR]: + directory.mkdir(exist_ok=True) + +def get_config_path(filename): + """Get full path to configuration file""" + return CONFIG_DIR / filename + +def get_temp_path(filename=None): + """Get temporary file path""" + if filename: + return TEMP_DIR / filename + return TEMP_DIR + +def get_log_path(filename): + """Get log file path""" + return LOG_DIR / filename \ No newline at end of file diff --git a/database.py b/database.py new file mode 100644 index 0000000..e364ac3 --- /dev/null +++ b/database.py @@ -0,0 +1,630 @@ +""" +作者:太一 +微信:taiyi1224 +邮箱:shoubo1224@qq.com +""" + + +from typing import Tuple, List, Dict, Optional + +import mysql.connector +from mysql.connector import Error +import uuid +from datetime import datetime, timedelta +import hashlib +import os + +# 导入文件管理器 +try: + from file_manager import FileManager +except ImportError: + FileManager = None + + +class LicenseDatabase: + def __init__(self, host=None, database=None, user=None, password=None): + self.host = host or os.environ.get('DB_HOST', 'taiyiagi.xyz') + self.database = database or os.environ.get('DB_NAME', 'filesend_db') + self.user = user or os.environ.get('DB_USER', 'taiyi') + self.password = password or os.environ.get('DB_PASSWORD', 'taiyi1224') + self.connection = None + + # 初始化文件管理器 + if FileManager: + self.file_manager = FileManager() + else: + self.file_manager = None + + def connect(self): + """连接到数据库""" + try: + self.connection = mysql.connector.connect( + host=self.host, + database=self.database, + user=self.user, + password=self.password + ) + if self.connection.is_connected(): + return True + return False + except Error as e: + print(f"数据库连接错误: {e}") + return False + + def create_tables(self): + """创建必要的数据库表""" + if not self.connection or not self.connection.is_connected(): + if not self.connect(): + return False + + try: + cursor = self.connection.cursor() + + # 创建软件产品表 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS software_products ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(100) NOT NULL UNIQUE, + description TEXT, + version VARCHAR(50), + exe_path VARCHAR(500), + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + ''') + + # 检查并添加缺失的字段 + try: + # 检查version字段是否存在 + cursor.execute("SHOW COLUMNS FROM software_products LIKE 'version'") + if not cursor.fetchone(): + cursor.execute("ALTER TABLE software_products ADD COLUMN version VARCHAR(50) AFTER description") + print("成功添加version字段") + except Error as e: + print(f"检查version字段时出错: {e}") + + try: + # 检查exe_path字段是否存在 + cursor.execute("SHOW COLUMNS FROM software_products LIKE 'exe_path'") + if not cursor.fetchone(): + cursor.execute("ALTER TABLE software_products ADD COLUMN exe_path VARCHAR(500) AFTER version") + print("成功添加exe_path字段") + except Error as e: + print(f"检查exe_path字段时出错: {e}") + + # 创建卡密表 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS license_keys ( + id INT AUTO_INCREMENT PRIMARY KEY, + key_code VARCHAR(50) NOT NULL UNIQUE, + software_id INT NOT NULL, + machine_code VARCHAR(100) DEFAULT NULL, + start_time DATETIME DEFAULT NULL, + end_time DATETIME NOT NULL, + status ENUM('unused', 'active', 'expired', 'banned') DEFAULT 'unused', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (software_id) REFERENCES software_products(id) + ) + ''') + + self.connection.commit() + cursor.close() + return True + except Error as e: + print(f"创建表错误: {e}") + return False + + def generate_key(self, days_valid, software_id): + """生成一个新的卡密并保存到数据库""" + if not self.connection or not self.connection.is_connected(): + if not self.connect(): + return None + + try: + # 生成UUID作为基础,然后进行哈希处理 + key_uuid = uuid.uuid4().hex + hash_obj = hashlib.sha256(key_uuid.encode()) + # 取前20个字符作为卡密 + key_code = hash_obj.hexdigest()[:20].upper() + + # 格式化卡密,每5个字符一组 + formatted_key = '-'.join([key_code[i:i + 5] for i in range(0, len(key_code), 5)]) + + # 计算过期时间 + end_time = datetime.now() + timedelta(days=days_valid) + + cursor = self.connection.cursor() + query = """ + INSERT INTO license_keys (key_code, software_id, end_time) + VALUES (%s, %s, %s) + """ + cursor.execute(query, (formatted_key, software_id, end_time)) + self.connection.commit() + cursor.close() + + return formatted_key + except Error as e: + print(f"生成卡密错误: {e}") + return None + + def validate_key(self, key_code, machine_code, software_id): + """验证卡密是否有效,并绑定机器码 - 严格一机一码""" + if not self.connection or not self.connection.is_connected(): + if not self.connect(): + return False, "数据库连接失败" + + try: + cursor = self.connection.cursor(dictionary=True) + + # 查询卡密信息 + query = "SELECT * FROM license_keys WHERE key_code = %s AND software_id = %s" + cursor.execute(query, (key_code, software_id)) + key_info = cursor.fetchone() + + if not key_info: + cursor.close() + return False, "无效的激活码" + + # 检查卡密状态 + if key_info['status'] == 'banned': + cursor.close() + return False, "激活码已被封禁" + + if key_info['status'] == 'expired': + cursor.close() + return False, "激活码已过期" + + # 一机一码严格检查:每个激活码只能在一台机器上使用 + if key_info['status'] == 'active': + if key_info['machine_code'] != machine_code: + # 这个码已经用过了,不能再次使用 + cursor.close() + return False, f"此激活码已在设备{key_info['machine_code'][:8]}...上使用,一个激活码只能在一台设备上使用一次" + else: + # 已经激活过这台机器,验证是否过期 + if datetime.now() > key_info['end_time']: + update_query = "UPDATE license_keys SET status = 'expired' WHERE key_code = %s" + cursor.execute(update_query, (key_code,)) + self.connection.commit() + cursor.close() + return False, "激活码已过期" + else: + cursor.close() + return True, "此设备已激活,继续使用" + + # 首次激活:验证通过后绑定到机器 + if key_info['status'] == 'unused': + update_query = """ + UPDATE license_keys + SET status = 'active', machine_code = %s, start_time = %s + WHERE key_code = %s AND status = 'unused' + """ + cursor.execute(update_query, (machine_code, datetime.now(), key_code)) + rows_affected = cursor.rowcount + self.connection.commit() + + if rows_affected == 0: + # 可能已经被其他并发操作激活 + cursor.close() + return False, "激活码已被使用,请使用新的激活码" + + # 再次检查是否过期(防止并发问题) + final_check_query = "SELECT * FROM license_keys WHERE key_code = %s" + cursor.execute(final_check_query, (key_code,)) + final_info = cursor.fetchone() + + if final_info and datetime.now() > final_info['end_time']: + update_query = "UPDATE license_keys SET status = 'expired' WHERE key_code = %s" + cursor.execute(update_query, (key_code,)) + self.connection.commit() + cursor.close() + return False, "激活码已过期" + + cursor.close() + return True, "激活成功" + + except Error as e: + print(f"验证激活码错误: {e}") + return False, f"验证过程出错: {str(e)}" + + def get_all_keys(self, software_id=None): + """获取所有卡密信息,可按软件ID筛选""" + if not self.connection or not self.connection.is_connected(): + print("警告: 数据库未连接,尝试重新连接...") + if not self.connect(): + print("错误: 数据库重连失败") + return [] + + cursor = None + try: + cursor = self.connection.cursor(dictionary=True) + + # 先检查license_keys表是否存在 + try: + cursor.execute("SHOW TABLES LIKE 'license_keys'") + if not cursor.fetchone(): + print("警告: license_keys表不存在,请先创建数据库表") + if cursor: + cursor.close() + return [] + except Error as e: + print(f"检查表存在性时出错: {e}") + if cursor: + cursor.close() + return [] + + # 检查software_products表是否存在以及字段是否完整 + use_join = False + try: + cursor.execute("SHOW TABLES LIKE 'software_products'") + if cursor.fetchone(): + # 表存在,检查字段 + cursor.execute("SHOW COLUMNS FROM software_products") + columns = [row['Field'] for row in cursor.fetchall()] + if 'name' in columns: # 如果有name字段,就使用JOIN查询 + use_join = True + except Error as e: + print(f"检查software_products表时出错: {e}") + use_join = False + + # 构建并执行查询 + try: + if use_join: + if software_id: + query = """ + SELECT lk.*, COALESCE(sp.name, 'Unknown') as software_name + FROM license_keys lk + LEFT JOIN software_products sp ON lk.software_id = sp.id + WHERE lk.software_id = %s + ORDER BY lk.created_at DESC + """ + cursor.execute(query, (software_id,)) + else: + query = """ + SELECT lk.*, COALESCE(sp.name, 'Unknown') as software_name + FROM license_keys lk + LEFT JOIN software_products sp ON lk.software_id = sp.id + ORDER BY lk.created_at DESC + """ + cursor.execute(query) + else: + # 不使用JOIN,只查询license_keys表 + if software_id: + query = "SELECT *, 'Unknown' as software_name FROM license_keys WHERE software_id = %s ORDER BY created_at DESC" + cursor.execute(query, (software_id,)) + else: + query = "SELECT *, 'Unknown' as software_name FROM license_keys ORDER BY created_at DESC" + cursor.execute(query) + + keys = cursor.fetchall() + + # 确保返回的是列表 + if keys is None: + keys = [] + + if len(keys) == 0: + print("提示: 数据库中暂无卡密记录") + else: + print(f"成功从数据库获取 {len(keys)} 个卡密记录") + + if cursor: + cursor.close() + return keys + + except Error as e: + print(f"执行查询时出错: {e}") + if cursor: + cursor.close() + return [] + + except Error as e: + print(f"获取卡密列表错误: {type(e).__name__}: {e}") + if cursor: + cursor.close() + return [] + except Exception as e: + print(f"获取卡密列表时发生未知错误: {type(e).__name__}: {e}") + import traceback + traceback.print_exc() + if cursor: + cursor.close() + return [] + + def update_key_status(self, key_code, status): + """更新卡密状态""" + if not self.connection or not self.connection.is_connected(): + if not self.connect(): + return False + + try: + cursor = self.connection.cursor() + query = "UPDATE license_keys SET status = %s WHERE key_code = %s" + cursor.execute(query, (status, key_code)) + self.connection.commit() + cursor.close() + return True + except Error as e: + print(f"更新卡密状态错误: {e}") + return False + + def release_key(self, key_code): + """释放已使用的激活码 - 将其重置为未使用状态,清空机器码""" + if not self.connection or not self.connection.is_connected(): + if not self.connect(): + return False, "数据库连接失败" + + try: + cursor = self.connection.cursor(dictionary=True) + + # 检查卡密是否存在且处于已激活状态 + check_query = "SELECT * FROM license_keys WHERE key_code = %s" + cursor.execute(check_query, (key_code,)) + key_info = cursor.fetchone() + + if not key_info: + cursor.close() + return False, "激活码不存在" + + if key_info['status'] != 'active': + cursor.close() + return False, f"激活码处于 {key_info['status']} 状态,只能释放已使用的激活码" + + # 释放激活码:重置为未使用状态,清空机器码和开始时间 + release_query = """ + UPDATE license_keys + SET status = 'unused', machine_code = NULL, start_time = NULL + WHERE key_code = %s + """ + cursor.execute(release_query, (key_code,)) + rows_affected = cursor.rowcount + self.connection.commit() + cursor.close() + + if rows_affected > 0: + return True, "激活码已释放,可以重新使用" + else: + return False, "释放激活码失败" + + except Error as e: + print(f"释放激活码错误: {e}") + return False, f"释放过程出错: {str(e)}" + + def close(self): + """关闭数据库连接""" + if self.connection and self.connection.is_connected(): + self.connection.close() + def unbind_key(self, key_code: str) -> Tuple[bool, str]: + """强制解除卡密与机器码的绑定""" + if not self.connection: + return False, "数据库未连接" + try: + cursor = self.connection.cursor(dictionary=True) + cursor.execute( + "UPDATE license_keys SET status='unused', machine_code=NULL, start_time=NULL WHERE key_code=%s", + (key_code,)) + self.connection.commit() + rows = cursor.rowcount + cursor.close() + return (True, "已解绑并释放") if rows else (False, "卡密不存在或状态异常") + except Exception as e: + return False, str(e) + + def add_software_product_with_file(self, name: str, description: str = "", version: str = "", + exe_source_path: str = "") -> Tuple[bool, str]: + """添加软件产品并上传exe文件""" + if not self.connection or not self.connection.is_connected(): + if not self.connect(): + return False, "数据库连接失败" + + try: + # 如果提供了exe文件路径,尝试上传 + local_exe_path = "" + if exe_source_path and self.file_manager: + success, msg, relative_path = self.file_manager.upload_exe_file( + exe_source_path, name + ) + if success: + local_exe_path = relative_path + else: + return False, f"文件上传失败: {msg}" + elif exe_source_path: + # 没有文件管理器,使用原来的逻辑 + local_exe_path = exe_source_path + + cursor = self.connection.cursor() + query = """ + INSERT INTO software_products (name, description, version, exe_path) + VALUES (%s, %s, %s, %s) + """ + cursor.execute(query, (name, description, version, local_exe_path)) + self.connection.commit() + cursor.close() + + if exe_source_path and self.file_manager: + return True, f"软件产品添加成功!\n\n✅ 文件已自动保存到本地目录\n目录: {self.file_manager.exe_dir}\n文件名: {local_exe_path}\n\n现在你的EXE文件已经被安全地存储在程序专属目录中,不用担心文件移动问题了!" + else: + return True, "软件产品添加成功" + + except Error as e: + if "Duplicate entry" in str(e): + return False, "软件产品已存在" + return False, f"添加软件产品失败: {str(e)}" + + def get_exe_full_path(self, relative_path: str) -> str: + """获取exe文件的完整路径""" + if not relative_path: + return "" + + # 如果是绝对路径,直接返回 + if os.path.isabs(relative_path): + return relative_path + + # 如果有文件管理器,使用它获取路径 + if self.file_manager: + return self.file_manager.get_exe_full_path(relative_path) + else: + # 退回到相对路径逻辑 + base_dir = os.path.dirname(__file__) + return os.path.join(base_dir, "files", "executables", relative_path) + + def delete_software_product_with_file(self, product_id: int) -> Tuple[bool, str]: + """删除软件产品并删除关联的exe文件""" + if not self.connection or not self.connection.is_connected(): + if not self.connect(): + return False, "数据库连接失败" + + try: + cursor = self.connection.cursor(dictionary=True) + + # 获取软件信息 + cursor.execute("SELECT * FROM software_products WHERE id = %s", (product_id,)) + product = cursor.fetchone() + + if not product: + cursor.close() + return False, "软件产品不存在" + + # 检查是否有关联的卡密 + cursor.execute("SELECT COUNT(*) FROM license_keys WHERE software_id = %s", (product_id,)) + key_count = cursor.fetchone()['COUNT(*)'] + + if key_count > 0: + cursor.close() + return False, f"无法删除,还有 {key_count} 个关联的卡密" + + # 删除数据库记录 + cursor.execute("DELETE FROM software_products WHERE id = %s", (product_id,)) + self.connection.commit() + cursor.close() + + # 删除关联的exe文件 + exe_path = product.get('exe_path', '') + if exe_path and self.file_manager: + # 如果是相对路径,删除文件 + if not os.path.isabs(exe_path): + file_success, file_msg = self.file_manager.delete_exe_file(exe_path) + if not file_success: + return True, f"软件产品删除成功,但文件删除失败: {file_msg}" + else: + return True, f"软件产品和文件删除成功: {file_msg}" + + return True, "软件产品删除成功" + + except Error as e: + return False, f"删除软件产品失败: {str(e)}" + + def get_file_storage_info(self) -> Dict: + """获取文件存储信息""" + if self.file_manager: + return self.file_manager.get_storage_info() + else: + return { + 'total_files': 0, + 'total_size': 0, + 'existing_files': 0, + 'missing_files': 0, + 'base_dir': '文件管理器未初始化', + 'exe_dir': '', + 'backup_dir': '' + } + + def get_software_products(self) -> List[Dict]: + """获取所有软件产品列表""" + if not self.connection or not self.connection.is_connected(): + if not self.connect(): + return [] + + try: + cursor = self.connection.cursor(dictionary=True) + query = "SELECT * FROM software_products ORDER BY name" + cursor.execute(query) + products = cursor.fetchall() + cursor.close() + return products + except Error as e: + print(f"获取软件产品列表错误: {e}") + return [] + + def get_software_by_name(self, name: str) -> Optional[Dict]: + """根据名称获取软件产品信息""" + if not self.connection or not self.connection.is_connected(): + if not self.connect(): + return None + + try: + cursor = self.connection.cursor(dictionary=True) + query = "SELECT * FROM software_products WHERE name = %s" + cursor.execute(query, (name,)) + product = cursor.fetchone() + cursor.close() + return product + except Error as e: + print(f"获取软件产品信息错误: {e}") + return None + + def update_software_product(self, product_id: int, name: str = None, description: str = None, + version: str = None, exe_path: str = None) -> Tuple[bool, str]: + """更新软件产品信息""" + if not self.connection or not self.connection.is_connected(): + if not self.connect(): + return False, "数据库连接失败" + + try: + cursor = self.connection.cursor() + + # 构建更新语句 + updates = [] + params = [] + + if name is not None: + updates.append("name = %s") + params.append(name) + if description is not None: + updates.append("description = %s") + params.append(description) + if version is not None: + updates.append("version = %s") + params.append(version) + if exe_path is not None: + updates.append("exe_path = %s") + params.append(exe_path) + + if not updates: + return False, "没有提供更新内容" + + query = f"UPDATE software_products SET {', '.join(updates)} WHERE id = %s" + params.append(product_id) + + cursor.execute(query, params) + self.connection.commit() + cursor.close() + + return True, "软件产品更新成功" + except Error as e: + if "Duplicate entry" in str(e): + return False, "软件名称已存在" + return False, f"更新软件产品失败: {str(e)}" + + def delete_software_product(self, product_id: int) -> Tuple[bool, str]: + """删除软件产品""" + if not self.connection or not self.connection.is_connected(): + if not self.connect(): + return False, "数据库连接失败" + + try: + cursor = self.connection.cursor() + + # 检查是否有关联的卡密 + cursor.execute("SELECT COUNT(*) FROM license_keys WHERE software_id = %s", (product_id,)) + key_count = cursor.fetchone()[0] + + if key_count > 0: + cursor.close() + return False, f"无法删除,还有 {key_count} 个关联的卡密" + + cursor.execute("DELETE FROM software_products WHERE id = %s", (product_id,)) + self.connection.commit() + cursor.close() + + return True, "软件产品删除成功" + except Error as e: + return False, f"删除软件产品失败: {str(e)}" \ No newline at end of file diff --git a/db_config.json b/db_config.json new file mode 100644 index 0000000..478322b --- /dev/null +++ b/db_config.json @@ -0,0 +1 @@ +{"host": "129.211.65.73", "database": "exeprotector", "user": "root", "password": "taiyi1224"} \ No newline at end of file diff --git a/machine_code.py b/machine_code.py new file mode 100644 index 0000000..b045657 --- /dev/null +++ b/machine_code.py @@ -0,0 +1,281 @@ +""" +作者:太一 +微信:taiyi1224 +邮箱:shoubo1224@qq.com +""" + +import hashlib +import platform +import subprocess +import sys +import uuid + + +def get_windows_machine_code(): + """获取Windows系统的机器码""" + try: + # 尝试获取主板序列号 + try: + wmic_output = subprocess.check_output( + 'wmic baseboard get serialnumber', + shell=True, + stderr=subprocess.STDOUT, + timeout=10 + ).decode().strip() + + if "SerialNumber" in wmic_output: + lines = [line.strip() for line in wmic_output.split("\n") if line.strip()] + if len(lines) > 1: + serial = lines[1] + if serial and serial not in ["To Be Filled By O.E.M.", "Default string", "N/A", ""]: + return hashlib.md5(serial.encode()).hexdigest()[:16].upper() + except: + pass + + # 尝试获取硬盘序列号 + try: + wmic_output = subprocess.check_output( + 'wmic diskdrive get serialnumber', + shell=True, + stderr=subprocess.STDOUT, + timeout=10 + ).decode().strip() + + if "SerialNumber" in wmic_output: + lines = [line.strip() for line in wmic_output.split("\n") if line.strip()] + for line in lines[1:]: # 跳过标题行 + if line and line not in ["", "N/A"]: + return hashlib.md5(line.encode()).hexdigest()[:16].upper() + except: + pass + + # 尝试获取CPU序列号 + try: + wmic_output = subprocess.check_output( + 'wmic cpu get processorid', + shell=True, + stderr=subprocess.STDOUT, + timeout=10 + ).decode().strip() + + if "ProcessorId" in wmic_output: + lines = [line.strip() for line in wmic_output.split("\n") if line.strip()] + if len(lines) > 1: + cpu_id = lines[1] + if cpu_id and cpu_id != "": + return hashlib.md5(cpu_id.encode()).hexdigest()[:16].upper() + except: + pass + + # 尝试获取BIOS序列号 + try: + wmic_output = subprocess.check_output( + 'wmic bios get serialnumber', + shell=True, + stderr=subprocess.STDOUT, + timeout=10 + ).decode().strip() + + if "SerialNumber" in wmic_output: + lines = [line.strip() for line in wmic_output.split("\n") if line.strip()] + if len(lines) > 1: + bios_serial = lines[1] + if bios_serial and bios_serial not in ["To Be Filled By O.E.M.", "Default string", "N/A", ""]: + return hashlib.md5(bios_serial.encode()).hexdigest()[:16].upper() + except: + pass + + # 组合多个系统信息生成唯一标识 + try: + # 获取网卡MAC地址 + mac = uuid.getnode() + mac_str = ':'.join(['{:02x}'.format((mac >> elements) & 0xff) for elements in range(0, 2 * 6, 2)][::-1]) + + # 获取计算机名 + computer_name = platform.node() + + # 获取系统信息 + system_info = f"{platform.system()}-{platform.release()}-{platform.version()}" + + # 组合信息 + combined_info = f"{mac_str}-{computer_name}-{system_info}" + return hashlib.md5(combined_info.encode()).hexdigest()[:16].upper() + + except Exception as e: + # 最后的备用方案 + fallback = f"{platform.uname()}-{uuid.getnode()}" + return hashlib.md5(fallback.encode()).hexdigest()[:16].upper() + + except Exception as e: + print(f"获取Windows机器码错误: {e}") + # 生成一个基于系统信息的备用哈希 + try: + fallback = f"{platform.node()}-{uuid.getnode()}-{platform.processor()}" + return hashlib.md5(fallback.encode()).hexdigest()[:16].upper() + except: + # 终极备用方案 + import time + fallback = f"FALLBACK-{int(time.time())}-{platform.system()}" + return hashlib.md5(fallback.encode()).hexdigest()[:16].upper() + + +def get_linux_machine_code(): + """获取Linux系统的机器码""" + try: + # 尝试读取machine-id + try: + with open('/etc/machine-id', 'r') as f: + machine_id = f.read().strip() + if machine_id and len(machine_id) > 10: + return machine_id[:16].upper() + except: + pass + + # 尝试读取/var/lib/dbus/machine-id + try: + with open('/var/lib/dbus/machine-id', 'r') as f: + machine_id = f.read().strip() + if machine_id and len(machine_id) > 10: + return machine_id[:16].upper() + except: + pass + + # 尝试获取主板信息 + try: + with open('/sys/class/dmi/id/board_serial', 'r') as f: + board_serial = f.read().strip() + if board_serial and board_serial not in ["", "N/A", "To be filled by O.E.M."]: + return hashlib.md5(board_serial.encode()).hexdigest()[:16].upper() + except: + pass + + # 尝试获取产品UUID + try: + with open('/sys/class/dmi/id/product_uuid', 'r') as f: + product_uuid = f.read().strip() + if product_uuid and product_uuid != "": + return hashlib.md5(product_uuid.encode()).hexdigest()[:16].upper() + except: + pass + + # 获取MAC地址和主机名组合 + mac = uuid.getnode() + mac_str = ':'.join(['{:02x}'.format((mac >> elements) & 0xff) for elements in range(0, 2 * 6, 2)][::-1]) + hostname = platform.node() + combined = f"{mac_str}-{hostname}" + return hashlib.md5(combined.encode()).hexdigest()[:16].upper() + + except Exception as e: + print(f"获取Linux机器码错误: {e}") + # 备用方案 + system_info = f"{platform.node()}-{uuid.getnode()}-{platform.machine()}" + return hashlib.md5(system_info.encode()).hexdigest()[:16].upper() + + +def get_mac_machine_code(): + """获取macOS系统的机器码""" + try: + # 尝试获取硬件UUID + try: + result = subprocess.check_output( + ['system_profiler', 'SPHardwareDataType'], + stderr=subprocess.STDOUT, + timeout=10 + ).decode().strip() + + for line in result.split('\n'): + if 'Hardware UUID' in line: + uuid_part = line.split(':')[1].strip() + return hashlib.md5(uuid_part.encode()).hexdigest()[:16].upper() + except: + pass + + # 尝试获取序列号 + try: + serial = subprocess.check_output( + ['system_profiler', 'SPHardwareDataType', '|', 'grep', 'Serial'], + shell=True, + stderr=subprocess.STDOUT, + timeout=10 + ).decode().strip() + + if serial: + serial_number = serial.split(':')[1].strip() + return hashlib.md5(serial_number.encode()).hexdigest()[:16].upper() + except: + pass + + # 备用方案 + mac = uuid.getnode() + hostname = platform.node() + combined = f"{mac}-{hostname}-{platform.machine()}" + return hashlib.md5(combined.encode()).hexdigest()[:16].upper() + + except Exception as e: + print(f"获取macOS机器码错误: {e}") + # 备用方案 + system_info = f"{platform.node()}-{uuid.getnode()}-{platform.machine()}" + return hashlib.md5(system_info.encode()).hexdigest()[:16].upper() + + +def get_machine_code(): + """获取当前系统的机器码""" + try: + system = platform.system() + + if system == "Windows": + return get_windows_machine_code() + elif system == "Linux": + return get_linux_machine_code() + elif system == "Darwin": # macOS + return get_mac_machine_code() + else: + # 未知系统,使用通用方法 + system_info = f"{platform.node()}-{uuid.getnode()}-{platform.processor()}-{platform.machine()}" + return hashlib.md5(system_info.encode()).hexdigest()[:16].upper() + + except Exception as e: + print(f"获取机器码失败: {e}") + # 终极备用方案 + try: + fallback = f"{platform.system()}-{uuid.getnode()}-{platform.node()}" + return hashlib.md5(fallback.encode()).hexdigest()[:16].upper() + except: + import time + ultimate_fallback = f"MACHINE-{int(time.time())}" + return hashlib.md5(ultimate_fallback.encode()).hexdigest()[:16].upper() + + +def format_machine_code(machine_code): + """格式化机器码为更易读的格式""" + if len(machine_code) >= 16: + # 将16位机器码格式化为 XXXX-XXXX-XXXX-XXXX + return f"{machine_code[:4]}-{machine_code[4:8]}-{machine_code[8:12]}-{machine_code[12:16]}" + else: + return machine_code + + +def verify_machine_code(stored_code, current_code): + """验证机器码是否匹配""" + # 移除格式化字符进行比较 + stored_clean = stored_code.replace('-', '').upper() + current_clean = current_code.replace('-', '').upper() + return stored_clean == current_clean + + +if __name__ == "__main__": + print("机器码生成器测试") + print("-" * 30) + + machine_code = get_machine_code() + formatted_code = format_machine_code(machine_code) + + print(f"系统: {platform.system()} {platform.release()}") + print(f"原始机器码: {machine_code}") + print(f"格式化机器码: {formatted_code}") + print(f"机器码长度: {len(machine_code)}") + + # 测试验证功能 + print(f"\n验证测试: {verify_machine_code(formatted_code, machine_code)}") + + input("\n按回车键退出...") \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..af76227 --- /dev/null +++ b/main.py @@ -0,0 +1,1588 @@ +""" +作者:太一 +微信:taiyi1224 +邮箱:shoubo1224@qq.com +""" + +import tkinter as tk +from tkinter import ttk, filedialog, messagebox, scrolledtext +import os +import json +import pyperclip +from database import LicenseDatabase +# from encryptor import EXEEncryptor # 已弃用,使用 encryptor_secure.SecureEXEEncryptor + + + + +def set_dark_theme(root): + style = ttk.Style(root) + # 仅 Windows 支持 'vista',其它系统可改为 'clam' + style.theme_use('clam') + style.configure('.', background='#2e2e2e', foreground='#ffffff', + fieldbackground='#3c3f41', selectbackground='#0078d4', + insertbackground='#ffffff', borderwidth=1, + focuscolor='none') + style.map('.', background=[('active', '#0078d4')]) + style.configure('TButton', padding=6, relief='flat', + background='#0078d4', foreground='#ffffff') + style.map('TButton', background=[('active', '#106ebe')]) + style.configure('TLabel', background='#2e2e2e', foreground='#ffffff') + style.configure('TEntry', fieldbackground='#3c3f41', foreground='#ffffff', + insertbackground='#ffffff', relief='flat', padding=5) + style.configure('Treeview', background='#252526', foreground='#ffffff', + fieldbackground='#252526') + style.configure('Treeview.Heading', background='#3c3f41', foreground='#ffffff') + +class EXEEncryptionTool(tk.Tk): + def __init__(self): + super().__init__() + self.title("EXE文件加密保护系统") + self.geometry("800x600") + self.minsize(800, 600) + # set_dark_theme(self) + + # 数据库配置 + self.db_config = { + 'host': '', + 'database': 'license_system', + 'user': '', + 'password': '' + } + + # 初始化数据库连接 + self.db = None + + # 创建界面 + self.create_widgets() + + # 加载保存的配置 + self.load_config() + + def create_widgets(self): + """创建界面组件""" + # 创建标签页 + tab_control = ttk.Notebook(self) + + # 数据库配置标签页 + self.tab_db_config = ttk.Frame(tab_control) + tab_control.add(self.tab_db_config, text="数据库配置") + + # 卡密生成标签页 + self.tab_key_gen = ttk.Frame(tab_control) + tab_control.add(self.tab_key_gen, text="卡密生成") + + # 卡密管理标签页 + self.tab_key_manage = ttk.Frame(tab_control) + tab_control.add(self.tab_key_manage, text="卡密管理") + + # 软件管理标签页(融合了EXE加密功能) + self.tab_software_manage = ttk.Frame(tab_control) + tab_control.add(self.tab_software_manage, text="软件管理与加密") + + tab_control.pack(expand=1, fill="both") + + # 初始化各个标签页 + self.init_db_config_tab() + self.init_key_gen_tab() + self.init_key_manage_tab() + self.init_software_manage_tab() + + + + # 状态栏 + self.status_var = tk.StringVar(value="就绪") + status_bar = ttk.Label(self, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W) + status_bar.pack(side=tk.BOTTOM, fill=tk.X) + + # def unbind_selected_key(self): + # key_code = self.get_selected_key_code() + # if not key_code: + # return + # if messagebox.askyesno("确认", f"确定解除卡密 {key_code} 与当前机器的绑定?"): + # ok, msg = self.db.unbind_key(key_code) + # messagebox.showinfo("结果", msg) + # self.load_all_keys() + + def unbind_selected_key(self): + """后台解除卡密与当前机器的绑定""" + key_code = self.get_selected_key_code() + if not key_code: + return + if not self.db or not self.db.connection.is_connected(): + messagebox.showerror("错误", "请先连接数据库") + return + if messagebox.askyesno("确认", f"确定解除卡密 {key_code} 与当前机器的绑定?"): + ok, msg = self.db.unbind_key(key_code) + messagebox.showinfo("结果", msg) + self.load_all_keys() # 刷新列表 + def init_db_config_tab(self): + """初始化数据库配置标签页""" + frame = ttk.LabelFrame(self.tab_db_config, text="数据库连接设置") + frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + + # 表单布局 + grid_frame = ttk.Frame(frame) + grid_frame.pack(padx=10, pady=10, fill=tk.X) + + # 主机 + ttk.Label(grid_frame, text="主机:").grid(row=0, column=0, padx=5, pady=5, sticky=tk.W) + self.entry_db_host = ttk.Entry(grid_frame) + self.entry_db_host.grid(row=0, column=1, padx=5, pady=5, sticky=tk.EW) + + # 数据库名 + ttk.Label(grid_frame, text="数据库:").grid(row=1, column=0, padx=5, pady=5, sticky=tk.W) + self.entry_db_name = ttk.Entry(grid_frame) + self.entry_db_name.grid(row=1, column=1, padx=5, pady=5, sticky=tk.EW) + self.entry_db_name.insert(0, "") + + # 用户名 + ttk.Label(grid_frame, text="用户名:").grid(row=2, column=0, padx=5, pady=5, sticky=tk.W) + self.entry_db_user = ttk.Entry(grid_frame) + self.entry_db_user.grid(row=2, column=1, padx=5, pady=5, sticky=tk.EW) + + # 密码 + ttk.Label(grid_frame, text="密码:").grid(row=3, column=0, padx=5, pady=5, sticky=tk.W) + self.entry_db_password = ttk.Entry(grid_frame, show="*") + self.entry_db_password.grid(row=3, column=1, padx=5, pady=5, sticky=tk.EW) + + # 按钮 + button_frame = ttk.Frame(frame) + button_frame.pack(padx=10, pady=10, fill=tk.X) + + self.btn_connect_db = ttk.Button(button_frame, text="连接数据库", command=self.connect_db) + self.btn_connect_db.pack(side=tk.LEFT, padx=5) + + self.btn_create_tables = ttk.Button(button_frame, text="创建数据库表", command=self.create_db_tables) + self.btn_create_tables.pack(side=tk.LEFT, padx=5) + + self.btn_save_db_config = ttk.Button(button_frame, text="保存配置", command=self.save_db_config) + self.btn_save_db_config.pack(side=tk.RIGHT, padx=5) + + # 连接状态 + self.label_db_status = ttk.Label(frame, text="未连接数据库", foreground="red") + self.label_db_status.pack(anchor=tk.W, padx=10, pady=5) + + # 日志区域 + log_frame = ttk.LabelFrame(frame, text="操作日志") + log_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + + self.text_db_log = scrolledtext.ScrolledText(log_frame, height=10) + self.text_db_log.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) + self.text_db_log.config(state=tk.DISABLED) + + # 设置网格权重 + grid_frame.columnconfigure(1, weight=1) + + def init_key_gen_tab(self): + """初始化卡密生成标签页""" + frame = ttk.LabelFrame(self.tab_key_gen, text="卡密生成设置") + frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + + # 配置区域 + config_frame = ttk.Frame(frame) + config_frame.pack(fill=tk.X, padx=10, pady=10) + + # 软件选择 + ttk.Label(config_frame, text="选择软件:").grid(row=0, column=0, padx=5, pady=5, sticky=tk.W) + self.software_combo = ttk.Combobox(config_frame, width=25, state="readonly") + self.software_combo.grid(row=0, column=1, padx=5, pady=5) + self.load_software_for_combo() + + # 有效期 + ttk.Label(config_frame, text="有效期(天):").grid(row=0, column=2, padx=5, pady=5, sticky=tk.W) + self.entry_valid_days = ttk.Entry(config_frame, width=10) + self.entry_valid_days.grid(row=0, column=3, padx=5, pady=5) + self.entry_valid_days.insert(0, "30") + + # 生成数量 + ttk.Label(config_frame, text="生成数量:").grid(row=0, column=4, padx=5, pady=5, sticky=tk.W) + self.entry_key_count = ttk.Entry(config_frame, width=10) + self.entry_key_count.grid(row=0, column=5, padx=5, pady=5) + self.entry_key_count.insert(0, "1") + + # 生成按钮 + self.btn_generate_keys = ttk.Button(config_frame, text="生成卡密", command=self.generate_keys) + self.btn_generate_keys.grid(row=0, column=6, padx=20, pady=5) + + # 卡密列表 + list_frame = ttk.LabelFrame(frame, text="生成的卡密") + list_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + + self.text_keys = scrolledtext.ScrolledText(list_frame) + self.text_keys.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) + + # 操作按钮 + button_frame = ttk.Frame(frame) + button_frame.pack(fill=tk.X, padx=10, pady=10) + + self.btn_copy_keys = ttk.Button(button_frame, text="复制所有卡密", command=self.copy_keys) + self.btn_copy_keys.pack(side=tk.LEFT, padx=5) + + self.btn_export_keys = ttk.Button(button_frame, text="导出卡密到文件", command=self.export_keys) + self.btn_export_keys.pack(side=tk.LEFT, padx=5) + + # 设置网格权重 + config_frame.columnconfigure(5, weight=1) + + + def init_key_manage_tab(self): + """初始化卡密管理标签页""" + frame = ttk.LabelFrame(self.tab_key_manage, text="卡密管理") + frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + + # 搜索区域 + search_frame = ttk.Frame(frame) + search_frame.pack(fill=tk.X, padx=10, pady=5) + + ttk.Label(search_frame, text="搜索卡密:").pack(side=tk.LEFT, padx=5) + self.entry_key_search = ttk.Entry(search_frame) + self.entry_key_search.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5) + self.btn_search_keys = ttk.Button(search_frame, text="搜索", command=self.search_keys) + self.btn_search_keys.pack(side=tk.LEFT, padx=5) + self.btn_refresh_keys = ttk.Button(search_frame, text="刷新", command=self.load_all_keys) + self.btn_refresh_keys.pack(side=tk.LEFT, padx=5) + + # 卡密列表 + columns = ("id", "key_code", "machine_code", "start_time", "end_time", "status", "created_at") + self.tree_keys = ttk.Treeview(frame, columns=columns, show="headings") + + # 设置列标题 + self.tree_keys.heading("id", text="ID") + self.tree_keys.heading("key_code", text="卡密") + self.tree_keys.heading("machine_code", text="机器码") + self.tree_keys.heading("start_time", text="开始时间") + self.tree_keys.heading("end_time", text="结束时间") + self.tree_keys.heading("status", text="状态") + self.tree_keys.heading("created_at", text="创建时间") + + # 设置列宽 + self.tree_keys.column("id", width=50) + self.tree_keys.column("key_code", width=150) + self.tree_keys.column("machine_code", width=120) + self.tree_keys.column("start_time", width=120) + self.tree_keys.column("end_time", width=120) + self.tree_keys.column("status", width=80) + self.tree_keys.column("created_at", width=120) + + # 绑定右键菜单事件 + self.tree_keys.bind("", self.show_key_context_menu) + + # 添加滚动条 + scrollbar = ttk.Scrollbar(frame, orient=tk.VERTICAL, command=self.tree_keys.yview) + self.tree_keys.configure(yscroll=scrollbar.set) + + self.tree_keys.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=10, pady=5) + scrollbar.pack(side=tk.RIGHT, fill=tk.Y, pady=5) + + # 软件筛选 + filter_frame = ttk.Frame(frame) + filter_frame.pack(fill=tk.X, padx=10, pady=5) + + ttk.Label(filter_frame, text="筛选软件:").pack(side=tk.LEFT, padx=5) + self.software_filter_combo = ttk.Combobox(filter_frame, width=30) + self.software_filter_combo.pack(side=tk.LEFT, padx=5) + self.software_filter_combo.bind("<>", self.on_software_filter_change) + + ttk.Button(filter_frame, text="刷新", command=self.refresh_with_filter).pack(side=tk.LEFT, padx=5) + + # 操作按钮 + button_frame = ttk.Frame(frame) + button_frame.pack(fill=tk.X, padx=10, pady=10) + + self.btn_ban_key = ttk.Button(button_frame, text="封禁选中卡密", command=self.ban_selected_key) + self.btn_ban_key.pack(side=tk.LEFT, padx=5) + + self.btn_unban_key = ttk.Button(button_frame, text="解封选中卡密", command=self.unban_selected_key) + self.btn_unban_key.pack(side=tk.LEFT, padx=5) + + self.btn_release_key = ttk.Button(button_frame, text="释放选中卡密", command=self.release_selected_key) + self.btn_release_key.pack(side=tk.LEFT, padx=5) + + self.btn_delete_key = ttk.Button(button_frame, text="删除选中卡密", command=self.delete_selected_key) + self.btn_delete_key.pack(side=tk.LEFT, padx=5) + # ✅ 新增:解绑卡密按钮 + self.btn_unbind = ttk.Button(button_frame, text="解绑卡密", command=self.unbind_selected_key) + self.btn_unbind.pack(side=tk.LEFT, padx=5) + + # 数据库相关方法 + def log(self, text, widget=None): + """添加日志信息""" + if not widget: + widget = self.text_db_log + + widget.config(state=tk.NORMAL) + widget.insert(tk.END, text + "\n") + widget.see(tk.END) + widget.config(state=tk.DISABLED) + self.update_idletasks() + + def connect_db(self): + """连接到数据库""" + self.db_config['host'] = self.entry_db_host.get() + self.db_config['database'] = self.entry_db_name.get() + self.db_config['user'] = self.entry_db_user.get() + self.db_config['password'] = self.entry_db_password.get() + + self.log(f"尝试连接到数据库: {self.db_config['host']}/{self.db_config['database']}") + + self.db = LicenseDatabase( + self.db_config['host'], + self.db_config['database'], + self.db_config['user'], + self.db_config['password'] + ) + + # 检查文件管理器是否正常初始化 + if hasattr(self.db, 'file_manager') and self.db.file_manager: + print("✅ 文件管理器初始化成功") + else: + print("⚠️ 文件管理器初始化失败,将使用传统模式") + + if self.db.connect(): + self.label_db_status.config(text="已连接到数据库", foreground="green") + self.log("数据库连接成功") + messagebox.showinfo("成功", "数据库连接成功") + + # 连接成功后先检查表结构,再加载数据 + try: + # 先加载软件列表,然后再加载卡密列表 + print("开始加载软件列表...") + self.load_software_for_combo() + # 同时刷新软件管理页面 + self.load_software_products() + print("软件列表加载完成") + + print("开始加载卡密列表...") + self.load_all_keys() + print("卡密列表加载完成") + except Exception as e: + self.log(f"加载数据时出错: {e},可能需要先创建或修复数据库表") + print(f"加载数据时出错: {e}") + + else: + self.label_db_status.config(text="数据库连接失败", foreground="red") + self.log("数据库连接失败") + messagebox.showerror("错误", "无法连接到数据库,请检查配置") + + return self.db and self.db.connection and self.db.connection.is_connected() + + def create_db_tables(self): + """创建数据库表""" + if not self.db or not self.db.connection.is_connected(): + messagebox.showerror("错误", "请先连接数据库") + return + + self.log("尝试创建数据库表...") + if self.db.create_tables(): + self.log("数据库表创建成功") + messagebox.showinfo("成功", "数据库表创建成功") + else: + self.log("数据库表创建失败") + messagebox.showerror("错误", "数据库表创建失败") + + def save_db_config(self): + """保存数据库配置""" + self.db_config['host'] = self.entry_db_host.get() + self.db_config['database'] = self.entry_db_name.get() + self.db_config['user'] = self.entry_db_user.get() + self.db_config['password'] = self.entry_db_password.get() + + try: + with open('db_config.json', 'w') as f: + json.dump(self.db_config, f) + + self.log("数据库配置保存成功") + messagebox.showinfo("成功", "数据库配置已保存") + except Exception as e: + self.log(f"保存配置失败: {str(e)}") + messagebox.showerror("错误", f"保存配置失败: {str(e)}") + + def load_config(self): + """加载保存的配置""" + try: + if os.path.exists('db_config.json'): + with open('db_config.json', 'r') as f: + config = json.load(f) + + self.entry_db_host.insert(0, config.get('host', '')) + self.entry_db_name.insert(0, config.get('database', 'license_system')) + self.entry_db_user.insert(0, config.get('user', '')) + self.entry_db_password.insert(0, config.get('password', '')) + except Exception as e: + print(f"加载配置失败: {e}") + + # 卡密生成相关方法 + def generate_keys(self): + """生成卡密""" + if not self.db or not self.db.connection.is_connected(): + messagebox.showerror("错误", "请先连接数据库") + return + + try: + # 获取选中的软件 + selected_software = self.software_combo.get() + if not selected_software: + messagebox.showerror("错误", "请先选择软件") + return + + # 从选中的软件文本中提取软件名称 + software_name = selected_software.split(' v')[0] + + # 获取软件ID + software = self.db.get_software_by_name(software_name) + if not software: + messagebox.showerror("错误", "未找到选中的软件") + return + + software_id = software['id'] + days = int(self.entry_valid_days.get()) + count = int(self.entry_key_count.get()) + + if days <= 0 or count <= 0: + messagebox.showerror("错误", "有效期和数量必须为正数") + return + + self.text_keys.delete(1.0, tk.END) + # self.text_keys.insert(tk.END, f"开始生成 {count} 个有效期为 {days} 天的卡密...\n\n") + + keys = [] + for i in range(count): + key = self.db.generate_key(days, software_id) + if key: + keys.append(key) + self.text_keys.insert(tk.END, key + "\n") + self.text_keys.see(tk.END) + self.update_idletasks() + + # self.text_keys.insert(tk.END, f"\n成功生成 {len(keys)} 个卡密") + messagebox.showinfo("成功", f"成功生成 {len(keys)} 个卡密") + # 刷新卡密列表 + self.load_all_keys() + + except ValueError: + messagebox.showerror("错误", "请输入有效的数字") + except Exception as e: + messagebox.showerror("错误", f"生成卡密失败: {str(e)}") + + def copy_keys(self): + """复制卡密到剪贴板""" + keys = self.text_keys.get(1.0, tk.END).strip() + if keys: + pyperclip.copy(keys) + messagebox.showinfo("成功", "卡密已复制到剪贴板") + else: + messagebox.showinfo("提示", "没有可复制的卡密") + + def export_keys(self): + """导出卡密到文件""" + keys = self.text_keys.get(1.0, tk.END).strip() + if not keys: + messagebox.showinfo("提示", "没有可导出的卡密") + return + + file_path = filedialog.asksaveasfilename( + defaultextension=".txt", + filetypes=[("文本文件", "*.txt"), ("所有文件", "*.*")] + ) + + if file_path: + try: + with open(file_path, 'w') as f: + f.write(keys) + messagebox.showinfo("成功", f"卡密已导出到 {file_path}") + except Exception as e: + messagebox.showerror("错误", f"导出失败: {str(e)}") + + + # 卡密管理相关方法 + def refresh_with_filter(self): + """根据当前筛选条件刷新卡密列表""" + selected_software = self.software_filter_combo.get() + if not selected_software: + self.load_all_keys() + return + + # 获取软件ID + software = self.db.get_software_by_name(selected_software) + if software: + self.load_all_keys(software['id']) + else: + self.load_all_keys() + + def on_software_filter_change(self, event): + """软件筛选变化事件""" + selected_software = self.software_filter_combo.get() + if not selected_software: + self.load_all_keys() + return + + # 获取软件ID + software = self.db.get_software_by_name(selected_software) + if software: + self.load_all_keys(software['id']) + else: + self.load_all_keys() + + def load_all_keys(self, software_id=None): + """加载所有卡密,可按软件筛选""" + # 检查数据库连接 + if not self.db or not self.db.connection.is_connected(): + print("警告: 数据库未连接,无法加载卡密列表") + return + + # 检查UI组件是否已初始化 + if not hasattr(self, 'tree_keys'): + print("警告: 卡密列表组件尚未初始化") + return + + try: + # 清空现有列表 + for item in self.tree_keys.get_children(): + self.tree_keys.delete(item) + + # 获取卡密列表 + keys = self.db.get_all_keys(software_id) + + if not keys: + print("提示: 当前没有卡密记录") + return + + # 遍历并显示卡密 + for key in keys: + try: + # 格式化日期时间 + start_time = key['start_time'].strftime("%Y-%m-%d") if key['start_time'] else "" + end_time = key['end_time'].strftime("%Y-%m-%d") if key['end_time'] else "" + created_at = key['created_at'].strftime("%Y-%m-%d") if key['created_at'] else "" + + self.tree_keys.insert("", tk.END, values=( + key['id'], + key['key_code'], + key['machine_code'] or "", + start_time, + end_time, + key['status'], + created_at + )) + + # 根据状态设置行颜色 + item = self.tree_keys.get_children()[-1] + if key['status'] == 'active': + self.tree_keys.item(item, tags=('active',)) + elif key['status'] == 'expired': + self.tree_keys.item(item, tags=('expired',)) + elif key['status'] == 'banned': + self.tree_keys.item(item, tags=('banned',)) + except Exception as key_error: + print(f"处理卡密记录时出错: {key_error}, 卡密ID: {key.get('id', 'unknown')}") + continue + + # 设置标签样式 + self.tree_keys.tag_configure('active', foreground='green') + self.tree_keys.tag_configure('expired', foreground='gray') + self.tree_keys.tag_configure('banned', foreground='red') + + print(f"成功加载 {len(keys)} 个卡密记录") + + except Exception as e: + print(f"加载卡密列表失败,错误详情: {type(e).__name__}: {e}") + import traceback + traceback.print_exc() + + def search_keys(self): + """搜索卡密""" + if not self.db or not self.db.connection.is_connected(): + return + + search_text = self.entry_key_search.get().strip().lower() + if not search_text: + self.load_all_keys() + return + + # 清空现有列表 + for item in self.tree_keys.get_children(): + self.tree_keys.delete(item) + + try: + keys = self.db.get_all_keys() + for key in keys: + # 检查是否匹配搜索文本 + if (search_text in key['key_code'].lower() or + search_text in key['status'].lower() or + (key['machine_code'] and search_text in key['machine_code'].lower())): + # 格式化日期时间 + start_time = key['start_time'].strftime("%Y-%m-%d") if key['start_time'] else "" + end_time = key['end_time'].strftime("%Y-%m-%d") if key['end_time'] else "" + created_at = key['created_at'].strftime("%Y-%m-%d") if key['created_at'] else "" + + self.tree_keys.insert("", tk.END, values=( + key['id'], + key['key_code'], + key['machine_code'] or "", + start_time, + end_time, + key['status'], + created_at + )) + + except Exception as e: + print(f"搜索卡密失败: {e}") + + def get_selected_key_code(self): + """获取选中的卡密""" + selected_items = self.tree_keys.selection() + if not selected_items: + messagebox.showinfo("提示", "请先选择一个卡密") + return None + + item = selected_items[0] + key_code = self.tree_keys.item(item, "values")[1] + return key_code + + def show_key_context_menu(self, event): + """显示右键菜单""" + # 选择鼠标点击的行 + item = self.tree_keys.identify_row(event.y) + if item: + self.tree_keys.selection_set(item) + + # 创建右键菜单 + menu = tk.Menu(self, tearoff=0) + menu.add_command(label="复制卡密", command=self.copy_selected_key) + menu.add_command(label="复制机器码", command=self.copy_selected_machine_code) + menu.add_separator() + menu.add_command(label="复制整行信息", command=self.copy_selected_row_info) + + # 显示菜单 + menu.post(event.x_root, event.y_root) + + def copy_selected_key(self): + """复制选中的卡密""" + key_code = self.get_selected_key_code() + if key_code: + pyperclip.copy(key_code) + messagebox.showinfo("成功", f"卡密已复制到剪贴板: {key_code}") + + def copy_selected_machine_code(self): + """复制选中的机器码""" + selected_items = self.tree_keys.selection() + if not selected_items: + messagebox.showinfo("提示", "请先选择一个卡密") + return + + item = selected_items[0] + machine_code = self.tree_keys.item(item, "values")[2] + if machine_code: + pyperclip.copy(machine_code) + messagebox.showinfo("成功", f"机器码已复制到剪贴板: {machine_code}") + else: + messagebox.showinfo("提示", "该卡密没有绑定机器码") + + def copy_selected_row_info(self): + """复制整行信息""" + selected_items = self.tree_keys.selection() + if not selected_items: + messagebox.showinfo("提示", "请先选择一个卡密") + return + + item = selected_items[0] + values = self.tree_keys.item(item, "values") + info = f"ID: {values[0]}\n卡密: {values[1]}\n机器码: {values[2]}\n开始时间: {values[3]}\n结束时间: {values[4]}\n状态: {values[5]}\n创建时间: {values[6]}" + + pyperclip.copy(info) + messagebox.showinfo("成功", "整行信息已复制到剪贴板") + + def ban_selected_key(self): + """封禁选中的卡密""" + key_code = self.get_selected_key_code() + if not key_code: + return + + if messagebox.askyesno("确认", f"确定要封禁卡密 {key_code} 吗?"): + if self.db.update_key_status(key_code, 'banned'): + messagebox.showinfo("成功", "卡密已封禁") + self.load_all_keys() + else: + messagebox.showerror("错误", "封禁卡密失败") + + def unban_selected_key(self): + """解封选中的卡密""" + key_code = self.get_selected_key_code() + if not key_code: + return + + if messagebox.askyesno("确认", f"确定要解封卡密 {key_code} 吗?"): + # 检查原状态是未使用还是已激活 + try: + selected_items = self.tree_keys.selection() + item = selected_items[0] + original_status = self.tree_keys.item(item, "values")[5] + + new_status = 'unused' if original_status == 'banned' and not self.tree_keys.item(item, "values")[ + 2] else 'active' + + if self.db.update_key_status(key_code, new_status): + messagebox.showinfo("成功", "卡密已解封") + self.load_all_keys() + else: + messagebox.showerror("错误", "解封卡密失败") + except Exception as e: + messagebox.showerror("错误", f"操作失败: {str(e)}") + + def release_selected_key(self): + """释放选中的已使用激活码""" + key_code = self.get_selected_key_code() + if not key_code: + return + + # 获取选择项的状态信息 + selected_items = self.tree_keys.selection() + if selected_items: + item = selected_items[0] + status = self.tree_keys.item(item, "values")[5] + + if status != 'active': + messagebox.showwarning("警告", "只能释放处于'激活'状态的激活码") + return + + if messagebox.askyesno("确认释放", + f"确定要释放激活码 '{key_code}' 吗?\n\n释放后:\n1. 该激活码将变为未使用状态\n2. 机器码将被清空\n3. 可以重新在任何机器上使用\n\n此操作不可撤销!"): + if not self.db or not self.db.connection.is_connected(): + if not self.connect_db(): + return + + success, msg = self.db.release_key(key_code) + if success: + messagebox.showinfo("成功", f"激活码 '{key_code}' 已释放成功\n{msg}") + self.load_all_keys() # 刷新列表 + else: + messagebox.showerror("失败", msg) + + def delete_selected_key(self): + """删除选中的卡密""" + key_code = self.get_selected_key_code() + if not key_code: + return + + if messagebox.askyesno("确认", f"确定要删除卡密 {key_code} 吗?\n此操作不可恢复!"): + # 实际项目中应该实现delete_key方法 + messagebox.showinfo("提示", "为安全起见,当前版本不允许删除激活码,建议使用封禁或释放功能") + + def init_software_manage_tab(self): + """初始化软件管理标签页""" + # 创建框架 + frame_top = ttk.Frame(self.tab_software_manage) + frame_top.pack(fill="x", padx=10, pady=5) + + frame_middle = ttk.Frame(self.tab_software_manage) + frame_middle.pack(fill="both", expand=True, padx=10, pady=5) + + frame_bottom = ttk.Frame(self.tab_software_manage) + frame_bottom.pack(fill="x", padx=10, pady=5) + + # 顶部:添加软件表单 + ttk.Label(frame_top, text="软件名称:").grid(row=0, column=0, padx=5, pady=5, sticky="e") + self.software_name_var = tk.StringVar() + ttk.Entry(frame_top, textvariable=self.software_name_var, width=20).grid(row=0, column=1, padx=5, pady=5) + + ttk.Label(frame_top, text="版本:").grid(row=0, column=2, padx=5, pady=5, sticky="e") + self.software_version_var = tk.StringVar() + ttk.Entry(frame_top, textvariable=self.software_version_var, width=15).grid(row=0, column=3, padx=5, pady=5) + + ttk.Label(frame_top, text="描述:").grid(row=1, column=0, padx=5, pady=5, sticky="e") + self.software_desc_var = tk.StringVar() + ttk.Entry(frame_top, textvariable=self.software_desc_var, width=40).grid(row=1, column=1, columnspan=2, padx=5, pady=5, sticky="w") + + # EXE文件选择 + ttk.Label(frame_top, text="EXE文件:").grid(row=2, column=0, padx=5, pady=5, sticky="e") + self.software_exe_var = tk.StringVar() + exe_entry = ttk.Entry(frame_top, textvariable=self.software_exe_var, width=45) + exe_entry.grid(row=2, column=1, columnspan=2, padx=5, pady=5, sticky="ew") + ttk.Button(frame_top, text="浏览...", command=self.browse_exe_file).grid(row=2, column=3, padx=5, pady=5) + + # 提示信息 + ttk.Label(frame_top, text="提示:添加软件时将自动加密", foreground="blue").grid(row=3, column=1, columnspan=2, padx=5, pady=5, sticky="w") + + ttk.Button(frame_top, text="添加软件", command=self.add_software_product).grid(row=0, column=4, rowspan=4, padx=10, pady=5, sticky="ns") + + # 中部:软件列表 + columns = ("ID", "软件名称", "版本", "描述", "EXE路径", "创建时间") + self.tree_software = ttk.Treeview(frame_middle, columns=columns, show="headings", height=12) + + # 设置列宽和对齐方式 + self.tree_software.heading("ID", text="ID") + self.tree_software.column("ID", width=50, anchor="center") + self.tree_software.heading("软件名称", text="软件名称") + self.tree_software.column("软件名称", width=150, anchor="w") + self.tree_software.heading("版本", text="版本") + self.tree_software.column("版本", width=80, anchor="center") + self.tree_software.heading("描述", text="描述") + self.tree_software.column("描述", width=200, anchor="w") + self.tree_software.heading("EXE路径", text="EXE路径") + self.tree_software.column("EXE路径", width=250, anchor="w") + self.tree_software.heading("创建时间", text="创建时间") + self.tree_software.column("创建时间", width=120, anchor="center") + + # 添加滚动条 + scrollbar = ttk.Scrollbar(frame_middle, orient="vertical", command=self.tree_software.yview) + self.tree_software.configure(yscrollcommand=scrollbar.set) + + self.tree_software.pack(side="left", fill="both", expand=True) + scrollbar.pack(side="right", fill="y") + + # 绑定右键菜单 + self.tree_software.bind("", self.show_software_context_menu) + + # 底部:操作按钮 + ttk.Button(frame_bottom, text="刷新列表", command=self.load_software_products).pack(side="left", padx=5) + ttk.Button(frame_bottom, text="编辑软件", command=self.edit_software_product).pack(side="left", padx=5) + ttk.Button(frame_bottom, text="删除软件", command=self.delete_software_product).pack(side="left", padx=5) + ttk.Button(frame_bottom, text="🔐 加密软件", command=self.encrypt_selected_software).pack(side="left", padx=5) + ttk.Button(frame_bottom, text="打开EXE目录", command=self.open_exe_directory).pack(side="left", padx=5) + ttk.Button(frame_bottom, text="文件管理", command=self.show_file_manager).pack(side="left", padx=5) + ttk.Button(frame_bottom, text="验证存储", command=self.verify_storage).pack(side="left", padx=5) + + # 加载软件列表 + self.load_software_products() + self.load_software_for_combo() + + def add_software_product(self): + """添加软件产品(自动加密)""" + name = self.software_name_var.get().strip() + version = self.software_version_var.get().strip() + description = self.software_desc_var.get().strip() + source_exe_path = self.software_exe_var.get().strip() + + if not name: + messagebox.showwarning("警告", "软件名称不能为空") + return + + if not source_exe_path: + messagebox.showwarning("警告", "请选择EXE文件") + return + + if not os.path.exists(source_exe_path): + messagebox.showerror("错误", f"EXE文件不存在:{source_exe_path}") + return + + if not self.db or not self.db.connection.is_connected(): + if not self.connect_db(): + return + + try: + # 1. 先加密EXE文件 + # 加载API配置 + api_config = self._load_api_config() + if not api_config: + return + + # 生成加密后的文件名 + encrypted_filename = f"{name}_encrypted.exe" + + # 如果有文件管理器,使用文件管理器的路径 + if hasattr(self.db, 'file_manager') and self.db.file_manager: + encrypted_path = os.path.join(self.db.file_manager.executables_dir, encrypted_filename) + else: + # 否则使用当前目录 + encrypted_path = os.path.join(os.getcwd(), "files", "executables", encrypted_filename) + os.makedirs(os.path.dirname(encrypted_path), exist_ok=True) + + # 创建进度窗口 + progress_window = tk.Toplevel(self) + progress_window.title("加密并添加软件") + progress_window.geometry("400x150") + progress_window.resizable(False, False) + + # 居中窗口 + progress_window.update_idletasks() + x = (progress_window.winfo_screenwidth() - progress_window.winfo_width()) // 2 + y = (progress_window.winfo_screenheight() - progress_window.winfo_height()) // 2 + progress_window.geometry(f"400x150+{x}+{y}") + + ttk.Label(progress_window, text=f"正在加密: {name}", + font=("Microsoft YaHei", 10)).pack(pady=15) + + progress_label = ttk.Label(progress_window, text="正在加密EXE文件...", + foreground="blue") + progress_label.pack(pady=5) + + progress_bar = ttk.Progressbar(progress_window, mode='indeterminate', length=350) + progress_bar.pack(pady=10) + progress_bar.start(10) + + progress_window.update() + + # 执行加密 + from encryptor_secure import SecureEXEEncryptor + + encryptor = SecureEXEEncryptor() + success, msg = encryptor.encrypt_exe( + source_path=source_exe_path, + output_path=encrypted_path, + api_config=api_config, + software_name=name + ) + + progress_bar.stop() + progress_window.destroy() + + if not success: + messagebox.showerror("错误", f"加密失败: {msg}") + return + + # 2. 加密成功后,将软件添加到数据库 + # 使用相对路径(相对于executables目录) + relative_path = encrypted_filename + + if hasattr(self.db, 'add_software_product_with_file'): + success, msg = self.db.add_software_product_with_file(name, description, version, encrypted_path) + else: + success, msg = self.db.add_software_product(name, description, version, relative_path) + + if success: + messagebox.showinfo("成功", f"软件已加密并添加成功!\n\n加密文件: {encrypted_filename}") + + # 清空表单 + self.software_name_var.set("") + self.software_version_var.set("") + self.software_desc_var.set("") + self.software_exe_var.set("") + + # 刷新列表 + self.load_software_products() + self.load_software_for_combo() + else: + messagebox.showerror("错误", f"添加软件失败: {msg}") + # 如果添加失败,删除加密文件 + if os.path.exists(encrypted_path): + try: + os.remove(encrypted_path) + except: + pass + + except Exception as e: + messagebox.showerror("错误", f"添加软件时出错: {str(e)}") + + def load_software_products(self): + """加载软件产品列表""" + if not self.db or not self.db.connection.is_connected(): + messagebox.showwarning("提示", "请先连接数据库") + return + + # 清空现有数据 + for item in self.tree_software.get_children(): + self.tree_software.delete(item) + + products = self.db.get_software_products() + for product in products: + # 检查EXE文件是否存在 + exe_path = product.get('exe_path', '') or '' + exe_status = '' + + if exe_path: + # 获取完整路径 + if hasattr(self.db, 'get_exe_full_path'): + full_path = self.db.get_exe_full_path(exe_path) + else: + full_path = exe_path + + if os.path.exists(full_path): + # 判断是相对路径还是绝对路径 + if os.path.isabs(exe_path): + exe_status = '✓ [绝对] ' + exe_path + else: + exe_status = '✓ [本地] ' + exe_path + else: + if os.path.isabs(exe_path): + exe_status = '✗ [绝对] ' + exe_path + else: + exe_status = '✗ [本地] ' + exe_path + else: + exe_status = '未设置' + + self.tree_software.insert("", "end", values=( + product['id'], + product['name'], + product['version'] or "", + product['description'] or "", + exe_status, + product['created_at'].strftime('%Y-%m-%d %H:%M:%S') if product['created_at'] else "" + )) + + def load_software_for_combo(self): + """加载软件列表到下拉框""" + if not self.db or not self.db.connection.is_connected(): + return + + products = self.db.get_software_products() + software_list = [f"{p['name']} v{p['version']}" if p['version'] else p['name'] for p in products] + + # 更新卡密生成页面的下拉框 + if hasattr(self, 'software_combo'): + self.software_combo['values'] = software_list + if software_list: + self.software_combo.current(0) + + # 更新卡密管理页面的筛选下拉框 + if hasattr(self, 'software_filter_combo'): + filter_list = [''] + software_list # 添加空选项用于显示全部 + self.software_filter_combo['values'] = filter_list + self.software_filter_combo.set('') # 默认显示全部 + + def show_software_context_menu(self, event): + """显示软件右键菜单""" + item = self.tree_software.identify_row(event.y) + if item: + self.tree_software.selection_set(item) + menu = tk.Menu(self, tearoff=0) + menu.add_command(label="🔐 加密软件", command=self.encrypt_selected_software) + menu.add_separator() + menu.add_command(label="编辑软件", command=self.edit_software_product) + menu.add_command(label="删除软件", command=self.delete_software_product) + menu.add_separator() + menu.add_command(label="打开EXE目录", command=self.open_exe_directory) + menu.post(event.x_root, event.y_root) + + def browse_exe_file(self): + """浏览选择EXE文件""" + file_path = filedialog.askopenfilename( + title="选择EXE文件", + filetypes=[("EXE文件", "*.exe"), ("所有文件", "*.*")] + ) + if file_path: + self.software_exe_var.set(file_path) + + def edit_software_product(self): + """编辑软件产品""" + selected_item = self.tree_software.selection() + if not selected_item: + messagebox.showwarning("警告", "请先选择要编辑的软件") + return + + item_values = self.tree_software.item(selected_item[0], 'values') + product_id = int(item_values[0]) + + # 创建编辑窗口 + edit_window = tk.Toplevel(self) + edit_window.title("编辑软件") + edit_window.geometry("500x300") + edit_window.resizable(False, False) + + # 获取原数据 + original_name = item_values[1] + original_version = item_values[2] + original_desc = item_values[3] + # 解析exe路径(移除状态符号) + original_exe = item_values[4] + if original_exe.startswith('✓ ') or original_exe.startswith('✗ '): + original_exe = original_exe[2:] + + # 软件名称 + ttk.Label(edit_window, text="软件名称:").grid(row=0, column=0, padx=10, pady=10, sticky="e") + name_var = tk.StringVar(value=original_name) + ttk.Entry(edit_window, textvariable=name_var, width=30).grid(row=0, column=1, padx=10, pady=10) + + # 版本 + ttk.Label(edit_window, text="版本:").grid(row=1, column=0, padx=10, pady=10, sticky="e") + version_var = tk.StringVar(value=original_version) + ttk.Entry(edit_window, textvariable=version_var, width=30).grid(row=1, column=1, padx=10, pady=10) + + # 描述 + ttk.Label(edit_window, text="描述:").grid(row=2, column=0, padx=10, pady=10, sticky="e") + desc_var = tk.StringVar(value=original_desc) + ttk.Entry(edit_window, textvariable=desc_var, width=30).grid(row=2, column=1, padx=10, pady=10) + + # EXE文件 + ttk.Label(edit_window, text="EXE文件:").grid(row=3, column=0, padx=10, pady=10, sticky="e") + exe_var = tk.StringVar(value=original_exe) + exe_frame = ttk.Frame(edit_window) + exe_frame.grid(row=3, column=1, padx=10, pady=10, sticky="ew") + ttk.Entry(exe_frame, textvariable=exe_var, width=25).pack(side="left", padx=(0, 5)) + ttk.Button(exe_frame, text="浏览...", + command=lambda: self._browse_exe_for_edit(exe_var)).pack(side="left") + + # 按钮 + button_frame = ttk.Frame(edit_window) + button_frame.grid(row=4, column=0, columnspan=2, pady=20) + + def save_changes(): + new_name = name_var.get().strip() + new_version = version_var.get().strip() + new_desc = desc_var.get().strip() + new_exe = exe_var.get().strip() + + if not new_name: + messagebox.showwarning("警告", "软件名称不能为空") + return + + # 验证EXE文件 + if new_exe and not os.path.exists(new_exe): + if not messagebox.askyesno("警告", f"EXE文件不存在:{new_exe}\n是否继续保存?"): + return + + success, msg = self.db.update_software_product( + product_id, new_name, new_desc, new_version, new_exe + ) + + if success: + messagebox.showinfo("成功", msg) + edit_window.destroy() + self.load_software_products() + self.load_software_for_combo() + else: + messagebox.showerror("错误", msg) + + ttk.Button(button_frame, text="保存", command=save_changes).pack(side="left", padx=5) + ttk.Button(button_frame, text="取消", command=edit_window.destroy).pack(side="left", padx=5) + + def _browse_exe_for_edit(self, exe_var): + """为编辑窗口浏览EXE文件""" + file_path = filedialog.askopenfilename( + title="选择EXE文件", + filetypes=[("EXE文件", "*.exe"), ("所有文件", "*.*")] + ) + if file_path: + exe_var.set(file_path) + + def open_exe_directory(self): + """打开选中软件的EXE目录""" + selected_item = self.tree_software.selection() + if not selected_item: + messagebox.showwarning("警告", "请先选择软件") + return + + item_values = self.tree_software.item(selected_item[0], 'values') + exe_path = item_values[4] + + # 解析exe路径(移除状态符号) + if exe_path.startswith('✓ ') or exe_path.startswith('✗ '): + exe_path = exe_path[2:] + + # 去除类型标识 + if exe_path.startswith('[绝对] '): + exe_path = exe_path[5:] + elif exe_path.startswith('[本地] '): + exe_path = exe_path[5:] + + if not exe_path: + messagebox.showwarning("警告", "该软件没有设置EXE文件路径") + return + + # 使用数据库的路径解析功能获取完整路径 + if self.db and hasattr(self.db, 'get_exe_full_path'): + full_path = self.db.get_exe_full_path(exe_path) + else: + full_path = exe_path + + if not os.path.exists(full_path): + messagebox.showerror("错误", f"文件不存在:{full_path}") + return + + exe_dir = os.path.dirname(full_path) + if os.path.exists(exe_dir): + os.startfile(exe_dir) + else: + messagebox.showerror("错误", f"目录不存在:{exe_dir}") + + + def delete_software_product(self): + """删除软件产品""" + selection = self.tree_software.selection() + if not selection: + messagebox.showwarning("警告", "请先选择要删除的软件") + return + + item = selection[0] + software_id = self.tree_software.item(item, "values")[0] + software_name = self.tree_software.item(item, "values")[1] + + if messagebox.askyesno("确认", f"确定要删除软件 '{software_name}' 吗?\n注意:删除软件将同时删除其所有关联的卡密!"): + try: + cursor = self.db.connection.cursor() + # 先删除关联的卡密 + cursor.execute("DELETE FROM license_keys WHERE software_id = %s", (software_id,)) + # 再删除软件 + cursor.execute("DELETE FROM software_products WHERE id = %s", (software_id,)) + self.db.connection.commit() + cursor.close() + messagebox.showinfo("成功", f"软件 '{software_name}' 及其关联卡密已删除") + self.load_software_products() + except Exception as e: + messagebox.showerror("错误", f"删除软件失败: {str(e)}") + + def show_file_manager(self): + """显示文件管理窗口""" + if not self.db or not hasattr(self.db, 'file_manager') or not self.db.file_manager: + messagebox.showwarning("警告", "文件管理器未初始化") + return + + # 创建文件管理窗口 + file_window = tk.Toplevel(self) + file_window.title("文件管理") + file_window.geometry("800x600") + file_window.resizable(True, True) + + # 创建主框架 + main_frame = ttk.Frame(file_window) + main_frame.pack(fill="both", expand=True, padx=10, pady=10) + + # 存储信息框架 + info_frame = ttk.LabelFrame(main_frame, text="存储信息") + info_frame.pack(fill="x", pady=(0, 10)) + + # 获取存储信息 + try: + storage_info = self.db.get_file_storage_info() + except: + storage_info = {} + + info_text = f""" +存储目录: {storage_info.get('base_dir', '')} +EXE目录: {storage_info.get('exe_dir', '')} +备份目录: {storage_info.get('backup_dir', '')} +总文件数: {storage_info.get('total_files', 0)} +存在文件: {storage_info.get('existing_files', 0)} +丢失文件: {storage_info.get('missing_files', 0)} +总大小: {storage_info.get('total_size', 0) / 1024 / 1024:.2f} MB + """.strip() + + info_label = ttk.Label(info_frame, text=info_text, justify="left") + info_label.pack(padx=10, pady=10, anchor="w") + + # 文件列表框架 + list_frame = ttk.LabelFrame(main_frame, text="文件列表") + list_frame.pack(fill="both", expand=True, pady=(0, 10)) + + # 文件列表 + columns = ("文件ID", "软件名称", "原文件名", "本地文件名", "大小", "上传时间", "状态") + file_tree = ttk.Treeview(list_frame, columns=columns, show="headings", height=15) + + # 设置列标题和宽度 + file_tree.heading("文件ID", text="文件ID") + file_tree.column("文件ID", width=80, anchor="center") + file_tree.heading("软件名称", text="软件名称") + file_tree.column("软件名称", width=120, anchor="w") + file_tree.heading("原文件名", text="原文件名") + file_tree.column("原文件名", width=150, anchor="w") + file_tree.heading("本地文件名", text="本地文件名") + file_tree.column("本地文件名", width=180, anchor="w") + file_tree.heading("大小", text="大小") + file_tree.column("大小", width=80, anchor="center") + file_tree.heading("上传时间", text="上传时间") + file_tree.column("上传时间", width=140, anchor="center") + file_tree.heading("状态", text="状态") + file_tree.column("状态", width=80, anchor="center") + + # 添加滚动条 + file_scrollbar = ttk.Scrollbar(list_frame, orient="vertical", command=file_tree.yview) + file_tree.configure(yscrollcommand=file_scrollbar.set) + + file_tree.pack(side="left", fill="both", expand=True) + file_scrollbar.pack(side="right", fill="y") + + # 加载文件列表 + def load_file_list(): + # 清空现有数据 + for item in file_tree.get_children(): + file_tree.delete(item) + + try: + files = self.db.file_manager.list_exe_files() + for file_info in files: + file_id = file_info.get('file_id', '')[:8] + '...' if len(file_info.get('file_id', '')) > 8 else file_info.get('file_id', '') + software_name = file_info.get('software_name', '') + original_name = file_info.get('original_name', '') + local_path = file_info.get('local_path', '') + size = f"{file_info.get('size', 0) / 1024 / 1024:.2f} MB" + upload_time = file_info.get('upload_time', '').split('T')[0] if file_info.get('upload_time') else '' + status = "✓ 存在" if file_info.get('exists', False) else "✗ 丢失" + + file_tree.insert("", "end", values=( + file_id, software_name, original_name, local_path, + size, upload_time, status + )) + except Exception as e: + messagebox.showerror("错误", f"加载文件列表失败: {str(e)}") + + load_file_list() + + # 操作按钮框架 + button_frame = ttk.Frame(main_frame) + button_frame.pack(fill="x") + + ttk.Button(button_frame, text="刷新列表", command=load_file_list).pack(side="left", padx=5) + ttk.Button(button_frame, text="清理孤立文件", + command=lambda: self._cleanup_orphaned_files(load_file_list)).pack(side="left", padx=5) + ttk.Button(button_frame, text="打开存储目录", + command=lambda: os.startfile(storage_info.get('base_dir', '')) if storage_info.get('base_dir') and os.path.exists(storage_info.get('base_dir', '')) else messagebox.showerror("错误", "目录不存在")).pack(side="left", padx=5) + def verify_storage(self): + """验证文件存储状态""" + if not self.db or not hasattr(self.db, 'file_manager') or not self.db.file_manager: + messagebox.showwarning("警告", "文件管理器未初始化") + return + + try: + storage_info = self.db.get_file_storage_info() + products = self.db.get_software_products() + + # 统计信息 + total_products = len(products) + local_stored = 0 + absolute_path = 0 + missing_files = 0 + + details = [] + + for p in products: + exe_path = p.get('exe_path', '') + if not exe_path: + details.append(f"\u26a0\ufe0f {p['name']}: 未设置EXE路径") + continue + + if os.path.isabs(exe_path): + absolute_path += 1 + if os.path.exists(exe_path): + details.append(f"\u2705 {p['name']}: [绝对路径] {exe_path}") + else: + missing_files += 1 + details.append(f"\u274c {p['name']}: [绝对路径-丢失] {exe_path}") + else: + local_stored += 1 + full_path = self.db.get_exe_full_path(exe_path) + if os.path.exists(full_path): + details.append(f"\u2705 {p['name']}: [本地存储] {exe_path}") + else: + missing_files += 1 + details.append(f"\u274c {p['name']}: [本地存储-丢失] {exe_path}") + + # 生成报告 + report = f"""【EXE文件存储验证报告】 + +📈 统计信息: +• 总软件数量: {total_products} +• 本地存储: {local_stored} 个 +• 绝对路径: {absolute_path} 个 +• 丢失文件: {missing_files} 个 + +📁 存储目录: +{storage_info.get('base_dir', '')} + +📝 详细信息: +{chr(10).join(details) if details else '无软件记录'} + +💡 建议: +""" + + if missing_files > 0: + report += f"• 有 {missing_files} 个文件丢失,请检查文件状态\n" + if absolute_path > 0: + report += f"• 有 {absolute_path} 个软件使用绝对路径,建议重新添加以使用本地存储\n" + if local_stored > 0: + report += f"• 有 {local_stored} 个文件已正确保存到本地目录✅\n" + if missing_files == 0 and local_stored > 0: + report += "• 所有本地文件都存在,存储状态良好!\ud83c\udf89" + + # 显示报告 + report_window = tk.Toplevel(self) + report_window.title("存储验证报告") + report_window.geometry("600x500") + report_window.resizable(True, True) + + text_widget = scrolledtext.ScrolledText(report_window, wrap=tk.WORD) + text_widget.pack(fill="both", expand=True, padx=10, pady=10) + text_widget.insert("1.0", report) + text_widget.config(state="disabled") + + # 关闭按钮 + ttk.Button(report_window, text="关闭", command=report_window.destroy).pack(pady=10) + + except Exception as e: + messagebox.showerror("错误", f"验证存储状态时出错: {str(e)}") + + def _cleanup_orphaned_files(self, refresh_callback): + """清理孤立文件""" + if not self.db or not hasattr(self.db, 'file_manager') or not self.db.file_manager: + return + + try: + count, files = self.db.file_manager.cleanup_orphaned_files() + if count > 0: + messagebox.showinfo("成功", f"清理了 {count} 个孤立文件:\n" + "\n".join(files)) + else: + messagebox.showinfo("信息", "没有发现孤立文件") + refresh_callback() + except Exception as e: + messagebox.showerror("错误", f"清理孤立文件时出错: {str(e)}") + + def _load_api_config(self): + """加载API配置""" + config_file = 'api_config.json' + + # 检查配置文件是否存在 + if not os.path.exists(config_file): + if messagebox.askyesno("配置缺失", + f"未找到API配置文件 '{config_file}'\n\n" + "是否创建默认配置文件?"): + try: + default_config = { + "api_url": "https://your-domain.com/api", + "api_key": "your-api-key-here", + "comment": "请修改上面的配置为你的实际服务器地址和API密钥" + } + with open(config_file, 'w', encoding='utf-8') as f: + json.dump(default_config, f, indent=4, ensure_ascii=False) + messagebox.showinfo("成功", + f"已创建默认配置文件:{config_file}\n\n" + "请编辑此文件,填写你的API地址和密钥") + except Exception as e: + messagebox.showerror("错误", f"创建配置文件失败: {str(e)}") + return None + + # 加载配置 + try: + with open(config_file, 'r', encoding='utf-8') as f: + config = json.load(f) + + api_url = config.get('api_url', '') + api_key = config.get('api_key', '') + + # 验证配置 + if not api_url or api_url == 'https://your-domain.com/api': + messagebox.showerror("配置错误", + f"请先编辑 '{config_file}' 文件\n\n" + "将 api_url 修改为你的实际服务器地址") + return None + + if not api_key or api_key == 'your-api-key-here': + messagebox.showerror("配置错误", + f"请先编辑 '{config_file}' 文件\n\n" + "将 api_key 修改为你的实际API密钥") + return None + + return { + 'api_url': api_url, + 'api_key': api_key + } + + except json.JSONDecodeError as e: + messagebox.showerror("配置错误", + f"配置文件格式错误: {str(e)}\n\n" + f"请检查 '{config_file}' 文件格式") + return None + except Exception as e: + messagebox.showerror("错误", + f"读取配置文件失败: {str(e)}") + return None + + def encrypt_selected_software(self): + """加密选中的软件""" + selected_item = self.tree_software.selection() + if not selected_item: + messagebox.showwarning("警告", "请先选择要加密的软件") + return + + item_values = self.tree_software.item(selected_item[0], 'values') + software_name = item_values[1] + + self.encrypt_software_by_name(software_name) + + def encrypt_software_by_name(self, software_name): + """根据软件名称加密软件""" + if not self.db or not self.db.connection.is_connected(): + if not self.connect_db(): + return + + # 获取软件信息 + software = self.db.get_software_by_name(software_name) + if not software: + messagebox.showerror("错误", f"未找到软件: {software_name}") + return + + exe_path = software.get('exe_path', '') + if not exe_path: + messagebox.showerror("错误", f"软件 '{software_name}' 没有设置EXE文件路径") + return + + # 获取完整路径 + if hasattr(self.db, 'get_exe_full_path'): + source_path = self.db.get_exe_full_path(exe_path) + else: + source_path = exe_path + + if not os.path.exists(source_path): + messagebox.showerror("错误", f"EXE文件不存在: {source_path}") + return + + # 询问加密输出路径 + default_name = f"{software_name}_encrypted.exe" + dest_path = filedialog.asksaveasfilename( + title="选择加密后的文件保存位置", + defaultextension=".exe", + initialfile=default_name, + filetypes=[("EXE文件", "*.exe"), ("所有文件", "*.*")] + ) + + if not dest_path: + return + + # 加载API配置 + api_config = self._load_api_config() + if not api_config: + return + + # 创建进度窗口 + progress_window = tk.Toplevel(self) + progress_window.title("加密进度") + progress_window.geometry("400x150") + progress_window.resizable(False, False) + + # 居中窗口 + progress_window.update_idletasks() + x = (progress_window.winfo_screenwidth() - progress_window.winfo_width()) // 2 + y = (progress_window.winfo_screenheight() - progress_window.winfo_height()) // 2 + progress_window.geometry(f"400x150+{x}+{y}") + + ttk.Label(progress_window, text=f"正在加密: {software_name}", + font=("Microsoft YaHei", 10)).pack(pady=15) + + progress_label = ttk.Label(progress_window, text="正在准备...", + foreground="blue") + progress_label.pack(pady=5) + + progress_bar = ttk.Progressbar(progress_window, mode='indeterminate', length=350) + progress_bar.pack(pady=10) + progress_bar.start(10) + + progress_window.update() + + # 执行加密 + try: + # 使用新的安全加密器 + from encryptor_secure import SecureEXEEncryptor + + encryptor = SecureEXEEncryptor() + success, msg = encryptor.encrypt_exe( + source_path=source_path, + output_path=dest_path, + api_config=api_config, + software_name=software_name + ) + + progress_bar.stop() + progress_window.destroy() + + if success: + messagebox.showinfo("成功", f"软件加密成功!\n\n加密文件: {dest_path}") + else: + messagebox.showerror("错误", f"加密失败: {msg}") + + except Exception as e: + progress_bar.stop() + progress_window.destroy() + messagebox.showerror("错误", f"加密过程出错: {str(e)}") + + +if __name__ == "__main__": + app = EXEEncryptionTool() + app.mainloop() \ No newline at end of file