Warhummer/app.py
2025-06-25 09:35:26 +08:00

1002 lines
33 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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端口避开常用端口