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.utils.auth import permission_required # 更改为导入permission_required装饰器 from datetime import datetime, timedelta # 创建蓝图 log_bp = Blueprint('log', __name__, url_prefix='/log') @log_bp.route('/list') @login_required @permission_required('view_logs') # 替代 @admin_required def log_list(): """日志列表页面""" # 获取筛选参数 page = request.args.get('page', 1, type=int) user_id = request.args.get('user_id', type=int) action = request.args.get('action') target_type = request.args.get('target_type') # 处理日期范围参数 date_range = request.args.get('date_range', '7') # 默认显示7天内的日志 end_date = datetime.now() start_date = None if date_range == '1': start_date = end_date - timedelta(days=1) elif date_range == '7': start_date = end_date - timedelta(days=7) elif date_range == '30': start_date = end_date - timedelta(days=30) elif date_range == 'custom': start_date_str = request.args.get('start_date') end_date_str = request.args.get('end_date') if start_date_str: start_date = datetime.strptime(start_date_str, '%Y-%m-%d') if end_date_str: end_date = datetime.strptime(end_date_str + ' 23:59:59', '%Y-%m-%d %H:%M:%S') # 获取分页数据 pagination = Log.get_logs( page=page, per_page=20, user_id=user_id, action=action, target_type=target_type, start_date=start_date, end_date=end_date ) # 获取用户列表和操作类型列表,用于筛选 users = User.query.all() # 统计各类操作的数量 action_types = db.session.query(Log.action, db.func.count(Log.id)) \ .group_by(Log.action).all() target_types = db.session.query(Log.target_type, db.func.count(Log.id)) \ .filter(Log.target_type != None) \ .group_by(Log.target_type).all() return render_template( 'log/list.html', pagination=pagination, users=users, action_types=action_types, target_types=target_types, filters={ 'user_id': user_id, 'action': action, 'target_type': target_type, 'date_range': date_range, 'start_date': start_date.strftime('%Y-%m-%d') if start_date else '', 'end_date': end_date.strftime('%Y-%m-%d') if end_date != datetime.now() else '' } ) @log_bp.route('/detail/') @login_required @permission_required('view_logs') # 替代 @admin_required def log_detail(log_id): """日志详情页面""" log = Log.query.get_or_404(log_id) return render_template('log/detail.html', log=log) @log_bp.route('/api/export', methods=['POST']) @login_required @permission_required('view_logs') # 替代 @admin_required def export_logs(): """导出日志API""" data = request.get_json() user_id = data.get('user_id') action = data.get('action') target_type = data.get('target_type') start_date_str = data.get('start_date') end_date_str = data.get('end_date') export_format = data.get('format', 'csv') # 获取导出格式参数 # 处理日期范围 start_date = None end_date = datetime.now() if start_date_str: start_date = datetime.strptime(start_date_str, '%Y-%m-%d') if end_date_str: end_date = datetime.strptime(end_date_str + ' 23:59:59', '%Y-%m-%d %H:%M:%S') # 查询日志 query = Log.query.order_by(Log.created_at.desc()) if user_id: query = query.filter(Log.user_id == user_id) if action: query = query.filter(Log.action == action) if target_type: query = query.filter(Log.target_type == target_type) if start_date: query = query.filter(Log.created_at >= start_date) if end_date: query = query.filter(Log.created_at <= end_date) logs = query.all() try: # 根据格式选择导出方法 if export_format == 'xlsx': return export_as_xlsx(logs) else: return export_as_csv(logs) except Exception as e: # 记录错误以便调试 import traceback error_details = traceback.format_exc() current_app.logger.error(f"Export error: {str(e)}\n{error_details}") return jsonify({ 'success': False, 'message': f'导出失败: {str(e)}' }), 500 def export_as_csv(logs): """导出为CSV格式""" import csv from io import StringIO import base64 # 创建CSV文件 output = StringIO() output.write('\ufeff') # 添加BOM标记,解决Excel中文乱码 csv_writer = csv.writer(output) # 写入标题行 csv_writer.writerow(['ID', '用户', '操作类型', '目标类型', '目标ID', 'IP地址', '描述', '创建时间']) # 写入数据行 for log in logs: username = log.user.username if log.user else "未登录" csv_writer.writerow([ log.id, username, log.action, log.target_type or '', log.target_id or '', log.ip_address or '', log.description or '', log.created_at.strftime('%Y-%m-%d %H:%M:%S') ]) # 获取CSV字符串并进行Base64编码 csv_string = output.getvalue() csv_bytes = csv_string.encode('utf-8') b64_encoded = base64.b64encode(csv_bytes).decode('utf-8') # 设置文件名 filename = f"system_logs_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv" return jsonify({ 'success': True, 'message': f'已生成 {len(logs)} 条日志记录', 'count': len(logs), 'filename': filename, 'filedata': b64_encoded, 'filetype': 'text/csv' }) def export_as_xlsx(logs): """导出为XLSX格式""" import base64 from io import BytesIO try: # 动态导入openpyxl,如果不存在则抛出异常 import openpyxl except ImportError: raise Exception("未安装openpyxl库,无法导出Excel格式。请安装后重试: pip install openpyxl") # 创建工作簿和工作表 wb = openpyxl.Workbook() ws = wb.active ws.title = "系统日志" # 写入标题行 headers = ['ID', '用户', '操作类型', '目标类型', '目标ID', 'IP地址', '描述', '创建时间'] for col_idx, header in enumerate(headers, 1): ws.cell(row=1, column=col_idx, value=header) # 写入数据行 for row_idx, log in enumerate(logs, 2): username = log.user.username if log.user else "未登录" ws.cell(row=row_idx, column=1, value=log.id) ws.cell(row=row_idx, column=2, value=username) ws.cell(row=row_idx, column=3, value=log.action) ws.cell(row=row_idx, column=4, value=log.target_type or '') ws.cell(row=row_idx, column=5, value=log.target_id or '') ws.cell(row=row_idx, column=6, value=log.ip_address or '') ws.cell(row=row_idx, column=7, value=log.description or '') ws.cell(row=row_idx, column=8, value=log.created_at.strftime('%Y-%m-%d %H:%M:%S')) # 调整列宽 for col_idx, header in enumerate(headers, 1): column_letter = openpyxl.utils.get_column_letter(col_idx) if header == '描述': ws.column_dimensions[column_letter].width = 40 else: ws.column_dimensions[column_letter].width = 15 # 保存到内存 output = BytesIO() wb.save(output) output.seek(0) # 编码为Base64 xlsx_data = output.getvalue() b64_encoded = base64.b64encode(xlsx_data).decode('utf-8') # 设置文件名 filename = f"system_logs_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx" return jsonify({ 'success': True, 'message': f'已生成 {len(logs)} 条日志记录', 'count': len(logs), 'filename': filename, 'filedata': b64_encoded, 'filetype': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }) @log_bp.route('/api/clear', methods=['POST']) @login_required @permission_required('view_logs') # 替代 @admin_required def clear_logs(): """清空日志API""" data = request.get_json() days = data.get('days', 0) try: if days > 0: # 清除指定天数前的日志 cutoff_date = datetime.now() - timedelta(days=days) deleted = Log.query.filter(Log.created_at < cutoff_date).delete() else: # 清空全部日志 deleted = Log.query.delete() db.session.commit() return jsonify({ 'success': True, 'message': f'成功清除 {deleted} 条日志记录', 'count': deleted }) except Exception as e: db.session.rollback() return jsonify({ 'success': False, 'message': f'清除日志失败: {str(e)}' }), 500