# 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') now = datetime.now() # 记录获取借阅趋势数据的日志 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.return_date.is_(None), # 未归还 BorrowRecord.due_date < now # 应还日期早于当前时间 ).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() # 当天逾期未还的数量 now = datetime.now() overdue_count = BorrowRecord.query.filter( BorrowRecord.return_date.is_(None), BorrowRecord.due_date < now ).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() # 当月逾期未还的数量 now = datetime.now() overdue_count = BorrowRecord.query.filter( BorrowRecord.return_date.is_(None), BorrowRecord.due_date < now ).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)