diff --git a/app/__init__.py b/app/__init__.py index b90eb0b..ff80be6 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -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 diff --git a/app/controllers/announcement.py b/app/controllers/announcement.py index 55dedb3..3e2c721 100644 --- a/app/controllers/announcement.py +++ b/app/controllers/announcement.py @@ -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/', 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/', 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/', methods=['POST']) @login_required -@admin_required +@permission_required('manage_announcements') # 替代 @admin_required def change_top_status(announcement_id): """更改公告置顶状态""" data = request.get_json() diff --git a/app/controllers/book.py b/app/controllers/book.py index 88d1bf1..89d3c38 100644 --- a/app/controllers/book.py +++ b/app/controllers/book.py @@ -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/') @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/', 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/', 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/', 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/', 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作为模板 diff --git a/app/controllers/borrow.py b/app/controllers/borrow.py index 23433bb..250d169 100644 --- a/app/controllers/borrow.py +++ b/app/controllers/borrow.py @@ -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/', methods=['POST']) @login_required -@admin_required +@permission_required('manage_overdue') # 替代 @admin_required def notify_overdue(borrow_id): """发送逾期通知""" from app.models.notification import Notification diff --git a/app/controllers/inventory.py b/app/controllers/inventory.py index 66f8e19..83e5dab 100644 --- a/app/controllers/inventory.py +++ b/app/controllers/inventory.py @@ -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/', 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//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', diff --git a/app/controllers/log.py b/app/controllers/log.py index c530184..399c722 100644 --- a/app/controllers/log.py +++ b/app/controllers/log.py @@ -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/') @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() diff --git a/app/controllers/statistics.py b/app/controllers/statistics.py index 6ba3ed6..dc388f0 100644 --- a/app/controllers/statistics.py +++ b/app/controllers/statistics.py @@ -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""" # 记录获取逾期统计数据的日志 diff --git a/app/controllers/user.py b/app/controllers/user.py index 918feb7..d672bb3 100644 --- a/app/controllers/user.py +++ b/app/controllers/user.py @@ -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//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//count', methods=['GET']) +# 角色删除API +@user_bp.route('/role/delete/', 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//count', methods=['GET']) @login_required @admin_required def get_role_user_count(role_id): diff --git a/app/init_permissions.py b/app/init_permissions.py new file mode 100644 index 0000000..7f54c82 --- /dev/null +++ b/app/init_permissions.py @@ -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() diff --git a/app/models/database.py b/app/models/database.py new file mode 100644 index 0000000..29d8e9d --- /dev/null +++ b/app/models/database.py @@ -0,0 +1,4 @@ +from flask_sqlalchemy import SQLAlchemy + +# 创建共享的SQLAlchemy实例 +db = SQLAlchemy() diff --git a/app/models/permission.py b/app/models/permission.py new file mode 100644 index 0000000..fa1c5c3 --- /dev/null +++ b/app/models/permission.py @@ -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='权限分配时间') + diff --git a/app/models/user.py b/app/models/user.py index 069c2a7..e69d29f 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -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') diff --git a/app/services/user_service.py b/app/services/user_service.py index 2c749d5..b342f55 100644 --- a/app/services/user_service.py +++ b/app/services/user_service.py @@ -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)}' \ No newline at end of file + return False, f'创建用户失败: {str(e)}' + diff --git a/app/static/css/user-roles.css b/app/static/css/user-roles.css index dffd7a1..7c99b11 100644 --- a/app/static/css/user-roles.css +++ b/app/static/css/user-roles.css @@ -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; +} diff --git a/app/static/js/user-roles.js b/app/static/js/user-roles.js index 26cd665..700027d 100644 --- a/app/static/js/user-roles.js +++ b/app/static/js/user-roles.js @@ -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 = '
加载权限中...
'; + 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 = '
加载权限失败
'; + } + }) + .catch(error => { + console.error('Error:', error); + bookPermissions.innerHTML = '
加载权限失败,请刷新页面重试
'; + }); + } + + // 渲染权限组 + function renderPermissionGroup(container, permissions) { + if (permissions.length === 0) { + container.innerHTML = '
暂无相关权限
'; + return; + } + + let html = ''; + permissions.forEach(perm => { + html += ` +
+ + +
+ `; + }); + + 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) { // 显示加载状态 diff --git a/app/templates/user/roles.html b/app/templates/user/roles.html index 910d550..90073a3 100644 --- a/app/templates/user/roles.html +++ b/app/templates/user/roles.html @@ -135,7 +135,7 @@