635 lines
19 KiB
Python
635 lines
19 KiB
Python
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)}'
|
||
})
|