完成权限管理分类

This commit is contained in:
superlishunqin 2025-05-14 15:08:06 +08:00
parent 73a08d0ab2
commit 89d17f1ba6
19 changed files with 1608 additions and 218 deletions

View File

@ -1,6 +1,7 @@
from flask import Flask, render_template, session, g, Markup, redirect, url_for from flask import Flask, render_template, session, g, Markup, redirect, url_for
from flask_login import LoginManager from flask_login import LoginManager
from app.models.user import db, User from app.models.database import db
from app.models.user import User
from app.controllers.user import user_bp from app.controllers.user import user_bp
from app.controllers.book import book_bp from app.controllers.book import book_bp
from app.controllers.borrow import borrow_bp from app.controllers.borrow import borrow_bp

View File

@ -1,10 +1,11 @@
from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify
from app.models.announcement import Announcement from app.models.announcement import Announcement
from app.models.log import Log from app.models.log import Log
from app.utils.auth import admin_required from app.utils.auth import permission_required # 修改导入
from flask_login import login_required, current_user from flask_login import login_required, current_user
from datetime import datetime from datetime import datetime
from app.models.notification import Notification from app.models.notification import Notification
from app import db # 为mark_all_as_read函数添加db导入
# 创建蓝图 # 创建蓝图
announcement_bp = Blueprint('announcement', __name__) announcement_bp = Blueprint('announcement', __name__)
@ -41,7 +42,7 @@ def announcement_detail(announcement_id):
@announcement_bp.route('/manage', methods=['GET']) @announcement_bp.route('/manage', methods=['GET'])
@login_required @login_required
@admin_required @permission_required('manage_announcements') # 替代 @admin_required
def manage_announcements(): def manage_announcements():
"""管理员公告管理页面""" """管理员公告管理页面"""
page = request.args.get('page', 1, type=int) page = request.args.get('page', 1, type=int)
@ -86,7 +87,7 @@ def manage_announcements():
@announcement_bp.route('/add', methods=['GET', 'POST']) @announcement_bp.route('/add', methods=['GET', 'POST'])
@login_required @login_required
@admin_required @permission_required('manage_announcements') # 替代 @admin_required
def add_announcement(): def add_announcement():
"""添加公告""" """添加公告"""
if request.method == 'POST': if request.method == 'POST':
@ -127,7 +128,7 @@ def add_announcement():
@announcement_bp.route('/edit/<int:announcement_id>', methods=['GET', 'POST']) @announcement_bp.route('/edit/<int:announcement_id>', methods=['GET', 'POST'])
@login_required @login_required
@admin_required @permission_required('manage_announcements') # 替代 @admin_required
def edit_announcement(announcement_id): def edit_announcement(announcement_id):
"""编辑公告""" """编辑公告"""
announcement = Announcement.get_announcement_by_id(announcement_id) announcement = Announcement.get_announcement_by_id(announcement_id)
@ -174,7 +175,7 @@ def edit_announcement(announcement_id):
@announcement_bp.route('/status/<int:announcement_id>', methods=['POST']) @announcement_bp.route('/status/<int:announcement_id>', methods=['POST'])
@login_required @login_required
@admin_required @permission_required('manage_announcements') # 替代 @admin_required
def change_status(announcement_id): def change_status(announcement_id):
"""更改公告状态""" """更改公告状态"""
data = request.get_json() data = request.get_json()
@ -209,7 +210,7 @@ def change_status(announcement_id):
@announcement_bp.route('/top/<int:announcement_id>', methods=['POST']) @announcement_bp.route('/top/<int:announcement_id>', methods=['POST'])
@login_required @login_required
@admin_required @permission_required('manage_announcements') # 替代 @admin_required
def change_top_status(announcement_id): def change_top_status(announcement_id):
"""更改公告置顶状态""" """更改公告置顶状态"""
data = request.get_json() data = request.get_json()

View File

@ -1,21 +1,21 @@
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
from app.models.book import Book, Category from app.models.book import Book, Category
from app.models.user import db from app.models.user import db
from app.utils.auth import login_required, admin_required from app.utils.auth import login_required, permission_required # 修改导入,替换admin_required为permission_required
from flask_login import current_user # 移除重复的 login_required 导入 from flask_login import current_user
import os import os
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
import datetime import datetime
import pandas as pd import pandas as pd
import uuid import uuid
from app.models.log import Log # 导入日志模型 from app.models.log import Log
book_bp = Blueprint('book', __name__) book_bp = Blueprint('book', __name__)
@book_bp.route('/admin/list') @book_bp.route('/admin/list')
@login_required @login_required
@admin_required @permission_required('manage_books') # 替换 @admin_required
def admin_book_list(): def admin_book_list():
print(f"DEBUG: admin_book_list 函数被调用,用户={current_user.username},认证状态={current_user.is_authenticated}") print(f"DEBUG: admin_book_list 函数被调用,用户={current_user.username},认证状态={current_user.is_authenticated}")
page = request.args.get('page', 1, type=int) page = request.args.get('page', 1, type=int)
@ -62,11 +62,11 @@ def admin_book_list():
category_id=category_id, category_id=category_id,
sort=sort, sort=sort,
order=order, order=order,
current_user=current_user, # 使用current_user替代g.user current_user=current_user,
is_admin_view=True) # 指明这是管理视图 is_admin_view=True)
# 图书列表页面 # 图书列表页面 - 不需要修改,已经只有@login_required
@book_bp.route('/list') @book_bp.route('/list')
@login_required @login_required
def book_list(): def book_list():
@ -122,10 +122,10 @@ def book_list():
category_id=category_id, category_id=category_id,
sort=sort, sort=sort,
order=order, order=order,
current_user=current_user) # 使用current_user替代g.user current_user=current_user)
# 图书详情页面 # 图书详情页面 - 不需要修改,已经只有@login_required
@book_bp.route('/detail/<int:book_id>') @book_bp.route('/detail/<int:book_id>')
@login_required @login_required
def book_detail(book_id): def book_detail(book_id):
@ -164,7 +164,7 @@ def book_detail(book_id):
# 添加图书页面 # 添加图书页面
@book_bp.route('/add', methods=['GET', 'POST']) @book_bp.route('/add', methods=['GET', 'POST'])
@login_required @login_required
@admin_required @permission_required('manage_books') # 替换 @admin_required
def add_book(): def add_book():
if request.method == 'POST': if request.method == 'POST':
title = request.form.get('title') title = request.form.get('title')
@ -269,7 +269,7 @@ def add_book():
change_type='入库', change_type='入库',
change_amount=stock, change_amount=stock,
after_stock=stock, after_stock=stock,
operator_id=current_user.id, # 使用current_user.id替代g.user.id operator_id=current_user.id,
remark='新书入库', remark='新书入库',
changed_at=datetime.datetime.now() changed_at=datetime.datetime.now()
) )
@ -329,7 +329,7 @@ def add_book():
# 编辑图书 # 编辑图书
@book_bp.route('/edit/<int:book_id>', methods=['GET', 'POST']) @book_bp.route('/edit/<int:book_id>', methods=['GET', 'POST'])
@login_required @login_required
@admin_required @permission_required('manage_books') # 替换 @admin_required
def edit_book(book_id): def edit_book(book_id):
book = Book.query.get_or_404(book_id) book = Book.query.get_or_404(book_id)
@ -422,7 +422,7 @@ def edit_book(book_id):
change_type=change_type, change_type=change_type,
change_amount=abs(change_amount), change_amount=abs(change_amount),
after_stock=new_stock, after_stock=new_stock,
operator_id=current_user.id, # 使用current_user.id替代g.user.id operator_id=current_user.id,
remark=f'管理员编辑图书库存 - {book.title}', remark=f'管理员编辑图书库存 - {book.title}',
changed_at=datetime.datetime.now() changed_at=datetime.datetime.now()
) )
@ -500,7 +500,7 @@ def edit_book(book_id):
# 删除图书 # 删除图书
@book_bp.route('/delete/<int:book_id>', methods=['POST']) @book_bp.route('/delete/<int:book_id>', methods=['POST'])
@login_required @login_required
@admin_required @permission_required('manage_books') # 替换 @admin_required
def delete_book(book_id): def delete_book(book_id):
book = Book.query.get_or_404(book_id) book = Book.query.get_or_404(book_id)
@ -541,7 +541,7 @@ def delete_book(book_id):
# 图书分类管理 # 图书分类管理
@book_bp.route('/categories', methods=['GET']) @book_bp.route('/categories', methods=['GET'])
@login_required @login_required
@admin_required @permission_required('manage_categories') # 替换 @admin_required
def category_list(): def category_list():
categories = Category.query.all() categories = Category.query.all()
@ -559,7 +559,7 @@ def category_list():
# 添加分类 # 添加分类
@book_bp.route('/categories/add', methods=['POST']) @book_bp.route('/categories/add', methods=['POST'])
@login_required @login_required
@admin_required @permission_required('manage_categories') # 替换 @admin_required
def add_category(): def add_category():
name = request.form.get('name') name = request.form.get('name')
parent_id = request.form.get('parent_id') or None parent_id = request.form.get('parent_id') or None
@ -586,7 +586,7 @@ def add_category():
# 编辑分类 # 编辑分类
@book_bp.route('/categories/edit/<int:category_id>', methods=['POST']) @book_bp.route('/categories/edit/<int:category_id>', methods=['POST'])
@login_required @login_required
@admin_required @permission_required('manage_categories') # 替换 @admin_required
def edit_category(category_id): def edit_category(category_id):
category = Category.query.get_or_404(category_id) category = Category.query.get_or_404(category_id)
@ -622,7 +622,7 @@ def edit_category(category_id):
# 删除分类 # 删除分类
@book_bp.route('/categories/delete/<int:category_id>', methods=['POST']) @book_bp.route('/categories/delete/<int:category_id>', methods=['POST'])
@login_required @login_required
@admin_required @permission_required('manage_categories') # 替换 @admin_required
def delete_category(category_id): def delete_category(category_id):
category = Category.query.get_or_404(category_id) category = Category.query.get_or_404(category_id)
@ -675,7 +675,7 @@ def delete_category(category_id):
# 批量导入图书 # 批量导入图书
@book_bp.route('/import', methods=['GET', 'POST']) @book_bp.route('/import', methods=['GET', 'POST'])
@login_required @login_required
@admin_required @permission_required('import_export_books') # 替换 @admin_required
def import_books(): def import_books():
if request.method == 'POST': if request.method == 'POST':
if 'file' not in request.files: if 'file' not in request.files:
@ -741,7 +741,7 @@ def import_books():
change_type='入库', change_type='入库',
change_amount=book.stock, change_amount=book.stock,
after_stock=book.stock, after_stock=book.stock,
operator_id=current_user.id, # 使用current_user.id operator_id=current_user.id,
remark='批量导入图书', remark='批量导入图书',
changed_at=datetime.datetime.now() changed_at=datetime.datetime.now()
) )
@ -784,13 +784,13 @@ def import_books():
flash('只支持Excel文件(.xlsx, .xls)', 'danger') flash('只支持Excel文件(.xlsx, .xls)', 'danger')
return redirect(request.url) return redirect(request.url)
return render_template('book/import.html', current_user=current_user) # 使用current_user return render_template('book/import.html', current_user=current_user)
# 导出图书 # 导出图书
@book_bp.route('/export') @book_bp.route('/export')
@login_required @login_required
@admin_required @permission_required('import_export_books') # 替换 @admin_required
def export_books(): def export_books():
# 获取查询参数 # 获取查询参数
search = request.args.get('search', '') search = request.args.get('search', '')
@ -871,8 +871,7 @@ def test_permissions():
""" """
# 添加到app/controllers/book.py文件中 # 图书浏览页面 - 不需要修改,已经只有@login_required
@book_bp.route('/browse') @book_bp.route('/browse')
@login_required @login_required
def browse_books(): def browse_books():
@ -927,12 +926,12 @@ def browse_books():
categories=categories, categories=categories,
category_id=category_id, category_id=category_id,
sort=sort, sort=sort,
order=order) # current_user自动传递到模板 order=order)
@book_bp.route('/template/download') @book_bp.route('/template/download')
@login_required @login_required
@admin_required @permission_required('import_export_books') # 替换 @admin_required
def download_template(): def download_template():
"""生成并下载Excel图书导入模板""" """生成并下载Excel图书导入模板"""
# 创建一个简单的DataFrame作为模板 # 创建一个简单的DataFrame作为模板

View File

@ -6,7 +6,7 @@ from app.models.inventory import InventoryLog
from app.models.user import db, User from app.models.user import db, User
from app.models.log import Log # 导入日志模型 from app.models.log import Log # 导入日志模型
import datetime import datetime
from app.utils.auth import admin_required from app.utils.auth import permission_required # 修改导入使用permission_required而不是admin_required
# 创建借阅蓝图 # 创建借阅蓝图
borrow_bp = Blueprint('borrow', __name__, url_prefix='/borrow') borrow_bp = Blueprint('borrow', __name__, url_prefix='/borrow')
@ -191,7 +191,7 @@ def return_book(borrow_id):
borrow_record = BorrowRecord.query.get_or_404(borrow_id) borrow_record = BorrowRecord.query.get_or_404(borrow_id)
# 检查是否是自己的借阅记录或者是管理员 # 检查是否是自己的借阅记录或者是管理员
if borrow_record.user_id != current_user.id and current_user.role_id != 1: if borrow_record.user_id != current_user.id and not current_user.has_permission('manage_borrows'):
return jsonify({ return jsonify({
'success': False, 'success': False,
'message': '您无权执行此操作' 'message': '您无权执行此操作'
@ -268,7 +268,7 @@ def renew_book(borrow_id):
borrow_record = BorrowRecord.query.get_or_404(borrow_id) borrow_record = BorrowRecord.query.get_or_404(borrow_id)
# 检查是否是自己的借阅记录或者是管理员 # 检查是否是自己的借阅记录或者是管理员
if borrow_record.user_id != current_user.id and current_user.role_id != 1: if borrow_record.user_id != current_user.id and not current_user.has_permission('manage_borrows'):
return jsonify({ return jsonify({
'success': False, 'success': False,
'message': '您无权执行此操作' 'message': '您无权执行此操作'
@ -376,7 +376,7 @@ def my_borrows():
@borrow_bp.route('/manage') @borrow_bp.route('/manage')
@login_required @login_required
@admin_required @permission_required('manage_borrows') # 替代 @admin_required
def manage_borrows(): def manage_borrows():
"""管理员查看所有借阅记录""" """管理员查看所有借阅记录"""
page = request.args.get('page', 1, type=int) page = request.args.get('page', 1, type=int)
@ -444,7 +444,7 @@ def manage_borrows():
@borrow_bp.route('/admin/add', methods=['POST']) @borrow_bp.route('/admin/add', methods=['POST'])
@login_required @login_required
@admin_required @permission_required('manage_borrows') # 替代 @admin_required
def admin_add_borrow(): def admin_add_borrow():
"""管理员为用户添加借阅记录""" """管理员为用户添加借阅记录"""
user_id = request.form.get('user_id', type=int) user_id = request.form.get('user_id', type=int)
@ -532,7 +532,7 @@ def admin_add_borrow():
@borrow_bp.route('/overdue') @borrow_bp.route('/overdue')
@login_required @login_required
@admin_required @permission_required('manage_overdue') # 替代 @admin_required
def overdue_borrows(): def overdue_borrows():
"""查看逾期借阅""" """查看逾期借阅"""
page = request.args.get('page', 1, type=int) page = request.args.get('page', 1, type=int)
@ -566,7 +566,7 @@ def overdue_borrows():
@borrow_bp.route('/overdue/notify/<int:borrow_id>', methods=['POST']) @borrow_bp.route('/overdue/notify/<int:borrow_id>', methods=['POST'])
@login_required @login_required
@admin_required @permission_required('manage_overdue') # 替代 @admin_required
def notify_overdue(borrow_id): def notify_overdue(borrow_id):
"""发送逾期通知""" """发送逾期通知"""
from app.models.notification import Notification from app.models.notification import Notification

View File

@ -5,7 +5,7 @@ from app.models.book import Book
from app.models.inventory import InventoryLog from app.models.inventory import InventoryLog
from app.models.log import Log # 导入日志模型 from app.models.log import Log # 导入日志模型
from app.models.user import db from app.models.user import db
from app.utils.auth import admin_required from app.utils.auth import permission_required # 修改导入使用permission_required替代admin_required
from datetime import datetime from datetime import datetime
inventory_bp = Blueprint('inventory', __name__, url_prefix='/inventory') inventory_bp = Blueprint('inventory', __name__, url_prefix='/inventory')
@ -13,9 +13,9 @@ inventory_bp = Blueprint('inventory', __name__, url_prefix='/inventory')
@inventory_bp.route('/') @inventory_bp.route('/')
@login_required @login_required
@admin_required @permission_required('manage_inventory') # 替代 @admin_required
def inventory_list(): def inventory_list():
"""库存管理页面 - 只有管理员有权限进入""" """库存管理页面 - 只有拥有库存管理权限的用户有权限进入"""
page = request.args.get('page', 1, type=int) page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 20, type=int) per_page = request.args.get('per_page', 20, type=int)
@ -47,7 +47,7 @@ def inventory_list():
user_id=current_user.id, user_id=current_user.id,
target_type="inventory", target_type="inventory",
ip_address=request.remote_addr, ip_address=request.remote_addr,
description=f"管理员访问库存管理页面,搜索条件:{search if search else ''}" description=f"用户访问库存管理页面,搜索条件:{search if search else ''}"
) )
return render_template('inventory/list.html', return render_template('inventory/list.html',
@ -60,7 +60,7 @@ def inventory_list():
@inventory_bp.route('/adjust/<int:book_id>', methods=['GET', 'POST']) @inventory_bp.route('/adjust/<int:book_id>', methods=['GET', 'POST'])
@login_required @login_required
@admin_required @permission_required('manage_inventory') # 替代 @admin_required
def adjust_inventory(book_id): def adjust_inventory(book_id):
"""调整图书库存""" """调整图书库存"""
book = Book.query.get_or_404(book_id) book = Book.query.get_or_404(book_id)
@ -73,7 +73,7 @@ def adjust_inventory(book_id):
target_type="book", target_type="book",
target_id=book.id, target_id=book.id,
ip_address=request.remote_addr, ip_address=request.remote_addr,
description=f"管理员查看图书《{book.title}》的库存调整页面" description=f"用户查看图书《{book.title}》的库存调整页面"
) )
if request.method == 'POST': if request.method == 'POST':
@ -123,7 +123,7 @@ def adjust_inventory(book_id):
target_type="book", target_type="book",
target_id=book.id, target_id=book.id,
ip_address=request.remote_addr, ip_address=request.remote_addr,
description=f"管理员对图书《{book.title}》进行{operation_desc}操作,数量:{change_amount}" description=f"用户对图书《{book.title}》进行{operation_desc}操作,数量:{change_amount}"
f"原库存:{original_stock},现库存:{after_stock},备注:{remark}" f"原库存:{original_stock},现库存:{after_stock},备注:{remark}"
) )
@ -140,7 +140,7 @@ def adjust_inventory(book_id):
@inventory_bp.route('/logs') @inventory_bp.route('/logs')
@login_required @login_required
@admin_required @permission_required('manage_inventory') # 替代 @admin_required
def inventory_logs(): def inventory_logs():
"""查看库存变动日志""" """查看库存变动日志"""
page = request.args.get('page', 1, type=int) page = request.args.get('page', 1, type=int)
@ -189,7 +189,7 @@ def inventory_logs():
user_id=current_user.id, user_id=current_user.id,
target_type="inventory_log", target_type="inventory_log",
ip_address=request.remote_addr, ip_address=request.remote_addr,
description=f"管理员查看库存变动日志,筛选条件:{', '.join(filter_desc) if filter_desc else ''}" description=f"用户查看库存变动日志,筛选条件:{', '.join(filter_desc) if filter_desc else ''}"
) )
return render_template('inventory/logs.html', return render_template('inventory/logs.html',
@ -205,7 +205,7 @@ def inventory_logs():
@inventory_bp.route('/book/<int:book_id>/logs') @inventory_bp.route('/book/<int:book_id>/logs')
@login_required @login_required
@admin_required @permission_required('manage_inventory') # 替代 @admin_required
def book_inventory_logs(book_id): def book_inventory_logs(book_id):
"""查看特定图书的库存变动日志""" """查看特定图书的库存变动日志"""
book = Book.query.get_or_404(book_id) book = Book.query.get_or_404(book_id)
@ -223,7 +223,7 @@ def book_inventory_logs(book_id):
target_type="book", target_type="book",
target_id=book.id, target_id=book.id,
ip_address=request.remote_addr, ip_address=request.remote_addr,
description=f"管理员查看图书《{book.title}》的库存变动日志" description=f"用户查看图书《{book.title}》的库存变动日志"
) )
return render_template('inventory/book_logs.html', return render_template('inventory/book_logs.html',

View File

@ -2,7 +2,7 @@ from flask import Blueprint, render_template, request, jsonify
from flask_login import current_user, login_required from flask_login import current_user, login_required
from app.models.log import Log from app.models.log import Log
from app.models.user import User, db # 导入db from app.models.user import User, db # 导入db
from app.controllers.user import admin_required # 导入admin_required装饰器 from app.utils.auth import permission_required # 更改为导入permission_required装饰器
from datetime import datetime, timedelta from datetime import datetime, timedelta
# 创建蓝图 # 创建蓝图
@ -11,7 +11,7 @@ log_bp = Blueprint('log', __name__, url_prefix='/log')
@log_bp.route('/list') @log_bp.route('/list')
@login_required @login_required
@admin_required @permission_required('view_logs') # 替代 @admin_required
def log_list(): def log_list():
"""日志列表页面""" """日志列表页面"""
# 获取筛选参数 # 获取筛选参数
@ -81,7 +81,7 @@ def log_list():
@log_bp.route('/detail/<int:log_id>') @log_bp.route('/detail/<int:log_id>')
@login_required @login_required
@admin_required @permission_required('view_logs') # 替代 @admin_required
def log_detail(log_id): def log_detail(log_id):
"""日志详情页面""" """日志详情页面"""
log = Log.query.get_or_404(log_id) log = Log.query.get_or_404(log_id)
@ -90,7 +90,7 @@ def log_detail(log_id):
@log_bp.route('/api/export', methods=['POST']) @log_bp.route('/api/export', methods=['POST'])
@login_required @login_required
@admin_required @permission_required('view_logs') # 替代 @admin_required
def export_logs(): def export_logs():
"""导出日志API""" """导出日志API"""
import csv import csv
@ -171,7 +171,7 @@ def export_logs():
@log_bp.route('/api/clear', methods=['POST']) @log_bp.route('/api/clear', methods=['POST'])
@login_required @login_required
@admin_required @permission_required('view_logs') # 替代 @admin_required
def clear_logs(): def clear_logs():
"""清空日志API""" """清空日志API"""
data = request.get_json() data = request.get_json()

View File

@ -4,7 +4,7 @@ from flask_login import login_required, current_user
from app.models.book import Book, db from app.models.book import Book, db
from app.models.borrow import BorrowRecord from app.models.borrow import BorrowRecord
from app.models.user import User from app.models.user import User
from app.utils.auth import admin_required from app.utils.auth import permission_required # 修改为导入permission_required
from app.models.log import Log # 导入日志模型 from app.models.log import Log # 导入日志模型
from sqlalchemy import func, case, desc, and_ from sqlalchemy import func, case, desc, and_
from datetime import datetime, timedelta from datetime import datetime, timedelta
@ -15,7 +15,7 @@ statistics_bp = Blueprint('statistics', __name__, url_prefix='/statistics')
@statistics_bp.route('/') @statistics_bp.route('/')
@login_required @login_required
@admin_required @permission_required('view_statistics') # 替代 @admin_required
def index(): def index():
"""统计分析首页""" """统计分析首页"""
# 记录访问统计分析首页的日志 # 记录访问统计分析首页的日志
@ -30,7 +30,7 @@ def index():
@statistics_bp.route('/book-ranking') @statistics_bp.route('/book-ranking')
@login_required @login_required
@admin_required @permission_required('view_statistics') # 替代 @admin_required
def book_ranking(): def book_ranking():
"""热门图书排行榜页面""" """热门图书排行榜页面"""
# 记录访问热门图书排行的日志 # 记录访问热门图书排行的日志
@ -45,7 +45,7 @@ def book_ranking():
@statistics_bp.route('/api/book-ranking') @statistics_bp.route('/api/book-ranking')
@login_required @login_required
@admin_required @permission_required('view_statistics') # 替代 @admin_required
def api_book_ranking(): def api_book_ranking():
"""获取热门图书排行数据API""" """获取热门图书排行数据API"""
time_range = request.args.get('time_range', 'month') time_range = request.args.get('time_range', 'month')
@ -98,7 +98,7 @@ def api_book_ranking():
@statistics_bp.route('/borrow-statistics') @statistics_bp.route('/borrow-statistics')
@login_required @login_required
@admin_required @permission_required('view_statistics') # 替代 @admin_required
def borrow_statistics(): def borrow_statistics():
"""借阅统计分析页面""" """借阅统计分析页面"""
# 记录访问借阅统计分析的日志 # 记录访问借阅统计分析的日志
@ -113,7 +113,7 @@ def borrow_statistics():
@statistics_bp.route('/api/borrow-trend') @statistics_bp.route('/api/borrow-trend')
@login_required @login_required
@admin_required @permission_required('view_statistics') # 替代 @admin_required
def api_borrow_trend(): def api_borrow_trend():
"""获取借阅趋势数据API""" """获取借阅趋势数据API"""
time_range = request.args.get('time_range', 'month') time_range = request.args.get('time_range', 'month')
@ -252,7 +252,7 @@ def api_borrow_trend():
@statistics_bp.route('/api/category-distribution') @statistics_bp.route('/api/category-distribution')
@login_required @login_required
@admin_required @permission_required('view_statistics') # 替代 @admin_required
def api_category_distribution(): def api_category_distribution():
"""获取图书分类分布数据API""" """获取图书分类分布数据API"""
# 记录获取图书分类分布数据的日志 # 记录获取图书分类分布数据的日志
@ -295,7 +295,7 @@ def api_category_distribution():
@statistics_bp.route('/user-activity') @statistics_bp.route('/user-activity')
@login_required @login_required
@admin_required @permission_required('view_statistics') # 替代 @admin_required
def user_activity(): def user_activity():
"""用户活跃度分析页面""" """用户活跃度分析页面"""
# 记录访问用户活跃度分析的日志 # 记录访问用户活跃度分析的日志
@ -310,7 +310,7 @@ def user_activity():
@statistics_bp.route('/api/user-activity') @statistics_bp.route('/api/user-activity')
@login_required @login_required
@admin_required @permission_required('view_statistics') # 替代 @admin_required
def api_user_activity(): def api_user_activity():
"""获取用户活跃度数据API""" """获取用户活跃度数据API"""
# 记录获取用户活跃度数据的日志 # 记录获取用户活跃度数据的日志
@ -347,7 +347,7 @@ def api_user_activity():
@statistics_bp.route('/overdue-analysis') @statistics_bp.route('/overdue-analysis')
@login_required @login_required
@admin_required @permission_required('view_statistics') # 替代 @admin_required
def overdue_analysis(): def overdue_analysis():
"""逾期分析页面""" """逾期分析页面"""
# 记录访问逾期分析的日志 # 记录访问逾期分析的日志
@ -362,7 +362,7 @@ def overdue_analysis():
@statistics_bp.route('/api/overdue-statistics') @statistics_bp.route('/api/overdue-statistics')
@login_required @login_required
@admin_required @permission_required('view_statistics') # 替代 @admin_required
def api_overdue_statistics(): def api_overdue_statistics():
"""获取逾期统计数据API""" """获取逾期统计数据API"""
# 记录获取逾期统计数据的日志 # 记录获取逾期统计数据的日志

View File

@ -492,21 +492,89 @@ def role_list():
return render_template('user/roles.html', roles=roles) return render_template('user/roles.html', roles=roles)
# 创建/编辑角色API # 获取所有系统权限的API
@user_bp.route('/permissions', methods=['GET'])
@login_required
@admin_required
def get_permissions():
"""获取所有可用的系统权限"""
from app.models.permission import Permission
try:
permissions = Permission.query.order_by(Permission.code).all()
# 转换为JSON格式
permissions_data = [{
'id': p.id,
'code': p.code,
'name': p.name,
'description': p.description
} for p in permissions]
return jsonify({
'success': True,
'permissions': permissions_data
})
except Exception as e:
logging.error(f"获取权限列表失败: {str(e)}")
return jsonify({
'success': False,
'message': f"获取权限列表失败: {str(e)}"
}), 500
# 获取特定角色的权限
@user_bp.route('/role/<int:role_id>/permissions', methods=['GET'])
@login_required
@admin_required
def get_role_permissions(role_id):
"""获取指定角色的权限ID列表"""
from app.models.user import Role
try:
role = Role.query.get(role_id)
if not role:
return jsonify({
'success': False,
'message': '角色不存在'
}), 404
# 获取角色的所有权限ID
permissions = [p.id for p in role.permissions]
return jsonify({
'success': True,
'permissions': permissions
})
except Exception as e:
logging.error(f"获取角色权限失败: {str(e)}")
return jsonify({
'success': False,
'message': f"获取角色权限失败: {str(e)}"
}), 500
# 修改角色保存路由,支持权限管理
@user_bp.route('/role/save', methods=['POST']) @user_bp.route('/role/save', methods=['POST'])
@login_required @login_required
@admin_required @admin_required
def role_save(): def role_save():
"""创建或更新角色,包括权限分配"""
data = request.get_json() data = request.get_json()
role_id = data.get('id') role_id = data.get('id')
role_name = data.get('role_name') role_name = data.get('role_name')
description = data.get('description') description = data.get('description')
permission_ids = data.get('permissions', []) # 获取权限ID列表
if not role_name: if not role_name:
return jsonify({'success': False, 'message': '角色名不能为空'}) return jsonify({'success': False, 'message': '角色名不能为空'})
if role_id: # 更新 # 处理系统内置角色的权限保护
success, message = UserService.update_role(role_id, role_name, description) if role_id and int(role_id) in [1, 2]:
permission_ids = None # 不修改内置角色的权限
if role_id: # 更新角色
success, message = UserService.update_role(role_id, role_name, description, permission_ids)
if success: if success:
# 记录编辑角色日志 # 记录编辑角色日志
Log.add_log( Log.add_log(
@ -515,15 +583,12 @@ def role_save():
target_type="角色", target_type="角色",
target_id=role_id, target_id=role_id,
ip_address=request.remote_addr, ip_address=request.remote_addr,
description=f"管理员 {current_user.username} 编辑角色 {role_name}" description=f"管理员 {current_user.username} 编辑角色 {role_name},包含权限设置"
) )
else: # 创建 else: # 创建角色
success, message = UserService.create_role(role_name, description) success, message, new_role_id = UserService.create_role(role_name, description, permission_ids)
if success: if success:
# 获取新创建的角色ID role_id = new_role_id
new_role = db.session.query(User.Role).filter_by(role_name=role_name).first()
role_id = new_role.id if new_role else None
# 记录创建角色日志 # 记录创建角色日志
Log.add_log( Log.add_log(
action="创建角色", action="创建角色",
@ -531,13 +596,72 @@ def role_save():
target_type="角色", target_type="角色",
target_id=role_id, target_id=role_id,
ip_address=request.remote_addr, ip_address=request.remote_addr,
description=f"管理员 {current_user.username} 创建新角色 {role_name}" description=f"管理员 {current_user.username} 创建新角色 {role_name},设置了 {len(permission_ids)} 个权限"
) )
return jsonify({'success': success, 'message': message}) return jsonify({'success': success, 'message': message})
@user_bp.route('/user/role/<int:role_id>/count', methods=['GET']) # 角色删除API
@user_bp.route('/role/delete/<int:role_id>', methods=['POST'])
@login_required
@admin_required
def role_delete(role_id):
"""删除角色"""
# 保护系统内置角色
if role_id in [1, 2]:
return jsonify({
'success': False,
'message': '不能删除系统内置角色'
})
from app.models.user import Role
try:
# 获取角色信息用于日志记录
role = Role.query.get(role_id)
if not role:
return jsonify({
'success': False,
'message': '角色不存在'
}), 404
role_name = role.role_name
# 检查是否有用户在使用该角色
user_count = User.query.filter_by(role_id=role_id).count()
if user_count > 0:
return jsonify({
'success': False,
'message': f'无法删除:该角色下存在 {user_count} 个用户'
})
# 删除角色
db.session.delete(role)
db.session.commit()
# 记录删除角色日志
Log.add_log(
action="删除角色",
user_id=current_user.id,
target_type="角色",
ip_address=request.remote_addr,
description=f"管理员 {current_user.username} 删除了角色 {role_name}"
)
return jsonify({
'success': True,
'message': '角色删除成功'
})
except Exception as e:
db.session.rollback()
logging.error(f"删除角色失败: {str(e)}")
return jsonify({
'success': False,
'message': f"删除角色失败: {str(e)}"
}), 500
@user_bp.route('/role/<int:role_id>/count', methods=['GET'])
@login_required @login_required
@admin_required @admin_required
def get_role_user_count(role_id): def get_role_user_count(role_id):

103
app/init_permissions.py Normal file
View File

@ -0,0 +1,103 @@
from app import create_app
from app.models.database import db
from app.models.user import Role
from app.models.permission import Permission
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def init_permissions():
"""初始化系统权限"""
logger.info("开始初始化系统权限...")
# 只定义管理类权限,对应现有的 @admin_required 装饰的路由
permissions = [
# 公告管理权限
{'code': 'manage_announcements', 'name': '公告管理', 'description': '允许管理系统公告(发布、编辑、删除、置顶等)'},
# 图书管理权限
{'code': 'manage_books', 'name': '图书管理', 'description': '允许管理图书(添加、编辑、删除图书)'},
{'code': 'manage_categories', 'name': '分类管理', 'description': '允许管理图书分类'},
{'code': 'import_export_books', 'name': '导入导出图书', 'description': '允许批量导入和导出图书数据'},
# 借阅管理权限
{'code': 'manage_borrows', 'name': '借阅管理', 'description': '允许管理全系统借阅记录和处理借还书操作'},
{'code': 'manage_overdue', 'name': '逾期管理', 'description': '允许查看和处理逾期借阅'},
# 库存管理权限
{'code': 'manage_inventory', 'name': '库存管理', 'description': '允许查看和调整图书库存'},
# 日志权限
{'code': 'view_logs', 'name': '查看日志', 'description': '允许查看系统操作日志'},
# 统计权限
{'code': 'view_statistics', 'name': '查看统计', 'description': '允许查看统计分析数据'},
# 用户管理权限
{'code': 'manage_users', 'name': '用户管理', 'description': '允许管理用户(添加、编辑、禁用、删除用户)'},
{'code': 'manage_roles', 'name': '角色管理', 'description': '允许管理角色和权限'},
]
# 添加权限记录
added_count = 0
updated_count = 0
for perm_data in permissions:
# 检查权限是否已存在
existing_perm = Permission.query.filter_by(code=perm_data['code']).first()
if existing_perm:
# 更新现有权限信息
existing_perm.name = perm_data['name']
existing_perm.description = perm_data['description']
updated_count += 1
else:
# 创建新权限
permission = Permission(**perm_data)
db.session.add(permission)
added_count += 1
# 提交所有权限
db.session.commit()
logger.info(f"权限初始化完成: 新增 {added_count} 个, 更新 {updated_count}")
# 处理角色权限分配
assign_role_permissions()
def assign_role_permissions():
"""为系统默认角色分配权限"""
logger.info("开始分配角色权限...")
# 获取所有权限
all_permissions = Permission.query.all()
# 获取系统内置角色
admin_role = Role.query.get(1) # 管理员角色
user_role = Role.query.get(2) # 普通用户角色
if admin_role and user_role:
# 管理员拥有所有权限
admin_role.permissions = all_permissions
# 普通用户无需特殊管理权限
user_role.permissions = []
db.session.commit()
logger.info(f"管理员角色分配了 {len(all_permissions)} 个权限")
logger.info(f"普通用户角色无管理权限")
else:
logger.error("无法找到内置角色,请确保角色表已正确初始化")
def main():
"""主函数"""
app = create_app()
with app.app_context():
init_permissions()
if __name__ == "__main__":
main()

4
app/models/database.py Normal file
View File

@ -0,0 +1,4 @@
from flask_sqlalchemy import SQLAlchemy
# 创建共享的SQLAlchemy实例
db = SQLAlchemy()

20
app/models/permission.py Normal file
View File

@ -0,0 +1,20 @@
from app.models.database import db
from datetime import datetime
# 这是权限表 model
class Permission(db.Model):
__tablename__ = 'permissions'
id = db.Column(db.Integer, primary_key=True)
code = db.Column(db.String(64), unique=True, nullable=False, comment='权限代码,用于系统识别')
name = db.Column(db.String(64), nullable=False, comment='权限名称,用于界面显示')
description = db.Column(db.String(255), comment='权限描述,说明权限用途')
# 角色-权限 关联表辅助对象模式方便ORM关系管理
class RolePermission(db.Model):
__tablename__ = 'role_permissions'
role_id = db.Column(db.Integer, db.ForeignKey('roles.id', ondelete='CASCADE'), primary_key=True, comment='角色ID关联roles表')
permission_id = db.Column(db.Integer, db.ForeignKey('permissions.id', ondelete='CASCADE'), primary_key=True, comment='权限ID关联permissions表')
created_at = db.Column(db.DateTime, default=datetime.now, comment='权限分配时间')

View File

@ -1,9 +1,10 @@
from flask_sqlalchemy import SQLAlchemy from app.models.database import db
from werkzeug.security import generate_password_hash, check_password_hash from werkzeug.security import generate_password_hash, check_password_hash
from datetime import datetime from datetime import datetime
from flask_login import UserMixin from flask_login import UserMixin
from app.models.permission import RolePermission, Permission
db = SQLAlchemy() #db = SQLAlchemy()
class User(db.Model, UserMixin): class User(db.Model, UserMixin):
@ -77,4 +78,11 @@ class Role(db.Model):
role_name = db.Column(db.String(32), unique=True, nullable=False) role_name = db.Column(db.String(32), unique=True, nullable=False)
description = db.Column(db.String(128)) description = db.Column(db.String(128))
permissions = db.relationship(
'Permission',
secondary='role_permissions',
backref=db.backref('roles', lazy='dynamic'),
lazy='dynamic'
)
users = db.relationship('User', backref='role') users = db.relationship('User', backref='role')

View File

@ -125,37 +125,58 @@ class UserService:
return Role.query.all() return Role.query.all()
@staticmethod @staticmethod
def create_role(role_name, description=None): def create_role(role_name, description, permission_ids=None):
"""创建新角色""" """创建新角色,包括权限分配"""
existing = Role.query.filter_by(role_name=role_name).first() from app.models.user import Role
if existing: from app.models.permission import Permission
return False, "角色名已存在"
# 检查角色名是否已存在
if Role.query.filter_by(role_name=role_name).first():
return False, "角色名称已存在", None
role = Role(role_name=role_name, description=description)
# 添加权限
if permission_ids:
permissions = Permission.query.filter(Permission.id.in_(permission_ids)).all()
role.permissions = permissions
try: try:
role = Role(role_name=role_name, description=description)
db.session.add(role) db.session.add(role)
db.session.commit() db.session.commit()
return True, "角色创建成功" return True, "角色创建成功", role.id
except Exception as e: except Exception as e:
db.session.rollback() db.session.rollback()
return False, f"创建失败: {str(e)}" return False, f"创建失败: {str(e)}", None
@staticmethod @staticmethod
def update_role(role_id, role_name, description=None): def update_role(role_id, role_name, description, permission_ids=None):
"""更新角色信息""" """更新角色信息,包括权限"""
from app.models.user import Role
from app.models.permission import Permission
role = Role.query.get(role_id) role = Role.query.get(role_id)
if not role: if not role:
return False, "角色不存在" return False, "角色不存在"
# 检查角色名是否已被使用 # 系统内置角色不允许修改名称
existing = Role.query.filter(Role.role_name == role_name, Role.id != role_id).first() if role_id in [1, 2] and role.role_name != role_name:
if existing: return False, "系统内置角色不允许修改名称"
return False, "角色名已存在"
# 检查角色名是否已存在
existing_role = Role.query.filter(Role.role_name == role_name, Role.id != role_id).first()
if existing_role:
return False, "角色名称已存在"
role.role_name = role_name
role.description = description
# 更新权限(如果提供了权限列表且不是内置角色)
if permission_ids is not None and role_id not in [1, 2]:
permissions = Permission.query.filter(Permission.id.in_(permission_ids)).all()
role.permissions = permissions
try: try:
role.role_name = role_name
if description is not None:
role.description = description
db.session.commit() db.session.commit()
return True, "角色更新成功" return True, "角色更新成功"
except Exception as e: except Exception as e:
@ -181,4 +202,5 @@ class UserService:
except Exception as e: except Exception as e:
db.session.rollback() db.session.rollback()
logging.error(f"创建用户失败: {str(e)}") logging.error(f"创建用户失败: {str(e)}")
return False, f'创建用户失败: {str(e)}' return False, f'创建用户失败: {str(e)}'

View File

@ -251,3 +251,94 @@
margin-top: 15px; margin-top: 15px;
} }
} }
/* 权限选择部分样式 */
.permissions-container {
max-height: 350px;
overflow-y: auto;
border: 1px solid #e9ecef;
border-radius: 4px;
padding: 15px;
background-color: #f8f9fa;
}
.permission-group {
margin-bottom: 20px;
}
.permission-group:last-child {
margin-bottom: 0;
}
.permission-group-title {
font-weight: 600;
color: #495057;
margin-bottom: 10px;
padding-bottom: 5px;
border-bottom: 1px solid #dee2e6;
}
.permission-items {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 10px;
}
.permission-item {
display: flex;
align-items: center;
}
.permission-checkbox {
margin-right: 8px;
}
.permission-name {
font-weight: 500;
margin-bottom: 5px;
display: block;
}
.permission-description {
font-size: 0.85rem;
color: #6c757d;
display: block;
}
.loading-permissions {
grid-column: 1 / -1;
text-align: center;
padding: 20px;
color: #6c757d;
}
/* 将模态框调整为大一点 */
.modal-lg {
max-width: 800px;
}
/* 权限项样式 */
.permission-item {
position: relative;
padding: 8px 12px;
border-radius: 4px;
transition: background-color 0.2s;
}
.permission-item:hover {
background-color: #e9ecef;
}
.permission-item label {
display: flex;
flex-direction: column;
cursor: pointer;
margin-bottom: 0;
padding-left: 25px;
}
.permission-item input[type="checkbox"] {
position: absolute;
left: 12px;
top: 12px;
}

View File

@ -16,6 +16,9 @@ document.addEventListener('DOMContentLoaded', function() {
// 加载角色用户统计 // 加载角色用户统计
fetchRoleUserCounts(); fetchRoleUserCounts();
// 初始化时加载权限列表
loadPermissions();
// 添加角色按钮点击事件 // 添加角色按钮点击事件
if (addRoleBtn) { if (addRoleBtn) {
addRoleBtn.addEventListener('click', function() { addRoleBtn.addEventListener('click', function() {
@ -27,6 +30,18 @@ document.addEventListener('DOMContentLoaded', function() {
// 更新模态框标题 // 更新模态框标题
document.getElementById('roleModalLabel').textContent = '添加角色'; document.getElementById('roleModalLabel').textContent = '添加角色';
// 启用所有权限复选框
document.querySelectorAll('.permission-checkbox').forEach(checkbox => {
checkbox.checked = false;
checkbox.disabled = false;
});
// 隐藏系统角色警告
const systemRoleAlert = document.getElementById('systemRoleAlert');
if (systemRoleAlert) {
systemRoleAlert.style.display = 'none';
}
// 显示模态框 // 显示模态框
roleModal.modal('show'); roleModal.modal('show');
}); });
@ -54,6 +69,9 @@ document.addEventListener('DOMContentLoaded', function() {
// 更新模态框标题 // 更新模态框标题
document.getElementById('roleModalLabel').textContent = '编辑角色'; document.getElementById('roleModalLabel').textContent = '编辑角色';
// 加载角色权限
loadRolePermissions(roleId);
// 显示模态框 // 显示模态框
roleModal.modal('show'); roleModal.modal('show');
}); });
@ -79,10 +97,17 @@ document.addEventListener('DOMContentLoaded', function() {
return; return;
} }
// 收集选中的权限ID
const permissionIds = [];
document.querySelectorAll('.permission-checkbox:checked:not(:disabled)').forEach(checkbox => {
permissionIds.push(parseInt(checkbox.value));
});
const roleData = { const roleData = {
id: roleIdInput.value || null, id: roleIdInput.value || null,
role_name: roleNameInput.value.trim(), role_name: roleNameInput.value.trim(),
description: roleDescriptionInput.value.trim() || null description: roleDescriptionInput.value.trim() || null,
permissions: permissionIds
}; };
saveRole(roleData); saveRole(roleData);
@ -99,6 +124,182 @@ document.addEventListener('DOMContentLoaded', function() {
}); });
} }
// 加载权限列表
function loadPermissions() {
// 获取权限容器
const bookPermissions = document.getElementById('book-permissions');
const userPermissions = document.getElementById('user-permissions');
const borrowPermissions = document.getElementById('borrow-permissions');
const systemPermissions = document.getElementById('system-permissions');
if (!bookPermissions) return; // 如果元素不存在就退出
// 设置加载中状态
bookPermissions.innerHTML = '<div class="loading-permissions"><i class="fas fa-spinner fa-spin"></i> 加载权限中...</div>';
userPermissions.innerHTML = '';
borrowPermissions.innerHTML = '';
systemPermissions.innerHTML = '';
// 获取权限数据
fetch('/user/permissions', {
method: 'GET',
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => {
if (!response.ok) {
throw new Error('网络响应异常');
}
return response.json();
})
.then(data => {
if (data.success) {
// 清除加载中状态
bookPermissions.innerHTML = '';
// 按分组整理权限
const permissionGroups = {
book: [], // 图书相关权限
user: [], // 用户相关权限
borrow: [], // 借阅相关权限
system: [] // 系统相关权限
};
// 定义权限分组映射
const permGroupMap = {
'manage_books': 'book',
'manage_categories': 'book',
'import_export_books': 'book',
'manage_users': 'user',
'manage_roles': 'user',
'manage_borrows': 'borrow',
'manage_overdue': 'borrow',
// 系统相关权限
'manage_announcements': 'system',
'manage_inventory': 'system',
'view_logs': 'system',
'view_statistics': 'system'
};
// 根据映射表分组
data.permissions.forEach(perm => {
const group = permGroupMap[perm.code] || 'system';
permissionGroups[group].push(perm);
});
// 渲染各组权限
renderPermissionGroup(bookPermissions, permissionGroups.book);
renderPermissionGroup(userPermissions, permissionGroups.user);
renderPermissionGroup(borrowPermissions, permissionGroups.borrow);
renderPermissionGroup(systemPermissions, permissionGroups.system);
} else {
bookPermissions.innerHTML = '<div class="text-danger">加载权限失败</div>';
}
})
.catch(error => {
console.error('Error:', error);
bookPermissions.innerHTML = '<div class="text-danger">加载权限失败,请刷新页面重试</div>';
});
}
// 渲染权限组
function renderPermissionGroup(container, permissions) {
if (permissions.length === 0) {
container.innerHTML = '<div class="text-muted">暂无相关权限</div>';
return;
}
let html = '';
permissions.forEach(perm => {
html += `
<div class="permission-item">
<input type="checkbox" id="perm_${perm.id}" class="permission-checkbox" value="${perm.id}">
<label for="perm_${perm.id}">
<span class="permission-name">${perm.name}</span>
<span class="permission-description">${perm.description || '无描述'}</span>
</label>
</div>
`;
});
container.innerHTML = html;
}
// 加载角色的权限
function loadRolePermissions(roleId) {
if (!roleId) return;
// 先清空所有已选权限
document.querySelectorAll('.permission-checkbox').forEach(checkbox => {
checkbox.checked = false;
});
// 如果是系统内置角色,显示警告并禁用权限选择
const isSystemRole = (roleId == 1 || roleId == 2);
const systemRoleAlert = document.getElementById('systemRoleAlert');
const permissionCheckboxes = document.querySelectorAll('.permission-checkbox');
if (systemRoleAlert) {
systemRoleAlert.style.display = isSystemRole ? 'block' : 'none';
}
// 管理员角色自动选中所有权限并禁用
if (roleId == 1) { // 管理员
permissionCheckboxes.forEach(checkbox => {
checkbox.checked = true;
checkbox.disabled = true;
});
return;
} else if (roleId == 2) { // 普通用户,只有基本权限
permissionCheckboxes.forEach(checkbox => {
checkbox.checked = false;
checkbox.disabled = true; // 普通用户权限不可修改
});
// 获取普通用户已分配的权限
fetch(`/user/role/${roleId}/permissions`, {
method: 'GET',
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
permissionCheckboxes.forEach(checkbox => {
// 如果权限ID在返回的列表中则选中
checkbox.checked = data.permissions.includes(parseInt(checkbox.value));
});
}
});
return;
}
// 为自定义角色加载并选中权限
fetch(`/user/role/${roleId}/permissions`, {
method: 'GET',
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
permissionCheckboxes.forEach(checkbox => {
// 启用所有复选框
checkbox.disabled = false;
// 如果权限ID在返回的列表中则选中
checkbox.checked = data.permissions.includes(parseInt(checkbox.value));
});
}
});
}
// 保存角色 // 保存角色
function saveRole(roleData) { function saveRole(roleData) {
// 显示加载状态 // 显示加载状态

View File

@ -135,7 +135,7 @@
<!-- 角色编辑模态框 --> <!-- 角色编辑模态框 -->
<div class="modal fade" id="roleModal" tabindex="-1" role="dialog" aria-labelledby="roleModalLabel" aria-hidden="true"> <div class="modal fade" id="roleModal" tabindex="-1" role="dialog" aria-labelledby="roleModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document"> <div class="modal-dialog modal-lg" role="document">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title" id="roleModalLabel">添加角色</h5> <h5 class="modal-title" id="roleModalLabel">添加角色</h5>
@ -156,6 +156,51 @@
<label for="roleDescription">角色描述</label> <label for="roleDescription">角色描述</label>
<textarea class="form-control" id="roleDescription" rows="3"></textarea> <textarea class="form-control" id="roleDescription" rows="3"></textarea>
</div> </div>
<!-- 权限选择部分 -->
<div class="form-group" id="permissionsSection">
<label>角色权限</label>
<div class="alert alert-info" id="systemRoleAlert" style="display: none;">
<i class="fas fa-info-circle"></i> 系统内置角色的权限配置受到限制。
</div>
<div class="permissions-container">
<!-- 权限分组:图书管理 -->
<div class="permission-group">
<h6 class="permission-group-title">图书管理</h6>
<div class="permission-items" id="book-permissions">
<!-- 权限项将通过JS动态加载 -->
<div class="loading-permissions">
<i class="fas fa-spinner fa-spin"></i> 加载权限中...
</div>
</div>
</div>
<!-- 权限分组:用户管理 -->
<div class="permission-group">
<h6 class="permission-group-title">用户管理</h6>
<div class="permission-items" id="user-permissions">
<!-- 权限项将通过JS动态加载 -->
</div>
</div>
<!-- 权限分组:借阅管理 -->
<div class="permission-group">
<h6 class="permission-group-title">借阅管理</h6>
<div class="permission-items" id="borrow-permissions">
<!-- 权限项将通过JS动态加载 -->
</div>
</div>
<!-- 权限分组:系统管理 -->
<div class="permission-group">
<h6 class="permission-group-title">系统管理</h6>
<div class="permission-items" id="system-permissions">
<!-- 权限项将通过JS动态加载 -->
</div>
</div>
</div>
</div>
</form> </form>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">

View File

@ -30,3 +30,52 @@ def admin_required(f):
return f(*args, **kwargs) return f(*args, **kwargs)
return decorated_function return decorated_function
def permission_required(permission_code):
"""
检查用户是否拥有特定权限的装饰器
:param permission_code: 权限代码例如 'manage_books'
"""
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
print(
f"DEBUG: permission_required({permission_code}) 检查 - current_user.is_authenticated = {current_user.is_authenticated}")
# 检查用户是否登录
if not current_user.is_authenticated:
flash('请先登录', 'warning')
return redirect(url_for('user.login', next=request.url))
# 管理员拥有所有权限
if getattr(current_user, 'role_id', None) == 1:
return f(*args, **kwargs)
# 获取用户角色并检查是否有指定权限
from app.models.user import Role
role = Role.query.get(current_user.role_id)
if not role:
flash('用户角色异常', 'danger')
return redirect(url_for('index'))
# 检查角色是否有指定权限
has_permission = False
for perm in role.permissions:
if perm.code == permission_code:
has_permission = True
break
if not has_permission:
print(f"DEBUG: 用户 {current_user.username} 缺少权限 {permission_code}")
flash('您没有执行此操作的权限', 'danger')
return redirect(url_for('index'))
return f(*args, **kwargs)
return decorated_function
return decorator

File diff suppressed because it is too large Load Diff

View File

@ -20,6 +20,45 @@ CREATE TABLE `users` (
FOREIGN KEY (`role_id`) REFERENCES `roles`(`id`) FOREIGN KEY (`role_id`) REFERENCES `roles`(`id`)
); );
-- ----------------------------
-- Table structure for permissions
-- ----------------------------
-- 这是权限表,用于存储系统中所有可分配的权限
-- id: 权限的唯一标识符,自增主键
-- code: 权限的唯一代码,用于程序中进行权限验证,如'manage_books'、'view_users'等
-- name: 权限的显示名称,便于管理员理解,如'管理图书'、'查看用户'等
-- description: 权限的详细描述,解释该权限允许用户执行什么操作
DROP TABLE IF EXISTS `permissions`;
CREATE TABLE `permissions` (
`id` int NOT NULL AUTO_INCREMENT,
`code` varchar(64) COLLATE utf8mb4_general_ci NOT NULL COMMENT '权限代码,用于系统识别',
`name` varchar(64) COLLATE utf8mb4_general_ci NOT NULL COMMENT '权限名称,用于界面显示',
`description` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '权限描述,说明权限用途',
PRIMARY KEY (`id`),
UNIQUE KEY `code` (`code`) COMMENT '权限代码必须唯一'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='系统权限表';
-- ----------------------------
-- Table structure for role_permissions
-- ----------------------------
-- 这是角色-权限关联表,用于建立角色和权限之间的多对多关系
-- role_id: 角色ID外键关联到roles表
-- permission_id: 权限ID外键关联到permissions表
-- created_at: 记录权限分配给角色的时间
-- 该表使用role_id和permission_id的组合作为复合主键确保一个角色不会重复分配同一权限
DROP TABLE IF EXISTS `role_permissions`;
CREATE TABLE `role_permissions` (
`role_id` int NOT NULL COMMENT '角色ID关联roles表的id字段',
`permission_id` int NOT NULL COMMENT '权限ID关联permissions表的id字段',
`created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '权限分配时间',
PRIMARY KEY (`role_id`,`permission_id`), -- 复合主键,确保一个角色不会重复分配同一权限
KEY `permission_id` (`permission_id`), -- 权限ID索引提高查询效率
CONSTRAINT `role_permissions_ibfk_1` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ON DELETE CASCADE,
CONSTRAINT `role_permissions_ibfk_2` FOREIGN KEY (`permission_id`) REFERENCES `permissions` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='角色-权限关联表';
-- (可选)初始化角色数据 -- (可选)初始化角色数据
INSERT INTO `roles` (`role_name`, `description`) VALUES INSERT INTO `roles` (`role_name`, `description`) VALUES
('admin', '管理员'), ('admin', '管理员'),