This commit is contained in:
DengDai
2025-12-09 13:08:38 +08:00
commit 02ecea06f8
36 changed files with 5876 additions and 0 deletions

0
api/__init__.py Normal file
View File

300
api/statistics.py Normal file
View File

@@ -0,0 +1,300 @@
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/<int:user_id>', 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

250
api/tasks.py Normal file
View File

@@ -0,0 +1,250 @@
from flask import Blueprint, request, jsonify
from flask_jwt_extended import get_jwt_identity
from models import db, Task, TaskLog, Group, User
from auth import login_required, admin_required, group_member_required, get_current_user
from datetime import datetime
from sqlalchemy import func, and_
tasks_bp = Blueprint('tasks', __name__)
# 获取任务列表
@tasks_bp.route('/groups/<int:group_id>/tasks', methods=['GET'])
@login_required
def get_tasks(group_id):
status = request.args.get('status') # pending/claimed/completed
user_id = request.args.get('user_id', type=int)
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 20, type=int)
query = Task.query.filter_by(group_id=group_id)
if status:
query = query.filter_by(status=status)
if user_id:
query = query.filter_by(claimed_by=user_id)
query = query.order_by(Task.series_date.desc(), Task.created_at.desc())
pagination = query.paginate(page=page, per_page=per_page, error_out=False)
tasks = []
for task in pagination.items:
tasks.append({
'id': task.id,
'series_name': task.series_name,
'series_link': task.series_link,
'series_date': task.series_date.strftime('%Y-%m-%d') if task.series_date else None,
'priority': task.priority,
'status': task.status,
'claimed_by': task.claimer.username if task.claimer else None,
'claimed_by_id': task.claimed_by,
'claimed_at': task.claimed_at.strftime('%Y-%m-%d %H:%M') if task.claimed_at else None,
'claim_note': task.claim_note,
'torrent_id': task.torrent_id,
'complete_note': task.complete_note,
'completed_at': task.completed_at.strftime('%Y-%m-%d %H:%M') if task.completed_at else None,
'creator': task.creator.username,
'created_at': task.created_at.strftime('%Y-%m-%d %H:%M')
})
return jsonify({
'tasks': tasks,
'total': pagination.total,
'page': page,
'pages': pagination.pages
})
# 创建任务(管理员)
@tasks_bp.route('/groups/<int:group_id>/tasks', methods=['POST'])
@admin_required
def create_task(group_id):
data = request.json
# 验证必填字段
if not data.get('series_name'):
return jsonify({'error': '剧集名称不能为空'}), 400
if not data.get('series_date'):
return jsonify({'error': '剧集更新日期不能为空'}), 400
try:
series_date = datetime.strptime(data['series_date'], '%Y-%m-%d').date()
except:
return jsonify({'error': '日期格式错误'}), 400
user_id = get_jwt_identity()
task = Task(
group_id=group_id,
series_name=data['series_name'],
series_link=data.get('series_link'),
series_date=series_date,
priority=data.get('priority', ''),
created_by=user_id
)
db.session.add(task)
db.session.flush()
# 记录日志
log = TaskLog(
task_id=task.id,
user_id=user_id,
action='create',
comment=f'创建任务:{task.series_name}'
)
db.session.add(log)
db.session.commit()
return jsonify({'message': '任务创建成功', 'task_id': task.id}), 201
# 认领任务
@tasks_bp.route('/tasks/<int:task_id>/claim', methods=['POST'])
@login_required
def claim_task(task_id):
task = db.session.get(Task, task_id)
if not task:
return jsonify({'error': '任务不存在'}), 404
if task.status != 'pending':
return jsonify({'error': '该任务已被认领或已完成'}), 400
user_id = get_jwt_identity()
data = request.json or {}
# 使用数据库锁防止并发认领
task = db.session.query(Task).filter_by(id=task_id, status='pending').with_for_update().first()
if not task:
return jsonify({'error': '任务已被他人认领'}), 400
task.status = 'claimed'
task.claimed_by = user_id
task.claimed_at = datetime.utcnow()
task.claim_note = data.get('claim_note')
log = TaskLog(
task_id=task_id,
user_id=user_id,
action='claim',
comment=data.get('claim_note')
)
db.session.add(log)
db.session.commit()
return jsonify({'message': '认领成功'})
# 完成任务
# 完成任务
@tasks_bp.route('/tasks/<int:task_id>/complete', methods=['POST'])
@login_required
def complete_task(task_id):
task = db.session.get(Task, task_id)
if not task:
return jsonify({'error': '任务不存在'}), 404
user_id = get_jwt_identity()
# 确保类型一致,转换为整数比较
if task.claimed_by != int(user_id):
return jsonify({'error': f'只能完成自己认领的任务 (任务认领者: {task.claimed_by}, 当前用户: {user_id})'}), 403
if task.status != 'claimed':
return jsonify({'error': '任务状态错误'}), 400
data = request.json
if not data.get('torrent_id'):
return jsonify({'error': '种子ID不能为空'}), 400
task.status = 'completed'
task.torrent_id = data['torrent_id']
task.complete_note = data.get('complete_note')
task.completed_at = datetime.utcnow()
log = TaskLog(
task_id=task_id,
user_id=int(user_id),
action='complete',
comment=f'种子ID: {data["torrent_id"]}'
)
db.session.add(log)
db.session.commit()
return jsonify({'message': '任务完成'})
# 取消认领(用户自己)
@tasks_bp.route('/tasks/<int:task_id>/cancel-claim', methods=['POST'])
@login_required
def cancel_claim(task_id):
task = db.session.get(Task, task_id)
if not task:
return jsonify({'error': '任务不存在'}), 404
user_id = get_jwt_identity()
if task.claimed_by != user_id:
return jsonify({'error': '只能取消自己认领的任务'}), 403
if task.status != 'claimed':
return jsonify({'error': '任务状态错误'}), 400
task.status = 'pending'
task.claimed_by = None
task.claimed_at = None
task.claim_note = None
log = TaskLog(
task_id=task_id,
user_id=user_id,
action='cancel_claim',
comment='取消认领'
)
db.session.add(log)
db.session.commit()
return jsonify({'message': '已取消认领'})
# 删除任务(管理员)
@tasks_bp.route('/tasks/<int:task_id>', methods=['DELETE'])
@admin_required
def delete_task(task_id):
task = db.session.get(Task, task_id)
if not task:
return jsonify({'error': '任务不存在'}), 404
db.session.delete(task)
db.session.commit()
return jsonify({'message': '任务已删除'})
# 获取任务详情
@tasks_bp.route('/tasks/<int:task_id>', methods=['GET'])
@login_required
def get_task(task_id):
task = db.session.get(Task, task_id)
if not task:
return jsonify({'error': '任务不存在'}), 404
return jsonify({
'id': task.id,
'series_name': task.series_name,
'series_link': task.series_link,
'series_date': task.series_date.strftime('%Y-%m-%d'),
'priority': task.priority,
'status': task.status,
'claimed_by': task.claimer.username if task.claimer else None,
'claimed_at': task.claimed_at.strftime('%Y-%m-%d %H:%M') if task.claimed_at else None,
'claim_note': task.claim_note,
'torrent_id': task.torrent_id,
'complete_note': task.complete_note,
'completed_at': task.completed_at.strftime('%Y-%m-%d %H:%M') if task.completed_at else None,
'creator': task.creator.username,
'created_at': task.created_at.strftime('%Y-%m-%d %H:%M')
})

199
api/user_management.py Normal file
View File

@@ -0,0 +1,199 @@
from flask import Blueprint, request, jsonify
from flask_jwt_extended import get_jwt_identity
from models import db, User
from auth import login_required, admin_required
from datetime import datetime
user_mgmt_bp = Blueprint('user_management', __name__)
# 用户注册
@user_mgmt_bp.route('/register', methods=['POST'])
def register():
data = request.json
if not all([data.get('username'), data.get('email'), data.get('password')]):
return jsonify({'error': '用户名、邮箱和密码不能为空'}), 400
if User.query.filter_by(username=data['username']).first():
return jsonify({'error': '用户名已存在'}), 400
if User.query.filter_by(email=data['email']).first():
return jsonify({'error': '邮箱已被注册'}), 400
user = User(
username=data['username'],
email=data['email'],
uid=data.get('uid'),
status='pending'
)
user.set_password(data['password'])
db.session.add(user)
db.session.commit()
return jsonify({'message': '注册成功,请等待管理员审核'}), 201
# 获取用户列表(管理员)
@user_mgmt_bp.route('/users', methods=['GET'])
@admin_required
def get_users():
status = request.args.get('status')
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 20, type=int)
query = User.query
if status:
query = query.filter_by(status=status)
pagination = query.order_by(User.created_at.desc()).paginate(
page=page, per_page=per_page, error_out=False
)
return jsonify({
'users': [{
'id': u.id,
'username': u.username,
'email': u.email,
'uid': u.uid,
'role': u.role,
'status': u.status,
'tags': u.tags,
'note': u.note,
'created_at': u.created_at.strftime('%Y-%m-%d %H:%M'),
'approved_at': u.approved_at.strftime('%Y-%m-%d %H:%M') if u.approved_at else None
} for u in pagination.items],
'total': pagination.total,
'page': page,
'pages': pagination.pages
})
# 审核用户(管理员)
@user_mgmt_bp.route('/users/<int:user_id>/approve', methods=['POST'])
@admin_required
def approve_user(user_id):
user = db.session.get(User, user_id)
if not user:
return jsonify({'error': '用户不存在'}), 404
data = request.json or {}
action = data.get('action') # approve/reject
if action == 'approve':
user.status = 'active'
user.approved_at = datetime.utcnow()
user.approved_by = get_jwt_identity()
db.session.commit()
return jsonify({'message': '已通过审核'})
elif action == 'reject':
db.session.delete(user)
db.session.commit()
return jsonify({'message': '已拒绝申请'})
return jsonify({'error': '无效的操作'}), 400
# 编辑用户(管理员)
@user_mgmt_bp.route('/users/<int:user_id>', methods=['PUT'])
@admin_required
def update_user(user_id):
current_user_id = get_jwt_identity()
if current_user_id == user_id:
return jsonify({'error': '不能修改自己的信息'}), 403
user = db.session.get(User, user_id)
if not user:
return jsonify({'error': '用户不存在'}), 404
data = request.json
if 'email' in data:
user.email = data['email']
if 'uid' in data:
user.uid = data['uid']
if 'role' in data:
user.role = data['role']
if 'status' in data:
user.status = data['status']
if 'tags' in data:
user.tags = data['tags']
if 'note' in data:
user.note = data['note']
db.session.commit()
return jsonify({'message': '用户信息已更新'})
# 删除用户(管理员)
@user_mgmt_bp.route('/users/<int:user_id>', methods=['DELETE'])
@admin_required
def delete_user(user_id):
current_user_id = get_jwt_identity()
if current_user_id == user_id:
return jsonify({'error': '不能删除自己'}), 403
user = db.session.get(User, user_id)
if not user:
return jsonify({'error': '用户不存在'}), 404
db.session.delete(user)
db.session.commit()
return jsonify({'message': '用户已删除'})
# 修改密码(用户自己)
@user_mgmt_bp.route('/users/change-password', methods=['POST'])
@login_required
def change_password():
user_id = get_jwt_identity()
user = db.session.get(User, user_id)
data = request.json
old_password = data.get('old_password')
new_password = data.get('new_password')
if not old_password or not new_password:
return jsonify({'error': '旧密码和新密码不能为空'}), 400
if not user.check_password(old_password):
return jsonify({'error': '旧密码错误'}), 400
user.set_password(new_password)
db.session.commit()
return jsonify({'message': '密码修改成功'})
# 获取个人信息
@user_mgmt_bp.route('/users/profile', methods=['GET'])
@login_required
def get_profile():
user_id = get_jwt_identity()
user = db.session.get(User, user_id)
return jsonify({
'id': user.id,
'username': user.username,
'email': user.email,
'uid': user.uid,
'role': user.role,
'status': user.status,
'created_at': user.created_at.strftime('%Y-%m-%d %H:%M')
})
# 更新个人信息
@user_mgmt_bp.route('/users/profile', methods=['PUT'])
@login_required
def update_profile():
user_id = get_jwt_identity()
user = db.session.get(User, user_id)
data = request.json
if 'email' in data:
if User.query.filter(User.email == data['email'], User.id != user_id).first():
return jsonify({'error': '邮箱已被使用'}), 400
user.email = data['email']
if 'uid' in data:
user.uid = data['uid']
db.session.commit()
return jsonify({'message': '个人信息已更新'})

81
api/users.py Normal file
View File

@@ -0,0 +1,81 @@
from flask import Blueprint, request, jsonify
from flask_jwt_extended import create_access_token, jwt_required, get_jwt_identity
from models import User, db
from datetime import timedelta
users_bp = Blueprint('users', __name__)
@users_bp.route('/login', methods=['POST'])
def login():
data = request.json
user = User.query.filter_by(username=data['username']).first()
if not user or not user.check_password(data['password']):
return jsonify({'error': '用户名或密码错误'}), 401
if user.status != 'active':
return jsonify({'error': '账号未激活或已被禁用'}), 403
access_token = create_access_token(
identity=str(user.id),
expires_delta=timedelta(hours=24)
)
return jsonify({
'access_token': access_token,
'user': {
'id': user.id,
'username': user.username,
'role': user.role
}
})
@users_bp.route('/me', methods=['GET'])
@jwt_required()
def get_current_user():
user_id = get_jwt_identity()
user = db.session.get(User, user_id)
if not user:
return jsonify({'error': '用户不存在'}), 404
return jsonify({
'id': user.id,
'username': user.username,
'email': user.email,
'role': user.role,
'status': user.status
})
@users_bp.route('/users', methods=['POST'])
@jwt_required()
def create_user():
current_user_id = get_jwt_identity()
current_user = db.session.get(User, current_user_id)
if current_user.role != 'admin':
return jsonify({'error': '权限不足'}), 403
data = request.json
if not data.get('username') or not data.get('password'):
return jsonify({'error': '用户名和密码不能为空'}), 400
if User.query.filter_by(username=data['username']).first():
return jsonify({'error': '用户名已存在'}), 400
user = User(
username=data['username'],
email=data.get('email'),
role=data.get('role', 'user')
)
user.set_password(data['password'])
db.session.add(user)
db.session.commit()
return jsonify({
'id': user.id,
'username': user.username,
'email': user.email,
'role': user.role
}), 201