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

1018 lines
37 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, render_template, request, redirect, url_for, flash, current_app, jsonify
from app.models.book import Book, Category
from app.models.user import db
from app.utils.auth import login_required, permission_required # 修改导入替换admin_required为permission_required
from flask_login import current_user
import os
from werkzeug.utils import secure_filename
import datetime
import pandas as pd
import uuid
from app.models.log import Log
book_bp = Blueprint('book', __name__)
@book_bp.route('/admin/list')
@login_required
@permission_required('manage_books') # 替换 @admin_required
def admin_book_list():
print(f"DEBUG: admin_book_list 函数被调用,用户={current_user.username},认证状态={current_user.is_authenticated}")
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 10, type=int)
# 只显示状态为1的图书未下架的图书
query = Book.query.filter_by(status=1)
# 搜索功能
search = request.args.get('search', '')
if search:
query = query.filter(
(Book.title.contains(search)) |
(Book.author.contains(search)) |
(Book.isbn.contains(search))
)
# 分类筛选
category_id = request.args.get('category_id', type=int)
if category_id:
query = query.filter_by(category_id=category_id)
# 排序
sort = request.args.get('sort', 'id')
order = request.args.get('order', 'desc')
if order == 'desc':
query = query.order_by(getattr(Book, sort).desc())
else:
query = query.order_by(getattr(Book, sort))
pagination = query.paginate(page=page, per_page=per_page)
books = pagination.items
# 获取所有分类供筛选使用
categories = Category.query.all()
# 记录访问日志
Log.add_log(
action='访问管理图书列表',
user_id=current_user.id,
ip_address=request.remote_addr,
description=f"查询条件: 搜索={search}, 分类={category_id}, 排序={sort} {order}"
)
return render_template('book/list.html',
books=books,
pagination=pagination,
search=search,
categories=categories,
category_id=category_id,
sort=sort,
order=order,
current_user=current_user,
is_admin_view=True)
# 图书列表页面 - 不需要修改,已经只有@login_required
@book_bp.route('/list')
@login_required
def book_list():
print("访问图书列表页面") # 调试输出
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 10, type=int)
# 只显示状态为1的图书未下架的图书
query = Book.query.filter_by(status=1)
# 搜索功能
search = request.args.get('search', '')
if search:
query = query.filter(
(Book.title.contains(search)) |
(Book.author.contains(search)) |
(Book.isbn.contains(search))
)
# 分类筛选
category_id = request.args.get('category_id', type=int)
if category_id:
query = query.filter_by(category_id=category_id)
# 排序
sort = request.args.get('sort', 'id')
order = request.args.get('order', 'desc')
if order == 'desc':
query = query.order_by(getattr(Book, sort).desc())
else:
query = query.order_by(getattr(Book, sort))
pagination = query.paginate(page=page, per_page=per_page)
books = pagination.items
# 获取所有分类供筛选使用
categories = Category.query.all()
# 记录访问日志
Log.add_log(
action='访问图书列表',
user_id=current_user.id,
ip_address=request.remote_addr,
description=f"查询条件: 搜索={search}, 分类={category_id}, 排序={sort} {order}"
)
return render_template('book/list.html',
books=books,
pagination=pagination,
search=search,
categories=categories,
category_id=category_id,
sort=sort,
order=order,
current_user=current_user)
# 图书详情页面 - 不需要修改,已经只有@login_required
@book_bp.route('/detail/<int:book_id>')
@login_required
def book_detail(book_id):
book = Book.query.get_or_404(book_id)
# 添加当前时间用于判断借阅是否逾期
now = datetime.datetime.now()
# 如果用户是管理员,预先查询并排序借阅记录
borrow_records = []
# 使用current_user代替g.user
if current_user.is_authenticated and current_user.role_id == 1:
from app.models.borrow import BorrowRecord
borrow_records = BorrowRecord.query.filter_by(book_id=book_id).order_by(BorrowRecord.borrow_date.desc()).limit(
10).all()
# 记录访问日志
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}"
)
return render_template(
'book/detail.html',
book=book,
current_user=current_user,
borrow_records=borrow_records,
now=now
)
# 添加图书页面
@book_bp.route('/add', methods=['GET', 'POST'])
@login_required
@permission_required('manage_books') # 替换 @admin_required
def add_book():
if request.method == 'POST':
title = request.form.get('title')
author = request.form.get('author')
publisher = request.form.get('publisher')
category_id = request.form.get('category_id')
tags = request.form.get('tags')
isbn = request.form.get('isbn')
publish_year = request.form.get('publish_year')
description = request.form.get('description')
stock = request.form.get('stock', type=int, default=0)
price = request.form.get('price')
# 表单验证
errors = []
if not title:
errors.append('书名不能为空')
if not author:
errors.append('作者不能为空')
# 检查ISBN是否已存在(如果提供了ISBN)
if isbn:
existing_book = Book.query.filter_by(isbn=isbn).first()
if existing_book:
errors.append(f'ISBN "{isbn}" 已存在请检查ISBN或查找现有图书')
if errors:
for error in errors:
flash(error, 'danger')
categories = Category.query.all()
# 保留已填写的表单数据
book_data = {
'title': title,
'author': author,
'publisher': publisher,
'category_id': category_id,
'tags': tags,
'isbn': isbn,
'publish_year': publish_year,
'description': description,
'stock': stock,
'price': price
}
return render_template('book/add.html', categories=categories,
current_user=current_user, book=book_data)
# 处理封面图片上传
cover_url = None
if 'cover' in request.files:
cover_file = request.files['cover']
if cover_file and cover_file.filename != '':
try:
# 更清晰的文件命名
original_filename = secure_filename(cover_file.filename)
# 保留原始文件扩展名
_, ext = os.path.splitext(original_filename)
if not ext:
ext = '.jpg' # 默认扩展名
filename = f"{uuid.uuid4()}{ext}"
upload_folder = os.path.join(current_app.static_folder, 'covers')
# 确保上传目录存在
if not os.path.exists(upload_folder):
os.makedirs(upload_folder)
file_path = os.path.join(upload_folder, filename)
cover_file.save(file_path)
cover_url = f'/static/covers/{filename}'
except Exception as e:
current_app.logger.error(f"封面上传失败: {str(e)}")
flash(f"封面上传失败: {str(e)}", 'warning')
try:
# 创建新图书
book = Book(
title=title,
author=author,
publisher=publisher,
category_id=category_id,
tags=tags,
isbn=isbn,
publish_year=publish_year,
description=description,
cover_url=cover_url,
stock=stock,
price=price,
status=1,
created_at=datetime.datetime.now(),
updated_at=datetime.datetime.now()
)
db.session.add(book)
# 先提交以获取book的id
db.session.commit()
# 记录库存日志 - 在获取 book.id 后
if stock and int(stock) > 0:
from app.models.inventory import InventoryLog
inventory_log = InventoryLog(
book_id=book.id,
change_type='入库',
change_amount=stock,
after_stock=stock,
operator_id=current_user.id,
remark='新书入库',
changed_at=datetime.datetime.now()
)
db.session.add(inventory_log)
db.session.commit()
# 记录操作日志
Log.add_log(
action='添加图书',
user_id=current_user.id,
target_type='book',
target_id=book.id,
ip_address=request.remote_addr,
description=f"添加图书: {title}, ISBN: {isbn}, 初始库存: {stock}"
)
flash(f'{title}》添加成功', 'success')
return redirect(url_for('book.book_list'))
except Exception as e:
db.session.rollback()
error_msg = str(e)
# 记录详细错误日志
current_app.logger.error(f"添加图书失败: {error_msg}")
# 记录操作失败日志
Log.add_log(
action='添加图书失败',
user_id=current_user.id,
ip_address=request.remote_addr,
description=f"添加图书失败: {title}, 错误: {error_msg}"
)
flash(f'添加图书失败: {error_msg}', 'danger')
categories = Category.query.all()
# 保留已填写的表单数据
book_data = {
'title': title,
'author': author,
'publisher': publisher,
'category_id': category_id,
'tags': tags,
'isbn': isbn,
'publish_year': publish_year,
'description': description,
'stock': stock,
'price': price
}
return render_template('book/add.html', categories=categories,
current_user=current_user, book=book_data)
categories = Category.query.all()
return render_template('book/add.html', categories=categories, current_user=current_user)
# 编辑图书
@book_bp.route('/edit/<int:book_id>', methods=['GET', 'POST'])
@login_required
@permission_required('manage_books') # 替换 @admin_required
def edit_book(book_id):
book = Book.query.get_or_404(book_id)
if request.method == 'POST':
# 获取表单数据
title = request.form.get('title')
author = request.form.get('author')
publisher = request.form.get('publisher')
category_id = request.form.get('category_id')
tags = request.form.get('tags')
isbn = request.form.get('isbn')
publish_year = request.form.get('publish_year')
description = request.form.get('description')
price = request.form.get('price')
status = request.form.get('status', type=int)
# 基本验证
if not title or not author:
flash('书名和作者不能为空', 'danger')
categories = Category.query.all()
return render_template('book/edit.html', book=book, categories=categories, current_user=current_user)
# ISBN验证
if isbn and isbn.strip(): # 确保ISBN不是空字符串
# 移除连字符和空格
clean_isbn = isbn.replace('-', '').replace(' ', '')
# 长度检查
if len(clean_isbn) != 10 and len(clean_isbn) != 13:
flash('ISBN必须是10位或13位', 'danger')
categories = Category.query.all()
return render_template('book/edit.html', book=book, categories=categories, current_user=current_user)
# ISBN-10验证
if len(clean_isbn) == 10:
# 检查前9位是否为数字
if not clean_isbn[:9].isdigit():
flash('ISBN-10的前9位必须是数字', 'danger')
categories = Category.query.all()
return render_template('book/edit.html', book=book, categories=categories, current_user=current_user)
# 检查最后一位是否为数字或'X'
if not (clean_isbn[9].isdigit() or clean_isbn[9].upper() == 'X'):
flash('ISBN-10的最后一位必须是数字或X', 'danger')
categories = Category.query.all()
return render_template('book/edit.html', book=book, categories=categories, current_user=current_user)
# 校验和验证
sum = 0
for i in range(9):
sum += int(clean_isbn[i]) * (10 - i)
check_digit = 10 if clean_isbn[9].upper() == 'X' else int(clean_isbn[9])
sum += check_digit
if sum % 11 != 0:
flash('ISBN-10校验和无效', 'danger')
categories = Category.query.all()
return render_template('book/edit.html', book=book, categories=categories, current_user=current_user)
# ISBN-13验证
if len(clean_isbn) == 13:
# 检查是否全是数字
if not clean_isbn.isdigit():
flash('ISBN-13必须全是数字', 'danger')
categories = Category.query.all()
return render_template('book/edit.html', book=book, categories=categories, current_user=current_user)
# 校验和验证
sum = 0
for i in range(12):
sum += int(clean_isbn[i]) * (1 if i % 2 == 0 else 3)
check_digit = (10 - (sum % 10)) % 10
if check_digit != int(clean_isbn[12]):
flash('ISBN-13校验和无效', 'danger')
categories = Category.query.all()
return render_template('book/edit.html', book=book, categories=categories, current_user=current_user)
# 处理库存变更
new_stock = request.form.get('stock', type=int) or 0 # 默认为0而非None
if new_stock != book.stock:
from app.models.inventory import InventoryLog
change_amount = new_stock - book.stock
change_type = '入库' if change_amount > 0 else '出库'
inventory_log = InventoryLog(
book_id=book.id,
change_type=change_type,
change_amount=abs(change_amount),
after_stock=new_stock,
operator_id=current_user.id,
remark=f'管理员编辑图书库存 - {book.title}',
changed_at=datetime.datetime.now()
)
db.session.add(inventory_log)
book.stock = new_stock
# 处理封面图片上传
if 'cover' in request.files:
cover_file = request.files['cover']
if cover_file and cover_file.filename != '':
filename = secure_filename(f"{uuid.uuid4()}_{cover_file.filename}")
upload_folder = os.path.join(current_app.static_folder, 'covers')
# 确保上传目录存在
if not os.path.exists(upload_folder):
os.makedirs(upload_folder)
file_path = os.path.join(upload_folder, filename)
cover_file.save(file_path)
book.cover_url = f'/static/covers/{filename}'
# 记录更新前的图书信息
old_info = f"原信息: 书名={book.title}, 作者={book.author}, ISBN={book.isbn}, 库存={book.stock}"
# 更新图书信息
book.title = title
book.author = author
book.publisher = publisher
book.category_id = category_id
book.tags = tags
book.isbn = isbn
book.publish_year = publish_year
book.description = description
book.price = price
book.status = status
book.updated_at = datetime.datetime.now()
try:
db.session.commit()
# 记录操作日志
Log.add_log(
action='编辑图书',
user_id=current_user.id,
target_type='book',
target_id=book.id,
ip_address=request.remote_addr,
description=f"编辑图书: {title}, ISBN: {isbn}, 新库存: {new_stock}\n{old_info}"
)
flash('图书信息更新成功', 'success')
return redirect(url_for('book.book_list'))
except Exception as e:
db.session.rollback()
# 记录操作失败日志
Log.add_log(
action='编辑图书失败',
user_id=current_user.id,
target_type='book',
target_id=book.id,
ip_address=request.remote_addr,
description=f"编辑图书失败: {title}, 错误: {str(e)}"
)
flash(f'保存失败: {str(e)}', 'danger')
categories = Category.query.all()
return render_template('book/edit.html', book=book, categories=categories, current_user=current_user)
# GET 请求
categories = Category.query.all()
return render_template('book/edit.html', book=book, categories=categories, current_user=current_user)
# 删除图书
@book_bp.route('/delete/<int:book_id>', methods=['POST'])
@login_required
@permission_required('manage_books') # 替换 @admin_required
def delete_book(book_id):
book = Book.query.get_or_404(book_id)
# 检查该书是否有借阅记录
from app.models.borrow import BorrowRecord
active_borrows = BorrowRecord.query.filter_by(book_id=book_id, status=1).count()
if active_borrows > 0:
# 记录操作失败日志
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}, 原因: 该图书有未归还的借阅记录"
)
return jsonify({'success': False, 'message': '该图书有未归还的借阅记录,无法删除'})
# 考虑软删除而不是物理删除
book.status = 0 # 0表示已删除/下架
book.updated_at = datetime.datetime.now()
db.session.commit()
# 记录操作日志
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}, ISBN: {book.isbn}"
)
return jsonify({'success': True, 'message': '图书已成功下架'})
# 图书分类管理
@book_bp.route('/categories', methods=['GET'])
@login_required
@permission_required('manage_categories') # 替换 @admin_required
def category_list():
categories = Category.query.all()
# 记录访问日志
Log.add_log(
action='访问分类管理',
user_id=current_user.id,
ip_address=request.remote_addr,
description="访问图书分类管理页面"
)
return render_template('book/categories.html', categories=categories, current_user=current_user)
# 添加分类
@book_bp.route('/categories/add', methods=['POST'])
@login_required
@permission_required('manage_categories') # 替换 @admin_required
def add_category():
name = request.form.get('name')
parent_id = request.form.get('parent_id') or None
sort = request.form.get('sort', 0, type=int)
if not name:
return jsonify({'success': False, 'message': '分类名称不能为空'})
category = Category(name=name, parent_id=parent_id, sort=sort)
db.session.add(category)
db.session.commit()
# 记录操作日志
Log.add_log(
action='添加图书分类',
user_id=current_user.id,
ip_address=request.remote_addr,
description=f"添加图书分类: {name}, 上级分类ID: {parent_id}, 排序: {sort}"
)
return jsonify({'success': True, 'message': '分类添加成功', 'id': category.id, 'name': category.name})
# 编辑分类
@book_bp.route('/categories/edit/<int:category_id>', methods=['POST'])
@login_required
@permission_required('manage_categories') # 替换 @admin_required
def edit_category(category_id):
category = Category.query.get_or_404(category_id)
old_name = category.name
old_parent_id = category.parent_id
old_sort = category.sort
name = request.form.get('name')
parent_id = request.form.get('parent_id') or None
sort = request.form.get('sort', 0, type=int)
if not name:
return jsonify({'success': False, 'message': '分类名称不能为空'})
category.name = name
category.parent_id = parent_id
category.sort = sort
db.session.commit()
# 记录操作日志
Log.add_log(
action='编辑图书分类',
user_id=current_user.id,
target_type='category',
target_id=category_id,
ip_address=request.remote_addr,
description=f"编辑图书分类: 从 [名称={old_name}, 上级={old_parent_id}, 排序={old_sort}] 修改为 [名称={name}, 上级={parent_id}, 排序={sort}]"
)
return jsonify({'success': True, 'message': '分类更新成功'})
# 删除分类
@book_bp.route('/categories/delete/<int:category_id>', methods=['POST'])
@login_required
@permission_required('manage_categories') # 替换 @admin_required
def delete_category(category_id):
category = Category.query.get_or_404(category_id)
# 检查是否有书籍使用此分类
books_count = Book.query.filter_by(category_id=category_id).count()
if books_count > 0:
# 记录操作失败日志
Log.add_log(
action='删除图书分类失败',
user_id=current_user.id,
target_type='category',
target_id=category_id,
ip_address=request.remote_addr,
description=f"删除图书分类失败: {category.name}, 原因: 该分类下有{books_count}本图书"
)
return jsonify({'success': False, 'message': f'该分类下有{books_count}本图书,无法删除'})
# 检查是否有子分类
children_count = Category.query.filter_by(parent_id=category_id).count()
if children_count > 0:
# 记录操作失败日志
Log.add_log(
action='删除图书分类失败',
user_id=current_user.id,
target_type='category',
target_id=category_id,
ip_address=request.remote_addr,
description=f"删除图书分类失败: {category.name}, 原因: 该分类下有{children_count}个子分类"
)
return jsonify({'success': False, 'message': f'该分类下有{children_count}个子分类,无法删除'})
category_name = category.name # 保存分类名称以便记录日志
db.session.delete(category)
db.session.commit()
# 记录操作日志
Log.add_log(
action='删除图书分类',
user_id=current_user.id,
target_type='category',
target_id=category_id,
ip_address=request.remote_addr,
description=f"删除图书分类: {category_name}"
)
return jsonify({'success': True, 'message': '分类删除成功'})
# 批量导入图书
@book_bp.route('/import', methods=['GET', 'POST'])
@login_required
@permission_required('import_export_books') # 替换 @admin_required
def import_books():
if request.method == 'POST':
if 'file' not in request.files:
flash('未选择文件', 'danger')
return redirect(request.url)
file = request.files['file']
if file.filename == '':
flash('未选择文件', 'danger')
return redirect(request.url)
if file and file.filename.endswith(('.xlsx', '.xls')):
try:
# 读取Excel文件
df = pd.read_excel(file)
success_count = 0
error_count = 0
errors = []
# 处理每一行数据
for index, row in df.iterrows():
try:
# 检查必填字段
if pd.isna(row.get('title')) or pd.isna(row.get('author')):
errors.append(f'{index + 2}行: 书名或作者为空')
error_count += 1
continue
# 检查ISBN是否已存在
isbn = row.get('isbn')
if isbn and not pd.isna(isbn) and Book.query.filter_by(isbn=str(isbn)).first():
errors.append(f'{index + 2}行: ISBN {isbn} 已存在')
error_count += 1
continue
# 创建新书籍记录
book = Book(
title=row.get('title'),
author=row.get('author'),
publisher=row.get('publisher') if not pd.isna(row.get('publisher')) else None,
category_id=row.get('category_id') if not pd.isna(row.get('category_id')) else None,
tags=row.get('tags') if not pd.isna(row.get('tags')) else None,
isbn=str(row.get('isbn')) if not pd.isna(row.get('isbn')) else None,
publish_year=str(row.get('publish_year')) if not pd.isna(row.get('publish_year')) else None,
description=row.get('description') if not pd.isna(row.get('description')) else None,
cover_url=row.get('cover_url') if not pd.isna(row.get('cover_url')) else None,
stock=int(row.get('stock')) if not pd.isna(row.get('stock')) else 0,
price=float(row.get('price')) if not pd.isna(row.get('price')) else None,
status=1,
created_at=datetime.datetime.now(),
updated_at=datetime.datetime.now()
)
db.session.add(book)
# 提交以获取book的id
db.session.flush()
# 创建库存日志
if book.stock > 0:
from app.models.inventory import InventoryLog
inventory_log = InventoryLog(
book_id=book.id,
change_type='入库',
change_amount=book.stock,
after_stock=book.stock,
operator_id=current_user.id,
remark='批量导入图书',
changed_at=datetime.datetime.now()
)
db.session.add(inventory_log)
success_count += 1
except Exception as e:
errors.append(f'{index + 2}行: {str(e)}')
error_count += 1
db.session.commit()
# 记录操作日志
Log.add_log(
action='批量导入图书',
user_id=current_user.id,
ip_address=request.remote_addr,
description=f"批量导入图书: 成功{success_count}条,失败{error_count}条,文件名:{file.filename}"
)
flash(f'导入完成: 成功{success_count}条,失败{error_count}', 'info')
if errors:
flash('<br>'.join(errors[:10]) + (f'<br>...等共{len(errors)}个错误' if len(errors) > 10 else ''),
'warning')
return redirect(url_for('book.book_list'))
except Exception as e:
# 记录操作失败日志
Log.add_log(
action='批量导入图书失败',
user_id=current_user.id,
ip_address=request.remote_addr,
description=f"批量导入图书失败: {str(e)}, 文件名:{file.filename}"
)
flash(f'导入失败: {str(e)}', 'danger')
return redirect(request.url)
else:
flash('只支持Excel文件(.xlsx, .xls)', 'danger')
return redirect(request.url)
return render_template('book/import.html', current_user=current_user)
# 导出图书
@book_bp.route('/export')
@login_required
@permission_required('import_export_books') # 替换 @admin_required
def export_books():
# 获取查询参数
search = request.args.get('search', '')
category_id = request.args.get('category_id', type=int)
query = Book.query
if search:
query = query.filter(
(Book.title.contains(search)) |
(Book.author.contains(search)) |
(Book.isbn.contains(search))
)
if category_id:
query = query.filter_by(category_id=category_id)
books = query.all()
# 创建DataFrame
data = []
for book in books:
category_name = book.category.name if book.category else ""
data.append({
'id': book.id,
'title': book.title,
'author': book.author,
'publisher': book.publisher,
'category': category_name,
'tags': book.tags,
'isbn': book.isbn,
'publish_year': book.publish_year,
'description': book.description,
'stock': book.stock,
'price': book.price,
'status': '上架' if book.status == 1 else '下架',
'created_at': book.created_at.strftime('%Y-%m-%d %H:%M:%S') if book.created_at else '',
'updated_at': book.updated_at.strftime('%Y-%m-%d %H:%M:%S') if book.updated_at else ''
})
df = pd.DataFrame(data)
# 创建临时文件
timestamp = datetime.datetime.now().strftime('%Y%m%d%H%M%S')
filename = f'books_export_{timestamp}.xlsx'
filepath = os.path.join(current_app.static_folder, 'temp', filename)
# 确保目录存在
os.makedirs(os.path.dirname(filepath), exist_ok=True)
# 写入Excel
df.to_excel(filepath, index=False)
# 记录操作日志
Log.add_log(
action='导出图书',
user_id=current_user.id,
ip_address=request.remote_addr,
description=f"导出图书数据: {len(data)}条记录, 查询条件: 搜索={search}, 分类ID={category_id}"
)
# 提供下载链接
return redirect(url_for('static', filename=f'temp/{filename}'))
@book_bp.route('/test-permissions')
def test_permissions():
"""测试当前用户权限"""
if not current_user.is_authenticated:
return "未登录"
return f"""
<h1>用户权限信息</h1>
<p>用户名: {current_user.username}</p>
<p>角色ID: {current_user.role_id}</p>
<p>是否管理员: {'' if current_user.role_id == 1 else ''}</p>
<p><a href="/book/admin/list">尝试访问管理页面</a></p>
"""
# 图书浏览页面 - 不需要修改,已经只有@login_required
@book_bp.route('/browse')
@login_required
def browse_books():
"""图书浏览页面 - 面向普通用户的友好界面"""
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 12, type=int) # 增加每页数量
# 只显示状态为1的图书未下架的图书
query = Book.query.filter_by(status=1)
# 搜索功能
search = request.args.get('search', '')
if search:
query = query.filter(
(Book.title.contains(search)) |
(Book.author.contains(search)) |
(Book.isbn.contains(search))
)
# 分类筛选
category_id = request.args.get('category_id', type=int)
if category_id:
query = query.filter_by(category_id=category_id)
# 排序
sort = request.args.get('sort', 'id')
order = request.args.get('order', 'desc')
if order == 'desc':
query = query.order_by(getattr(Book, sort).desc())
else:
query = query.order_by(getattr(Book, sort))
pagination = query.paginate(page=page, per_page=per_page)
books = pagination.items
# 获取所有分类供筛选使用
categories = Category.query.all()
# 记录访问日志
Log.add_log(
action='浏览图书',
user_id=current_user.id,
ip_address=request.remote_addr,
description=f"浏览图书: 搜索={search}, 分类={category_id}, 排序={sort} {order}"
)
return render_template('book/browse.html',
books=books,
pagination=pagination,
search=search,
categories=categories,
category_id=category_id,
sort=sort,
order=order)
@book_bp.route('/template/download')
@login_required
@permission_required('import_export_books') # 替换 @admin_required
def download_template():
"""生成并下载Excel图书导入模板"""
# 创建一个简单的DataFrame作为模板
data = {
'title': ['三体', '解忧杂货店'],
'author': ['刘慈欣', '东野圭吾'],
'publisher': ['重庆出版社', '南海出版公司'],
'category_id': [2, 1], # 对应于科幻小说和文学分类
'tags': ['科幻,宇宙', '治愈,悬疑'],
'isbn': ['9787229100605', '9787544270878'],
'publish_year': ['2008', '2014'],
'description': ['中国著名科幻小说,三体世界的故事。', '通过信件为人们解忧的杂货店故事。'],
'cover_url': ['', ''], # 示例中为空
'stock': [10, 5],
'price': [45.00, 39.80]
}
# 创建一个分类ID和名称的映射
categories = Category.query.all()
category_map = {cat.id: cat.name for cat in categories}
# 创建一个pandas DataFrame
df = pd.DataFrame(data)
# 添加说明工作表
with pd.ExcelWriter('book_import_template.xlsx', engine='openpyxl') as writer:
df.to_excel(writer, sheet_name='图书数据示例', index=False)
# 创建说明工作表
info_df = pd.DataFrame({
'字段名': ['title', 'author', 'publisher', 'category_id', 'tags',
'isbn', 'publish_year', 'description', 'cover_url', 'stock', 'price'],
'说明': ['图书标题 (必填)', '作者名称 (必填)', '出版社', '分类ID (对应系统分类)',
'标签 (多个标签用逗号分隔)', 'ISBN编号', '出版年份', '图书简介',
'封面图片URL', '库存数量', '价格'],
'示例': ['三体', '刘慈欣', '重庆出版社', '2 (科幻小说)', '科幻,宇宙',
'9787229100605', '2008', '中国著名科幻小说...', 'http://example.com/cover.jpg', '10', '45.00'],
'是否必填': ['', '', '', '', '', '', '', '', '', '', '']
})
info_df.to_excel(writer, sheet_name='填写说明', index=False)
# 创建分类ID工作表
category_df = pd.DataFrame({
'分类ID': list(category_map.keys()),
'分类名称': list(category_map.values())
})
category_df.to_excel(writer, sheet_name='分类对照表', index=False)
# 设置响应
timestamp = datetime.datetime.now().strftime('%Y%m%d%H%M%S')
filename = f'book_import_template_{timestamp}.xlsx'
# 记录操作日志
Log.add_log(
action='下载图书导入模板',
user_id=current_user.id,
ip_address=request.remote_addr,
description="下载图书批量导入Excel模板"
)
# 直接返回生成的文件
from flask import send_file
from io import BytesIO
# 将文件转换为二进制流
output = BytesIO()
with open('book_import_template.xlsx', 'rb') as f:
output.write(f.read())
output.seek(0)
# 删除临时文件
try:
os.remove('book_import_template.xlsx')
except:
pass
# 通过send_file返回
return send_file(
output,
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
as_attachment=True,
download_name=filename
)