From 36c92b7fafb0455465b099ab9a7c71b45324306b Mon Sep 17 00:00:00 2001 From: wang <3202024218@qq.com> Date: Tue, 24 Jun 2025 23:42:47 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E5=9F=BA=E4=BA=8EUDP?= =?UTF-8?q?=E5=8F=91=E9=80=81=E8=AF=AD=E9=9F=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- goldminer/backend/app.py | 483 +++++- goldminer/backend/run_server.py | 9 + goldminer/backend/voice_udp_server.py | 210 +++ .../node_modules/.cache/eslint/123ee647.json | 2 +- .../frontend/src/components/ChatRoom.vue | 1351 ++++++++++++++++- goldminer/frontend/src/components/Login.vue | 69 +- goldminer/frontend/src/main.js | 35 +- 7 files changed, 1983 insertions(+), 176 deletions(-) create mode 100644 goldminer/backend/voice_udp_server.py diff --git a/goldminer/backend/app.py b/goldminer/backend/app.py index edec19da..addddfdd 100644 --- a/goldminer/backend/app.py +++ b/goldminer/backend/app.py @@ -9,6 +9,8 @@ from datetime import datetime from sqlalchemy.exc import OperationalError import logging from functools import wraps +import voice_udp_server # 导入UDP语音服务器模块 +import time logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -66,12 +68,22 @@ socketio = SocketIO(app, ping_interval=25, always_connect=True) -# 跟踪所有活跃游戏会话 -active_games = {} # 用户ID -> 游戏状态 -sid_to_user = {} # Socket ID -> 用户ID映射 +# 存储用户令牌 +user_tokens = {} -# 添加一个简单的令牌存储 -user_tokens = {} # token -> user_id 映射 +# 存储活跃游戏状态 +active_games = {} + +# 存储在线用户和他们的sid(Socket ID)映射 +online_users = {} +sid_to_user = {} # sid -> user_id映射,保留此变量以兼容现有代码 + +# 启动UDP语音服务器 +voice_server_started = voice_udp_server.start_udp_server() +if voice_server_started: + logger.info("UDP语音服务器已成功启动") +else: + logger.error("UDP语音服务器启动失败") # 用户模型 class User(db.Model): @@ -219,49 +231,58 @@ def register(): # 登录API @app.route('/api/login', methods=['POST']) def login(): - data = request.get_json() - logger.info(f"收到登录请求: {data.get('username') if data else 'No data'}") - - # 检查必要的字段 - if not data or not data.get('username') or not data.get('password'): - logger.warning("登录请求缺少用户名或密码") - return jsonify({'error': '用户名和密码是必填项'}), 400 - - # 查找用户 - user = User.query.filter_by(username=data['username']).first() - - # 检查密码 - if user and user.check_password(data['password']): - # 生成会话ID和令牌 - session_id = str(uuid.uuid4()) - token = str(uuid.uuid4()) + try: + data = request.get_json() + logger.info(f"收到登录请求: {data.get('username') if data else 'No data'}") - # 存储到会话和令牌映射 - session['user_id'] = user.id - session['session_id'] = session_id - user_tokens[token] = user.id + # 检查请求数据 + if request.content_type != 'application/json': + logger.warning(f"登录请求Content-Type不正确: {request.content_type}") + return jsonify({'error': '请求格式必须为JSON'}), 400 - # 更新最后登录时间 - user.last_login = datetime.utcnow() - db.session.commit() + # 检查必要的字段 + if not data or not data.get('username') or not data.get('password'): + logger.warning("登录请求缺少用户名或密码") + return jsonify({'error': '用户名和密码是必填项'}), 400 - logger.info(f"用户 {user.username} (ID: {user.id}) 登录成功,设置会话ID: {session_id} 和令牌: {token[:8]}...") - logger.info(f"当前会话内容: {dict(session)}") + # 查找用户 + user = User.query.filter_by(username=data['username']).first() - return jsonify({ - 'message': '登录成功', - 'user': { - 'id': user.id, - 'username': user.username, - 'high_score': user.high_score, - 'is_admin': user.is_admin - }, - 'session_id': session_id, - 'token': token - }) - - logger.warning(f"用户名或密码不正确: {data.get('username')}") - return jsonify({'error': '用户名或密码不正确'}), 401 + # 检查密码 + if user and user.check_password(data['password']): + # 生成会话ID和令牌 + session_id = str(uuid.uuid4()) + token = str(uuid.uuid4()) + + # 存储到会话和令牌映射 + session['user_id'] = user.id + session['session_id'] = session_id + user_tokens[token] = user.id + + # 更新最后登录时间 + user.last_login = datetime.utcnow() + db.session.commit() + + logger.info(f"用户 {user.username} (ID: {user.id}) 登录成功,设置会话ID: {session_id} 和令牌: {token[:8]}...") + logger.info(f"当前会话内容: {dict(session)}") + + return jsonify({ + 'message': '登录成功', + 'user': { + 'id': user.id, + 'username': user.username, + 'high_score': user.high_score, + 'is_admin': user.is_admin + }, + 'session_id': session_id, + 'token': token + }) + + logger.warning(f"用户名或密码不正确: {data.get('username')}") + return jsonify({'error': '用户名或密码不正确'}), 401 + except Exception as e: + logger.error(f"登录处理时发生错误: {str(e)}") + return jsonify({'error': '服务器处理请求时出错'}), 500 # 登出API @app.route('/api/logout', methods=['POST']) @@ -517,6 +538,12 @@ def handle_connect(): # 记录用户的Socket ID sid_to_user[request.sid] = user_id + + # 同时更新online_users映射 + if user_id not in online_users: + online_users[user_id] = [] + online_users[user_id].append(request.sid) + print(f'Client connected: {request.sid}, User ID: {user_id}') # 发送当前的排行榜和活跃游戏状态 @@ -533,18 +560,19 @@ def handle_disconnect(): if request.sid in sid_to_user: user_id = sid_to_user.pop(request.sid) + # 更新online_users映射 + if user_id in online_users: + if request.sid in online_users[user_id]: + online_users[user_id].remove(request.sid) + # 如果用户没有其他连接,从映射中删除 + if not online_users[user_id]: + del online_users[user_id] + # 如果该用户没有其他活跃连接,从游戏状态中移除 - if user_id in active_games: - user_still_active = False - for sid, uid in sid_to_user.items(): - if uid == user_id: - user_still_active = True - break - - if not user_still_active: - active_games.pop(user_id, None) - # 广播活跃游戏状态更新 - send_active_games_update() + if user_id in active_games and user_id not in online_users: + active_games.pop(user_id, None) + # 广播活跃游戏状态更新 + send_active_games_update() @socketio.on('join_room') def handle_join_room(data): @@ -649,37 +677,52 @@ def handle_send_audio_message(data): audio_duration = data.get('audio_duration', 0) # 音频持续时间(秒) if not room_id or not audio_data: + logger.error("语音消息缺少必要参数: room_id或audio_data") return - # 获取用户和房间信息 - user = User.query.get(user_id) - room = ChatRoom.query.get(room_id) - - if not user or not room: - return - - # 保存消息到数据库 - message = ChatMessage( - room_id=room_id, - user_id=user_id, - message=audio_data, # 存储Base64编码的音频数据 - message_type='audio', - audio_duration=audio_duration - ) + # 检查audio_data是否为有效的base64编码数据 + if not audio_data.startswith('data:audio'): + logger.warning(f"接收到非标准格式的音频数据,前缀: {audio_data[:20]}") - db.session.add(message) - db.session.commit() + logger.info(f"处理语音消息: 用户ID={user_id}, 房间ID={room_id}, 音频时长={audio_duration}秒, 数据长度={len(audio_data)}") - # 广播消息给房间内的所有用户 - emit('new_message', { - 'id': message.id, - 'user_id': user_id, - 'username': user.username, - 'message': audio_data, - 'message_type': 'audio', - 'audio_duration': audio_duration, - 'created_at': message.created_at.isoformat() - }, room=str(room_id)) + try: + # 获取用户和房间信息 + user = User.query.get(user_id) + room = ChatRoom.query.get(room_id) + + if not user or not room: + logger.error(f"用户或房间不存在: user_id={user_id}, room_id={room_id}") + return + + # 保存消息到数据库 + message = ChatMessage( + room_id=room_id, + user_id=user_id, + message=audio_data, # 存储Base64编码的音频数据 + message_type='audio', + audio_duration=audio_duration + ) + + db.session.add(message) + db.session.commit() + logger.info(f"语音消息已保存到数据库: ID={message.id}") + + # 广播消息给房间内的所有用户 + emit('new_message', { + 'id': message.id, + 'user_id': user_id, + 'username': user.username, + 'message': audio_data, + 'message_type': 'audio', + 'audio_duration': audio_duration, + 'created_at': message.created_at.isoformat() + }, room=str(room_id)) + logger.info(f"语音消息已广播到房间: {room_id}") + + except Exception as e: + logger.error(f"处理语音消息时出错: {e}") + db.session.rollback() # 更新游戏状态处理 @socketio.on('update_game_status') @@ -1023,6 +1066,282 @@ def test_admin_status(): def handle_options(path): return '', 200 +# 添加UDP语音服务器状态API +@app.route('/api/voice/status', methods=['GET']) +def get_voice_status(): + user_id = session.get('user_id') + if not user_id: + return jsonify({'error': '未登录'}), 401 + + status = voice_udp_server.get_voice_room_status() + return jsonify({ + 'status': 'online' if voice_server_started else 'offline', + 'rooms': status, + 'port': voice_udp_server.UDP_PORT + }) + +# 添加UDP语音数据包发送API +@app.route('/api/voice/send', methods=['POST']) +def send_voice_packet(): + """模拟将数据包通过UDP发送""" + user_id = session.get('user_id') + if not user_id: + return jsonify({'error': '未登录'}), 401 + + if not voice_server_started: + return jsonify({'error': 'UDP语音服务器未启动'}), 503 + + try: + data = request.get_json() + if not data: + return jsonify({'error': '无效的请求数据'}), 400 + + # 从请求中获取必要信息 + client_id = data.get('client_id') + room_id = data.get('room_id') + packet_type = data.get('type') + + # 验证用户身份 + if str(user_id) != str(client_id): + logger.warning(f"用户ID不匹配: {user_id} vs {client_id}") + return jsonify({'error': '用户身份验证失败'}), 403 + + # 如果是语音数据,直接转发 + if packet_type == 'voice': + voice_data = data.get('data') + if voice_data and room_id: + # 转发到UDP服务器的相应函数 + voice_udp_server.forward_voice_data(room_id, client_id, voice_data) + return jsonify({'success': True}) + + # 加入/离开房间 + elif packet_type in ['join', 'leave']: + if packet_type == 'join': + voice_udp_server.join_voice_room(client_id, room_id) + else: + voice_udp_server.leave_voice_room(client_id, room_id) + return jsonify({'success': True}) + + # 心跳包 + elif packet_type == 'heartbeat': + # 更新客户端最后活跃时间 + voice_udp_server.CLIENT_LAST_SEEN[client_id] = time.time() + return jsonify({'success': True}) + + return jsonify({'error': '不支持的数据包类型'}), 400 + + except Exception as e: + logger.error(f"处理语音数据包时出错: {str(e)}") + return jsonify({'error': '服务器处理请求时出错'}), 500 + +# 添加UDP语音事件通知函数 +def notify_voice_event(event_type, data): + """通过WebSocket通知客户端UDP语音相关事件""" + event_data = { + 'type': event_type, + **data + } + socketio.emit('voice_event', event_data, room=f"voice_room_{data.get('room_id')}") + +# 添加UDP语音WebSocket事件处理 +@socketio.on('join_voice_room') +def handle_join_voice_room(data): + """用户加入语音房间""" + user_id = session.get('user_id') + if not user_id: + return + + room_id = data.get('room_id') + if not room_id: + return + + # 加入WebSocket房间(用于通知事件) + join_room(f"voice_room_{room_id}") + + # 获取用户信息 + user = User.query.get(user_id) + if not user: + return + + # 通知其他用户 + socketio.emit('voice_event', { + 'type': 'user_joined_voice', + 'user_id': user_id, + 'username': user.username, + 'room_id': room_id + }, room=f"voice_room_{room_id}", skip_sid=request.sid) + + logger.info(f"用户 {user.username} (ID: {user_id}) 加入语音房间 {room_id}") + +@socketio.on('leave_voice_room') +def handle_leave_voice_room(data): + """用户离开语音房间""" + user_id = session.get('user_id') + if not user_id: + return + + room_id = data.get('room_id') + if not room_id: + return + + # 离开WebSocket房间 + leave_room(f"voice_room_{room_id}") + + # 获取用户信息 + user = User.query.get(user_id) + if not user: + return + + # 通知其他用户 + socketio.emit('voice_event', { + 'type': 'user_left_voice', + 'user_id': user_id, + 'username': user.username, + 'room_id': room_id + }, room=f"voice_room_{room_id}") + + logger.info(f"用户 {user.username} (ID: {user_id}) 离开语音房间 {room_id}") + +# 添加WebRTC信令处理 +@socketio.on('webrtc_signal') +def handle_webrtc_signal(data): + """处理WebRTC信令""" + user_id = session.get('user_id') + if not user_id: + return + + room_id = data.get('room_id') + target_id = data.get('target_id') + signal_type = data.get('signal_type') + signal_data = data.get('signal_data') + + if not room_id or not signal_type: + return + + # 获取用户信息 + user = User.query.get(user_id) + if not user: + return + + # 创建要发送的信令数据 + signal_message = { + 'sender_id': user_id, + 'sender_name': user.username, + 'room_id': room_id, + 'signal_type': signal_type, + 'signal_data': signal_data + } + + # 如果指定了目标用户,只发送给目标用户 + if target_id: + # 查找目标用户的会话ID列表 + target_sids = online_users.get(int(target_id), []) + + if target_sids: + # 向目标用户的所有会话发送信令 + for target_sid in target_sids: + emit('webrtc_signal', signal_message, room=target_sid) + else: + # 如果找不到目标用户的会话ID,则发送到整个房间 + emit('webrtc_signal', signal_message, room=f"voice_room_{room_id}", skip_sid=request.sid) + else: + # 如果没有指定目标用户,则发送到整个房间 + emit('webrtc_signal', signal_message, room=f"voice_room_{room_id}", skip_sid=request.sid) + + logger.info(f"WebRTC信令: {user.username} (ID: {user_id}) 发送 {signal_type} 到房间 {room_id}" + + (f", 目标用户: {target_id}" if target_id else "")) + +# WebRTC语音聊天相关处理 +@socketio.on('join_voice_chat') +def handle_join_voice_chat(data): + """用户加入语音聊天""" + user_id = session.get('user_id') + if not user_id: + return + + room_id = data.get('room_id') + if not room_id: + return + + # 获取用户信息 + user = User.query.get(user_id) + if not user: + return + + # 加入WebSocket房间(用于通知语音相关事件) + voice_room = f"voice_room_{room_id}" + join_room(voice_room) + + # 通知房间内其他用户 + emit('voice_chat_event', { + 'type': 'user_joined', + 'user_id': user_id, + 'username': user.username, + 'room_id': room_id + }, room=voice_room, skip_sid=request.sid) + + logger.info(f"用户 {user.username} (ID: {user_id}) 加入语音聊天 {room_id}") + +@socketio.on('leave_voice_chat') +def handle_leave_voice_chat(data): + """用户离开语音聊天""" + user_id = session.get('user_id') + if not user_id: + return + + room_id = data.get('room_id') + if not room_id: + return + + # 获取用户信息 + user = User.query.get(user_id) + if not user: + return + + # 离开WebSocket房间 + voice_room = f"voice_room_{room_id}" + leave_room(voice_room) + + # 通知房间内其他用户 + emit('voice_chat_event', { + 'type': 'user_left', + 'user_id': user_id, + 'username': user.username, + 'room_id': room_id + }, room=voice_room) + + logger.info(f"用户 {user.username} (ID: {user_id}) 离开语音聊天 {room_id}") + +@socketio.on('voice_status_change') +def handle_voice_status_change(data): + """用户更改语音状态(如静音)""" + user_id = session.get('user_id') + if not user_id: + return + + room_id = data.get('room_id') + muted = data.get('muted', False) + + if not room_id: + return + + # 获取用户信息 + user = User.query.get(user_id) + if not user: + return + + # 通知房间内其他用户 + voice_room = f"voice_room_{room_id}" + emit('voice_chat_event', { + 'type': 'mute_change', + 'user_id': user_id, + 'username': user.username, + 'room_id': room_id, + 'muted': muted + }, room=voice_room) + + logger.info(f"用户 {user.username} (ID: {user_id}) 在语音聊天 {room_id} 中{'静音' if muted else '取消静音'}") + if __name__ == '__main__': # 在启动应用前初始化数据库 init_db() diff --git a/goldminer/backend/run_server.py b/goldminer/backend/run_server.py index 165863e0..43bd777c 100644 --- a/goldminer/backend/run_server.py +++ b/goldminer/backend/run_server.py @@ -2,6 +2,7 @@ import os import logging from dotenv import load_dotenv from app import app, socketio, init_db +import voice_udp_server # 加载环境变量 load_dotenv() @@ -22,6 +23,13 @@ if __name__ == '__main__': logger.error(f"数据库初始化失败: {e}") # 继续执行,因为可能会在应用启动后自动重试 + # 启动UDP语音服务器 + udp_started = voice_udp_server.start_udp_server() + if udp_started: + logger.info("UDP语音服务器启动成功") + else: + logger.warning("UDP语音服务器启动失败,实时语音功能将不可用") + # 从环境变量获取主机和端口,如果没有则使用默认值 host = os.environ.get('HOST', '0.0.0.0') port = int(os.environ.get('PORT', 5000)) @@ -29,6 +37,7 @@ if __name__ == '__main__': logger.info(f"环境: {os.environ.get('FLASK_ENV', 'production')}") logger.info(f"启动服务器,监听 {host}:{port}") + logger.info(f"UDP语音服务器状态: {'在线' if udp_started else '离线'}") logger.info(f"SECRET_KEY已设置: {app.config['SECRET_KEY'][:3]}..." if 'SECRET_KEY' in app.config else "SECRET_KEY未设置") logger.info(f"SESSION_COOKIE_SECURE: {app.config.get('SESSION_COOKIE_SECURE', False)}") logger.info(f"CORS supports_credentials: {app.config.get('CORS_SUPPORTS_CREDENTIALS', False)}") diff --git a/goldminer/backend/voice_udp_server.py b/goldminer/backend/voice_udp_server.py new file mode 100644 index 00000000..801205a9 --- /dev/null +++ b/goldminer/backend/voice_udp_server.py @@ -0,0 +1,210 @@ +import socket +import threading +import json +import time +import logging +import base64 +from collections import defaultdict + +# 配置日志 +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') +logger = logging.getLogger('voice_udp_server') + +# 配置 +UDP_HOST = '0.0.0.0' # 监听所有网络接口 +UDP_PORT = 5001 # UDP端口 +MAX_PACKET_SIZE = 65507 # UDP包最大大小 +ROOM_CLIENTS = defaultdict(set) # 存储房间和客户端的映射关系 +CLIENT_ADDRESSES = {} # 存储客户端ID和地址的映射关系 +HEARTBEAT_INTERVAL = 5 # 心跳检测间隔(秒) +CLIENT_TIMEOUT = 15 # 客户端超时时间(秒) +CLIENT_LAST_SEEN = {} # 记录客户端最后活跃时间 + +# UDP套接字 +udp_socket = None + +def start_udp_server(): + """启动UDP服务器""" + global udp_socket + try: + udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + udp_socket.bind((UDP_HOST, UDP_PORT)) + logger.info(f"UDP语音服务器已启动,监听于 {UDP_HOST}:{UDP_PORT}") + + # 启动心跳检测线程 + heartbeat_thread = threading.Thread(target=check_client_heartbeats) + heartbeat_thread.daemon = True + heartbeat_thread.start() + + # 启动UDP监听线程 + udp_thread = threading.Thread(target=udp_listener) + udp_thread.daemon = True + udp_thread.start() + + return True + except Exception as e: + logger.error(f"启动UDP服务器失败: {str(e)}") + return False + +def udp_listener(): + """UDP监听线程,处理接收到的UDP数据包""" + global udp_socket + logger.info("UDP监听线程已启动") + + while True: + try: + data, addr = udp_socket.recvfrom(MAX_PACKET_SIZE) + # 解析数据 + process_udp_packet(data, addr) + except Exception as e: + logger.error(f"处理UDP数据包时出错: {str(e)}") + +def process_udp_packet(data, addr): + """处理UDP数据包""" + try: + # 尝试解析为JSON + packet = json.loads(data.decode('utf-8')) + + packet_type = packet.get('type') + client_id = packet.get('client_id') + room_id = packet.get('room_id') + + # 更新客户端最后活跃时间 + if client_id: + CLIENT_LAST_SEEN[client_id] = time.time() + CLIENT_ADDRESSES[client_id] = addr + + # 根据数据包类型处理 + if packet_type == 'join': + # 客户端加入房间 + if room_id and client_id: + join_voice_room(client_id, room_id) + logger.info(f"客户端 {client_id} 加入语音房间 {room_id}") + + elif packet_type == 'leave': + # 客户端离开房间 + if room_id and client_id: + leave_voice_room(client_id, room_id) + logger.info(f"客户端 {client_id} 离开语音房间 {room_id}") + + elif packet_type == 'heartbeat': + # 心跳包,只需更新最后活跃时间,已在上面处理 + send_udp_packet({ + 'type': 'heartbeat_ack', + 'server_time': time.time() + }, addr) + + elif packet_type == 'voice': + # 语音数据包,转发给房间内其他用户 + voice_data = packet.get('data') + if room_id and client_id and voice_data: + forward_voice_data(room_id, client_id, voice_data) + + except json.JSONDecodeError: + logger.warning(f"收到无效的JSON数据: {data[:100]}...") + except Exception as e: + logger.error(f"处理UDP数据包时出错: {str(e)}") + +def join_voice_room(client_id, room_id): + """客户端加入语音房间""" + ROOM_CLIENTS[room_id].add(client_id) + # 通知房间内其他用户有新用户加入 + notify_room_clients(room_id, { + 'type': 'user_joined', + 'client_id': client_id, + 'room_id': room_id, + 'time': time.time() + }, exclude_client=client_id) + +def leave_voice_room(client_id, room_id): + """客户端离开语音房间""" + if client_id in ROOM_CLIENTS[room_id]: + ROOM_CLIENTS[room_id].remove(client_id) + # 如果房间空了,删除房间 + if not ROOM_CLIENTS[room_id]: + del ROOM_CLIENTS[room_id] + else: + # 通知房间内其他用户有用户离开 + notify_room_clients(room_id, { + 'type': 'user_left', + 'client_id': client_id, + 'room_id': room_id, + 'time': time.time() + }, exclude_client=client_id) + +def forward_voice_data(room_id, sender_id, voice_data): + """转发语音数据给房间内其他用户""" + packet = { + 'type': 'voice', + 'client_id': sender_id, + 'room_id': room_id, + 'data': voice_data, + 'time': time.time() + } + notify_room_clients(room_id, packet, exclude_client=sender_id) + +def notify_room_clients(room_id, packet, exclude_client=None): + """通知房间内的所有客户端""" + if room_id not in ROOM_CLIENTS: + return + + for client_id in ROOM_CLIENTS[room_id]: + if client_id != exclude_client and client_id in CLIENT_ADDRESSES: + send_udp_packet(packet, CLIENT_ADDRESSES[client_id]) + +def send_udp_packet(packet, addr): + """发送UDP数据包""" + try: + data = json.dumps(packet).encode('utf-8') + udp_socket.sendto(data, addr) + except Exception as e: + logger.error(f"发送UDP数据包失败: {str(e)}") + +def check_client_heartbeats(): + """检查客户端心跳,移除超时的客户端""" + while True: + try: + current_time = time.time() + timeout_clients = [] + + # 检查所有客户端 + for client_id, last_seen in list(CLIENT_LAST_SEEN.items()): + if current_time - last_seen > CLIENT_TIMEOUT: + timeout_clients.append(client_id) + + # 处理超时的客户端 + for client_id in timeout_clients: + logger.info(f"客户端 {client_id} 超时,移除") + # 从所有房间中移除 + for room_id in list(ROOM_CLIENTS.keys()): + if client_id in ROOM_CLIENTS[room_id]: + leave_voice_room(client_id, room_id) + + # 删除客户端记录 + if client_id in CLIENT_ADDRESSES: + del CLIENT_ADDRESSES[client_id] + if client_id in CLIENT_LAST_SEEN: + del CLIENT_LAST_SEEN[client_id] + + # 等待下一次检查 + time.sleep(HEARTBEAT_INTERVAL) + except Exception as e: + logger.error(f"心跳检测线程出错: {str(e)}") + time.sleep(HEARTBEAT_INTERVAL) + +def get_voice_room_status(): + """获取语音房间状态""" + result = {} + for room_id, clients in ROOM_CLIENTS.items(): + result[room_id] = list(clients) + return result + +# 当模块直接运行时,启动UDP服务器 +if __name__ == "__main__": + start_udp_server() + try: + # 保持主线程运行 + while True: + time.sleep(1) + except KeyboardInterrupt: + logger.info("UDP语音服务器已关闭") \ No newline at end of file diff --git a/goldminer/frontend/node_modules/.cache/eslint/123ee647.json b/goldminer/frontend/node_modules/.cache/eslint/123ee647.json index c96a0e19..6f107444 100644 --- a/goldminer/frontend/node_modules/.cache/eslint/123ee647.json +++ b/goldminer/frontend/node_modules/.cache/eslint/123ee647.json @@ -1 +1 @@ -[{"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\main.js":"1","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\App.vue":"2","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\router.js":"3","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Home.vue":"4","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Register.vue":"5","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Game.vue":"6","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Login.vue":"7","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\UserProfile.vue":"8","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\ChatRoom.vue":"9","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Leaderboard.vue":"10","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Header.vue":"11","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Admin.vue":"12","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\AdminSetup.vue":"13"},{"size":1590,"mtime":1750768017958,"results":"14","hashOfConfig":"15"},{"size":1004,"mtime":1750128639386,"results":"16","hashOfConfig":"15"},{"size":2623,"mtime":1750303858493,"results":"17","hashOfConfig":"15"},{"size":5735,"mtime":1750209720346,"results":"18","hashOfConfig":"15"},{"size":4492,"mtime":1750147385471,"results":"19","hashOfConfig":"15"},{"size":53697,"mtime":1750767242119,"results":"20","hashOfConfig":"15"},{"size":5329,"mtime":1750336307891,"results":"21","hashOfConfig":"15"},{"size":11652,"mtime":1750381631833,"results":"22","hashOfConfig":"15"},{"size":24471,"mtime":1750768438839,"results":"23","hashOfConfig":"15"},{"size":13225,"mtime":1750128639387,"results":"24","hashOfConfig":"15"},{"size":4886,"mtime":1750381631832,"results":"25","hashOfConfig":"15"},{"size":19436,"mtime":1750391010410,"results":"26","hashOfConfig":"15"},{"size":4836,"mtime":1750303858491,"results":"27","hashOfConfig":"15"},{"filePath":"28","messages":"29","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"i331ru",{"filePath":"30","messages":"31","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"32"},{"filePath":"33","messages":"34","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"35"},{"filePath":"36","messages":"37","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"32"},{"filePath":"38","messages":"39","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"32"},{"filePath":"40","messages":"41","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"32"},{"filePath":"42","messages":"43","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"32"},{"filePath":"44","messages":"45","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"32"},{"filePath":"46","messages":"47","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"48","messages":"49","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"32"},{"filePath":"50","messages":"51","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"32"},{"filePath":"52","messages":"53","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"32"},{"filePath":"54","messages":"55","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"32"},"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\main.js",[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\App.vue",[],[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\router.js",[],[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Home.vue",[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Register.vue",[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Game.vue",[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Login.vue",[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\UserProfile.vue",[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\ChatRoom.vue",[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Leaderboard.vue",[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Header.vue",[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Admin.vue",[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\AdminSetup.vue",[]] \ No newline at end of file +[{"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\main.js":"1","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\App.vue":"2","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\router.js":"3","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Home.vue":"4","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Register.vue":"5","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Game.vue":"6","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Login.vue":"7","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\UserProfile.vue":"8","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\ChatRoom.vue":"9","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Leaderboard.vue":"10","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Header.vue":"11","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Admin.vue":"12","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\AdminSetup.vue":"13"},{"size":2697,"mtime":1750769523218,"results":"14","hashOfConfig":"15"},{"size":1004,"mtime":1750128639386,"results":"16","hashOfConfig":"15"},{"size":2623,"mtime":1750303858493,"results":"17","hashOfConfig":"15"},{"size":5735,"mtime":1750209720346,"results":"18","hashOfConfig":"15"},{"size":4492,"mtime":1750147385471,"results":"19","hashOfConfig":"15"},{"size":53697,"mtime":1750767242119,"results":"20","hashOfConfig":"15"},{"size":7100,"mtime":1750769571067,"results":"21","hashOfConfig":"15"},{"size":11652,"mtime":1750381631833,"results":"22","hashOfConfig":"15"},{"size":59225,"mtime":1750770950073,"results":"23","hashOfConfig":"15"},{"size":13225,"mtime":1750128639387,"results":"24","hashOfConfig":"15"},{"size":4886,"mtime":1750381631832,"results":"25","hashOfConfig":"15"},{"size":19436,"mtime":1750391010410,"results":"26","hashOfConfig":"15"},{"size":4836,"mtime":1750303858491,"results":"27","hashOfConfig":"15"},{"filePath":"28","messages":"29","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"30"},"i331ru",{"filePath":"31","messages":"32","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"33"},{"filePath":"34","messages":"35","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"30"},{"filePath":"36","messages":"37","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"33"},{"filePath":"38","messages":"39","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"33"},{"filePath":"40","messages":"41","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"33"},{"filePath":"42","messages":"43","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"33"},{"filePath":"44","messages":"45","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"33"},{"filePath":"46","messages":"47","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"48","messages":"49","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"33"},{"filePath":"50","messages":"51","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"33"},{"filePath":"52","messages":"53","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"33"},{"filePath":"54","messages":"55","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"33"},"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\main.js",[],[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\App.vue",[],[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\router.js",[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Home.vue",[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Register.vue",[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Game.vue",[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Login.vue",[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\UserProfile.vue",[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\ChatRoom.vue",[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Leaderboard.vue",[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Header.vue",[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Admin.vue",[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\AdminSetup.vue",[]] \ No newline at end of file diff --git a/goldminer/frontend/src/components/ChatRoom.vue b/goldminer/frontend/src/components/ChatRoom.vue index 4612d1b5..1cca7fae 100644 --- a/goldminer/frontend/src/components/ChatRoom.vue +++ b/goldminer/frontend/src/components/ChatRoom.vue @@ -2,11 +2,11 @@

游戏聊天室

-

与其他玩家交流游戏心得

+

与其他玩家聊天,分享游戏经验

-

加载中...

+

正在加载聊天室列表...

@@ -51,51 +51,137 @@