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 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)
|