#!/usr/bin/env python3 """ Happy Games 服务器(Flask + 登录认证 + WebSocket) """ from flask import Flask, send_from_directory, jsonify, request from flask_cors import CORS from flask_login import LoginManager, login_user, logout_user, login_required, current_user from flask_sqlalchemy import SQLAlchemy from flask_socketio import SocketIO, emit from werkzeug.security import generate_password_hash, check_password_hash from datetime import timedelta from functools import wraps from jose import jwt as jose_jwt, JWTError as JoseJWTError import os import json # ── SSO Cookie 验签(与 SSO 中心共享 SECRET_KEY)── SSO_SECRET_KEY = "your-secret-key-change-in-production" # 必须与 usercenter/sso_config.py 一致 SSO_ALGORITHM = "HS256" def verify_sso_cookie(req): """从请求 Cookie 中读取 SSO access_token,本地验签后返回 username""" token = req.cookies.get("access_token") if not token: return None try: payload = jose_jwt.decode(token, SSO_SECRET_KEY, algorithms=[SSO_ALGORITHM]) return payload.get("sub") except JoseJWTError: return None app = Flask(__name__) app.config['SECRET_KEY'] = 'happy-games-secret-key' app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///games_auth.db' app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False # Session cookie 配置(支持跨域) app.config['SESSION_COOKIE_HTTPONLY'] = True app.config['SESSION_COOKIE_SAMESITE'] = 'Lax' # HTTP环境下用Lax,HTTPS才用None app.config['SESSION_COOKIE_SECURE'] = False # 初始化扩展 db = SQLAlchemy(app) CORS(app, supports_credentials=True, origins='*') # Flask-Login login_manager = LoginManager() login_manager.init_app(app) # Flask-SocketIO (支持 WebSocket) socketio = SocketIO(app, cors_allowed_origins='*', async_mode='threading') # ========== 用户模型 ========== class User(db.Model): """用户模型 - 每个游戏独立积分""" __tablename__ = 'users' id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(80), unique=True, nullable=False) password_hash = db.Column(db.String(200), nullable=False) created_at = db.Column(db.DateTime, default=db.func.current_timestamp()) # 各游戏独立积分 (JSON 格式: {"doudizhu": 100, "tetris": 50, ...}) game_scores = db.Column(db.Text, default='{}') def set_password(self, password): self.password_hash = generate_password_hash(password) def check_password(self, password): return check_password_hash(self.password_hash, password) def get_game_score(self, game_id): """获取某个游戏的积分""" try: scores = json.loads(self.game_scores or '{}') return scores.get(game_id, 0) except: return 0 def add_game_score(self, game_id, points): """增加某个游戏的积分""" try: scores = json.loads(self.game_scores or '{}') scores[game_id] = scores.get(game_id, 0) + points self.game_scores = json.dumps(scores) return scores[game_id] except: return 0 def to_dict(self): return { 'id': self.id, 'username': self.username, 'created_at': self.created_at.isoformat() if self.created_at else None } # Flask-Login 要求的方法 def is_authenticated(self): return True def is_active(self): return True def is_anonymous(self): return False def get_id(self): return str(self.id) @login_manager.user_loader def load_user(user_id): return User.query.get(int(user_id)) def init_default_users(): """初始化默认用户""" default_users = [ {'username': 'admin', 'password': 'abcd1234'}, {'username': 'caixianming', 'password': 'cxm123456'} ] for user_data in default_users: existing = User.query.filter_by(username=user_data['username']).first() if not existing: user = User(username=user_data['username']) user.set_password(user_data['password']) db.session.add(user) print(f"✅ 创建默认用户: {user_data['username']}") db.session.commit() # ========== API 登录检查装饰器 ========== def api_login_required(f): """API 登录检查 - 返回 JSON 而不是重定向""" @wraps(f) def decorated_function(*args, **kwargs): if not current_user.is_authenticated: return jsonify({ 'error': 'Unauthorized', 'message': '请先登录', 'code': 401 }), 401 return f(*args, **kwargs) return decorated_function # ========== 认证 API ========== @app.route('/api/auth/status') def auth_status(): """检查登录状态 - 支持 SSO Cookie 自动登录""" if current_user.is_authenticated: return jsonify({ 'authenticated': True, 'user': current_user.to_dict() }) # 未登录时,检查 SSO Cookie sso_username = verify_sso_cookie(request) if sso_username: user = User.query.filter_by(username=sso_username).first() if user: login_user(user, remember=True, duration=timedelta(days=30)) return jsonify({ 'authenticated': True, 'user': user.to_dict() }) return jsonify({'authenticated': False, 'user': None}) @app.route('/api/auth/login', methods=['POST']) def login(): """用户登录""" data = request.get_json() if not data: return jsonify({'error': '请求数据为空'}), 400 username = data.get('username', '').strip() password = data.get('password', '').strip() if not username or not password: return jsonify({'error': '用户名和密码不能为空'}), 400 user = User.query.filter_by(username=username).first() if not user or not user.check_password(password): return jsonify({'error': '用户名或密码错误'}), 401 login_user(user, remember=True, duration=timedelta(days=30)) return jsonify({ 'success': True, 'message': '登录成功', 'user': user.to_dict() }) @app.route('/api/auth/logout', methods=['POST']) @login_required def logout(): """用户登出""" logout_user() resp = jsonify({'success': True, 'message': '已登出'}) # 同时清除 SSO Cookie resp.set_cookie('access_token', '', expires=0, domain='129.211.173.87', path='/') return resp @app.route('/api/user/score', methods=['POST']) @api_login_required def update_score(): """更新游戏积分(游戏结束后调用)""" data = request.get_json() if not data: return jsonify({'error': '请求数据为空'}), 400 game_id = data.get('game') # 游戏ID,如 'doudizhu' points = data.get('points', 0) # 增加的积分 if not game_id: return jsonify({'error': '缺少游戏ID'}), 400 user = User.query.get(current_user.id) if user: new_score = user.add_game_score(game_id, points) db.session.commit() return jsonify({ 'success': True, 'game': game_id, 'score': new_score, 'added': points }) return jsonify({'error': '用户不存在'}), 404 @app.route('/api/user/score/') @api_login_required def get_game_score(game_id): """获取某个游戏的积分""" user = User.query.get(current_user.id) if user: score = user.get_game_score(game_id) return jsonify({ 'game': game_id, 'score': score }) return jsonify({'error': '用户不存在'}), 404 # ========== 前端路由 ========== @app.route('/') def index(): """首页""" return send_from_directory('.', 'index.html') # 中国象棋 - 静态文件托管(无需Next.js服务器,无WebSocket问题) CHINESE_CHESS_DIR = os.path.join('games', 'chinese-chess', 'dist') @app.route('/games/chinese-chess/') def serve_chinese_chess(): """中国象棋首页 - 静态文件""" index_path = os.path.join(CHINESE_CHESS_DIR, 'index.html') if os.path.exists(index_path): return send_from_directory(CHINESE_CHESS_DIR, 'index.html') return "

中国象棋服务暂时不可用

请稍后刷新重试

", 503 @app.route('/games/chinese-chess/') def serve_chinese_chess_assets(subpath): """中国象棋静态资源""" file_path = os.path.join(CHINESE_CHESS_DIR, subpath) if os.path.exists(file_path) and os.path.isfile(file_path): # 根据文件类型设置Content-Type if subpath.endswith('.js'): mimetype = 'application/javascript' elif subpath.endswith('.css'): mimetype = 'text/css' elif subpath.endswith('.svg'): mimetype = 'image/svg+xml' elif subpath.endswith('.png'): mimetype = 'image/png' elif subpath.endswith('.jpg') or subpath.endswith('.jpeg'): mimetype = 'image/jpeg' else: mimetype = None return send_from_directory(CHINESE_CHESS_DIR, subpath, mimetype=mimetype) return jsonify({'error': '资源不存在'}), 404 @app.route('/games/') def serve_games(path): """游戏文件 - 支持目录和默认index.html""" # 如果是目录,返回index.html if not path.endswith(('.html', '.js', '.css', '.png', '.jpg', '.json')): full_path = os.path.join('games', path, 'index.html') if os.path.exists(full_path): return send_from_directory(os.path.join('games', path), 'index.html') return send_from_directory('games', path) @app.route('/') def static_files(path): """其他静态文件""" # 排除 API 路径 if path.startswith('api/'): return jsonify({'error': 'Not found'}), 404 return send_from_directory('.', path) # ========== 初始化 ========== def init_db(): """初始化数据库""" with app.app_context(): db.create_all() init_default_users() print("✅ 数据库初始化完成") # ========== WebSocket 事件处理 ========== @socketio.on('connect') def handle_connect(): """客户端连接""" print(f'[WebSocket] Client connected: {request.sid}') emit('connected', {'message': 'Connected to Happy Games server'}) @socketio.on('disconnect') def handle_disconnect(): """客户端断开""" print(f'[WebSocket] Client disconnected: {request.sid}') @socketio.on('ping') def handle_ping(data): """心跳测试""" emit('pong', {'time': data.get('time'), 'server': 'happy-games'}) if __name__ == '__main__': init_db() print("🎮 Happy Games 服务器(Flask + WebSocket)启动...") print(f" 访问: http://localhost:8895") print(f" WebSocket: ws://localhost:8895") print(f" 默认账号: admin / abcd1234") socketio.run(app, host='0.0.0.0', port=8895, debug=False, allow_unsafe_werkzeug=True)