diff --git a/app/controllers/book.py b/app/controllers/book.py
index 89d3c38..f3a249b 100644
--- a/app/controllers/book.py
+++ b/app/controllers/book.py
@@ -1,4 +1,4 @@
-from flask import Blueprint, render_template, request, redirect, url_for, flash, current_app, jsonify
+from flask import Blueprint, render_template, request, redirect, url_for, flash, current_app, jsonify, send_file
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
@@ -9,7 +9,9 @@ import datetime
import pandas as pd
import uuid
from app.models.log import Log
-
+from io import BytesIO
+import xlsxwriter
+from sqlalchemy import text
book_bp = Blueprint('book', __name__)
@@ -675,7 +677,7 @@ def delete_category(category_id):
# 批量导入图书
@book_bp.route('/import', methods=['GET', 'POST'])
@login_required
-@permission_required('import_export_books') # 替换 @admin_required
+@permission_required('import_export_books')
def import_books():
if request.method == 'POST':
if 'file' not in request.files:
@@ -689,41 +691,152 @@ def import_books():
if file and file.filename.endswith(('.xlsx', '.xls')):
try:
+ # 添加详细日志
+ current_app.logger.info(f"开始导入Excel文件: {file.filename}")
+
# 读取Excel文件
df = pd.read_excel(file)
+ current_app.logger.info(f"成功读取Excel文件,共有{len(df)}行数据")
+
+ # 打印DataFrame的列名和前几行数据,帮助诊断问题
+ current_app.logger.info(f"Excel文件列名: {df.columns.tolist()}")
+ current_app.logger.info(f"Excel文件前两行数据:\n{df.head(2)}")
+
success_count = 0
+ update_count = 0 # 新增:更新计数
error_count = 0
errors = []
+ # 检查必填列是否存在
+ required_columns = ['title', 'author']
+ missing_columns = [col for col in required_columns if col not in df.columns]
+ if missing_columns:
+ error_msg = f"Excel文件缺少必要的列: {', '.join(missing_columns)}"
+ current_app.logger.error(error_msg)
+ flash(error_msg, 'danger')
+ return redirect(request.url)
+
# 处理每一行数据
for index, row in df.iterrows():
try:
+ current_app.logger.debug(f"处理第{index + 2}行数据")
+
# 检查必填字段
if pd.isna(row.get('title')) or pd.isna(row.get('author')):
- errors.append(f'第{index + 2}行: 书名或作者为空')
+ error_msg = f'第{index + 2}行: 书名或作者为空'
+ current_app.logger.warning(error_msg)
+ errors.append(error_msg)
+ error_count += 1
+ continue
+
+ # 处理数据类型转换
+ try:
+ stock = int(row.get('stock')) if not pd.isna(row.get('stock')) else 0
+ except ValueError:
+ error_msg = f'第{index + 2}行: 库存数量必须是整数'
+ current_app.logger.warning(error_msg)
+ errors.append(error_msg)
+ error_count += 1
+ continue
+
+ try:
+ price = float(row.get('price')) if not pd.isna(row.get('price')) else None
+ except ValueError:
+ error_msg = f'第{index + 2}行: 价格必须是数字'
+ current_app.logger.warning(error_msg)
+ errors.append(error_msg)
+ error_count += 1
+ continue
+
+ try:
+ category_id = int(row.get('category_id')) if not pd.isna(row.get('category_id')) else None
+ if category_id and not Category.query.get(category_id):
+ error_msg = f'第{index + 2}行: 分类ID {category_id} 不存在'
+ current_app.logger.warning(error_msg)
+ errors.append(error_msg)
+ error_count += 1
+ continue
+ except ValueError:
+ error_msg = f'第{index + 2}行: 分类ID必须是整数'
+ current_app.logger.warning(error_msg)
+ errors.append(error_msg)
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
+ existing_book = None
+
+ if isbn and not pd.isna(isbn):
+ isbn = str(isbn)
+ existing_book = Book.query.filter_by(isbn=isbn).first()
+
+ # 如果ISBN已存在,检查状态
+ if existing_book:
+ if existing_book.status == 1:
+ # 活跃的图书,不允许重复导入
+ error_msg = f'第{index + 2}行: ISBN {isbn} 已存在于活跃图书中'
+ current_app.logger.warning(error_msg)
+ errors.append(error_msg)
+ error_count += 1
+ continue
+ else:
+ # 已软删除的图书,更新它
+ current_app.logger.info(f"第{index + 2}行: 发现已删除的ISBN {isbn},将更新该记录")
+
+ # 更新图书信息
+ existing_book.title = row.get('title')
+ existing_book.author = row.get('author')
+ existing_book.publisher = row.get('publisher') if not pd.isna(
+ row.get('publisher')) else None
+ existing_book.category_id = category_id
+ existing_book.tags = row.get('tags') if not pd.isna(row.get('tags')) else None
+ existing_book.publish_year = str(row.get('publish_year')) if not pd.isna(
+ row.get('publish_year')) else None
+ existing_book.description = row.get('description') if not pd.isna(
+ row.get('description')) else None
+ existing_book.cover_url = row.get('cover_url') if not pd.isna(
+ row.get('cover_url')) else None
+ existing_book.price = price
+ existing_book.status = 1 # 重新激活图书
+ existing_book.updated_at = datetime.datetime.now()
+
+ # 处理库存变更
+ stock_change = stock - existing_book.stock
+ existing_book.stock = stock
+
+ # 创建库存日志
+ if stock_change != 0:
+ from app.models.inventory import InventoryLog
+ change_type = '入库' if stock_change > 0 else '出库'
+
+ inventory_log = InventoryLog(
+ book_id=existing_book.id,
+ change_type=change_type,
+ change_amount=abs(stock_change),
+ after_stock=stock,
+ operator_id=current_user.id,
+ remark='批量导入更新图书',
+ changed_at=datetime.datetime.now()
+ )
+ db.session.add(inventory_log)
+
+ update_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,
+ category_id=category_id,
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,
+ isbn=isbn,
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,
+ stock=stock,
+ price=price,
status=1,
created_at=datetime.datetime.now(),
updated_at=datetime.datetime.now()
@@ -733,6 +846,8 @@ def import_books():
# 提交以获取book的id
db.session.flush()
+ current_app.logger.debug(f"书籍添加成功: {book.title}, ID: {book.id}")
+
# 创建库存日志
if book.stock > 0:
from app.models.inventory import InventoryLog
@@ -746,30 +861,59 @@ def import_books():
changed_at=datetime.datetime.now()
)
db.session.add(inventory_log)
+ current_app.logger.debug(f"库存日志添加成功: 书籍ID {book.id}, 数量 {book.stock}")
success_count += 1
+
except Exception as e:
- errors.append(f'第{index + 2}行: {str(e)}')
+ error_msg = f'第{index + 2}行: {str(e)}'
+ current_app.logger.error(f"处理第{index + 2}行时出错: {str(e)}")
+ errors.append(error_msg)
error_count += 1
+ db.session.rollback() # 每行错误单独回滚
- db.session.commit()
+ try:
+ # 最终提交所有成功的记录
+ if success_count > 0 or update_count > 0:
+ 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}"
- )
+ current_app.logger.info(f"导入完成: 新增{success_count}条,更新{update_count}条,失败{error_count}条")
- flash(f'导入完成: 成功{success_count}条,失败{error_count}条', 'info')
- if errors:
- flash('
'.join(errors[:10]) + (f'
...等共{len(errors)}个错误' if len(errors) > 10 else ''),
- 'warning')
+ # 记录操作日志
+ Log.add_log(
+ action='批量导入图书',
+ user_id=current_user.id,
+ ip_address=request.remote_addr,
+ description=f"批量导入图书: 新增{success_count}条,更新{update_count}条,失败{error_count}条,文件名:{file.filename}"
+ )
- return redirect(url_for('book.book_list'))
+ # 输出详细的错误信息
+ if success_count > 0 or update_count > 0:
+ flash(f'导入完成: 新增{success_count}条,更新{update_count}条,失败{error_count}条', 'success')
+ else:
+ flash(f'导入完成: 新增{success_count}条,更新{update_count}条,失败{error_count}条', 'warning')
+
+ if errors:
+ error_display = '
'.join(errors[:10])
+ if len(errors) > 10:
+ error_display += f'
...等共{len(errors)}个错误'
+ flash(error_display, 'warning')
+
+ return redirect(url_for('book.book_list'))
+
+ except Exception as commit_error:
+ db.session.rollback()
+ error_msg = f"提交数据库事务失败: {str(commit_error)}"
+ current_app.logger.error(error_msg)
+ flash(error_msg, 'danger')
+ return redirect(request.url)
except Exception as e:
+ db.session.rollback()
+ # 记录详细错误日志
+ error_msg = f"批量导入图书失败: {str(e)}"
+ current_app.logger.error(error_msg)
+
# 记录操作失败日志
Log.add_log(
action='批量导入图书失败',
@@ -788,72 +932,87 @@ def import_books():
# 导出图书
-@book_bp.route('/export')
+@book_bp.route('/export', methods=['GET'])
@login_required
-@permission_required('import_export_books') # 替换 @admin_required
+@permission_required('import_export_books')
def export_books():
- # 获取查询参数
- search = request.args.get('search', '')
- category_id = request.args.get('category_id', type=int)
+ try:
+ # 获取所有活跃的图书
+ books = Book.query.filter_by(status=1).all()
- query = Book.query
+ # 创建工作簿和工作表
+ output = BytesIO()
+ workbook = xlsxwriter.Workbook(output)
+ worksheet = workbook.add_worksheet()
- if search:
- query = query.filter(
- (Book.title.contains(search)) |
- (Book.author.contains(search)) |
- (Book.isbn.contains(search))
+ # 添加表头
+ headers = ['ID', '书名', '作者', '出版社', '分类', '标签', 'ISBN', '出版年份',
+ '描述', '封面链接', '库存', '价格', '创建时间', '更新时间']
+ for col_num, header in enumerate(headers):
+ worksheet.write(0, col_num, header)
+
+ # 不使用status过滤条件,因为Category模型没有status字段
+ category_dict = {}
+ try:
+ categories = db.session.execute(
+ text("SELECT id, name FROM categories")
+ ).fetchall()
+ for cat in categories:
+ category_dict[cat[0]] = cat[1]
+ except Exception as ex:
+ current_app.logger.warning(f"获取分类失败: {str(ex)}")
+
+ # 添加数据行
+ for row_num, book in enumerate(books, 1):
+ # 获取分类名称(如果有)
+ category_name = category_dict.get(book.category_id, "") if book.category_id 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 ""
+
+ # 写入图书数据
+ worksheet.write(row_num, 0, book.id)
+ worksheet.write(row_num, 1, book.title)
+ worksheet.write(row_num, 2, book.author)
+ worksheet.write(row_num, 3, book.publisher or "")
+ worksheet.write(row_num, 4, category_name)
+ worksheet.write(row_num, 5, book.tags or "")
+ worksheet.write(row_num, 6, book.isbn or "")
+ worksheet.write(row_num, 7, book.publish_year or "")
+ worksheet.write(row_num, 8, book.description or "")
+ worksheet.write(row_num, 9, book.cover_url or "")
+ worksheet.write(row_num, 10, book.stock)
+ worksheet.write(row_num, 11, float(book.price) if book.price else 0)
+ worksheet.write(row_num, 12, created_at)
+ worksheet.write(row_num, 13, updated_at)
+
+ # 关闭工作簿
+ workbook.close()
+
+ # 设置响应
+ output.seek(0)
+
+ # 记录操作日志
+ Log.add_log(
+ action='导出图书',
+ user_id=current_user.id,
+ ip_address=request.remote_addr,
+ description=f"导出了{len(books)}本图书"
)
- if category_id:
- query = query.filter_by(category_id=category_id)
+ # 返回文件
+ return send_file(
+ output,
+ as_attachment=True,
+ download_name=f"图书导出_{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}.xlsx",
+ mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
+ )
- 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}'))
+ except Exception as e:
+ current_app.logger.error(f"导出图书失败: {str(e)}")
+ flash(f'导出失败: {str(e)}', 'danger')
+ return redirect(url_for('book.book_list'))
@book_bp.route('/test-permissions')
@@ -931,87 +1090,219 @@ def browse_books():
@book_bp.route('/template/download')
@login_required
-@permission_required('import_export_books') # 替换 @admin_required
+@permission_required('import_export_books')
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]
+ """下载图书导入模板"""
+ import tempfile
+ import os
+ from openpyxl import Workbook
+ from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
+ from openpyxl.utils import get_column_letter
+ from flask import after_this_request, current_app as app, send_file
+
+ # 创建工作簿和工作表
+ wb = Workbook()
+ ws = wb.active
+ ws.title = "图书导入模板"
+
+ # 定义样式
+ header_font = Font(name='微软雅黑', size=11, bold=True, color="FFFFFF")
+ header_fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid")
+ required_font = Font(name='微软雅黑', size=11, bold=True, color="FF0000")
+ optional_font = Font(name='微软雅黑', size=11)
+ center_alignment = Alignment(horizontal='center', vertical='center', wrap_text=True)
+ left_alignment = Alignment(vertical='center', wrap_text=True)
+
+ # 定义边框样式
+ thin_border = Border(
+ left=Side(style='thin'),
+ right=Side(style='thin'),
+ top=Side(style='thin'),
+ bottom=Side(style='thin')
+ )
+
+ # 定义列宽
+ column_widths = {
+ 'A': 30, # title
+ 'B': 20, # author
+ 'C': 20, # publisher
+ 'D': 15, # category_id
+ 'E': 25, # tags
+ 'F': 15, # isbn
+ 'G': 15, # publish_year
+ 'H': 40, # description
+ 'I': 30, # cover_url
+ 'J': 10, # stock
+ 'K': 10, # price
}
- # 创建一个分类ID和名称的映射
- categories = Category.query.all()
- category_map = {cat.id: cat.name for cat in categories}
+ # 设置列宽
+ for col, width in column_widths.items():
+ ws.column_dimensions[col].width = width
- # 创建一个pandas DataFrame
- df = pd.DataFrame(data)
+ # 定义字段信息
+ fields = [
+ {'name': 'title', 'desc': '图书标题', 'required': True, 'example': '哈利·波特与魔法石'},
+ {'name': 'author', 'desc': '作者名称', 'required': True, 'example': 'J.K.罗琳'},
+ {'name': 'publisher', 'desc': '出版社', 'required': False, 'example': '人民文学出版社'},
+ {'name': 'category_id', 'desc': '分类ID', 'required': False, 'example': '1'},
+ {'name': 'tags', 'desc': '标签', 'required': False, 'example': '魔幻,冒险,青少年文学'},
+ {'name': 'isbn', 'desc': 'ISBN编号', 'required': False, 'example': '9787020042494'},
+ {'name': 'publish_year', 'desc': '出版年份', 'required': False, 'example': '2000'},
+ {'name': 'description', 'desc': '图书简介', 'required': False,
+ 'example': '这是一本关于魔法的书籍,讲述了小男孩哈利...'},
+ {'name': 'cover_url', 'desc': '封面图片URL', 'required': False, 'example': 'https://example.com/covers/hp.jpg'},
+ {'name': 'stock', 'desc': '库存数量', 'required': False, 'example': '10'},
+ {'name': 'price', 'desc': '价格', 'required': False, 'example': '39.5'},
+ ]
+
+ # 添加标题行
+ ws.append([f"{field['name']}" for field in fields])
+
+ # 设置标题行样式
+ for col_idx, field in enumerate(fields, start=1):
+ cell = ws.cell(row=1, column=col_idx)
+ cell.font = header_font
+ cell.fill = header_fill
+ cell.alignment = center_alignment
+ cell.border = thin_border
+
+ # 添加示例数据行
+ ws.append([field['example'] for field in fields])
+
+ # 设置示例行样式
+ for col_idx, field in enumerate(fields, start=1):
+ cell = ws.cell(row=2, column=col_idx)
+ cell.alignment = left_alignment
+ cell.border = thin_border
+ if field['required']:
+ cell.font = required_font
+ else:
+ cell.font = optional_font
+
+ # 冻结首行
+ ws.freeze_panes = "A2"
# 添加说明工作表
- with pd.ExcelWriter('book_import_template.xlsx', engine='openpyxl') as writer:
- df.to_excel(writer, sheet_name='图书数据示例', index=False)
+ instructions_ws = wb.create_sheet(title="填写说明")
- # 创建说明工作表
- 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)
+ # 设置说明工作表的列宽
+ instructions_ws.column_dimensions['A'].width = 15
+ instructions_ws.column_dimensions['B'].width = 20
+ instructions_ws.column_dimensions['C'].width = 60
- # 创建分类ID工作表
- category_df = pd.DataFrame({
- '分类ID': list(category_map.keys()),
- '分类名称': list(category_map.values())
- })
- category_df.to_excel(writer, sheet_name='分类对照表', index=False)
+ # 添加说明标题
+ instructions_ws.append(["字段名", "说明", "备注"])
- # 设置响应
- timestamp = datetime.datetime.now().strftime('%Y%m%d%H%M%S')
- filename = f'book_import_template_{timestamp}.xlsx'
+ # 设置标题行样式
+ for col_idx in range(1, 4):
+ cell = instructions_ws.cell(row=1, column=col_idx)
+ cell.font = header_font
+ cell.fill = header_fill
+ cell.alignment = center_alignment
+ cell.border = thin_border
- # 记录操作日志
- Log.add_log(
- action='下载图书导入模板',
- user_id=current_user.id,
- ip_address=request.remote_addr,
- description="下载图书批量导入Excel模板"
- )
+ # 字段详细说明
+ notes = {
+ 'title': "图书的完整名称,例如:'哈利·波特与魔法石'。必须填写且不能为空。",
+ 'author': "图书的作者名称,例如:'J.K.罗琳'。必须填写且不能为空。如有多个作者,请用逗号分隔。",
+ 'publisher': "出版社名称,例如:'人民文学出版社'。",
+ 'category_id': "图书分类的系统ID。请在系统的分类管理页面查看ID。如果不确定,可以留空,系统将使用默认分类。",
+ 'tags': "图书的标签,多个标签请用英文逗号分隔,例如:'魔幻,冒险,青少年文学'。",
+ 'isbn': "图书的ISBN号码,例如:'9787020042494'。请输入完整的13位或10位ISBN。",
+ 'publish_year': "图书的出版年份,例如:'2000'。请输入4位数字年份。",
+ 'description': "图书的简介或摘要。可以输入详细的描述信息,系统支持换行和基本格式。",
+ 'cover_url': "图书封面的在线URL地址。如果有图片网址,系统将自动下载并设置为封面。",
+ 'stock': "图书的库存数量,例如:'10'。请输入整数。",
+ 'price': "图书的价格,例如:'39.5'。可以输入小数。"
+ }
- # 直接返回生成的文件
- from flask import send_file
- from io import BytesIO
+ # 添加字段说明
+ for idx, field in enumerate(fields, start=2):
+ required_text = "【必填】" if field['required'] else "【选填】"
+ instructions_ws.append([
+ field['name'],
+ f"{field['desc']} {required_text}",
+ notes.get(field['name'], "")
+ ])
- # 将文件转换为二进制流
- output = BytesIO()
- with open('book_import_template.xlsx', 'rb') as f:
- output.write(f.read())
- output.seek(0)
+ # 设置单元格样式
+ for col_idx in range(1, 4):
+ cell = instructions_ws.cell(row=idx, column=col_idx)
+ cell.alignment = left_alignment
+ cell.border = thin_border
+ if field['required'] and col_idx == 2:
+ cell.font = required_font
+ else:
+ cell.font = optional_font
+
+ # 添加示例行
+ instructions_ws.append(["", "", ""])
+ instructions_ws.append(["示例", "", "以下是完整的示例数据:"])
+
+ example_data = [
+ {"title": "哈利·波特与魔法石", "author": "J.K.罗琳", "publisher": "人民文学出版社",
+ "category_id": "1", "tags": "魔幻,冒险,青少年文学", "isbn": "9787020042494",
+ "publish_year": "2000", "description": "这是一本关于魔法的书籍,讲述了小男孩哈利...",
+ "cover_url": "https://example.com/covers/hp.jpg", "stock": "10", "price": "39.5"},
+
+ {"title": "三体", "author": "刘慈欣", "publisher": "重庆出版社",
+ "category_id": "2", "tags": "科幻,硬科幻,中国科幻", "isbn": "9787536692930",
+ "publish_year": "2008", "description": "文化大革命如火如荼进行的同时...",
+ "cover_url": "https://example.com/covers/threebody.jpg", "stock": "15", "price": "59.8"},
+
+ {"title": "平凡的世界", "author": "路遥", "publisher": "北京十月文艺出版社",
+ "category_id": "3", "tags": "文学,现实主义,农村", "isbn": "9787530216781",
+ "publish_year": "2017", "description": "这是一部全景式地表现中国当代城乡社会...",
+ "cover_url": "https://example.com/covers/world.jpg", "stock": "8", "price": "128.0"}
+ ]
+
+ # 添加3个完整示例到主模板工作表
+ for example in example_data:
+ row_data = [example.get(field['name'], '') for field in fields]
+ ws.append(row_data)
+
+ # 设置示例数据样式
+ for row_idx in range(3, 6):
+ for col_idx in range(1, len(fields) + 1):
+ cell = ws.cell(row=row_idx, column=col_idx)
+ cell.alignment = left_alignment
+ cell.border = thin_border
+
+ # 使用临时文件保存工作簿
+ fd, temp_path = tempfile.mkstemp(suffix='.xlsx')
+ os.close(fd)
- # 删除临时文件
try:
- os.remove('book_import_template.xlsx')
- except:
- pass
+ wb.save(temp_path)
- # 通过send_file返回
- return send_file(
- output,
- mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
- as_attachment=True,
- download_name=filename
- )
+ # 注册一个函数在响应完成后删除临时文件
+ @after_this_request
+ def remove_file(response):
+ try:
+ os.unlink(temp_path)
+ except Exception as error:
+ app.logger.error("删除临时文件出错: %s", error)
+ return response
+
+ response = send_file(
+ temp_path,
+ as_attachment=True,
+ download_name='图书导入模板.xlsx',
+ mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
+ )
+
+ # 添加防止缓存的头信息
+ response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
+ response.headers["Pragma"] = "no-cache"
+ response.headers["Expires"] = "0"
+
+ return response
+ except Exception as e:
+ # 确保在出错时也删除临时文件
+ try:
+ os.unlink(temp_path)
+ except:
+ pass
+ app.logger.error("生成模板文件出错: %s", str(e))
+ return {"error": "生成模板文件失败,请稍后再试"}, 500
diff --git a/app/static/js/book-import.js b/app/static/js/book-import.js
index 8676cf5..9379fa9 100644
--- a/app/static/js/book-import.js
+++ b/app/static/js/book-import.js
@@ -22,28 +22,72 @@ document.addEventListener('DOMContentLoaded', function() {
});
}
- // 美化表单提交按钮的点击效果
- const importBtn = document.querySelector('.import-btn');
- if (importBtn) {
- importBtn.addEventListener('click', function(e) {
+ // 监听表单提交
+ const form = document.querySelector('form');
+ if (form) {
+ form.addEventListener('submit', function(e) {
+ const fileInput = document.getElementById('file');
if (!fileInput || !fileInput.files || !fileInput.files.length) {
e.preventDefault();
showMessage('请先选择要导入的Excel文件', 'warning');
return;
}
- this.innerHTML = ' 正在导入...';
- this.disabled = true;
+ const importBtn = document.querySelector('.import-btn');
+ if (importBtn) {
+ importBtn.innerHTML = ' 正在导入...';
+ importBtn.disabled = true;
+ }
// 添加花朵飘落动画效果
addFallingElements(10);
+
+ // 设置超时处理,如果30秒后还没响应,提示用户
+ window.importTimeout = setTimeout(function() {
+ showMessage('导入处理时间较长,请耐心等待...', 'info');
+ }, 30000);
+ });
+ }
+
+ // 美化表单提交按钮的点击效果
+ const importBtn = document.querySelector('.import-btn');
+ if (importBtn) {
+ importBtn.addEventListener('click', function(e) {
+ // 按钮的点击效果已由表单提交事件处理
+ // 避免重复处理
+ if (!form || form.reportValidity() === false) {
+ e.preventDefault();
+ }
});
}
// 为浮动元素添加动画
initFloatingElements();
+
+ // 检查页面中的flash消息
+ checkFlashMessages();
});
+// 检查页面中的flash消息
+function checkFlashMessages() {
+ // Flask的flash消息通常会渲染为带有特定类的元素
+ const flashMessages = document.querySelectorAll('.alert');
+ if (flashMessages && flashMessages.length > 0) {
+ // 如果存在flash消息,说明页面是提交后重新加载的
+ // 恢复按钮状态
+ const importBtn = document.querySelector('.import-btn');
+ if (importBtn) {
+ importBtn.innerHTML = ' 开始导入';
+ importBtn.disabled = false;
+ }
+
+ // 清除可能的超时
+ if (window.importTimeout) {
+ clearTimeout(window.importTimeout);
+ }
+ }
+}
+
// 检查文件类型并尝试预览
function checkFileAndPreview(file) {
if (!file) return;
diff --git a/app/static/temp/book_import_template_20250516215908.xlsx b/app/static/temp/book_import_template_20250516215908.xlsx
new file mode 100644
index 0000000..04f131a
Binary files /dev/null and b/app/static/temp/book_import_template_20250516215908.xlsx differ
diff --git a/app/static/temp/book_import_template_20250516215913.xlsx b/app/static/temp/book_import_template_20250516215913.xlsx
new file mode 100644
index 0000000..a152d8e
Binary files /dev/null and b/app/static/temp/book_import_template_20250516215913.xlsx differ
diff --git a/app/static/temp/book_import_template_20250516220032.xlsx b/app/static/temp/book_import_template_20250516220032.xlsx
new file mode 100644
index 0000000..e204e1b
Binary files /dev/null and b/app/static/temp/book_import_template_20250516220032.xlsx differ
diff --git a/app/static/temp/book_import_template_20250516220049.xlsx b/app/static/temp/book_import_template_20250516220049.xlsx
new file mode 100644
index 0000000..3a7e612
Binary files /dev/null and b/app/static/temp/book_import_template_20250516220049.xlsx differ
diff --git a/app/static/temp/book_import_template_20250516220442.xlsx b/app/static/temp/book_import_template_20250516220442.xlsx
new file mode 100644
index 0000000..5ed1a85
Binary files /dev/null and b/app/static/temp/book_import_template_20250516220442.xlsx differ
diff --git a/app/static/temp/book_import_template_20250516220453.xlsx b/app/static/temp/book_import_template_20250516220453.xlsx
new file mode 100644
index 0000000..fc796fe
Binary files /dev/null and b/app/static/temp/book_import_template_20250516220453.xlsx differ
diff --git a/app/static/temp/book_import_template_20250516220458.xlsx b/app/static/temp/book_import_template_20250516220458.xlsx
new file mode 100644
index 0000000..ed31cfe
Binary files /dev/null and b/app/static/temp/book_import_template_20250516220458.xlsx differ
diff --git a/app/static/temp/book_import_template_20250516220505.xlsx b/app/static/temp/book_import_template_20250516220505.xlsx
new file mode 100644
index 0000000..b0cc7be
Binary files /dev/null and b/app/static/temp/book_import_template_20250516220505.xlsx differ
diff --git a/app/static/temp/book_import_template_20250516220509.xlsx b/app/static/temp/book_import_template_20250516220509.xlsx
new file mode 100644
index 0000000..a798609
Binary files /dev/null and b/app/static/temp/book_import_template_20250516220509.xlsx differ
diff --git a/app/static/temp/book_import_template_20250516220625.xlsx b/app/static/temp/book_import_template_20250516220625.xlsx
new file mode 100644
index 0000000..8034093
Binary files /dev/null and b/app/static/temp/book_import_template_20250516220625.xlsx differ
diff --git a/app/templates/base.html b/app/templates/base.html
index 649bcc6..b2bc3f6 100644
--- a/app/templates/base.html
+++ b/app/templates/base.html
@@ -68,10 +68,11 @@
-
-
-
-
+
+
@@ -82,9 +84,91 @@
+
+
+
{% endblock %}
{% block scripts %}
+
{% endblock %}
diff --git a/requirements.txt b/requirements.txt
index 6cb8b47..75764d7 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -10,4 +10,7 @@ email-validator==2.0.0
pillow==9.5.0
numpy
pandas
-flask-login
\ No newline at end of file
+flask-login
+openpyxl
+xlrd
+xlsxwriter
\ No newline at end of file