2025-05-14 15:08:06 +08:00

635 lines
19 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from flask import Blueprint, request, redirect, url_for, flash, render_template, jsonify
from flask_login import current_user, login_required
from app.models.book import Book
from app.models.borrow import BorrowRecord
from app.models.inventory import InventoryLog
from app.models.user import db, User
from app.models.log import Log # 导入日志模型
import datetime
from app.utils.auth import permission_required # 修改导入使用permission_required而不是admin_required
# 创建借阅蓝图
borrow_bp = Blueprint('borrow', __name__, url_prefix='/borrow')
@borrow_bp.route('/book', methods=['POST'])
@login_required
def borrow_book():
book_id = request.form.get('book_id', type=int)
borrow_days = request.form.get('borrow_days', type=int, default=14)
if not book_id:
flash('请选择要借阅的图书', 'danger')
return redirect(url_for('book.book_list'))
book = Book.query.get_or_404(book_id)
# 检查库存
if book.stock <= 0:
flash(f'{book.title}》当前无库存,无法借阅', 'danger')
return redirect(url_for('book.book_detail', book_id=book_id))
# 检查当前用户是否已借阅此书
existing_borrow = BorrowRecord.query.filter_by(
user_id=current_user.id,
book_id=book_id,
status=1 # 1表示借阅中
).first()
if existing_borrow:
flash(f'您已借阅《{book.title}》,请勿重复借阅', 'warning')
return redirect(url_for('book.book_detail', book_id=book_id))
try:
# 创建借阅记录
now = datetime.datetime.now()
due_date = now + datetime.timedelta(days=borrow_days)
borrow_record = BorrowRecord(
user_id=current_user.id,
book_id=book_id,
borrow_date=now,
due_date=due_date,
status=1, # 1表示借阅中
created_at=now,
updated_at=now
)
# 更新图书库存
book.stock -= 1
book.updated_at = now
db.session.add(borrow_record)
db.session.commit()
# 添加库存变更日志
inventory_log = InventoryLog(
book_id=book_id,
change_type='借出',
change_amount=-1,
after_stock=book.stock,
operator_id=current_user.id,
remark='用户借书',
changed_at=now
)
db.session.add(inventory_log)
# 添加系统操作日志
Log.add_log(
action='借阅图书',
user_id=current_user.id,
target_type='book',
target_id=book_id,
ip_address=request.remote_addr,
description=f'用户借阅图书《{book.title}》,归还日期: {due_date.strftime("%Y-%m-%d")}'
)
db.session.commit()
flash(f'成功借阅《{book.title}》,请在 {due_date.strftime("%Y-%m-%d")} 前归还', 'success')
except Exception as e:
db.session.rollback()
flash(f'借阅失败: {str(e)}', 'danger')
return redirect(url_for('book.book_detail', book_id=book_id))
@borrow_bp.route('/add/<int:book_id>', methods=['POST'])
@login_required
def add_borrow(book_id):
# 验证图书存在
book = Book.query.get_or_404(book_id)
# 默认借阅天数
borrow_days = 14
# 检查库存
if book.stock <= 0:
return jsonify({
'success': False,
'message': f'{book.title}》当前无库存,无法借阅'
})
# 检查是否已借阅
existing_borrow = BorrowRecord.query.filter_by(
user_id=current_user.id,
book_id=book_id,
status=1 # 1表示借阅中
).first()
if existing_borrow:
return jsonify({
'success': False,
'message': f'您已借阅《{book.title}》,请勿重复借阅'
})
try:
# 创建借阅记录
now = datetime.datetime.now()
due_date = now + datetime.timedelta(days=borrow_days)
borrow_record = BorrowRecord(
user_id=current_user.id,
book_id=book_id,
borrow_date=now,
due_date=due_date,
status=1, # 1表示借阅中
created_at=now,
updated_at=now
)
# 更新图书库存
book.stock -= 1
book.updated_at = now
db.session.add(borrow_record)
db.session.commit()
# 添加库存变更日志
inventory_log = InventoryLog(
book_id=book_id,
change_type='借出',
change_amount=-1,
after_stock=book.stock,
operator_id=current_user.id,
remark='用户借书',
changed_at=now
)
db.session.add(inventory_log)
# 添加系统操作日志
Log.add_log(
action='借阅图书',
user_id=current_user.id,
target_type='book',
target_id=book_id,
ip_address=request.remote_addr,
description=f'用户借阅图书《{book.title}》,归还日期: {due_date.strftime("%Y-%m-%d")}'
)
db.session.commit()
return jsonify({
'success': True,
'message': f'成功借阅《{book.title}》,请在 {due_date.strftime("%Y-%m-%d")} 前归还'
})
except Exception as e:
db.session.rollback()
return jsonify({
'success': False,
'message': f'借阅失败: {str(e)}'
})
@borrow_bp.route('/return/<int:borrow_id>', methods=['POST'])
@login_required
def return_book(borrow_id):
"""还书操作"""
# 查找借阅记录
borrow_record = BorrowRecord.query.get_or_404(borrow_id)
# 检查是否是自己的借阅记录或者是管理员
if borrow_record.user_id != current_user.id and not current_user.has_permission('manage_borrows'):
return jsonify({
'success': False,
'message': '您无权执行此操作'
})
# 检查是否已还
if borrow_record.status != 1:
return jsonify({
'success': False,
'message': '此书已归还,请勿重复操作'
})
try:
book = Book.query.get(borrow_record.book_id)
now = datetime.datetime.now()
# 更新借阅记录
borrow_record.status = 0 # 0表示已归还
borrow_record.return_date = now
borrow_record.updated_at = now
# 更新图书库存
book.stock += 1
book.updated_at = now
db.session.commit()
# 添加库存变更日志
inventory_log = InventoryLog(
book_id=borrow_record.book_id,
change_type='归还',
change_amount=1,
after_stock=book.stock,
operator_id=current_user.id,
remark='用户还书',
changed_at=now
)
db.session.add(inventory_log)
# 添加系统操作日志
# 判断是否逾期归还
is_overdue = now > borrow_record.due_date
overdue_msg = '(逾期归还)' if is_overdue else ''
Log.add_log(
action='归还图书',
user_id=current_user.id,
target_type='book',
target_id=borrow_record.book_id,
ip_address=request.remote_addr,
description=f'用户归还图书《{book.title}{overdue_msg}'
)
db.session.commit()
return jsonify({
'success': True,
'message': f'成功归还《{book.title}'
})
except Exception as e:
db.session.rollback()
return jsonify({
'success': False,
'message': f'归还失败: {str(e)}'
})
@borrow_bp.route('/renew/<int:borrow_id>', methods=['POST'])
@login_required
def renew_book(borrow_id):
"""续借操作"""
# 查找借阅记录
borrow_record = BorrowRecord.query.get_or_404(borrow_id)
# 检查是否是自己的借阅记录或者是管理员
if borrow_record.user_id != current_user.id and not current_user.has_permission('manage_borrows'):
return jsonify({
'success': False,
'message': '您无权执行此操作'
})
# 检查是否已还
if borrow_record.status != 1:
return jsonify({
'success': False,
'message': '此书已归还,无法续借'
})
# 检查续借次数限制最多续借2次
if borrow_record.renew_count >= 2:
return jsonify({
'success': False,
'message': '此书已达到最大续借次数,无法继续续借'
})
try:
now = datetime.datetime.now()
book = Book.query.get(borrow_record.book_id)
# 检查是否已逾期
if now > borrow_record.due_date:
return jsonify({
'success': False,
'message': '此书已逾期,请先归还并处理逾期情况'
})
# 续借14天
new_due_date = borrow_record.due_date + datetime.timedelta(days=14)
# 更新借阅记录
borrow_record.due_date = new_due_date
borrow_record.renew_count += 1
borrow_record.updated_at = now
# 添加系统操作日志
Log.add_log(
action='续借图书',
user_id=current_user.id,
target_type='book',
target_id=borrow_record.book_id,
ip_address=request.remote_addr,
description=f'用户续借图书《{book.title}》,新归还日期: {new_due_date.strftime("%Y-%m-%d")}'
)
db.session.commit()
return jsonify({
'success': True,
'message': f'续借成功,新的归还日期为 {new_due_date.strftime("%Y-%m-%d")}'
})
except Exception as e:
db.session.rollback()
return jsonify({
'success': False,
'message': f'续借失败: {str(e)}'
})
@borrow_bp.route('/my_borrows')
@login_required
def my_borrows():
"""用户查看自己的借阅记录"""
page = request.args.get('page', 1, type=int)
status = request.args.get('status', default=None, type=int)
# 构建查询
query = BorrowRecord.query.filter_by(user_id=current_user.id)
# 根据状态筛选
if status is not None:
query = query.filter_by(status=status)
# 按借阅日期倒序排列
query = query.order_by(BorrowRecord.borrow_date.desc())
# 分页
pagination = query.paginate(page=page, per_page=10, error_out=False)
# 获取当前借阅数量和历史借阅数量(用于标签显示)
current_borrows_count = BorrowRecord.query.filter_by(user_id=current_user.id, status=1).count()
history_borrows_count = BorrowRecord.query.filter_by(user_id=current_user.id, status=0).count()
# 记录日志 - 用户查看借阅记录
Log.add_log(
action='查看借阅记录',
user_id=current_user.id,
ip_address=request.remote_addr,
description='用户查看个人借阅记录'
)
return render_template(
'borrow/my_borrows.html',
pagination=pagination,
current_borrows_count=current_borrows_count,
history_borrows_count=history_borrows_count,
status=status,
now=datetime.datetime.now() # 添加当前时间变量
)
@borrow_bp.route('/manage')
@login_required
@permission_required('manage_borrows') # 替代 @admin_required
def manage_borrows():
"""管理员查看所有借阅记录"""
page = request.args.get('page', 1, type=int)
status = request.args.get('status', default=None, type=int)
user_id = request.args.get('user_id', default=None, type=int)
book_id = request.args.get('book_id', default=None, type=int)
search = request.args.get('search', default='')
# 构建查询
query = BorrowRecord.query
# 根据状态筛选
if status is not None:
query = query.filter_by(status=status)
# 根据用户筛选
if user_id:
query = query.filter_by(user_id=user_id)
# 根据图书筛选
if book_id:
query = query.filter_by(book_id=book_id)
# 根据搜索条件筛选(用户名或图书名)
if search:
query = query.join(User, BorrowRecord.user_id == User.id) \
.join(Book, BorrowRecord.book_id == Book.id) \
.filter((User.username.like(f'%{search}%')) |
(Book.title.like(f'%{search}%')))
# 按借阅日期倒序排列
query = query.order_by(BorrowRecord.borrow_date.desc())
# 分页
pagination = query.paginate(page=page, per_page=10, error_out=False)
# 获取统计数据
current_borrows_count = BorrowRecord.query.filter_by(status=1).count()
history_borrows_count = BorrowRecord.query.filter_by(status=0).count()
# 获取所有用户(用于筛选)
users = User.query.all()
# 记录日志 - 管理员查看借阅记录
Log.add_log(
action='管理借阅记录',
user_id=current_user.id,
ip_address=request.remote_addr,
description='管理员查看借阅管理页面'
)
return render_template(
'borrow/borrow_management.html',
pagination=pagination,
current_borrows_count=current_borrows_count,
history_borrows_count=history_borrows_count,
status=status,
user_id=user_id,
book_id=book_id,
search=search,
users=users,
now=datetime.datetime.now() # 添加当前时间变量
)
@borrow_bp.route('/admin/add', methods=['POST'])
@login_required
@permission_required('manage_borrows') # 替代 @admin_required
def admin_add_borrow():
"""管理员为用户添加借阅记录"""
user_id = request.form.get('user_id', type=int)
book_id = request.form.get('book_id', type=int)
borrow_days = request.form.get('borrow_days', type=int, default=14)
if not user_id or not book_id:
flash('用户ID和图书ID不能为空', 'danger')
return redirect(url_for('borrow.manage_borrows'))
# 验证用户和图书是否存在
user = User.query.get_or_404(user_id)
book = Book.query.get_or_404(book_id)
# 检查库存
if book.stock <= 0:
flash(f'{book.title}》当前无库存,无法借阅', 'danger')
return redirect(url_for('borrow.manage_borrows'))
# 检查用户是否已借阅此书
existing_borrow = BorrowRecord.query.filter_by(
user_id=user_id,
book_id=book_id,
status=1 # 1表示借阅中
).first()
if existing_borrow:
flash(f'用户 {user.username} 已借阅《{book.title}》,请勿重复借阅', 'warning')
return redirect(url_for('borrow.manage_borrows'))
try:
# 创建借阅记录
now = datetime.datetime.now()
due_date = now + datetime.timedelta(days=borrow_days)
borrow_record = BorrowRecord(
user_id=user_id,
book_id=book_id,
borrow_date=now,
due_date=due_date,
status=1, # 1表示借阅中
created_at=now,
updated_at=now
)
# 更新图书库存
book.stock -= 1
book.updated_at = now
db.session.add(borrow_record)
db.session.commit()
# 添加库存变更日志
inventory_log = InventoryLog(
book_id=book_id,
change_type='借出',
change_amount=-1,
after_stock=book.stock,
operator_id=current_user.id,
remark=f'管理员 {current_user.username} 为用户 {user.username} 借书',
changed_at=now
)
db.session.add(inventory_log)
# 添加系统操作日志
Log.add_log(
action='管理员借阅操作',
user_id=current_user.id,
target_type='book',
target_id=book_id,
ip_address=request.remote_addr,
description=f'管理员为用户 {user.username} 借阅图书《{book.title}》,归还日期: {due_date.strftime("%Y-%m-%d")}'
)
db.session.commit()
flash(f'成功为用户 {user.username} 借阅《{book.title}》,归还日期: {due_date.strftime("%Y-%m-%d")}', 'success')
except Exception as e:
db.session.rollback()
flash(f'借阅失败: {str(e)}', 'danger')
return redirect(url_for('borrow.manage_borrows'))
@borrow_bp.route('/overdue')
@login_required
@permission_required('manage_overdue') # 替代 @admin_required
def overdue_borrows():
"""查看逾期借阅"""
page = request.args.get('page', 1, type=int)
now = datetime.datetime.now()
# 查询所有已逾期且未归还的借阅记录
query = BorrowRecord.query.filter(
BorrowRecord.status == 1, # 借阅中
BorrowRecord.due_date < now # 已过期
).order_by(BorrowRecord.due_date) # 按到期日期排序,最早到期的排在前面
pagination = query.paginate(page=page, per_page=10, error_out=False)
# 计算逾期总数
overdue_count = query.count()
# 记录日志 - 管理员查看逾期记录
Log.add_log(
action='查看逾期记录',
user_id=current_user.id,
ip_address=request.remote_addr,
description='管理员查看逾期借阅记录'
)
return render_template(
'borrow/overdue.html',
pagination=pagination,
overdue_count=overdue_count
)
@borrow_bp.route('/overdue/notify/<int:borrow_id>', methods=['POST'])
@login_required
@permission_required('manage_overdue') # 替代 @admin_required
def notify_overdue(borrow_id):
"""发送逾期通知"""
from app.models.notification import Notification
borrow_record = BorrowRecord.query.get_or_404(borrow_id)
# 检查是否已还
if borrow_record.status != 1:
return jsonify({
'success': False,
'message': '此书已归还,无需发送逾期通知'
})
now = datetime.datetime.now()
# 检查是否确实逾期
if borrow_record.due_date > now:
return jsonify({
'success': False,
'message': '此借阅记录尚未逾期'
})
try:
book = Book.query.get(borrow_record.book_id)
user = User.query.get(borrow_record.user_id)
# 创建通知
notification = Notification(
user_id=borrow_record.user_id,
title='图书逾期提醒',
content=f'您借阅的《{borrow_record.book.title}》已逾期,请尽快归还。应还日期: {borrow_record.due_date.strftime("%Y-%m-%d")}',
type='overdue',
sender_id=current_user.id,
created_at=now
)
db.session.add(notification)
# 更新借阅记录备注
borrow_record.remark = f'{borrow_record.remark or ""}[{now.strftime("%Y-%m-%d")} 已发送逾期通知]'
borrow_record.updated_at = now
# 添加系统操作日志
Log.add_log(
action='发送逾期通知',
user_id=current_user.id,
target_type='notification',
target_id=borrow_record.user_id,
ip_address=request.remote_addr,
description=f'管理员向用户 {user.username} 发送图书《{book.title}》逾期通知'
)
db.session.commit()
return jsonify({
'success': True,
'message': '已成功发送逾期通知'
})
except Exception as e:
db.session.rollback()
return jsonify({
'success': False,
'message': f'发送通知失败: {str(e)}'
})