447 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			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 admin_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
 | 
						|
@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
 | 
						|
@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
 | 
						|
@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
 | 
						|
@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
 | 
						|
@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
 | 
						|
@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
 | 
						|
@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
 | 
						|
@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
 | 
						|
@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
 | 
						|
@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)
 |