完成权限管理分类

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_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.book import book_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 app.models.announcement import Announcement
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 datetime import datetime
from app.models.notification import Notification
from app import db # 为mark_all_as_read函数添加db导入
# 创建蓝图
announcement_bp = Blueprint('announcement', __name__)
@ -41,7 +42,7 @@ def announcement_detail(announcement_id):
@announcement_bp.route('/manage', methods=['GET'])
@login_required
@admin_required
@permission_required('manage_announcements') # 替代 @admin_required
def manage_announcements():
"""管理员公告管理页面"""
page = request.args.get('page', 1, type=int)
@ -86,7 +87,7 @@ def manage_announcements():
@announcement_bp.route('/add', methods=['GET', 'POST'])
@login_required
@admin_required
@permission_required('manage_announcements') # 替代 @admin_required
def add_announcement():
"""添加公告"""
if request.method == 'POST':
@ -127,7 +128,7 @@ def add_announcement():
@announcement_bp.route('/edit/<int:announcement_id>', methods=['GET', 'POST'])
@login_required
@admin_required
@permission_required('manage_announcements') # 替代 @admin_required
def edit_announcement(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'])
@login_required
@admin_required
@permission_required('manage_announcements') # 替代 @admin_required
def change_status(announcement_id):
"""更改公告状态"""
data = request.get_json()
@ -209,7 +210,7 @@ def change_status(announcement_id):
@announcement_bp.route('/top/<int:announcement_id>', methods=['POST'])
@login_required
@admin_required
@permission_required('manage_announcements') # 替代 @admin_required
def change_top_status(announcement_id):
"""更改公告置顶状态"""
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 app.models.book import Book, Category
from app.models.user import db
from app.utils.auth import login_required, admin_required
from flask_login import current_user # 移除重复的 login_required 导入
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 # 导入日志模型
from app.models.log import Log
book_bp = Blueprint('book', __name__)
@book_bp.route('/admin/list')
@login_required
@admin_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)
@ -62,11 +62,11 @@ def admin_book_list():
category_id=category_id,
sort=sort,
order=order,
current_user=current_user, # 使用current_user替代g.user
is_admin_view=True) # 指明这是管理视图
current_user=current_user,
is_admin_view=True)
# 图书列表页面
# 图书列表页面 - 不需要修改,已经只有@login_required
@book_bp.route('/list')
@login_required
def book_list():
@ -122,10 +122,10 @@ def book_list():
category_id=category_id,
sort=sort,
order=order,
current_user=current_user) # 使用current_user替代g.user
current_user=current_user)
# 图书详情页面
# 图书详情页面 - 不需要修改,已经只有@login_required
@book_bp.route('/detail/<int:book_id>')
@login_required
def book_detail(book_id):
@ -164,7 +164,7 @@ def book_detail(book_id):
# 添加图书页面
@book_bp.route('/add', methods=['GET', 'POST'])
@login_required
@admin_required
@permission_required('manage_books') # 替换 @admin_required
def add_book():
if request.method == 'POST':
title = request.form.get('title')
@ -269,7 +269,7 @@ def add_book():
change_type='入库',
change_amount=stock,
after_stock=stock,
operator_id=current_user.id, # 使用current_user.id替代g.user.id
operator_id=current_user.id,
remark='新书入库',
changed_at=datetime.datetime.now()
)
@ -329,7 +329,7 @@ def add_book():
# 编辑图书
@book_bp.route('/edit/<int:book_id>', methods=['GET', 'POST'])
@login_required
@admin_required
@permission_required('manage_books') # 替换 @admin_required
def edit_book(book_id):
book = Book.query.get_or_404(book_id)
@ -422,7 +422,7 @@ def edit_book(book_id):
change_type=change_type,
change_amount=abs(change_amount),
after_stock=new_stock,
operator_id=current_user.id, # 使用current_user.id替代g.user.id
operator_id=current_user.id,
remark=f'管理员编辑图书库存 - {book.title}',
changed_at=datetime.datetime.now()
)
@ -500,7 +500,7 @@ def edit_book(book_id):
# 删除图书
@book_bp.route('/delete/<int:book_id>', methods=['POST'])
@login_required
@admin_required
@permission_required('manage_books') # 替换 @admin_required
def delete_book(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'])
@login_required
@admin_required
@permission_required('manage_categories') # 替换 @admin_required
def category_list():
categories = Category.query.all()
@ -559,7 +559,7 @@ def category_list():
# 添加分类
@book_bp.route('/categories/add', methods=['POST'])
@login_required
@admin_required
@permission_required('manage_categories') # 替换 @admin_required
def add_category():
name = request.form.get('name')
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'])
@login_required
@admin_required
@permission_required('manage_categories') # 替换 @admin_required
def edit_category(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'])
@login_required
@admin_required
@permission_required('manage_categories') # 替换 @admin_required
def delete_category(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'])
@login_required
@admin_required
@permission_required('import_export_books') # 替换 @admin_required
def import_books():
if request.method == 'POST':
if 'file' not in request.files:
@ -741,7 +741,7 @@ def import_books():
change_type='入库',
change_amount=book.stock,
after_stock=book.stock,
operator_id=current_user.id, # 使用current_user.id
operator_id=current_user.id,
remark='批量导入图书',
changed_at=datetime.datetime.now()
)
@ -784,13 +784,13 @@ def import_books():
flash('只支持Excel文件(.xlsx, .xls)', 'danger')
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')
@login_required
@admin_required
@permission_required('import_export_books') # 替换 @admin_required
def export_books():
# 获取查询参数
search = request.args.get('search', '')
@ -871,8 +871,7 @@ def test_permissions():
"""
# 添加到app/controllers/book.py文件中
# 图书浏览页面 - 不需要修改,已经只有@login_required
@book_bp.route('/browse')
@login_required
def browse_books():
@ -927,12 +926,12 @@ def browse_books():
categories=categories,
category_id=category_id,
sort=sort,
order=order) # current_user自动传递到模板
order=order)
@book_bp.route('/template/download')
@login_required
@admin_required
@permission_required('import_export_books') # 替换 @admin_required
def download_template():
"""生成并下载Excel图书导入模板"""
# 创建一个简单的DataFrame作为模板

View File

@ -6,7 +6,7 @@ from app.models.inventory import InventoryLog
from app.models.user import db, User
from app.models.log import Log # 导入日志模型
import datetime
from app.utils.auth import admin_required
from app.utils.auth import permission_required # 修改导入使用permission_required而不是admin_required
# 创建借阅蓝图
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)
# 检查是否是自己的借阅记录或者是管理员
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({
'success': False,
'message': '您无权执行此操作'
@ -268,7 +268,7 @@ def renew_book(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({
'success': False,
'message': '您无权执行此操作'
@ -376,7 +376,7 @@ def my_borrows():
@borrow_bp.route('/manage')
@login_required
@admin_required
@permission_required('manage_borrows') # 替代 @admin_required
def manage_borrows():
"""管理员查看所有借阅记录"""
page = request.args.get('page', 1, type=int)
@ -444,7 +444,7 @@ def manage_borrows():
@borrow_bp.route('/admin/add', methods=['POST'])
@login_required
@admin_required
@permission_required('manage_borrows') # 替代 @admin_required
def admin_add_borrow():
"""管理员为用户添加借阅记录"""
user_id = request.form.get('user_id', type=int)
@ -532,7 +532,7 @@ def admin_add_borrow():
@borrow_bp.route('/overdue')
@login_required
@admin_required
@permission_required('manage_overdue') # 替代 @admin_required
def overdue_borrows():
"""查看逾期借阅"""
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'])
@login_required
@admin_required
@permission_required('manage_overdue') # 替代 @admin_required
def notify_overdue(borrow_id):
"""发送逾期通知"""
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.log import Log # 导入日志模型
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
inventory_bp = Blueprint('inventory', __name__, url_prefix='/inventory')
@ -13,9 +13,9 @@ inventory_bp = Blueprint('inventory', __name__, url_prefix='/inventory')
@inventory_bp.route('/')
@login_required
@admin_required
@permission_required('manage_inventory') # 替代 @admin_required
def inventory_list():
"""库存管理页面 - 只有管理员有权限进入"""
"""库存管理页面 - 只有拥有库存管理权限的用户有权限进入"""
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 20, type=int)
@ -47,7 +47,7 @@ def inventory_list():
user_id=current_user.id,
target_type="inventory",
ip_address=request.remote_addr,
description=f"管理员访问库存管理页面,搜索条件:{search if search else ''}"
description=f"用户访问库存管理页面,搜索条件:{search if search else ''}"
)
return render_template('inventory/list.html',
@ -60,7 +60,7 @@ def inventory_list():
@inventory_bp.route('/adjust/<int:book_id>', methods=['GET', 'POST'])
@login_required
@admin_required
@permission_required('manage_inventory') # 替代 @admin_required
def adjust_inventory(book_id):
"""调整图书库存"""
book = Book.query.get_or_404(book_id)
@ -73,7 +73,7 @@ def adjust_inventory(book_id):
target_type="book",
target_id=book.id,
ip_address=request.remote_addr,
description=f"管理员查看图书《{book.title}》的库存调整页面"
description=f"用户查看图书《{book.title}》的库存调整页面"
)
if request.method == 'POST':
@ -123,7 +123,7 @@ def adjust_inventory(book_id):
target_type="book",
target_id=book.id,
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}"
)
@ -140,7 +140,7 @@ def adjust_inventory(book_id):
@inventory_bp.route('/logs')
@login_required
@admin_required
@permission_required('manage_inventory') # 替代 @admin_required
def inventory_logs():
"""查看库存变动日志"""
page = request.args.get('page', 1, type=int)
@ -189,7 +189,7 @@ def inventory_logs():
user_id=current_user.id,
target_type="inventory_log",
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',
@ -205,7 +205,7 @@ def inventory_logs():
@inventory_bp.route('/book/<int:book_id>/logs')
@login_required
@admin_required
@permission_required('manage_inventory') # 替代 @admin_required
def book_inventory_logs(book_id):
"""查看特定图书的库存变动日志"""
book = Book.query.get_or_404(book_id)
@ -223,7 +223,7 @@ def book_inventory_logs(book_id):
target_type="book",
target_id=book.id,
ip_address=request.remote_addr,
description=f"管理员查看图书《{book.title}》的库存变动日志"
description=f"用户查看图书《{book.title}》的库存变动日志"
)
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 app.models.log import Log
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
# 创建蓝图
@ -11,7 +11,7 @@ log_bp = Blueprint('log', __name__, url_prefix='/log')
@log_bp.route('/list')
@login_required
@admin_required
@permission_required('view_logs') # 替代 @admin_required
def log_list():
"""日志列表页面"""
# 获取筛选参数
@ -81,7 +81,7 @@ def log_list():
@log_bp.route('/detail/<int:log_id>')
@login_required
@admin_required
@permission_required('view_logs') # 替代 @admin_required
def log_detail(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'])
@login_required
@admin_required
@permission_required('view_logs') # 替代 @admin_required
def export_logs():
"""导出日志API"""
import csv
@ -171,7 +171,7 @@ def export_logs():
@log_bp.route('/api/clear', methods=['POST'])
@login_required
@admin_required
@permission_required('view_logs') # 替代 @admin_required
def clear_logs():
"""清空日志API"""
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.borrow import BorrowRecord
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 sqlalchemy import func, case, desc, and_
from datetime import datetime, timedelta
@ -15,7 +15,7 @@ statistics_bp = Blueprint('statistics', __name__, url_prefix='/statistics')
@statistics_bp.route('/')
@login_required
@admin_required
@permission_required('view_statistics') # 替代 @admin_required
def index():
"""统计分析首页"""
# 记录访问统计分析首页的日志
@ -30,7 +30,7 @@ def index():
@statistics_bp.route('/book-ranking')
@login_required
@admin_required
@permission_required('view_statistics') # 替代 @admin_required
def book_ranking():
"""热门图书排行榜页面"""
# 记录访问热门图书排行的日志
@ -45,7 +45,7 @@ def book_ranking():
@statistics_bp.route('/api/book-ranking')
@login_required
@admin_required
@permission_required('view_statistics') # 替代 @admin_required
def api_book_ranking():
"""获取热门图书排行数据API"""
time_range = request.args.get('time_range', 'month')
@ -98,7 +98,7 @@ def api_book_ranking():
@statistics_bp.route('/borrow-statistics')
@login_required
@admin_required
@permission_required('view_statistics') # 替代 @admin_required
def borrow_statistics():
"""借阅统计分析页面"""
# 记录访问借阅统计分析的日志
@ -113,7 +113,7 @@ def borrow_statistics():
@statistics_bp.route('/api/borrow-trend')
@login_required
@admin_required
@permission_required('view_statistics') # 替代 @admin_required
def api_borrow_trend():
"""获取借阅趋势数据API"""
time_range = request.args.get('time_range', 'month')
@ -252,7 +252,7 @@ def api_borrow_trend():
@statistics_bp.route('/api/category-distribution')
@login_required
@admin_required
@permission_required('view_statistics') # 替代 @admin_required
def api_category_distribution():
"""获取图书分类分布数据API"""
# 记录获取图书分类分布数据的日志
@ -295,7 +295,7 @@ def api_category_distribution():
@statistics_bp.route('/user-activity')
@login_required
@admin_required
@permission_required('view_statistics') # 替代 @admin_required
def user_activity():
"""用户活跃度分析页面"""
# 记录访问用户活跃度分析的日志
@ -310,7 +310,7 @@ def user_activity():
@statistics_bp.route('/api/user-activity')
@login_required
@admin_required
@permission_required('view_statistics') # 替代 @admin_required
def api_user_activity():
"""获取用户活跃度数据API"""
# 记录获取用户活跃度数据的日志
@ -347,7 +347,7 @@ def api_user_activity():
@statistics_bp.route('/overdue-analysis')
@login_required
@admin_required
@permission_required('view_statistics') # 替代 @admin_required
def overdue_analysis():
"""逾期分析页面"""
# 记录访问逾期分析的日志
@ -362,7 +362,7 @@ def overdue_analysis():
@statistics_bp.route('/api/overdue-statistics')
@login_required
@admin_required
@permission_required('view_statistics') # 替代 @admin_required
def api_overdue_statistics():
"""获取逾期统计数据API"""
# 记录获取逾期统计数据的日志

View File

@ -492,21 +492,89 @@ def role_list():
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'])
@login_required
@admin_required
def role_save():
"""创建或更新角色,包括权限分配"""
data = request.get_json()
role_id = data.get('id')
role_name = data.get('role_name')
description = data.get('description')
permission_ids = data.get('permissions', []) # 获取权限ID列表
if not role_name:
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:
# 记录编辑角色日志
Log.add_log(
@ -515,15 +583,12 @@ def role_save():
target_type="角色",
target_id=role_id,
ip_address=request.remote_addr,
description=f"管理员 {current_user.username} 编辑角色 {role_name}"
description=f"管理员 {current_user.username} 编辑角色 {role_name},包含权限设置"
)
else: # 创建
success, message = UserService.create_role(role_name, description)
else: # 创建角色
success, message, new_role_id = UserService.create_role(role_name, description, permission_ids)
if success:
# 获取新创建的角色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
role_id = new_role_id
# 记录创建角色日志
Log.add_log(
action="创建角色",
@ -531,13 +596,72 @@ def role_save():
target_type="角色",
target_id=role_id,
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})
@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
@admin_required
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 datetime import datetime
from flask_login import UserMixin
from app.models.permission import RolePermission, Permission
db = SQLAlchemy()
#db = SQLAlchemy()
class User(db.Model, UserMixin):
@ -77,4 +78,11 @@ class Role(db.Model):
role_name = db.Column(db.String(32), unique=True, nullable=False)
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')

View File

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

View File

@ -251,3 +251,94 @@
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();
// 初始化时加载权限列表
loadPermissions();
// 添加角色按钮点击事件
if (addRoleBtn) {
addRoleBtn.addEventListener('click', function() {
@ -27,6 +30,18 @@ document.addEventListener('DOMContentLoaded', function() {
// 更新模态框标题
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');
});
@ -54,6 +69,9 @@ document.addEventListener('DOMContentLoaded', function() {
// 更新模态框标题
document.getElementById('roleModalLabel').textContent = '编辑角色';
// 加载角色权限
loadRolePermissions(roleId);
// 显示模态框
roleModal.modal('show');
});
@ -79,10 +97,17 @@ document.addEventListener('DOMContentLoaded', function() {
return;
}
// 收集选中的权限ID
const permissionIds = [];
document.querySelectorAll('.permission-checkbox:checked:not(:disabled)').forEach(checkbox => {
permissionIds.push(parseInt(checkbox.value));
});
const roleData = {
id: roleIdInput.value || null,
role_name: roleNameInput.value.trim(),
description: roleDescriptionInput.value.trim() || null
description: roleDescriptionInput.value.trim() || null,
permissions: permissionIds
};
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) {
// 显示加载状态

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-dialog" role="document">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="roleModalLabel">添加角色</h5>
@ -156,6 +156,51 @@
<label for="roleDescription">角色描述</label>
<textarea class="form-control" id="roleDescription" rows="3"></textarea>
</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>
</div>
<div class="modal-footer">

View File

@ -30,3 +30,52 @@ def admin_required(f):
return f(*args, **kwargs)
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`)
);
-- ----------------------------
-- 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
('admin', '管理员'),