[patch] 新增用户余额不足 交互模式代码

This commit is contained in:
2025-10-17 17:38:02 +08:00
parent a5c2a44512
commit 8eb03ddedc
4 changed files with 225 additions and 77 deletions

View File

@@ -32,6 +32,7 @@ class BackendClient:
self.token_error_callback: Optional[Callable] = None # 新增token错误回调 self.token_error_callback: Optional[Callable] = None # 新增token错误回调
self.version_callback: Optional[Callable] = None # 新增:版本检查回调 self.version_callback: Optional[Callable] = None # 新增:版本检查回调
self.disconnect_callback: Optional[Callable] = None # 新增:被踢下线回调 self.disconnect_callback: Optional[Callable] = None # 新增:被踢下线回调
self.balance_insufficient_callback: Optional[Callable] = None # 新增:余额不足回调
self.log_callback: Optional[Callable] = None # 新增:日志回调 self.log_callback: Optional[Callable] = None # 新增:日志回调
self.is_connected = False self.is_connected = False
@@ -52,7 +53,7 @@ class BackendClient:
"""Unified logging method that works in both dev and packaged environments""" """Unified logging method that works in both dev and packaged environments"""
# Always print to console (visible in dev mode) # Always print to console (visible in dev mode)
print(f"[{level}] {message}") print(f"[{level}] {message}")
# Also call the log callback if available (for file logging) # Also call the log callback if available (for file logging)
if self.log_callback: if self.log_callback:
try: try:
@@ -130,10 +131,10 @@ class BackendClient:
self._log("已禁用心跳机制", "WARNING") self._log("已禁用心跳机制", "WARNING")
self.is_connected = True self.is_connected = True
# 🔥 在重置之前记录是否是重连(用于后续上报平台状态) # 🔥 在重置之前记录是否是重连(用于后续上报平台状态)
was_reconnecting = self.reconnect_attempts > 0 was_reconnecting = self.reconnect_attempts > 0
self.reconnect_attempts = 0 # 重置重连计数 self.reconnect_attempts = 0 # 重置重连计数
self.is_reconnecting = False self.is_reconnecting = False
self._log("后端WebSocket连接成功", "SUCCESS") self._log("后端WebSocket连接成功", "SUCCESS")
@@ -285,6 +286,7 @@ class BackendClient:
token_error: Callable = None, token_error: Callable = None,
version: Callable = None, version: Callable = None,
disconnect: Callable = None, disconnect: Callable = None,
balance_insufficient: Callable = None,
log: Callable = None): log: Callable = None):
"""设置各种消息类型的回调函数""" """设置各种消息类型的回调函数"""
if store_list: if store_list:
@@ -309,6 +311,8 @@ class BackendClient:
self.version_callback = version self.version_callback = version
if disconnect: if disconnect:
self.disconnect_callback = disconnect self.disconnect_callback = disconnect
if balance_insufficient:
self.balance_insufficient_callback = balance_insufficient
if log: if log:
self.log_callback = log self.log_callback = log
@@ -333,11 +337,11 @@ class BackendClient:
if hasattr(manager, 'platform_listeners') and manager.platform_listeners: if hasattr(manager, 'platform_listeners') and manager.platform_listeners:
platform_count = len(manager.platform_listeners) platform_count = len(manager.platform_listeners)
self._log(f"🔄 检测到 {platform_count} 个活跃平台连接,准备重新上报状态", "INFO") self._log(f"🔄 检测到 {platform_count} 个活跃平台连接,准备重新上报状态", "INFO")
# 延迟1秒确保后端完全准备好 # 延迟1秒确保后端完全准备好
import time import time
time.sleep(1.0) time.sleep(1.0)
for platform_key, listener_info in manager.platform_listeners.items(): for platform_key, listener_info in manager.platform_listeners.items():
store_id = listener_info.get('store_id') store_id = listener_info.get('store_id')
platform = listener_info.get('platform') platform = listener_info.get('platform')
@@ -353,7 +357,7 @@ class BackendClient:
"status": True, "status": True,
"cookies": "" # 重连时无需再次发送cookies "cookies": "" # 重连时无需再次发送cookies
} }
# 同步发送,确保发送成功 # 同步发送,确保发送成功
future = asyncio.run_coroutine_threadsafe( future = asyncio.run_coroutine_threadsafe(
self._send_to_backend(reconnect_message), self._send_to_backend(reconnect_message),
@@ -361,7 +365,7 @@ class BackendClient:
) )
# 等待发送完成最多2秒 # 等待发送完成最多2秒
future.result(timeout=2) future.result(timeout=2)
self._log(f"✅ 已重新上报 {platform} 平台状态: {store_display}", "SUCCESS") self._log(f"✅ 已重新上报 {platform} 平台状态: {store_display}", "SUCCESS")
except Exception as e: except Exception as e:
self._log(f"❌ 上报 {platform} 平台状态失败: {e}", "ERROR") self._log(f"❌ 上报 {platform} 平台状态失败: {e}", "ERROR")
@@ -386,7 +390,7 @@ class BackendClient:
try: try:
from WebSocket.backend_singleton import get_websocket_manager from WebSocket.backend_singleton import get_websocket_manager
manager = get_websocket_manager() manager = get_websocket_manager()
# 从 platform_listeners 获取活跃平台 # 从 platform_listeners 获取活跃平台
if hasattr(manager, 'platform_listeners') and manager.platform_listeners: if hasattr(manager, 'platform_listeners') and manager.platform_listeners:
# 获取第一个活跃平台的 store_id # 获取第一个活跃平台的 store_id
@@ -475,6 +479,8 @@ class BackendClient:
self._handle_version_response(message) self._handle_version_response(message)
elif msg_type == 'disconnect': # 新增:被踢下线 elif msg_type == 'disconnect': # 新增:被踢下线
self._handle_disconnect(message) self._handle_disconnect(message)
elif msg_type == 'balance_insufficient': # 新增:余额不足
self._handle_balance_insufficient(message)
else: else:
print(f"未知消息类型: {msg_type}") print(f"未知消息类型: {msg_type}")
@@ -1203,7 +1209,8 @@ class BackendClient:
self.login_callback(platform_name, store_id, full_message, store_name) self.login_callback(platform_name, store_id, full_message, store_name)
else: else:
# 普通Cookie模式 # 普通Cookie模式
print(f"收到登录指令: 平台={platform_name}, 店铺={store_name or store_id}, cookies_len={len(cookies) if cookies else 0}") print(
f"收到登录指令: 平台={platform_name}, 店铺={store_name or store_id}, cookies_len={len(cookies) if cookies else 0}")
if self.login_callback: if self.login_callback:
self.login_callback(platform_name, store_id, cookies, store_name) self.login_callback(platform_name, store_id, cookies, store_name)
@@ -1275,6 +1282,16 @@ class BackendClient:
if self.disconnect_callback: if self.disconnect_callback:
self.disconnect_callback(disconnect_message) self.disconnect_callback(disconnect_message)
def _handle_balance_insufficient(self, message: Dict[str, Any]):
"""处理余额不足消息"""
balance_message = message.get('content', '所有平台已断开。请充值条数后,重新连接')
self._log(f"⚠️ 余额不足: {balance_message}", "WARNING")
# 触发余额不足回调(在主线程中处理断开逻辑)
if self.balance_insufficient_callback:
self.balance_insufficient_callback(balance_message)
# ==================== 辅助方法 ==================== # ==================== 辅助方法 ====================
def set_token(self, token: str): def set_token(self, token: str):

View File

@@ -60,7 +60,8 @@ class WebSocketManager:
def set_callbacks(self, log: Callable = None, success: Callable = None, error: Callable = None, def set_callbacks(self, log: Callable = None, success: Callable = None, error: Callable = None,
platform_connected: Callable = None, platform_disconnected: Callable = None, platform_connected: Callable = None, platform_disconnected: Callable = None,
token_error: Callable = None, disconnect: Callable = None): token_error: Callable = None, disconnect: Callable = None,
balance_insufficient: Callable = None):
"""设置回调函数""" """设置回调函数"""
if log: if log:
self.callbacks['log'] = log self.callbacks['log'] = log
@@ -76,6 +77,8 @@ class WebSocketManager:
self.callbacks['token_error'] = token_error self.callbacks['token_error'] = token_error
if disconnect: if disconnect:
self.callbacks['disconnect'] = disconnect self.callbacks['disconnect'] = disconnect
if balance_insufficient:
self.callbacks['balance_insufficient'] = balance_insufficient
def _log(self, message: str, level: str = "INFO"): def _log(self, message: str, level: str = "INFO"):
"""内部日志方法""" """内部日志方法"""
@@ -104,15 +107,16 @@ class WebSocketManager:
except Exception as e: except Exception as e:
self._log(f"通知平台连接失败: {e}", "ERROR") self._log(f"通知平台连接失败: {e}", "ERROR")
def notify_platform_kicked(self, platform_name: str, store_name: str, reason: str = "账号在其他设备登录", store_id: str = None): def notify_platform_kicked(self, platform_name: str, store_name: str, reason: str = "账号在其他设备登录",
store_id: str = None):
"""通知GUI平台被踢下线供平台监听器调用""" """通知GUI平台被踢下线供平台监听器调用"""
try: try:
self._log(f"⚠️ 平台被踢下线: {platform_name} - {store_name}, 原因: {reason}", "WARNING") self._log(f"⚠️ 平台被踢下线: {platform_name} - {store_name}, 原因: {reason}", "WARNING")
# 从连接列表中移除 # 从连接列表中移除
if platform_name in self.connected_platforms: if platform_name in self.connected_platforms:
self.connected_platforms.remove(platform_name) self.connected_platforms.remove(platform_name)
# 🔥 发送平台断开消息给后端status=false # 🔥 发送平台断开消息给后端status=false
if store_id and self.backend_client: if store_id and self.backend_client:
try: try:
@@ -126,13 +130,63 @@ class WebSocketManager:
self._log(f"✅ 已通知后端 {platform_name} 平台断开: {store_id}", "INFO") self._log(f"✅ 已通知后端 {platform_name} 平台断开: {store_id}", "INFO")
except Exception as send_error: except Exception as send_error:
self._log(f"❌ 发送平台断开消息失败: {send_error}", "ERROR") self._log(f"❌ 发送平台断开消息失败: {send_error}", "ERROR")
# 通知GUI显示弹窗 # 通知GUI显示弹窗
if self.callbacks['platform_disconnected']: if self.callbacks['platform_disconnected']:
self.callbacks['platform_disconnected'](platform_name, store_name, reason) self.callbacks['platform_disconnected'](platform_name, store_name, reason)
except Exception as e: except Exception as e:
self._log(f"通知平台断开失败: {e}", "ERROR") self._log(f"通知平台断开失败: {e}", "ERROR")
def disconnect_all_async(self):
"""异步断开所有连接(余额不足时调用)- 不阻塞当前线程"""
try:
self._log("🔴 收到余额不足通知,开始断开所有连接...", "ERROR")
# 1. 断开所有平台连接
if self.platform_listeners:
platform_count = len(self.platform_listeners)
self._log(f"正在断开 {platform_count} 个平台连接...", "INFO")
# 复制键列表,避免遍历时修改字典
platform_keys = list(self.platform_listeners.keys())
for key in platform_keys:
try:
listener_info = self.platform_listeners.get(key)
if listener_info:
platform_type = listener_info.get('platform', '')
self._log(f"断开 {platform_type} 平台连接: {key}", "INFO")
# 移除连接记录
self.platform_listeners.pop(key, None)
except Exception as e:
self._log(f"断开平台连接失败: {e}", "ERROR")
self._log(f"✅ 已断开所有 {platform_count} 个平台连接", "INFO")
# 清空连接列表
self.connected_platforms.clear()
# 2. 延迟断开后端连接(在新线程中执行,避免阻塞)
import threading
def _delayed_backend_disconnect():
try:
import time
time.sleep(0.5) # 延迟0.5秒,确保上面的操作完成
if self.backend_client:
self.backend_client.should_stop = True
self.backend_client.is_connected = False
self._log("✅ 已标记后端连接为断开状态", "INFO")
except Exception as e:
self._log(f"断开后端连接失败: {e}", "ERROR")
disconnect_thread = threading.Thread(target=_delayed_backend_disconnect, daemon=True)
disconnect_thread.start()
except Exception as e:
self._log(f"断开所有连接失败: {e}", "ERROR")
def connect_backend(self, token: str) -> bool: def connect_backend(self, token: str) -> bool:
"""连接后端WebSocket""" """连接后端WebSocket"""
try: try:
@@ -188,12 +242,18 @@ class WebSocketManager:
if self.callbacks['disconnect']: if self.callbacks['disconnect']:
self.callbacks['disconnect'](disconnect_msg) self.callbacks['disconnect'](disconnect_msg)
def _on_balance_insufficient(balance_msg: str):
"""余额不足回调"""
if self.callbacks['balance_insufficient']:
self.callbacks['balance_insufficient'](balance_msg)
def _on_log(message: str, level: str = "INFO"): def _on_log(message: str, level: str = "INFO"):
"""Backend client log callback""" """Backend client log callback"""
self._log(message, level) self._log(message, level)
backend.set_callbacks(success=_on_backend_success, login=_on_backend_login, backend.set_callbacks(success=_on_backend_success, login=_on_backend_login,
token_error=_on_token_error, disconnect=_on_disconnect, token_error=_on_token_error, disconnect=_on_disconnect,
balance_insufficient=_on_balance_insufficient,
log=_on_log) log=_on_log)
if not backend.is_connected: if not backend.is_connected:
@@ -235,12 +295,18 @@ class WebSocketManager:
if self.callbacks['disconnect']: if self.callbacks['disconnect']:
self.callbacks['disconnect'](disconnect_msg) self.callbacks['disconnect'](disconnect_msg)
def _on_balance_insufficient(balance_msg: str):
"""余额不足回调"""
if self.callbacks['balance_insufficient']:
self.callbacks['balance_insufficient'](balance_msg)
def _on_log(message: str, level: str = "INFO"): def _on_log(message: str, level: str = "INFO"):
"""Backend client log callback""" """Backend client log callback"""
self._log(message, level) self._log(message, level)
backend.set_callbacks(login=_on_backend_login, success=_on_backend_success, backend.set_callbacks(login=_on_backend_login, success=_on_backend_success,
token_error=_on_token_error, disconnect=_on_disconnect, token_error=_on_token_error, disconnect=_on_disconnect,
balance_insufficient=_on_balance_insufficient,
log=_on_log) log=_on_log)
backend.connect() backend.connect()
@@ -261,15 +327,15 @@ class WebSocketManager:
# 🔥 检查并断开当前店铺的旧连接策略B先断开旧连接再建立新连接 # 🔥 检查并断开当前店铺的旧连接策略B先断开旧连接再建立新连接
store_key_pattern = f":{store_id}" # 匹配 "平台名:store_id" 格式 store_key_pattern = f":{store_id}" # 匹配 "平台名:store_id" 格式
keys_to_remove = [key for key in self.platform_listeners.keys() if key.endswith(store_key_pattern)] keys_to_remove = [key for key in self.platform_listeners.keys() if key.endswith(store_key_pattern)]
if keys_to_remove: if keys_to_remove:
self._log(f"🔄 检测到店铺 {store_id} 重复登录,断开 {len(keys_to_remove)} 个旧连接", "INFO") self._log(f"🔄 检测到店铺 {store_id} 重复登录,断开 {len(keys_to_remove)} 个旧连接", "INFO")
for key in keys_to_remove: for key in keys_to_remove:
listener_info = self.platform_listeners.get(key) listener_info = self.platform_listeners.get(key)
if listener_info: if listener_info:
platform_type = listener_info.get('platform', '') platform_type = listener_info.get('platform', '')
# 从各平台的 WebsocketManager 中获取连接并关闭WebSocket # 从各平台的 WebsocketManager 中获取连接并关闭WebSocket
try: try:
if platform_type == "京东": if platform_type == "京东":
@@ -289,7 +355,7 @@ class WebSocketManager:
pass pass
jd_mgr.remove_connection(key) jd_mgr.remove_connection(key)
self._log(f"✅ 已从京东管理器移除连接: {key}", "DEBUG") self._log(f"✅ 已从京东管理器移除连接: {key}", "DEBUG")
elif platform_type == "抖音": elif platform_type == "抖音":
from Utils.Dy.DyUtils import DouYinWebsocketManager as DYWSManager from Utils.Dy.DyUtils import DouYinWebsocketManager as DYWSManager
dy_mgr = DYWSManager() dy_mgr = DYWSManager()
@@ -307,13 +373,13 @@ class WebSocketManager:
pass pass
dy_mgr.remove_connection(key) dy_mgr.remove_connection(key)
self._log(f"✅ 已从抖音管理器移除连接: {key}", "DEBUG") self._log(f"✅ 已从抖音管理器移除连接: {key}", "DEBUG")
elif platform_type == "千牛": elif platform_type == "千牛":
from Utils.QianNiu.QianNiuUtils import QianNiuWebsocketManager as QNWSManager from Utils.QianNiu.QianNiuUtils import QianNiuWebsocketManager as QNWSManager
qn_mgr = QNWSManager() qn_mgr = QNWSManager()
qn_mgr.remove_connection(key) qn_mgr.remove_connection(key)
self._log(f"✅ 已从千牛管理器移除连接: {key}", "DEBUG") self._log(f"✅ 已从千牛管理器移除连接: {key}", "DEBUG")
elif platform_type == "拼多多": elif platform_type == "拼多多":
from Utils.Pdd.PddUtils import WebsocketManager as PDDWSManager from Utils.Pdd.PddUtils import WebsocketManager as PDDWSManager
pdd_mgr = PDDWSManager() pdd_mgr = PDDWSManager()
@@ -332,17 +398,17 @@ class WebSocketManager:
self._log(f"⚠️ 关闭WebSocket时出错: {ws_e}", "DEBUG") self._log(f"⚠️ 关闭WebSocket时出错: {ws_e}", "DEBUG")
pdd_mgr.remove_connection(key) pdd_mgr.remove_connection(key)
self._log(f"✅ 已从拼多多管理器移除连接: {key}", "DEBUG") self._log(f"✅ 已从拼多多管理器移除连接: {key}", "DEBUG")
except Exception as e: except Exception as e:
self._log(f"⚠️ 移除{platform_type}连接时出错: {e}", "WARNING") self._log(f"⚠️ 移除{platform_type}连接时出错: {e}", "WARNING")
# 从监听器字典中移除 # 从监听器字典中移除
self.platform_listeners.pop(key, None) self.platform_listeners.pop(key, None)
# 给WebSocket一点时间完全关闭 # 给WebSocket一点时间完全关闭
import time import time
time.sleep(0.5) time.sleep(0.5)
self._log(f"✅ 旧连接已全部断开,准备建立新连接", "INFO") self._log(f"✅ 旧连接已全部断开,准备建立新连接", "INFO")
# 平台名称映射 # 平台名称映射

View File

@@ -52,7 +52,7 @@ PLATFORMS = {
"ws_url": "wss://dongdong.jd.com/workbench/websocket" "ws_url": "wss://dongdong.jd.com/workbench/websocket"
}, },
"DOUYIN": { "DOUYIN": {
"name": "抖音", "name": "抖音",
"ws_url": None # 动态获取 "ws_url": None # 动态获取
}, },
"QIANNIU": { "QIANNIU": {

169
main.py
View File

@@ -12,7 +12,6 @@ from WebSocket.backend_singleton import get_websocket_manager
from windows_taskbar_fix import setup_windows_taskbar_icon from windows_taskbar_fix import setup_windows_taskbar_icon
import os import os
# ===================== 文件日志系统 - 生产环境启用 ===================== # ===================== 文件日志系统 - 生产环境启用 =====================
# 重定向所有输出到文件,确保有日志记录 # 重定向所有输出到文件,确保有日志记录
from exe_file_logger import setup_file_logging, log_to_file # 开发环境启用 from exe_file_logger import setup_file_logging, log_to_file # 开发环境启用
@@ -33,6 +32,12 @@ class DisconnectSignals(QObject):
disconnected = pyqtSignal(str) # (disconnect_message) disconnected = pyqtSignal(str) # (disconnect_message)
# 新增: 余额不足信号类(用于线程安全的余额不足提示)
class BalanceInsufficientSignals(QObject):
"""余额不足信号"""
balance_insufficient = pyqtSignal(str) # (balance_message)
# 新增: 用户名密码输入对话框类 # 新增: 用户名密码输入对话框类
class LoginWindow(QMainWindow): class LoginWindow(QMainWindow):
def __init__(self): def __init__(self):
@@ -62,6 +67,10 @@ class LoginWindow(QMainWindow):
self.disconnect_signals = DisconnectSignals() self.disconnect_signals = DisconnectSignals()
self.disconnect_signals.disconnected.connect(self._show_disconnect_dialog) self.disconnect_signals.disconnected.connect(self._show_disconnect_dialog)
# 创建余额不足信号对象(线程安全)
self.balance_insufficient_signals = BalanceInsufficientSignals()
self.balance_insufficient_signals.balance_insufficient.connect(self._show_balance_insufficient_dialog)
# 横幅相关 # 横幅相关
self.promo_banner = None self.promo_banner = None
self.banner_shadow = None # 阴影效果引用 self.banner_shadow = None # 阴影效果引用
@@ -170,7 +179,7 @@ class LoginWindow(QMainWindow):
# 系统初始化日志输出到终端 # 系统初始化日志输出到终端
print("[INFO] 系统初始化完成") print("[INFO] 系统初始化完成")
# 让窗口自适应内容高度(最小化) # 让窗口自适应内容高度(最小化)
self.adjustSize() self.adjustSize()
print("[INFO] 窗口已自适应内容大小") print("[INFO] 窗口已自适应内容大小")
@@ -182,41 +191,41 @@ class LoginWindow(QMainWindow):
banner_frame = QFrame() banner_frame = QFrame()
banner_frame.setObjectName("promoBanner") banner_frame.setObjectName("promoBanner")
banner_frame.setCursor(Qt.PointingHandCursor) # 鼠标变手型 banner_frame.setCursor(Qt.PointingHandCursor) # 鼠标变手型
# 保存横幅引用,用于动画 # 保存横幅引用,用于动画
self.promo_banner = banner_frame self.promo_banner = banner_frame
# 使用图片标签 # 使用图片标签
banner_image = QLabel() banner_image = QLabel()
banner_image.setObjectName("bannerImage") banner_image.setObjectName("bannerImage")
# 加载横幅图片 # 加载横幅图片
from windows_taskbar_fix import get_resource_path from windows_taskbar_fix import get_resource_path
image_path = get_resource_path("static/hengfu.jpg") image_path = get_resource_path("static/hengfu.jpg")
if os.path.exists(image_path): if os.path.exists(image_path):
from PyQt5.QtGui import QPixmap from PyQt5.QtGui import QPixmap
pixmap = QPixmap(image_path) pixmap = QPixmap(image_path)
# 设置固定的横幅尺寸 # 设置固定的横幅尺寸
target_width = 400 # 固定宽度 target_width = 400 # 固定宽度
target_height = 70 # 固定高度调整为70px更紧凑 target_height = 70 # 固定高度调整为70px更紧凑
# 缩放图片以适应目标尺寸 # 缩放图片以适应目标尺寸
scaled_pixmap = pixmap.scaled( scaled_pixmap = pixmap.scaled(
target_width, target_width,
target_height, target_height,
Qt.IgnoreAspectRatio, # 忽略宽高比,拉伸填充 Qt.IgnoreAspectRatio, # 忽略宽高比,拉伸填充
Qt.SmoothTransformation # 平滑缩放,提高图片质量 Qt.SmoothTransformation # 平滑缩放,提高图片质量
) )
# 设置图片 # 设置图片
banner_image.setPixmap(scaled_pixmap) banner_image.setPixmap(scaled_pixmap)
banner_image.setScaledContents(False) banner_image.setScaledContents(False)
banner_frame.setFixedHeight(target_height) banner_frame.setFixedHeight(target_height)
banner_image.setFixedSize(target_width, target_height) banner_image.setFixedSize(target_width, target_height)
# 🔧 使用CSS圆角来处理边角更简单稳定 # 🔧 使用CSS圆角来处理边角更简单稳定
banner_image.setStyleSheet(""" banner_image.setStyleSheet("""
QLabel#bannerImage { QLabel#bannerImage {
@@ -224,7 +233,7 @@ class LoginWindow(QMainWindow):
background-color: transparent; background-color: transparent;
} }
""") """)
print(f"[INFO] 横幅图片已加载: {image_path}, 尺寸: {target_width}x{target_height}") print(f"[INFO] 横幅图片已加载: {image_path}, 尺寸: {target_width}x{target_height}")
else: else:
# 图片不存在时的备用方案 # 图片不存在时的备用方案
@@ -241,14 +250,14 @@ class LoginWindow(QMainWindow):
""") """)
banner_frame.setFixedHeight(60) banner_frame.setFixedHeight(60)
print(f"[WARNING] 横幅图片未找到: {image_path},使用备用样式") print(f"[WARNING] 横幅图片未找到: {image_path},使用备用样式")
# 创建布局 # 创建布局
banner_layout = QHBoxLayout() banner_layout = QHBoxLayout()
banner_layout.setContentsMargins(0, 0, 0, 0) banner_layout.setContentsMargins(0, 0, 0, 0)
banner_layout.setSpacing(0) banner_layout.setSpacing(0)
banner_frame.setLayout(banner_layout) banner_frame.setLayout(banner_layout)
banner_layout.addWidget(banner_image) banner_layout.addWidget(banner_image)
# 设置圆角和边框 # 设置圆角和边框
banner_frame.setStyleSheet(""" banner_frame.setStyleSheet("""
QFrame#promoBanner { QFrame#promoBanner {
@@ -256,7 +265,7 @@ class LoginWindow(QMainWindow):
background: transparent; background: transparent;
} }
""") """)
# 初始阴影效果 # 初始阴影效果
self.banner_shadow = QGraphicsDropShadowEffect() self.banner_shadow = QGraphicsDropShadowEffect()
self.banner_shadow.setBlurRadius(15) self.banner_shadow.setBlurRadius(15)
@@ -264,7 +273,7 @@ class LoginWindow(QMainWindow):
self.banner_shadow.setYOffset(3) self.banner_shadow.setYOffset(3)
self.banner_shadow.setColor(QColor(0, 0, 0, 60)) self.banner_shadow.setColor(QColor(0, 0, 0, 60))
banner_frame.setGraphicsEffect(self.banner_shadow) banner_frame.setGraphicsEffect(self.banner_shadow)
# 点击事件 - 跳转官网 # 点击事件 - 跳转官网
def open_official_website(): def open_official_website():
try: try:
@@ -274,9 +283,9 @@ class LoginWindow(QMainWindow):
self.add_log(f"已打开官网: {official_url}", "INFO") self.add_log(f"已打开官网: {official_url}", "INFO")
except Exception as e: except Exception as e:
self.add_log(f"打开官网失败: {e}", "ERROR") self.add_log(f"打开官网失败: {e}", "ERROR")
banner_frame.mousePressEvent = lambda event: open_official_website() banner_frame.mousePressEvent = lambda event: open_official_website()
# 🎯 添加鼠标悬停事件(动态效果) # 🎯 添加鼠标悬停事件(动态效果)
def on_enter(event): def on_enter(event):
"""鼠标进入时的动画效果""" """鼠标进入时的动画效果"""
@@ -285,30 +294,30 @@ class LoginWindow(QMainWindow):
self.banner_shadow.setBlurRadius(25) self.banner_shadow.setBlurRadius(25)
self.banner_shadow.setYOffset(5) self.banner_shadow.setYOffset(5)
self.banner_shadow.setColor(QColor(0, 0, 0, 100)) self.banner_shadow.setColor(QColor(0, 0, 0, 100))
# 轻微放大动画 # 轻微放大动画
animation = QPropertyAnimation(banner_frame, b"geometry") animation = QPropertyAnimation(banner_frame, b"geometry")
original_rect = banner_frame.geometry() original_rect = banner_frame.geometry()
expanded_rect = QRect( expanded_rect = QRect(
original_rect.x() - 3, original_rect.x() - 3,
original_rect.y() - 2, original_rect.y() - 2,
original_rect.width() + 6, original_rect.width() + 6,
original_rect.height() + 4 original_rect.height() + 4
) )
animation.setDuration(200) animation.setDuration(200)
animation.setStartValue(original_rect) animation.setStartValue(original_rect)
animation.setEndValue(expanded_rect) animation.setEndValue(expanded_rect)
animation.setEasingCurve(QEasingCurve.OutCubic) animation.setEasingCurve(QEasingCurve.OutCubic)
animation.start() animation.start()
# 保存动画引用避免被垃圾回收 # 保存动画引用避免被垃圾回收
banner_frame.hover_animation = animation banner_frame.hover_animation = animation
except Exception as e: except Exception as e:
print(f"[DEBUG] 悬停动画错误: {e}") print(f"[DEBUG] 悬停动画错误: {e}")
def on_leave(event): def on_leave(event):
"""鼠标离开时的动画效果""" """鼠标离开时的动画效果"""
try: try:
@@ -316,42 +325,42 @@ class LoginWindow(QMainWindow):
self.banner_shadow.setBlurRadius(15) self.banner_shadow.setBlurRadius(15)
self.banner_shadow.setYOffset(3) self.banner_shadow.setYOffset(3)
self.banner_shadow.setColor(QColor(0, 0, 0, 60)) self.banner_shadow.setColor(QColor(0, 0, 0, 60))
# 恢复原始大小 # 恢复原始大小
animation = QPropertyAnimation(banner_frame, b"geometry") animation = QPropertyAnimation(banner_frame, b"geometry")
current_rect = banner_frame.geometry() current_rect = banner_frame.geometry()
original_rect = QRect( original_rect = QRect(
current_rect.x() + 3, current_rect.x() + 3,
current_rect.y() + 2, current_rect.y() + 2,
current_rect.width() - 6, current_rect.width() - 6,
current_rect.height() - 4 current_rect.height() - 4
) )
animation.setDuration(200) animation.setDuration(200)
animation.setStartValue(current_rect) animation.setStartValue(current_rect)
animation.setEndValue(original_rect) animation.setEndValue(original_rect)
animation.setEasingCurve(QEasingCurve.InCubic) animation.setEasingCurve(QEasingCurve.InCubic)
animation.start() animation.start()
# 保存动画引用避免被垃圾回收 # 保存动画引用避免被垃圾回收
banner_frame.leave_animation = animation banner_frame.leave_animation = animation
except Exception as e: except Exception as e:
print(f"[DEBUG] 离开动画错误: {e}") print(f"[DEBUG] 离开动画错误: {e}")
# 安装事件过滤器 # 安装事件过滤器
banner_frame.enterEvent = on_enter banner_frame.enterEvent = on_enter
banner_frame.leaveEvent = on_leave banner_frame.leaveEvent = on_leave
# 添加到主布局 # 添加到主布局
layout.addWidget(banner_frame) layout.addWidget(banner_frame)
print("[INFO] 宣传横幅已创建(使用图片,带悬停动画)") print("[INFO] 宣传横幅已创建(使用图片,带悬停动画)")
except Exception as e: except Exception as e:
print(f"[WARNING] 创建宣传横幅失败: {e}") print(f"[WARNING] 创建宣传横幅失败: {e}")
# 横幅颜色动画相关方法已移除(改用图片横幅) # 横幅颜色动画相关方法已移除(改用图片横幅)
def apply_modern_styles(self): def apply_modern_styles(self):
@@ -617,7 +626,7 @@ class LoginWindow(QMainWindow):
"""处理平台被踢下线 - 显示弹窗警告""" """处理平台被踢下线 - 显示弹窗警告"""
try: try:
self.add_log(f"⚠️ {platform_name}平台被踢下线: {store_name}, 原因: {reason}", "WARNING") self.add_log(f"⚠️ {platform_name}平台被踢下线: {store_name}, 原因: {reason}", "WARNING")
# 显示弹窗提示 # 显示弹窗提示
message_text = ( message_text = (
f"{store_name}】连接已断开\n\n" f"{store_name}】连接已断开\n\n"
@@ -634,12 +643,12 @@ class LoginWindow(QMainWindow):
message_text, message_text,
QMessageBox.Ok QMessageBox.Ok
) )
# 更新状态显示 # 更新状态显示
self.status_label.setText(f"⚠️ {platform_name}已断开") self.status_label.setText(f"⚠️ {platform_name}已断开")
self.status_label.setStyleSheet( self.status_label.setStyleSheet(
"color: #ff6b6b; background: rgba(255, 107, 107, 0.1); border-radius: 12px; padding: 5px 10px;") "color: #ff6b6b; background: rgba(255, 107, 107, 0.1); border-radius: 12px; padding: 5px 10px;")
except Exception as e: except Exception as e:
self.add_log(f"显示平台断开提示失败: {e}", "ERROR") self.add_log(f"显示平台断开提示失败: {e}", "ERROR")
@@ -678,6 +687,18 @@ class LoginWindow(QMainWindow):
import traceback import traceback
self.add_log(f"详细错误: {traceback.format_exc()}", "ERROR") self.add_log(f"详细错误: {traceback.format_exc()}", "ERROR")
def on_balance_insufficient(self, balance_msg: str):
"""处理余额不足 - 发射信号到主线程(可在任何线程中调用)"""
try:
self.add_log(f"📡 收到余额不足通知,准备发射信号: {balance_msg}", "INFO")
# 发射信号Qt 自动调度到主线程)
self.balance_insufficient_signals.balance_insufficient.emit(balance_msg)
self.add_log(f"✅ 余额不足信号已发射", "DEBUG")
except Exception as e:
self.add_log(f"❌ 发射余额不足信号失败: {e}", "ERROR")
import traceback
self.add_log(f"详细错误: {traceback.format_exc()}", "ERROR")
def _show_disconnect_dialog(self, disconnect_msg: str): def _show_disconnect_dialog(self, disconnect_msg: str):
"""显示断开连接提示框(信号槽函数,始终在主线程中执行)""" """显示断开连接提示框(信号槽函数,始终在主线程中执行)"""
try: try:
@@ -714,6 +735,49 @@ class LoginWindow(QMainWindow):
import traceback import traceback
self.add_log(f"详细错误: {traceback.format_exc()}", "ERROR") self.add_log(f"详细错误: {traceback.format_exc()}", "ERROR")
def _show_balance_insufficient_dialog(self, balance_msg: str):
"""显示余额不足提示框(信号槽函数,始终在主线程中执行)"""
try:
self.add_log(f"🎯 主线程收到余额不足信号: {balance_msg}", "INFO")
# 1. 断开所有连接(异步,不阻塞)
ws_manager = get_websocket_manager()
if ws_manager:
self.add_log("🔴 开始断开所有连接...", "INFO")
ws_manager.disconnect_all_async()
self.add_log("✅ 断开连接指令已发送", "INFO")
# 2. 更新UI状态
self.status_label.setText("🔴 余额不足")
self.status_label.setStyleSheet(
"color: #dc3545; background: rgba(220, 53, 69, 0.1); border-radius: 12px; padding: 5px 10px; font-weight: bold;")
# 3. 重置按钮状态
self.login_btn.setEnabled(True)
self.login_btn.setText("重新连接")
self.login_btn.setObjectName("loginButton")
self.login_btn.setStyleSheet(self.login_btn.styleSheet()) # 刷新样式
# 4. 清空已连接平台列表
self.connected_platforms.clear()
# 5. 显示弹窗提示(主线程中执行,不会卡顿)
QMessageBox.critical(
self,
"余额不足",
f"{balance_msg}\n\n"
f"已断开所有平台连接和后端连接。\n\n"
f"请充值后,点击「连接服务」按钮重新连接。",
QMessageBox.Ok
)
self.add_log("✅ 余额不足提示已显示,所有连接已断开", "INFO")
except Exception as e:
self.add_log(f"❌ 显示余额不足提示框失败: {e}", "ERROR")
import traceback
self.add_log(f"详细错误: {traceback.format_exc()}", "ERROR")
def delayed_platform_summary(self): def delayed_platform_summary(self):
"""定时器触发的汇总显示更新""" """定时器触发的汇总显示更新"""
try: try:
@@ -818,16 +882,16 @@ class LoginWindow(QMainWindow):
try: try:
# 清空现有菜单 # 清空现有菜单
self.tray_menu.clear() self.tray_menu.clear()
# 获取WebSocket管理器 # 获取WebSocket管理器
ws_manager = get_websocket_manager() ws_manager = get_websocket_manager()
# 1. 显示后端连接状态 # 1. 显示后端连接状态
if ws_manager and ws_manager.backend_client and ws_manager.backend_client.is_connected: if ws_manager and ws_manager.backend_client and ws_manager.backend_client.is_connected:
backend_status = QAction("✅ 后端已连接", self) backend_status = QAction("✅ 后端已连接", self)
backend_status.setEnabled(False) # 不可点击 backend_status.setEnabled(False) # 不可点击
self.tray_menu.addAction(backend_status) self.tray_menu.addAction(backend_status)
# 2. 显示已连接的平台信息 # 2. 显示已连接的平台信息
platform_listeners = ws_manager.platform_listeners platform_listeners = ws_manager.platform_listeners
if platform_listeners: if platform_listeners:
@@ -837,18 +901,18 @@ class LoginWindow(QMainWindow):
platform = info.get('platform', '') platform = info.get('platform', '')
store_name = info.get('store_name', '') store_name = info.get('store_name', '')
store_id = info.get('store_id', '') store_id = info.get('store_id', '')
if platform not in platform_stores: if platform not in platform_stores:
platform_stores[platform] = [] platform_stores[platform] = []
# 优先显示店铺名称,如果没有则显示 store_id 前8位 # 优先显示店铺名称,如果没有则显示 store_id 前8位
if store_name: if store_name:
display_name = store_name display_name = store_name
else: else:
display_name = store_id[:8] + "..." if len(store_id) > 8 else store_id display_name = store_id[:8] + "..." if len(store_id) > 8 else store_id
platform_stores[platform].append(display_name) platform_stores[platform].append(display_name)
# 显示每个平台的店铺 # 显示每个平台的店铺
for platform, stores in platform_stores.items(): for platform, stores in platform_stores.items():
stores_text = ", ".join(stores) stores_text = ", ".join(stores)
@@ -865,23 +929,23 @@ class LoginWindow(QMainWindow):
backend_disconnected = QAction("❌ 后端未连接", self) backend_disconnected = QAction("❌ 后端未连接", self)
backend_disconnected.setEnabled(False) backend_disconnected.setEnabled(False)
self.tray_menu.addAction(backend_disconnected) self.tray_menu.addAction(backend_disconnected)
# 添加分隔线 # 添加分隔线
self.tray_menu.addSeparator() self.tray_menu.addSeparator()
# 3. 显示主窗口 # 3. 显示主窗口
show_action = QAction("显示主窗口", self) show_action = QAction("显示主窗口", self)
show_action.triggered.connect(self.show_window) show_action.triggered.connect(self.show_window)
self.tray_menu.addAction(show_action) self.tray_menu.addAction(show_action)
# 添加分隔线 # 添加分隔线
self.tray_menu.addSeparator() self.tray_menu.addSeparator()
# 4. 退出程序 # 4. 退出程序
quit_action = QAction("退出程序", self) quit_action = QAction("退出程序", self)
quit_action.triggered.connect(self.quit_application) quit_action.triggered.connect(self.quit_application)
self.tray_menu.addAction(quit_action) self.tray_menu.addAction(quit_action)
except Exception as e: except Exception as e:
print(f"[ERROR] 更新托盘菜单失败: {e}") print(f"[ERROR] 更新托盘菜单失败: {e}")
# 如果更新失败,至少保证基本功能可用 # 如果更新失败,至少保证基本功能可用
@@ -889,7 +953,7 @@ class LoginWindow(QMainWindow):
show_action = QAction("显示主窗口", self) show_action = QAction("显示主窗口", self)
show_action.triggered.connect(self.show_window) show_action.triggered.connect(self.show_window)
self.tray_menu.addAction(show_action) self.tray_menu.addAction(show_action)
quit_action = QAction("退出程序", self) quit_action = QAction("退出程序", self)
quit_action.triggered.connect(self.quit_application) quit_action.triggered.connect(self.quit_application)
self.tray_menu.addAction(quit_action) self.tray_menu.addAction(quit_action)
@@ -1004,7 +1068,8 @@ class LoginWindow(QMainWindow):
platform_connected=self.on_platform_connected, # 新增:平台连接回调 platform_connected=self.on_platform_connected, # 新增:平台连接回调
platform_disconnected=self.on_platform_kicked, # 新增:平台被踢回调 platform_disconnected=self.on_platform_kicked, # 新增:平台被踢回调
token_error=self.on_token_error, # 新增token错误回调 token_error=self.on_token_error, # 新增token错误回调
disconnect=self.on_disconnect # 新增:被踢下线回调 disconnect=self.on_disconnect, # 新增:被踢下线回调
balance_insufficient=self.on_balance_insufficient # 新增:余额不足回调
) )
# 连接后端 # 连接后端