Book_system/app/controllers/statistics.py
2025-05-14 15:08:06 +08:00

447 lines
14 KiB
Python

# app/controllers/statistics.py
from flask import Blueprint, render_template, jsonify, request
from flask_login import login_required, current_user
from app.models.book import Book, db
from app.models.borrow import BorrowRecord
from app.models.user import User
from app.utils.auth import permission_required # 修改为导入permission_required
from app.models.log import Log # 导入日志模型
from sqlalchemy import func, case, desc, and_
from datetime import datetime, timedelta
import calendar
statistics_bp = Blueprint('statistics', __name__, url_prefix='/statistics')
@statistics_bp.route('/')
@login_required
@permission_required('view_statistics') # 替代 @admin_required
def index():
"""统计分析首页"""
# 记录访问统计分析首页的日志
Log.add_log(
action="访问统计分析",
user_id=current_user.id,
target_type="statistics",
description="访问统计分析首页"
)
return render_template('statistics/index.html')
@statistics_bp.route('/book-ranking')
@login_required
@permission_required('view_statistics') # 替代 @admin_required
def book_ranking():
"""热门图书排行榜页面"""
# 记录访问热门图书排行的日志
Log.add_log(
action="查看统计数据",
user_id=current_user.id,
target_type="statistics",
description="查看热门图书排行榜"
)
return render_template('statistics/book_ranking.html')
@statistics_bp.route('/api/book-ranking')
@login_required
@permission_required('view_statistics') # 替代 @admin_required
def api_book_ranking():
"""获取热门图书排行数据API"""
time_range = request.args.get('time_range', 'month')
limit = request.args.get('limit', 10, type=int)
# 记录获取热门图书排行数据的日志
Log.add_log(
action="获取数据",
user_id=current_user.id,
target_type="statistics",
description=f"获取热门图书排行数据(时间范围:{time_range}, 数量:{limit})"
)
# 根据时间范围设置过滤条件
if time_range == 'week':
start_date = datetime.now() - timedelta(days=7)
elif time_range == 'month':
start_date = datetime.now() - timedelta(days=30)
elif time_range == 'year':
start_date = datetime.now() - timedelta(days=365)
else: # all time
start_date = datetime(1900, 1, 1)
# 查询借阅次数最多的图书
popular_books = db.session.query(
Book.id, Book.title, Book.author, Book.cover_url,
func.count(BorrowRecord.id).label('borrow_count')
).join(
BorrowRecord, Book.id == BorrowRecord.book_id
).filter(
BorrowRecord.borrow_date >= start_date
).group_by(
Book.id
).order_by(
desc('borrow_count')
).limit(limit).all()
result = [
{
'id': book.id,
'title': book.title,
'author': book.author,
'cover_url': book.cover_url,
'borrow_count': book.borrow_count
} for book in popular_books
]
return jsonify(result)
@statistics_bp.route('/borrow-statistics')
@login_required
@permission_required('view_statistics') # 替代 @admin_required
def borrow_statistics():
"""借阅统计分析页面"""
# 记录访问借阅统计分析的日志
Log.add_log(
action="查看统计数据",
user_id=current_user.id,
target_type="statistics",
description="查看借阅统计分析"
)
return render_template('statistics/borrow_statistics.html')
@statistics_bp.route('/api/borrow-trend')
@login_required
@permission_required('view_statistics') # 替代 @admin_required
def api_borrow_trend():
"""获取借阅趋势数据API"""
time_range = request.args.get('time_range', 'month')
# 记录获取借阅趋势数据的日志
Log.add_log(
action="获取数据",
user_id=current_user.id,
target_type="statistics",
description=f"获取借阅趋势数据(时间范围:{time_range})"
)
if time_range == 'week':
# 获取过去7天每天的借阅和归还数量
start_date = datetime.now() - timedelta(days=6)
results = []
for i in range(7):
day = start_date + timedelta(days=i)
day_start = day.replace(hour=0, minute=0, second=0, microsecond=0)
day_end = day.replace(hour=23, minute=59, second=59, microsecond=999999)
# 查询当天借阅量
borrow_count = BorrowRecord.query.filter(
BorrowRecord.borrow_date >= day_start,
BorrowRecord.borrow_date <= day_end
).count()
# 查询当天归还量
return_count = BorrowRecord.query.filter(
BorrowRecord.return_date >= day_start,
BorrowRecord.return_date <= day_end
).count()
# 当天逾期未还的数量
overdue_count = BorrowRecord.query.filter(
BorrowRecord.due_date < day_end,
BorrowRecord.return_date.is_(None)
).count()
results.append({
'date': day.strftime('%m-%d'),
'borrow': borrow_count,
'return': return_count,
'overdue': overdue_count
})
return jsonify(results)
elif time_range == 'month':
# 获取过去30天每天的借阅和归还数量
start_date = datetime.now() - timedelta(days=29)
results = []
for i in range(30):
day = start_date + timedelta(days=i)
day_start = day.replace(hour=0, minute=0, second=0, microsecond=0)
day_end = day.replace(hour=23, minute=59, second=59, microsecond=999999)
# 查询当天借阅量
borrow_count = BorrowRecord.query.filter(
BorrowRecord.borrow_date >= day_start,
BorrowRecord.borrow_date <= day_end
).count()
# 查询当天归还量
return_count = BorrowRecord.query.filter(
BorrowRecord.return_date >= day_start,
BorrowRecord.return_date <= day_end
).count()
# 当天逾期未还的数量
overdue_count = BorrowRecord.query.filter(
BorrowRecord.due_date < day_end,
BorrowRecord.return_date.is_(None)
).count()
results.append({
'date': day.strftime('%m-%d'),
'borrow': borrow_count,
'return': return_count,
'overdue': overdue_count
})
return jsonify(results)
elif time_range == 'year':
# 获取过去12个月每月的借阅和归还数量
current_month = datetime.now().month
current_year = datetime.now().year
results = []
for i in range(12):
# 计算月份和年份
month = (current_month - i) % 12
if month == 0:
month = 12
year = current_year - ((i - (current_month - 1)) // 12)
# 计算该月的开始和结束日期
days_in_month = calendar.monthrange(year, month)[1]
month_start = datetime(year, month, 1)
month_end = datetime(year, month, days_in_month, 23, 59, 59, 999999)
# 查询当月借阅量
borrow_count = BorrowRecord.query.filter(
BorrowRecord.borrow_date >= month_start,
BorrowRecord.borrow_date <= month_end
).count()
# 查询当月归还量
return_count = BorrowRecord.query.filter(
BorrowRecord.return_date >= month_start,
BorrowRecord.return_date <= month_end
).count()
# 当月逾期未还的数量
overdue_count = BorrowRecord.query.filter(
BorrowRecord.due_date < month_end,
BorrowRecord.return_date.is_(None)
).count()
results.append({
'date': f'{year}-{month:02d}',
'borrow': borrow_count,
'return': return_count,
'overdue': overdue_count
})
# 按时间顺序排序
results.reverse()
return jsonify(results)
return jsonify([])
@statistics_bp.route('/api/category-distribution')
@login_required
@permission_required('view_statistics') # 替代 @admin_required
def api_category_distribution():
"""获取图书分类分布数据API"""
# 记录获取图书分类分布数据的日志
Log.add_log(
action="获取数据",
user_id=current_user.id,
target_type="statistics",
description="获取图书分类分布数据"
)
# 计算每个分类的总借阅次数
category_stats = db.session.query(
Book.category_id,
func.count(BorrowRecord.id).label('borrow_count')
).join(
BorrowRecord, Book.id == BorrowRecord.book_id
).group_by(
Book.category_id
).all()
# 获取分类名称
from app.models.book import Category
categories = {cat.id: cat.name for cat in Category.query.all()}
# 准备结果
result = [
{
'category': categories.get(stat.category_id, '未分类'),
'count': stat.borrow_count
} for stat in category_stats if stat.category_id is not None
]
# 添加未分类数据
uncategorized = next((stat for stat in category_stats if stat.category_id is None), None)
if uncategorized:
result.append({'category': '未分类', 'count': uncategorized.borrow_count})
return jsonify(result)
@statistics_bp.route('/user-activity')
@login_required
@permission_required('view_statistics') # 替代 @admin_required
def user_activity():
"""用户活跃度分析页面"""
# 记录访问用户活跃度分析的日志
Log.add_log(
action="查看统计数据",
user_id=current_user.id,
target_type="statistics",
description="查看用户活跃度分析"
)
return render_template('statistics/user_activity.html')
@statistics_bp.route('/api/user-activity')
@login_required
@permission_required('view_statistics') # 替代 @admin_required
def api_user_activity():
"""获取用户活跃度数据API"""
# 记录获取用户活跃度数据的日志
Log.add_log(
action="获取数据",
user_id=current_user.id,
target_type="statistics",
description="获取用户活跃度数据"
)
# 查询最活跃的用户(借阅量最多)
active_users = db.session.query(
User.id, User.username, User.nickname,
func.count(BorrowRecord.id).label('borrow_count')
).join(
BorrowRecord, User.id == BorrowRecord.user_id
).group_by(
User.id
).order_by(
desc('borrow_count')
).limit(10).all()
result = [
{
'id': user.id,
'username': user.username,
'nickname': user.nickname or user.username,
'borrow_count': user.borrow_count
} for user in active_users
]
return jsonify(result)
@statistics_bp.route('/overdue-analysis')
@login_required
@permission_required('view_statistics') # 替代 @admin_required
def overdue_analysis():
"""逾期分析页面"""
# 记录访问逾期分析的日志
Log.add_log(
action="查看统计数据",
user_id=current_user.id,
target_type="statistics",
description="查看借阅逾期分析"
)
return render_template('statistics/overdue_analysis.html')
@statistics_bp.route('/api/overdue-statistics')
@login_required
@permission_required('view_statistics') # 替代 @admin_required
def api_overdue_statistics():
"""获取逾期统计数据API"""
# 记录获取逾期统计数据的日志
Log.add_log(
action="获取数据",
user_id=current_user.id,
target_type="statistics",
description="获取借阅逾期统计数据"
)
now = datetime.now()
# 计算总借阅量
total_borrows = BorrowRecord.query.count()
# 计算已归还的逾期借阅
returned_overdue = BorrowRecord.query.filter(
BorrowRecord.return_date.isnot(None),
BorrowRecord.return_date > BorrowRecord.due_date
).count()
# 计算未归还的逾期借阅
current_overdue = BorrowRecord.query.filter(
BorrowRecord.return_date.is_(None),
BorrowRecord.due_date < now
).count()
# 计算总逾期率
overdue_rate = round((returned_overdue + current_overdue) / total_borrows * 100, 2) if total_borrows > 0 else 0
# 计算各逾期时长区间的数量
overdue_range_data = []
# 1-7天逾期
range1 = BorrowRecord.query.filter(
and_(
BorrowRecord.return_date.is_(None),
BorrowRecord.due_date < now,
BorrowRecord.due_date >= now - timedelta(days=7)
)
).count()
overdue_range_data.append({'range': '1-7天', 'count': range1})
# 8-14天逾期
range2 = BorrowRecord.query.filter(
and_(
BorrowRecord.return_date.is_(None),
BorrowRecord.due_date < now - timedelta(days=7),
BorrowRecord.due_date >= now - timedelta(days=14)
)
).count()
overdue_range_data.append({'range': '8-14天', 'count': range2})
# 15-30天逾期
range3 = BorrowRecord.query.filter(
and_(
BorrowRecord.return_date.is_(None),
BorrowRecord.due_date < now - timedelta(days=14),
BorrowRecord.due_date >= now - timedelta(days=30)
)
).count()
overdue_range_data.append({'range': '15-30天', 'count': range3})
# 30天以上逾期
range4 = BorrowRecord.query.filter(
and_(
BorrowRecord.return_date.is_(None),
BorrowRecord.due_date < now - timedelta(days=30)
)
).count()
overdue_range_data.append({'range': '30天以上', 'count': range4})
result = {
'total_borrows': total_borrows,
'returned_overdue': returned_overdue,
'current_overdue': current_overdue,
'overdue_rate': overdue_rate,
'overdue_ranges': overdue_range_data
}
return jsonify(result)