from flask import Blueprint, request, jsonify, make_response from flask_jwt_extended import get_jwt_identity from models import db, Task, User from auth import login_required, admin_required from datetime import datetime, timedelta from sqlalchemy import func, and_ import csv from io import StringIO statistics_bp = Blueprint('statistics', __name__) # 个人统计 @statistics_bp.route('/statistics/user/', methods=['GET']) @login_required def get_user_statistics(user_id): current_user_id = int(get_jwt_identity()) current_user = db.session.get(User, current_user_id) # 非管理员只能查看自己的统计 if current_user.role != 'admin' and current_user_id != user_id: return jsonify({'error': '无权查看他人统计'}), 403 date_from = request.args.get('date_from') date_to = request.args.get('date_to') group_id = request.args.get('group_id', type=int) query = Task.query.filter_by(claimed_by=user_id, status='completed') if group_id: query = query.filter_by(group_id=group_id) if date_from: try: date_from_obj = datetime.strptime(date_from, '%Y-%m-%d') query = query.filter(Task.completed_at >= date_from_obj) except: pass if date_to: try: date_to_obj = datetime.strptime(date_to, '%Y-%m-%d') date_to_obj = date_to_obj.replace(hour=23, minute=59, second=59) query = query.filter(Task.completed_at <= date_to_obj) except: pass # 总完成数 total_completed = query.count() # 按日期分组统计 daily_stats = db.session.query( func.date(Task.completed_at).label('date'), func.count(Task.id).label('count') ).filter( Task.claimed_by == user_id, Task.status == 'completed' ) if date_from: daily_stats = daily_stats.filter(Task.completed_at >= date_from_obj) if date_to: daily_stats = daily_stats.filter(Task.completed_at <= date_to_obj) daily_stats = daily_stats.group_by(func.date(Task.completed_at)).all() # 当前认领未完成 claimed_pending = Task.query.filter_by( claimed_by=user_id, status='claimed' ).count() user = db.session.get(User, user_id) return jsonify({ 'user': { 'id': user.id, 'username': user.username }, 'total_completed': total_completed, 'claimed_pending': claimed_pending, 'daily_stats': [ { 'date': str(stat.date), 'count': stat.count } for stat in daily_stats ] }) # 组别排行榜 @statistics_bp.route('/statistics/leaderboard', methods=['GET']) @login_required def get_leaderboard(): group_id = request.args.get('group_id', type=int) period = request.args.get('period', 'monthly') # daily/monthly now = datetime.utcnow() if period == 'daily': start_date = now.replace(hour=0, minute=0, second=0, microsecond=0) else: # monthly start_date = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0) query = db.session.query( User.id, User.username, func.count(Task.id).label('completed_count') ).join( Task, Task.claimed_by == User.id ).filter( Task.status == 'completed', Task.completed_at >= start_date ) if group_id: query = query.filter(Task.group_id == group_id) leaderboard = query.group_by(User.id, User.username).order_by( func.count(Task.id).desc() ).limit(20).all() return jsonify({ 'period': period, 'leaderboard': [ { 'rank': idx + 1, 'user_id': row.id, 'username': row.username, 'completed_count': row.completed_count } for idx, row in enumerate(leaderboard) ] }) # 整体统计(支持管理员和普通用户) @statistics_bp.route('/statistics/overview', methods=['GET']) @login_required def get_overview(): current_user_id = get_jwt_identity() current_user = db.session.get(User, current_user_id) group_id = request.args.get('group_id', type=int, default=1) # 获取时间范围参数 date_from = request.args.get('date_from') date_to = request.args.get('date_to') # 基础查询条件 base_filter = [Task.group_id == group_id] # 普通用户只能看自己的数据 if current_user.role != 'admin': base_filter.append(Task.claimed_by == current_user_id) # 解析时间范围 date_from_obj = None date_to_obj = None if date_from: try: date_from_obj = datetime.strptime(date_from, '%Y-%m-%d') except: pass if date_to: try: date_to_obj = datetime.strptime(date_to, '%Y-%m-%d') date_to_obj = date_to_obj.replace(hour=23, minute=59, second=59) except: pass # 各状态任务数量 status_query = db.session.query( Task.status, func.count(Task.id).label('count') ).filter(and_(*base_filter)).group_by(Task.status) status_counts = status_query.all() # 今日完成 today = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0) today_filter = base_filter + [ Task.status == 'completed', Task.completed_at >= today ] today_completed = Task.query.filter(and_(*today_filter)).count() # 本月完成 month_start = datetime.utcnow().replace(day=1, hour=0, minute=0, second=0, microsecond=0) month_filter = base_filter + [ Task.status == 'completed', Task.completed_at >= month_start ] month_completed = Task.query.filter(and_(*month_filter)).count() # 趋势图(根据时间范围或默认最近7天) if date_from_obj: trend_start = date_from_obj else: trend_start = datetime.utcnow() - timedelta(days=7) trend_filter = base_filter + [ Task.status == 'completed', Task.completed_at >= trend_start ] if date_to_obj: trend_filter.append(Task.completed_at <= date_to_obj) trend = db.session.query( func.date(Task.completed_at).label('date'), func.count(Task.id).label('count') ).filter(and_(*trend_filter)).group_by(func.date(Task.completed_at)).all() # 计算时间范围内的总完成数 completed_filter = base_filter + [Task.status == 'completed'] if date_from_obj: completed_filter.append(Task.completed_at >= date_from_obj) if date_to_obj: completed_filter.append(Task.completed_at <= date_to_obj) total_completed = Task.query.filter(and_(*completed_filter)).count() # 当前认领未完成数 claimed_filter = base_filter + [Task.status == 'claimed'] claimed_pending = Task.query.filter(and_(*claimed_filter)).count() return jsonify({ 'status_counts': {row.status: row.count for row in status_counts}, 'today_completed': today_completed, 'month_completed': month_completed, 'total_completed': total_completed, 'claimed_pending': claimed_pending, 'is_admin': current_user.role == 'admin', 'trend': [ {'date': str(row.date), 'count': row.count} for row in trend ] }) # 导出统计数据(CSV格式) @statistics_bp.route('/statistics/export', methods=['GET']) @login_required def export_statistics(): current_user_id = get_jwt_identity() current_user = db.session.get(User, current_user_id) group_id = request.args.get('group_id', type=int, default=1) date_from = request.args.get('date_from') date_to = request.args.get('date_to') # 构建查询条件 query = Task.query.filter_by(group_id=group_id, status='completed') # 普通用户只能导出自己的数据 if current_user.role != 'admin': query = query.filter_by(claimed_by=current_user_id) # 时间范围筛选 if date_from: try: date_from_obj = datetime.strptime(date_from, '%Y-%m-%d') query = query.filter(Task.completed_at >= date_from_obj) except: pass if date_to: try: date_to_obj = datetime.strptime(date_to, '%Y-%m-%d') date_to_obj = date_to_obj.replace(hour=23, minute=59, second=59) query = query.filter(Task.completed_at <= date_to_obj) except: pass tasks = query.order_by(Task.completed_at.desc()).all() # 生成CSV output = StringIO() writer = csv.writer(output) # 写入表头 writer.writerow(['任务ID', '剧集名称', '优先级', '认领人', '种子ID', '完成时间', '认领时间', '耗时(小时)']) # 写入数据 for task in tasks: duration = '' if task.claimed_at and task.completed_at: delta = task.completed_at - task.claimed_at duration = f'{delta.total_seconds() / 3600:.1f}' claimer_name = task.claimer.username if task.claimer else '-' writer.writerow([ task.id, task.series_name, task.priority or '-', claimer_name, task.torrent_id or '-', task.completed_at.strftime('%Y-%m-%d %H:%M:%S') if task.completed_at else '-', task.claimed_at.strftime('%Y-%m-%d %H:%M:%S') if task.claimed_at else '-', duration ]) # 创建响应 response = make_response(output.getvalue()) response.headers['Content-Type'] = 'text/csv; charset=utf-8-sig' response.headers['Content-Disposition'] = f'attachment; filename=statistics_{datetime.now().strftime("%Y%m%d_%H%M%S")}.csv' return response