1002 lines
33 KiB
Python
1002 lines
33 KiB
Python
from flask import Flask, render_template, request, redirect, url_for, jsonify, session
|
||
import json
|
||
import os
|
||
import uuid
|
||
import datetime
|
||
import time
|
||
import shutil
|
||
import logging
|
||
import threading
|
||
import hashlib
|
||
from werkzeug.security import generate_password_hash, check_password_hash
|
||
import uuid
|
||
import datetime
|
||
import os
|
||
import json
|
||
import logging
|
||
|
||
# 配置日志
|
||
logging.basicConfig(
|
||
filename='game_app.log',
|
||
level=logging.INFO,
|
||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||
)
|
||
logger = logging.getLogger('game_app')
|
||
|
||
app = Flask(__name__)
|
||
app.secret_key = os.urandom(24) # 用于会话加密
|
||
|
||
# 数据文件路径
|
||
DATA_DIR = 'data'
|
||
BACKUP_DIR = os.path.join(DATA_DIR, 'backup')
|
||
USERS_FILE = os.path.join(DATA_DIR, 'users.json')
|
||
GAME_STATES_FILE = os.path.join(DATA_DIR, 'game_states.json')
|
||
CARDS_FILE = os.path.join(DATA_DIR, 'cards.json')
|
||
SESSIONS_FILE = os.path.join(DATA_DIR, 'sessions.json') # 会话文件
|
||
|
||
# 文件锁,防止并发写入冲突
|
||
file_locks = {
|
||
USERS_FILE: threading.RLock(),
|
||
GAME_STATES_FILE: threading.RLock(),
|
||
SESSIONS_FILE: threading.RLock(),
|
||
CARDS_FILE: threading.RLock()
|
||
}
|
||
|
||
# 确保数据目录存在
|
||
def ensure_dirs_exist():
|
||
os.makedirs(DATA_DIR, exist_ok=True)
|
||
os.makedirs(BACKUP_DIR, exist_ok=True)
|
||
|
||
# 创建备份文件名
|
||
def get_backup_filename(original_file, include_timestamp=False):
|
||
base_name = os.path.basename(original_file)
|
||
if include_timestamp:
|
||
timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
|
||
return os.path.join(BACKUP_DIR, f"{base_name}.{timestamp}.bak")
|
||
else:
|
||
return os.path.join(BACKUP_DIR, f"{base_name}.bak")
|
||
|
||
# 管理备份文件,保留最近的N个备份
|
||
def manage_backups(file_pattern, max_backups=5):
|
||
try:
|
||
backup_files = [f for f in os.listdir(BACKUP_DIR) if f.startswith(file_pattern)]
|
||
backup_files.sort(reverse=True) # 最新的文件排在前面
|
||
|
||
# 如果备份文件超过最大数量,删除最旧的
|
||
if len(backup_files) > max_backups:
|
||
for old_file in backup_files[max_backups:]:
|
||
try:
|
||
os.remove(os.path.join(BACKUP_DIR, old_file))
|
||
logger.info(f"删除旧备份文件: {old_file}")
|
||
except Exception as e:
|
||
logger.error(f"删除旧备份文件失败: {old_file}, 错误: {str(e)}")
|
||
except Exception as e:
|
||
logger.error(f"管理备份文件失败, 错误: {str(e)}")
|
||
|
||
# 安全地保存数据到文件
|
||
def safe_save_data(file_path, data, create_backup=True):
|
||
# 获取文件锁
|
||
lock = file_locks.get(file_path, threading.RLock())
|
||
|
||
with lock:
|
||
# 确保目录存在
|
||
ensure_dirs_exist()
|
||
|
||
# 如果原文件存在且需要备份,先创建备份
|
||
if create_backup and os.path.exists(file_path):
|
||
try:
|
||
# 创建带时间戳的备份文件,每小时最多一个
|
||
current_hour = datetime.datetime.now().strftime('%Y%m%d_%H')
|
||
backup_marker = hashlib.md5(file_path.encode()).hexdigest()[:8]
|
||
backup_file = os.path.join(BACKUP_DIR, f"{os.path.basename(file_path)}.{current_hour}.{backup_marker}.bak")
|
||
|
||
# 检查是否已经有该小时的备份
|
||
if not os.path.exists(backup_file):
|
||
shutil.copy2(file_path, backup_file)
|
||
logger.info(f"创建备份: {backup_file}")
|
||
|
||
# 管理旧备份
|
||
file_prefix = os.path.basename(file_path)
|
||
manage_backups(file_prefix, max_backups=10)
|
||
except Exception as e:
|
||
logger.error(f"创建备份失败: {str(e)}")
|
||
|
||
# 先写入临时文件
|
||
temp_file = file_path + '.tmp'
|
||
try:
|
||
with open(temp_file, 'w', encoding='utf-8') as f:
|
||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||
f.flush()
|
||
os.fsync(f.fileno()) # 确保数据写入磁盘
|
||
|
||
# 替换原文件
|
||
os.replace(temp_file, file_path)
|
||
logger.info(f"成功保存数据到: {file_path}")
|
||
return True
|
||
except Exception as e:
|
||
logger.error(f"保存数据到 {file_path} 失败: {str(e)}")
|
||
if os.path.exists(temp_file):
|
||
try:
|
||
os.remove(temp_file)
|
||
except:
|
||
pass
|
||
return False
|
||
|
||
# 从文件或备份中安全地读取数据
|
||
def safe_load_data(file_path, default_value=None):
|
||
# 获取文件锁
|
||
lock = file_locks.get(file_path, threading.RLock())
|
||
|
||
with lock:
|
||
# 尝试读取原始文件
|
||
try:
|
||
with open(file_path, 'r', encoding='utf-8') as f:
|
||
return json.load(f)
|
||
except (FileNotFoundError, json.JSONDecodeError) as e:
|
||
logger.warning(f"读取文件 {file_path} 失败: {str(e)}, 尝试从备份恢复")
|
||
|
||
# 尝试从最新备份恢复
|
||
backup_file = get_backup_filename(file_path)
|
||
if os.path.exists(backup_file):
|
||
try:
|
||
with open(backup_file, 'r', encoding='utf-8') as f:
|
||
data = json.load(f)
|
||
logger.info(f"从备份 {backup_file} 恢复数据成功")
|
||
|
||
# 恢复成功后,保存回原始文件
|
||
safe_save_data(file_path, data, create_backup=False)
|
||
return data
|
||
except Exception as backup_error:
|
||
logger.error(f"从备份恢复失败: {str(backup_error)}")
|
||
|
||
# 如果指定了默认值,则返回默认值
|
||
if default_value is not None:
|
||
return default_value
|
||
|
||
# 否则,根据文件类型返回适当的空结构
|
||
if file_path == USERS_FILE:
|
||
return []
|
||
elif file_path == GAME_STATES_FILE:
|
||
return []
|
||
elif file_path == SESSIONS_FILE:
|
||
return {"active_sessions": []}
|
||
else:
|
||
return {}
|
||
|
||
# 初始化数据文件
|
||
def init_data_files():
|
||
ensure_dirs_exist()
|
||
|
||
# 初始化用户文件
|
||
if not os.path.exists(USERS_FILE):
|
||
safe_save_data(USERS_FILE, [], create_backup=False)
|
||
|
||
# 初始化游戏状态文件
|
||
if not os.path.exists(GAME_STATES_FILE):
|
||
safe_save_data(GAME_STATES_FILE, [], create_backup=False)
|
||
|
||
# 初始化会话文件
|
||
if not os.path.exists(SESSIONS_FILE):
|
||
safe_save_data(SESSIONS_FILE, {"active_sessions": []}, create_backup=False)
|
||
|
||
# 读取用户数据
|
||
def get_users():
|
||
return safe_load_data(USERS_FILE, [])
|
||
|
||
# 保存用户数据
|
||
def save_users(users):
|
||
return safe_save_data(USERS_FILE, users)
|
||
|
||
# 读取游戏状态数据
|
||
def get_game_states():
|
||
return safe_load_data(GAME_STATES_FILE, [])
|
||
|
||
# 保存游戏状态数据
|
||
def save_game_states(states):
|
||
return safe_save_data(GAME_STATES_FILE, states)
|
||
|
||
# 读取卡牌数据
|
||
def get_cards():
|
||
default_cards = {
|
||
"characters": [],
|
||
"gameSettings": {
|
||
"initialValues": {
|
||
"loyalty": 50, "chaos": 50, "population": 50, "military": 50, "resources": 50
|
||
},
|
||
"maxValues": {
|
||
"loyalty": 100, "chaos": 100, "population": 100, "military": 100, "resources": 100
|
||
},
|
||
"minValues": {
|
||
"loyalty": 0, "chaos": 0, "population": 0, "military": 0, "resources": 0
|
||
},
|
||
"statusIcons": {
|
||
"loyalty": "🦅", "chaos": "🌀", "population": "👥", "military": "🔫", "resources": "⚙️"
|
||
}
|
||
}
|
||
}
|
||
return safe_load_data(CARDS_FILE, default_cards)
|
||
|
||
# 读取会话数据
|
||
def get_sessions():
|
||
return safe_load_data(SESSIONS_FILE, {"active_sessions": []})
|
||
|
||
# 保存会话数据
|
||
def save_sessions(sessions):
|
||
return safe_save_data(SESSIONS_FILE, sessions)
|
||
|
||
# 更新会话
|
||
def update_session(user_id, status=None):
|
||
sessions = get_sessions()
|
||
active_sessions = sessions.get("active_sessions", [])
|
||
current_time = datetime.datetime.utcnow().isoformat()
|
||
|
||
# 查找当前用户会话
|
||
session_found = False
|
||
for session_data in active_sessions:
|
||
if session_data.get("user_id") == user_id:
|
||
# 更新现有会话
|
||
session_data["last_activity"] = current_time
|
||
if status:
|
||
session_data["status"] = status
|
||
session_found = True
|
||
break
|
||
|
||
# 保存更新后的会话数据
|
||
if session_found:
|
||
sessions["active_sessions"] = active_sessions
|
||
save_sessions(sessions)
|
||
return True
|
||
return False
|
||
|
||
# 添加会话
|
||
def add_session(user_id, ip_address):
|
||
sessions = get_sessions()
|
||
active_sessions = sessions.get("active_sessions", [])
|
||
|
||
# 检查是否已存在该用户的会话
|
||
for session_data in active_sessions:
|
||
if session_data.get("user_id") == user_id:
|
||
# 更新现有会话
|
||
session_data["login_time"] = datetime.datetime.utcnow().isoformat()
|
||
session_data["last_activity"] = datetime.datetime.utcnow().isoformat()
|
||
session_data["ip_address"] = ip_address
|
||
session_data["status"] = "idle"
|
||
sessions["active_sessions"] = active_sessions
|
||
save_sessions(sessions)
|
||
return True
|
||
|
||
# 创建新会话
|
||
new_session = {
|
||
"user_id": user_id,
|
||
"login_time": datetime.datetime.utcnow().isoformat(),
|
||
"last_activity": datetime.datetime.utcnow().isoformat(),
|
||
"ip_address": request.remote_addr,
|
||
"status": "idle"
|
||
}
|
||
|
||
active_sessions.append(new_session)
|
||
sessions["active_sessions"] = active_sessions
|
||
save_sessions(sessions)
|
||
return True
|
||
|
||
# 移除会话
|
||
def remove_session(user_id):
|
||
sessions = get_sessions()
|
||
active_sessions = sessions.get("active_sessions", [])
|
||
|
||
# 过滤掉指定用户的会话
|
||
original_count = len(active_sessions)
|
||
active_sessions = [s for s in active_sessions if s.get("user_id") != user_id]
|
||
|
||
if len(active_sessions) < original_count:
|
||
sessions["active_sessions"] = active_sessions
|
||
save_sessions(sessions)
|
||
return True
|
||
return False
|
||
|
||
# 清理过期会话 - 超过2小时未活动的会话
|
||
def clean_expired_sessions():
|
||
try:
|
||
sessions = get_sessions()
|
||
active_sessions = sessions.get("active_sessions", [])
|
||
current_time = datetime.datetime.utcnow()
|
||
|
||
# 只过滤掉超过2小时未活动的会话
|
||
original_count = len(active_sessions)
|
||
filtered_sessions = []
|
||
for session in active_sessions:
|
||
try:
|
||
last_activity = datetime.datetime.fromisoformat(session.get("last_activity", ""))
|
||
time_diff = (current_time - last_activity).total_seconds() / 60 # 小时差
|
||
if time_diff < 5: # 2小时超时
|
||
filtered_sessions.append(session)
|
||
except (ValueError, TypeError) as e:
|
||
logger.warning(f"处理会话时间格式错误: {str(e)}")
|
||
# 如果时间格式错误,保留该会话而不是直接删除
|
||
filtered_sessions.append(session)
|
||
|
||
# 只有在真正有会话被删除时才更新数据
|
||
if len(filtered_sessions) != original_count:
|
||
logger.info(f"清理过期会话: 清理前{original_count}个,清理后{len(filtered_sessions)}个")
|
||
sessions["active_sessions"] = filtered_sessions
|
||
save_sessions(sessions)
|
||
|
||
return True
|
||
except Exception as e:
|
||
logger.error(f"清理过期会话失败: {str(e)}")
|
||
return False
|
||
|
||
# 首页,显示排行榜
|
||
@app.route('/')
|
||
def index():
|
||
if 'user_id' not in session:
|
||
return redirect(url_for('login'))
|
||
|
||
# 获取所有用户数据,按照最高分排序
|
||
users = get_users()
|
||
leaderboard = sorted(users, key=lambda x: x.get('high_score', 0), reverse=True)
|
||
|
||
# 更新用户活动状态
|
||
update_session(session.get('user_id'))
|
||
|
||
return render_template('index.html', leaderboard=leaderboard, current_user=session.get('username'))
|
||
|
||
# 登录页面
|
||
@app.route('/login', methods=['GET', 'POST'])
|
||
def login():
|
||
error = None
|
||
|
||
if request.method == 'POST':
|
||
username = request.form['username']
|
||
password = request.form['password']
|
||
|
||
users = get_users()
|
||
user = next((u for u in users if u['username'] == username), None)
|
||
|
||
if user and check_password_hash(user['password'], password):
|
||
session['user_id'] = user['id']
|
||
session['username'] = user['username']
|
||
|
||
# 添加会话记录
|
||
add_session(user['id'], request.remote_addr)
|
||
|
||
return redirect(url_for('index'))
|
||
else:
|
||
error = '用户名或密码错误'
|
||
|
||
return render_template('login.html', error=error)
|
||
|
||
# 注册页面
|
||
@app.route('/register', methods=['GET', 'POST'])
|
||
def register():
|
||
error = None
|
||
|
||
if request.method == 'POST':
|
||
username = request.form['username']
|
||
password = request.form['password']
|
||
|
||
users = get_users()
|
||
|
||
# 检查用户名是否已存在
|
||
if any(u['username'] == username for u in users):
|
||
error = '用户名已存在'
|
||
else:
|
||
# 创建新用户
|
||
new_user = {
|
||
'id': str(uuid.uuid4()),
|
||
'username': username,
|
||
'password': generate_password_hash(password),
|
||
'created_at': datetime.datetime.utcnow().isoformat(),
|
||
'high_score': 0,
|
||
'total_games': 0,
|
||
'last_year': 41000 # 添加最后游戏年份字段,默认为41000
|
||
}
|
||
|
||
users.append(new_user)
|
||
save_users(users)
|
||
|
||
# 自动登录
|
||
session['user_id'] = new_user['id']
|
||
session['username'] = new_user['username']
|
||
|
||
# 添加会话记录
|
||
add_session(new_user['id'], request.remote_addr)
|
||
|
||
return redirect(url_for('index'))
|
||
|
||
return render_template('register.html', error=error)
|
||
|
||
# 注销
|
||
@app.route('/logout')
|
||
def logout():
|
||
if 'user_id' in session:
|
||
# 移除会话记录
|
||
remove_session(session['user_id'])
|
||
|
||
# 清除会话
|
||
session.pop('user_id', None)
|
||
session.pop('username', None)
|
||
|
||
return redirect(url_for('login'))
|
||
|
||
# 游戏页面
|
||
@app.route('/game')
|
||
def game():
|
||
if 'user_id' not in session:
|
||
return redirect(url_for('login'))
|
||
|
||
# 获取游戏数据
|
||
cards_data = get_cards()
|
||
|
||
# 获取用户最后游戏年份
|
||
users = get_users()
|
||
user = next((u for u in users if u['id'] == session['user_id']), None)
|
||
last_year = user.get('last_year', 41000) if user else 41000
|
||
|
||
# 添加最后年份到数据中
|
||
game_data = {
|
||
'cards': cards_data,
|
||
'events': cards_data.get('events', []),
|
||
'statuses': cards_data.get('statuses', []),
|
||
'lastYear': last_year
|
||
}
|
||
|
||
# 更新用户状态为游戏中
|
||
sessions = get_sessions()
|
||
active_sessions = sessions.get("active_sessions", [])
|
||
|
||
for session_data in active_sessions:
|
||
if session_data.get("user_id") == session['user_id']:
|
||
session_data["status"] = "playing"
|
||
session_data["game_start_time"] = datetime.datetime.utcnow().isoformat()
|
||
break
|
||
|
||
sessions["active_sessions"] = active_sessions
|
||
save_sessions(sessions)
|
||
|
||
return render_template('game.html',
|
||
current_user=session.get('username'),
|
||
game_data_json=json.dumps(game_data))
|
||
|
||
# API: 获取卡牌数据
|
||
@app.route('/api/cards', methods=['GET'])
|
||
def api_get_cards():
|
||
if 'user_id' not in session:
|
||
return jsonify({'error': '未授权'}), 401
|
||
|
||
cards_data = get_cards()
|
||
return jsonify(cards_data)
|
||
|
||
# API: 保存游戏状态
|
||
@app.route('/api/save_game', methods=['POST'])
|
||
def api_save_game():
|
||
if 'user_id' not in session:
|
||
return jsonify({'error': '未授权'}), 401
|
||
|
||
user_id = session['user_id']
|
||
game_data = request.json
|
||
|
||
# 更新用户的最后游戏年份
|
||
if 'year' in game_data:
|
||
users = get_users()
|
||
user = next((u for u in users if u['id'] == user_id), None)
|
||
if user:
|
||
user['last_year'] = game_data['year']
|
||
user['last_game_time'] = datetime.datetime.utcnow().isoformat()
|
||
save_users(users)
|
||
|
||
# 获取现有游戏状态
|
||
game_states = get_game_states()
|
||
|
||
# 查找用户的游戏状态
|
||
user_state = next((s for s in game_states if s['user_id'] == user_id), None)
|
||
|
||
if user_state:
|
||
# 更新现有状态
|
||
user_state['game_data'] = game_data
|
||
user_state['updated_at'] = datetime.datetime.utcnow().isoformat()
|
||
else:
|
||
# 创建新状态
|
||
game_states.append({
|
||
'id': str(uuid.uuid4()),
|
||
'user_id': user_id,
|
||
'game_data': game_data,
|
||
'created_at': datetime.datetime.utcnow().isoformat(),
|
||
'updated_at': datetime.datetime.utcnow().isoformat()
|
||
})
|
||
|
||
save_game_states(game_states)
|
||
|
||
# 更新会话状态
|
||
update_session(user_id, "playing")
|
||
|
||
return jsonify({'success': True})
|
||
|
||
# API: 加载游戏状态
|
||
@app.route('/api/load_game', methods=['GET'])
|
||
def api_load_game():
|
||
if 'user_id' not in session:
|
||
return jsonify({'error': '未授权'}), 401
|
||
|
||
user_id = session['user_id']
|
||
|
||
# 获取所有游戏状态
|
||
game_states = get_game_states()
|
||
|
||
# 查找用户的游戏状态
|
||
user_state = next((s for s in game_states if s['user_id'] == user_id), None)
|
||
|
||
# 获取用户的最后游戏年份
|
||
users = get_users()
|
||
user = next((u for u in users if u['id'] == user_id), None)
|
||
last_year = user.get('last_year', 41000) if user else 41000
|
||
|
||
if user_state:
|
||
# 确保游戏状态中包含最新的年份
|
||
game_data = user_state['game_data']
|
||
if isinstance(game_data, dict) and 'year' not in game_data:
|
||
game_data['year'] = last_year
|
||
return jsonify({'game_data': game_data, 'lastYear': last_year})
|
||
else:
|
||
return jsonify({'game_data': None, 'lastYear': last_year})
|
||
|
||
# API: 更新高分
|
||
@app.route('/api/update_score', methods=['POST'])
|
||
def api_update_score():
|
||
if 'user_id' not in session:
|
||
return jsonify({'error': '未授权'}), 401
|
||
|
||
user_id = session['user_id']
|
||
score = request.json.get('score', 0)
|
||
year = request.json.get('year', 41000) # 获取当前年份
|
||
|
||
# 获取所有用户
|
||
users = get_users()
|
||
|
||
# 查找当前用户
|
||
user = next((u for u in users if u['id'] == user_id), None)
|
||
|
||
if user:
|
||
# 只有当新分数更高时才更新
|
||
if score > user.get('high_score', 0):
|
||
user['high_score'] = score
|
||
|
||
# 增加游戏次数
|
||
user['total_games'] = user.get('total_games', 0) + 1
|
||
|
||
# 更新最后游戏年份
|
||
user['last_year'] = year
|
||
|
||
# 更新最后游戏时间
|
||
user['last_game_time'] = datetime.datetime.utcnow().isoformat()
|
||
|
||
save_users(users)
|
||
|
||
# 更新会话状态为空闲
|
||
update_session(user_id, "idle")
|
||
|
||
return jsonify({'success': True})
|
||
else:
|
||
return jsonify({'error': '用户不存在'}), 404
|
||
|
||
# API: 获取排行榜数据
|
||
@app.route('/api/leaderboard', methods=['GET'])
|
||
def api_leaderboard():
|
||
if 'user_id' not in session:
|
||
return jsonify({'error': '未授权'}), 401
|
||
|
||
# 获取所有用户,按高分排序
|
||
users = get_users()
|
||
leaderboard = sorted(users, key=lambda x: x.get('high_score', 0), reverse=True)
|
||
|
||
leaderboard = leaderboard[:100] # 添加这一行,限制为前100名
|
||
|
||
# 只返回需要的字段 (不再限制为前10名)
|
||
leaderboard_data = [
|
||
{
|
||
'username': user['username'],
|
||
'high_score': user.get('high_score', 0),
|
||
'total_games': user.get('total_games', 0),
|
||
'last_year': user.get('last_year', 41000)
|
||
}
|
||
for user in leaderboard # 移除了[:10]限制
|
||
]
|
||
|
||
return jsonify(leaderboard_data)
|
||
|
||
# 每小时自动清理过期会话的定时任务
|
||
def scheduled_cleanup():
|
||
while True:
|
||
try:
|
||
# 每小时清理一次过期会话
|
||
clean_expired_sessions()
|
||
# 休眠1小时
|
||
time.sleep(3600)
|
||
except Exception as e:
|
||
logger.error(f"定时清理任务失败: {str(e)}")
|
||
# 出错后仍然继续
|
||
time.sleep(60)
|
||
# 自制卡牌数据文件路径
|
||
CUSTOM_CARDS_FILE = os.path.join(DATA_DIR, 'custom_cards.json')
|
||
CARD_VOTES_FILE = os.path.join(DATA_DIR, 'card_votes.json')
|
||
|
||
# 读取自制卡牌数据
|
||
def get_custom_cards():
|
||
return safe_load_data(CUSTOM_CARDS_FILE, {"cards": []})
|
||
|
||
# 保存自制卡牌数据
|
||
def save_custom_cards(data):
|
||
return safe_save_data(CUSTOM_CARDS_FILE, data)
|
||
|
||
# 读取卡牌投票数据
|
||
def get_card_votes():
|
||
return safe_load_data(CARD_VOTES_FILE, {"user_votes": {}, "card_votes": {}})
|
||
|
||
# 保存卡牌投票数据
|
||
def save_card_votes(data):
|
||
return safe_save_data(CARD_VOTES_FILE, data)
|
||
|
||
# 自制卡牌页面路由
|
||
@app.route('/custom_cards')
|
||
def custom_cards():
|
||
if 'user_id' not in session:
|
||
return redirect(url_for('login'))
|
||
|
||
return render_template('custom_cards.html')
|
||
|
||
# 创建卡牌页面路由
|
||
@app.route('/create_card')
|
||
def create_card():
|
||
if 'user_id' not in session:
|
||
return redirect(url_for('login'))
|
||
|
||
return render_template('create_card.html')
|
||
|
||
# API: 获取当前用户ID
|
||
@app.route('/api/current_user', methods=['GET'])
|
||
def api_current_user():
|
||
if 'user_id' not in session:
|
||
return jsonify({'error': '未授权'}), 401
|
||
|
||
return jsonify({
|
||
'user_id': session['user_id'],
|
||
'username': session.get('username', '')
|
||
})
|
||
|
||
# API: 获取用户投票历史
|
||
@app.route('/api/user_votes', methods=['GET'])
|
||
def api_user_votes():
|
||
if 'user_id' not in session:
|
||
return jsonify({'error': '未授权'}), 401
|
||
|
||
user_id = session['user_id']
|
||
votes_data = get_card_votes()
|
||
|
||
# 获取用户的投票记录
|
||
user_votes = votes_data.get('user_votes', {}).get(user_id, {})
|
||
|
||
return jsonify({
|
||
'votes': user_votes
|
||
})
|
||
|
||
# API: 获取自制卡牌列表
|
||
@app.route('/api/custom_cards', methods=['GET'])
|
||
def api_custom_cards():
|
||
if 'user_id' not in session:
|
||
return jsonify({'error': '未授权'}), 401
|
||
|
||
# 获取卡牌数据
|
||
cards_data = get_custom_cards()
|
||
cards = cards_data.get('cards', [])
|
||
|
||
# 获取投票数据
|
||
votes_data = get_card_votes()
|
||
card_votes = votes_data.get('card_votes', {})
|
||
|
||
# 给每个卡片添加投票信息
|
||
for card in cards:
|
||
card_id = card['id']
|
||
if card_id in card_votes:
|
||
card['upvotes'] = card_votes[card_id].get('upvotes', 0)
|
||
card['downvotes'] = card_votes[card_id].get('downvotes', 0)
|
||
else:
|
||
card['upvotes'] = 0
|
||
card['downvotes'] = 0
|
||
|
||
return jsonify({
|
||
'cards': cards
|
||
})
|
||
|
||
# API: 创建自制卡牌
|
||
@app.route('/api/create_custom_card', methods=['POST'])
|
||
def api_create_custom_card():
|
||
if 'user_id' not in session:
|
||
return jsonify({'error': '未授权'}), 401
|
||
|
||
user_id = session['user_id']
|
||
username = session.get('username', '未知用户')
|
||
card_data = request.json
|
||
|
||
# 验证必要字段
|
||
required_fields = ['title', 'character', 'description', 'option_a', 'option_b']
|
||
for field in required_fields:
|
||
if field not in card_data:
|
||
return jsonify({'error': f'缺少必要字段: {field}'}), 400
|
||
|
||
# 验证字符长度
|
||
if len(card_data['title']) > 50:
|
||
return jsonify({'error': '卡牌标题不能超过50个字符'}), 400
|
||
|
||
if len(card_data['description']) > 200:
|
||
return jsonify({'error': '卡牌描述不能超过200个字符'}), 400
|
||
|
||
if len(card_data['option_a']['text']) > 50:
|
||
return jsonify({'error': '选项A文本不能超过50个字符'}), 400
|
||
|
||
if len(card_data['option_b']['text']) > 50:
|
||
return jsonify({'error': '选项B文本不能超过50个字符'}), 400
|
||
|
||
# 验证角色字段
|
||
character_fields = ['name', 'title', 'avatar']
|
||
for field in character_fields:
|
||
if field not in card_data['character']:
|
||
return jsonify({'error': f'缺少角色字段: {field}'}), 400
|
||
|
||
if len(card_data['character']['name']) > 30:
|
||
return jsonify({'error': '角色名称不能超过30个字符'}), 400
|
||
|
||
if len(card_data['character']['title']) > 30:
|
||
return jsonify({'error': '角色头衔不能超过30个字符'}), 400
|
||
|
||
if len(card_data['character']['avatar']) > 5:
|
||
return jsonify({'error': '角色图标不能超过5个字符'}), 400
|
||
|
||
# 验证效果值
|
||
option_a_effects = card_data['option_a']['effects']
|
||
option_b_effects = card_data['option_b']['effects']
|
||
|
||
# 验证效果值范围
|
||
for effect_name, effect_value in option_a_effects.items():
|
||
if abs(effect_value) > 25:
|
||
return jsonify({'error': f'选项A的{effect_name}效果值不能超过±25'}), 400
|
||
|
||
for effect_name, effect_value in option_b_effects.items():
|
||
if abs(effect_value) > 25:
|
||
return jsonify({'error': f'选项B的{effect_name}效果值不能超过±25'}), 400
|
||
|
||
# 验证效果值总和
|
||
option_a_total = sum(abs(v) for v in option_a_effects.values())
|
||
option_b_total = sum(abs(v) for v in option_b_effects.values())
|
||
|
||
if option_a_total > 80:
|
||
return jsonify({'error': '选项A的效果总和不能超过80'}), 400
|
||
|
||
if option_b_total > 80:
|
||
return jsonify({'error': '选项B的效果总和不能超过80'}), 400
|
||
|
||
# 获取现有卡牌数据
|
||
cards_data = get_custom_cards()
|
||
cards = cards_data.get('cards', [])
|
||
|
||
# 创建新卡牌
|
||
new_card = {
|
||
'id': str(uuid.uuid4()),
|
||
'title': card_data['title'],
|
||
'character': card_data['character'],
|
||
'description': card_data['description'],
|
||
'option_a': card_data['option_a'],
|
||
'option_b': card_data['option_b'],
|
||
'creator_id': user_id,
|
||
'creator_name': username,
|
||
'created_at': datetime.datetime.utcnow().isoformat()
|
||
}
|
||
|
||
# 添加到卡牌列表
|
||
cards.append(new_card)
|
||
cards_data['cards'] = cards
|
||
|
||
# 保存卡牌数据
|
||
save_custom_cards(cards_data)
|
||
|
||
# 获取投票数据
|
||
votes_data = get_card_votes()
|
||
card_votes = votes_data.get('card_votes', {})
|
||
|
||
# 初始化卡牌投票数据
|
||
card_votes[new_card['id']] = {
|
||
'upvotes': 0,
|
||
'downvotes': 0,
|
||
'users': {}
|
||
}
|
||
|
||
votes_data['card_votes'] = card_votes
|
||
save_card_votes(votes_data)
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'card': new_card
|
||
})
|
||
|
||
# API: 投票自制卡牌
|
||
@app.route('/api/vote_card', methods=['POST'])
|
||
def api_vote_card():
|
||
if 'user_id' not in session:
|
||
return jsonify({'error': '未授权'}), 401
|
||
|
||
user_id = session['user_id']
|
||
request_data = request.json
|
||
|
||
# 验证参数
|
||
if 'card_id' not in request_data or 'vote_type' not in request_data:
|
||
return jsonify({'error': '缺少必要参数'}), 400
|
||
|
||
card_id = request_data['card_id']
|
||
vote_type = request_data['vote_type']
|
||
|
||
# 验证投票类型
|
||
if vote_type not in ['upvote', 'downvote', 'none']:
|
||
return jsonify({'error': '无效的投票类型'}), 400
|
||
|
||
# 获取投票数据
|
||
votes_data = get_card_votes()
|
||
user_votes = votes_data.get('user_votes', {})
|
||
card_votes = votes_data.get('card_votes', {})
|
||
|
||
# 初始化用户投票数据
|
||
if user_id not in user_votes:
|
||
user_votes[user_id] = {}
|
||
|
||
# 获取卡牌数据
|
||
cards_data = get_custom_cards()
|
||
cards = cards_data.get('cards', [])
|
||
|
||
# 查找卡牌
|
||
card = next((c for c in cards if c['id'] == card_id), None)
|
||
if not card:
|
||
return jsonify({'error': '卡牌不存在'}), 404
|
||
|
||
# 初始化卡牌投票数据
|
||
if card_id not in card_votes:
|
||
card_votes[card_id] = {
|
||
'upvotes': 0,
|
||
'downvotes': 0,
|
||
'users': {}
|
||
}
|
||
|
||
# 获取用户之前的投票
|
||
previous_vote = user_votes.get(user_id, {}).get(card_id)
|
||
|
||
# 处理投票逻辑
|
||
if vote_type == 'none' or (previous_vote == vote_type):
|
||
# 取消投票
|
||
if previous_vote:
|
||
if previous_vote == 'upvote':
|
||
card_votes[card_id]['upvotes'] = max(0, card_votes[card_id]['upvotes'] - 1)
|
||
else:
|
||
card_votes[card_id]['downvotes'] = max(0, card_votes[card_id]['downvotes'] - 1)
|
||
|
||
# 从用户投票中移除
|
||
if card_id in user_votes[user_id]:
|
||
del user_votes[user_id][card_id]
|
||
|
||
# 从卡牌用户列表中移除
|
||
if user_id in card_votes[card_id]['users']:
|
||
del card_votes[card_id]['users'][user_id]
|
||
|
||
new_vote = None
|
||
else:
|
||
# 如果之前投过票,先取消之前的投票
|
||
if previous_vote:
|
||
if previous_vote == 'upvote':
|
||
card_votes[card_id]['upvotes'] = max(0, card_votes[card_id]['upvotes'] - 1)
|
||
else:
|
||
card_votes[card_id]['downvotes'] = max(0, card_votes[card_id]['downvotes'] - 1)
|
||
|
||
# 添加新投票
|
||
if vote_type == 'upvote':
|
||
card_votes[card_id]['upvotes'] += 1
|
||
else:
|
||
card_votes[card_id]['downvotes'] += 1
|
||
|
||
# 更新用户投票
|
||
user_votes[user_id][card_id] = vote_type
|
||
card_votes[card_id]['users'][user_id] = vote_type
|
||
new_vote = vote_type
|
||
|
||
# 保存投票数据
|
||
votes_data['user_votes'] = user_votes
|
||
votes_data['card_votes'] = card_votes
|
||
save_card_votes(votes_data)
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'upvotes': card_votes[card_id]['upvotes'],
|
||
'downvotes': card_votes[card_id]['downvotes'],
|
||
'newVote': new_vote
|
||
})
|
||
|
||
# 管理员API:获取自制卡牌列表
|
||
@app.route('/api/admin/custom_cards', methods=['GET'])
|
||
def api_admin_custom_cards():
|
||
# 获取卡牌数据
|
||
cards_data = get_custom_cards()
|
||
cards = cards_data.get('cards', [])
|
||
|
||
# 获取投票数据
|
||
votes_data = get_card_votes()
|
||
card_votes = votes_data.get('card_votes', {})
|
||
|
||
# 给每个卡片添加投票信息
|
||
for card in cards:
|
||
card_id = card['id']
|
||
if card_id in card_votes:
|
||
card['upvotes'] = card_votes[card_id].get('upvotes', 0)
|
||
card['downvotes'] = card_votes[card_id].get('downvotes', 0)
|
||
else:
|
||
card['upvotes'] = 0
|
||
card['downvotes'] = 0
|
||
|
||
return jsonify({
|
||
'cards': cards
|
||
})
|
||
|
||
# 管理员API:删除自制卡牌
|
||
@app.route('/api/admin/delete_card/<card_id>', methods=['DELETE'])
|
||
def api_admin_delete_card(card_id):
|
||
# 获取卡牌数据
|
||
cards_data = get_custom_cards()
|
||
cards = cards_data.get('cards', [])
|
||
|
||
# 查找并删除卡牌
|
||
initial_count = len(cards)
|
||
cards = [c for c in cards if c['id'] != card_id]
|
||
|
||
if len(cards) < initial_count:
|
||
# 更新卡牌数据
|
||
cards_data['cards'] = cards
|
||
save_custom_cards(cards_data)
|
||
|
||
# 也从投票数据中删除
|
||
votes_data = get_card_votes()
|
||
card_votes = votes_data.get('card_votes', {})
|
||
user_votes = votes_data.get('user_votes', {})
|
||
|
||
# 从卡牌投票中删除
|
||
if card_id in card_votes:
|
||
del card_votes[card_id]
|
||
|
||
# 从用户投票中删除
|
||
for user_id in user_votes:
|
||
if card_id in user_votes[user_id]:
|
||
del user_votes[user_id][card_id]
|
||
|
||
votes_data['card_votes'] = card_votes
|
||
votes_data['user_votes'] = user_votes
|
||
save_card_votes(votes_data)
|
||
|
||
return jsonify({'success': True})
|
||
else:
|
||
return jsonify({'error': '卡牌不存在'}), 404
|
||
|
||
# 添加到现有管理员页面
|
||
@app.route('/admin/custom_cards')
|
||
def admin_custom_cards():
|
||
return render_template('admin/custom_cards.html')
|
||
@app.after_request
|
||
def add_cache_headers(response):
|
||
# 只对静态文件设置缓存
|
||
if request.path.startswith('/static/'):
|
||
# 设置一天的缓存时间
|
||
response.headers['Cache-Control'] = 'public, max-age=43200'
|
||
return response
|
||
|
||
# 初始化应用
|
||
init_data_files()
|
||
|
||
# 启动定时清理线程
|
||
cleanup_thread = threading.Thread(target=scheduled_cleanup, daemon=True)
|
||
cleanup_thread.start()
|
||
|
||
# 启动服务器
|
||
if __name__ == '__main__':
|
||
app.run(debug=True, port=5001) # 使用5005端口避开常用端口 |