diff --git a/dvadmin-backend/application/settings.py b/dvadmin-backend/application/settings.py index 9d459d5..33c32fe 100644 --- a/dvadmin-backend/application/settings.py +++ b/dvadmin-backend/application/settings.py @@ -118,7 +118,7 @@ USE_I18N = True USE_L10N = True -USE_TZ = True +USE_TZ = False # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/1.11/howto/static-files/ @@ -316,4 +316,4 @@ CAPTCHA_CHALLENGE_FUNCT = 'captcha.helpers.math_challenge' API_LOG_ENABLE = True # API_LOG_METHODS = 'ALL' # ['POST', 'DELETE'] -API_LOG_METHODS = ['POST', 'DELETE'] # ['POST', 'DELETE'] +# API_LOG_METHODS = ['POST', 'DELETE'] # ['POST', 'DELETE'] diff --git a/dvadmin-backend/apps/vadmin/op_drf/logging/view_logger.py b/dvadmin-backend/apps/vadmin/op_drf/logging/view_logger.py index 9f513a7..f60f3ee 100644 --- a/dvadmin-backend/apps/vadmin/op_drf/logging/view_logger.py +++ b/dvadmin-backend/apps/vadmin/op_drf/logging/view_logger.py @@ -201,3 +201,12 @@ class CustomerModelViewLogger(ModelViewLogger): operator = self.user.username model_name = getattr(self.model, '_meta').verbose_name self.logger(f'{self.log_prefix}用户[username={operator}]删除{model_name}:[{instance}]') + + def handle_other(self, request: Request, instance: Model = None, *args, **kwargs): + """ + 仅 其他 请求才会触发此方法 + """ + pass + operator = self.user.username + model_name = getattr(self.model, '_meta').verbose_name + self.logger(f'{self.log_prefix}用户[username={operator}]其他请求{model_name}:[{instance}]') diff --git a/dvadmin-backend/apps/vadmin/op_drf/middleware.py b/dvadmin-backend/apps/vadmin/op_drf/middleware.py index 0ab7caa..8b37b53 100644 --- a/dvadmin-backend/apps/vadmin/op_drf/middleware.py +++ b/dvadmin-backend/apps/vadmin/op_drf/middleware.py @@ -5,7 +5,7 @@ django中间件 from django.conf import settings from django.utils.deprecation import MiddlewareMixin -from apps.vadmin.system.models import RequestLog +from apps.vadmin.system.models import OperationLog from ..utils.request_util import get_request_ip, get_request_data, get_request_path, get_browser, get_os, \ get_login_location @@ -39,7 +39,7 @@ class ApiLoggingMiddleware(MiddlewareMixin): 'request_method': request.method, 'request_path': request.request_path, 'request_body': body, - 'response_code': response.status_code, + 'response_code': response.data.get('code'), 'request_location': get_login_location(request), 'request_os': get_os(request), 'request_browser': get_browser(request), @@ -48,7 +48,7 @@ class ApiLoggingMiddleware(MiddlewareMixin): 'json_result': {"code": response.data.get('code'), "msg": response.data.get('msg')}, 'request_modular': request.session.get('model_name'), } - log = RequestLog(**info) + log = OperationLog(**info) log.save() def process_request(self, request): diff --git a/dvadmin-backend/apps/vadmin/op_drf/mixins.py b/dvadmin-backend/apps/vadmin/op_drf/mixins.py index 1489d83..3d9b674 100644 --- a/dvadmin-backend/apps/vadmin/op_drf/mixins.py +++ b/dvadmin-backend/apps/vadmin/op_drf/mixins.py @@ -7,6 +7,7 @@ from rest_framework.request import Request from .response import SuccessResponse from ..utils.export_excel import excel_to_data, export_excel_save_model +from ..utils.request_util import get_verbose_name class CreateModelMixin(mixins.CreateModelMixin): @@ -35,10 +36,10 @@ class ListModelMixin(mixins.ListModelMixin): list_serializer_class = None def list(self, request: Request, *args, **kwargs): - queryset = self.filter_queryset(self.get_queryset()) - page = self.paginate_queryset(queryset) if hasattr(self, 'handle_logging'): self.handle_logging(request, *args, **kwargs) + queryset = self.filter_queryset(self.get_queryset()) + page = self.paginate_queryset(queryset) if page is not None: if getattr(self, 'values_queryset', None): return self.get_paginated_response(page) @@ -298,8 +299,7 @@ class ImportSerializerMixin: # 导入序列化器 import_serializer_class = None - - @transaction.atomic # Django 事物 + @transaction.atomic # Django 事物 def importTemplate(self, request: Request, *args, **kwargs): """ 用户导人模板 @@ -358,4 +358,5 @@ class ExportSerializerMixin: % self.__class__.__name__ ) data = self.export_serializer_class(self.get_queryset(), many=True).data - return SuccessResponse(export_excel_save_model(request, self.export_field_data, data, '导出用户数据.xls')) + return SuccessResponse(export_excel_save_model(request, self.export_field_data, data, + f'导出{get_verbose_name(self.get_queryset())}.xls')) diff --git a/dvadmin-backend/apps/vadmin/op_drf/views.py b/dvadmin-backend/apps/vadmin/op_drf/views.py index 1844dd4..7f5b455 100644 --- a/dvadmin-backend/apps/vadmin/op_drf/views.py +++ b/dvadmin-backend/apps/vadmin/op_drf/views.py @@ -72,7 +72,7 @@ class CustomAPIView(APIView): method = request.method.lower() for view_logger in view_loggers: view_logger.handle(request, *args, **kwargs) - logger_fun = getattr(view_logger, f'handle_{method}', None) + logger_fun = getattr(view_logger, f'handle_{method}', f'handle_other') if logger_fun and isinstance(logger_fun, (FunctionType, MethodType)): logger_fun(request, *args, **kwargs) diff --git a/dvadmin-backend/apps/vadmin/system/filters.py b/dvadmin-backend/apps/vadmin/system/filters.py index a9dae47..1601f03 100644 --- a/dvadmin-backend/apps/vadmin/system/filters.py +++ b/dvadmin-backend/apps/vadmin/system/filters.py @@ -1,6 +1,6 @@ import django_filters -from .models import LoginInfor +from .models import LoginInfor, OperationLog from ..system.models import DictDetails, DictData, ConfigSettings, MessagePush, SaveFile @@ -70,3 +70,15 @@ class LoginInforFilter(django_filters.rest_framework.FilterSet): class Meta: model = LoginInfor fields = '__all__' + + +class OperationLogFilter(django_filters.rest_framework.FilterSet): + """ + 操作日志 简单过滤器 + """ + request_modular = django_filters.CharFilter(lookup_expr='icontains') + creator_username = django_filters.CharFilter(field_name='creator__username', lookup_expr='icontains') + + class Meta: + model = OperationLog + fields = '__all__' diff --git a/dvadmin-backend/apps/vadmin/system/models/__init__.py b/dvadmin-backend/apps/vadmin/system/models/__init__.py index a5f45bd..3d85a6d 100644 --- a/dvadmin-backend/apps/vadmin/system/models/__init__.py +++ b/dvadmin-backend/apps/vadmin/system/models/__init__.py @@ -6,5 +6,5 @@ from ..models.save_file import SaveFile from ..models.message_push import MessagePush from ..models.message_push import MessagePushUser from ..models.logininfor import LoginInfor -from ..models.request_log import RequestLog +from ..models.operation_log import OperationLog diff --git a/dvadmin-backend/apps/vadmin/system/models/request_log.py b/dvadmin-backend/apps/vadmin/system/models/operation_log.py similarity index 91% rename from dvadmin-backend/apps/vadmin/system/models/request_log.py rename to dvadmin-backend/apps/vadmin/system/models/operation_log.py index 5f44ac2..d596983 100644 --- a/dvadmin-backend/apps/vadmin/system/models/request_log.py +++ b/dvadmin-backend/apps/vadmin/system/models/operation_log.py @@ -3,12 +3,12 @@ from django.db.models import TextField, CharField, BooleanField from ...op_drf.models import CoreModel -class RequestLog(CoreModel): +class OperationLog(CoreModel): request_modular = CharField(max_length=64, verbose_name="请求模块", null=True, blank=True) request_path = CharField(max_length=400, verbose_name="请求地址", null=True, blank=True) request_body = TextField(verbose_name="请求参数", null=True, blank=True) request_method = CharField(max_length=64, verbose_name="请求方式", null=True, blank=True) - request_msg = CharField(max_length=64, verbose_name="操作说明", null=True, blank=True) + request_msg = TextField(verbose_name="操作说明", null=True, blank=True) request_ip = CharField(max_length=32, verbose_name="请求ip地址", null=True, blank=True) request_browser = CharField(max_length=32, verbose_name="请求浏览器", null=True, blank=True) response_code = CharField(max_length=32, verbose_name="响应状态码", null=True, blank=True) diff --git a/dvadmin-backend/apps/vadmin/system/serializers.py b/dvadmin-backend/apps/vadmin/system/serializers.py index 38ac18a..89e490a 100644 --- a/dvadmin-backend/apps/vadmin/system/serializers.py +++ b/dvadmin-backend/apps/vadmin/system/serializers.py @@ -1,6 +1,6 @@ from rest_framework import serializers -from .models import LoginInfor +from .models import LoginInfor, OperationLog from ..op_drf.serializers import CustomModelSerializer from ..system.models import DictData, DictDetails, ConfigSettings, SaveFile, MessagePush, MessagePushUser @@ -196,6 +196,7 @@ class ExportMessagePushSerializer(CustomModelSerializer): 'id', 'title', 'content', 'message_type', 'is_reviewed', 'status', 'users', 'creator', 'modifier', 'update_datetime', 'create_datetime') + class MessagePushUserSerializer(CustomModelSerializer): """ 消息通知 用户查询简单序列化器 @@ -208,7 +209,7 @@ class MessagePushUserSerializer(CustomModelSerializer): # return UserProfileSerializer(obj.user.all(), many=True).data # 返回这个消息是否已读 def get_is_read(self, obj): - object = MessagePushUser.objects.filter(message_push=obj,user=self.context.get('request').user).first() + object = MessagePushUser.objects.filter(message_push=obj, user=self.context.get('request').user).first() return object.is_read if object else False class Meta: @@ -218,6 +219,7 @@ class MessagePushUserSerializer(CustomModelSerializer): def save(self, **kwargs): return super().save(**kwargs) + # ================================================= # # ************** 登录日志 序列化器 ************** # # ================================================= # @@ -230,3 +232,30 @@ class LoginInforSerializer(CustomModelSerializer): class Meta: model = LoginInfor fields = "__all__" + + +# ================================================= # +# ************** 操作日志 序列化器 ************** # +# ================================================= # + +class OperationLogSerializer(CustomModelSerializer): + """ + 操作日志 简单序列化器 + """ + creator_name = serializers.SlugRelatedField(slug_field="username", source="creator", read_only=True) + + class Meta: + model = OperationLog + fields = "__all__" + + +class ExportOperationLogSerializer(CustomModelSerializer): + """ + 导出 操作日志 简单序列化器 + """ + creator_name = serializers.SlugRelatedField(slug_field="username", source="creator", read_only=True) + class Meta: + model = OperationLog + fields = ('request_modular', 'request_path', 'request_body', 'request_method', 'request_msg', 'request_ip', + 'request_browser', 'response_code', 'request_location', 'request_os', 'json_result', 'status', + 'creator_name') diff --git a/dvadmin-backend/apps/vadmin/system/urls.py b/dvadmin-backend/apps/vadmin/system/urls.py index 92ec250..42c342f 100644 --- a/dvadmin-backend/apps/vadmin/system/urls.py +++ b/dvadmin-backend/apps/vadmin/system/urls.py @@ -2,7 +2,8 @@ from django.urls import re_path from rest_framework.routers import DefaultRouter from ..system.views import DictDataModelViewSet, DictDetailsModelViewSet, \ - ConfigSettingsModelViewSet, SaveFileModelViewSet, MessagePushModelViewSet, LoginInforModelViewSet + ConfigSettingsModelViewSet, SaveFileModelViewSet, MessagePushModelViewSet, LoginInforModelViewSet, \ + OperationLogModelViewSet router = DefaultRouter() router.register(r'dict/type', DictDataModelViewSet) @@ -11,6 +12,8 @@ router.register(r'config', ConfigSettingsModelViewSet) router.register(r'savefile', SaveFileModelViewSet) router.register(r'message', MessagePushModelViewSet) router.register(r'logininfor', LoginInforModelViewSet) +router.register(r'operation_log', OperationLogModelViewSet) + urlpatterns = [ re_path('dict/get/type/(?P.*)/', DictDetailsModelViewSet.as_view({'get': 'dict_details_list'})), re_path('config/configKey/(?P.*)/', ConfigSettingsModelViewSet.as_view({'get': 'get_config_key'})), @@ -30,5 +33,9 @@ urlpatterns = [ re_path('message/user_messages/', MessagePushModelViewSet.as_view({'get': 'get_user_messages', })), # 改为已读 re_path('message/is_read/(?P.*)/', MessagePushModelViewSet.as_view({'put': 'update_is_read', })), + # 清空操作日志 + re_path('operation_log/clean/', OperationLogModelViewSet.as_view({'delete': 'clean_all', })), + # 导出操作日志 + re_path('operation_log/export/', OperationLogModelViewSet.as_view({'get': 'export', })), ] urlpatterns += router.urls diff --git a/dvadmin-backend/apps/vadmin/system/views.py b/dvadmin-backend/apps/vadmin/system/views.py index ac63a01..03f1a8e 100644 --- a/dvadmin-backend/apps/vadmin/system/views.py +++ b/dvadmin-backend/apps/vadmin/system/views.py @@ -1,18 +1,19 @@ from django.db.models import Q from rest_framework.request import Request -from .models import LoginInfor +from .models import LoginInfor, OperationLog from ..op_drf.filters import DataLevelPermissionsFilter from ..op_drf.viewsets import CustomModelViewSet from ..system.filters import DictDetailsFilter, DictDataFilter, ConfigSettingsFilter, MessagePushFilter, \ - SaveFileFilter, LoginInforFilter + SaveFileFilter, LoginInforFilter, OperationLogFilter from ..system.models import DictData, DictDetails, ConfigSettings, SaveFile, MessagePush from ..system.models import MessagePushUser from ..system.serializers import DictDataSerializer, DictDataCreateUpdateSerializer, DictDetailsSerializer, \ DictDetailsCreateUpdateSerializer, DictDetailsListSerializer, ConfigSettingsSerializer, \ ConfigSettingsCreateUpdateSerializer, SaveFileSerializer, SaveFileCreateUpdateSerializer, \ ExportConfigSettingsSerializer, ExportDictDataSerializer, ExportDictDetailsSerializer, \ - MessagePushSerializer, MessagePushCreateUpdateSerializer, ExportMessagePushSerializer, LoginInforSerializer + MessagePushSerializer, MessagePushCreateUpdateSerializer, ExportMessagePushSerializer, LoginInforSerializer, \ + OperationLogSerializer, ExportOperationLogSerializer from ..utils.export_excel import export_excel_save_model from ..utils.response import SuccessResponse @@ -224,3 +225,28 @@ class LoginInforModelViewSet(CustomModelViewSet): filter_class = LoginInforFilter extra_filter_backends = [DataLevelPermissionsFilter] ordering = 'create_datetime' # 默认排序 + + +class OperationLogModelViewSet(CustomModelViewSet): + """ + 操作日志 模型的CRUD视图 + """ + queryset = OperationLog.objects.all() + serializer_class = OperationLogSerializer + filter_class = OperationLogFilter + extra_filter_backends = [DataLevelPermissionsFilter] + ordering = '-create_datetime' # 默认排序 + export_field_data = ['请求模块', '请求地址', '请求参数', '请求方式', '操作说明', '请求ip地址', + '请求浏览器', '响应状态码', '操作地点', '操作系统', '返回信息', '响应状态', '操作用户名'] + export_serializer_class = ExportOperationLogSerializer + + def clean_all(self, request: Request, *args, **kwargs): + """ + 清空操作日志 + :param request: + :param args: + :param kwargs: + :return: + """ + self.get_queryset().delete() + return SuccessResponse(msg="清空成功") diff --git a/dvadmin-backend/apps/vadmin/utils/exceptions.py b/dvadmin-backend/apps/vadmin/utils/exceptions.py index 1f38afa..b07262b 100644 --- a/dvadmin-backend/apps/vadmin/utils/exceptions.py +++ b/dvadmin-backend/apps/vadmin/utils/exceptions.py @@ -4,6 +4,7 @@ import traceback from rest_framework import serializers, exceptions from rest_framework.views import set_rollback +from .request_util import get_verbose_name from .response import ErrorResponse logger = logging.getLogger(__name__) @@ -17,9 +18,9 @@ class APIException(Exception): """ def __init__(self, code=201, message='API异常', args=('API异常',)): - self.args = args - self.code = code - self.message = message + args = args + code = code + message = message def __str__(self): return self.message @@ -36,7 +37,7 @@ class FrameworkException(Exception): def __init__(self, message='框架异常', *args: object, **kwargs: object) -> None: super().__init__(*args, **kwargs) - self.message = message + message = message def __str__(self) -> str: return f"{self.message}" @@ -64,6 +65,9 @@ def op_exception_handler(ex, context): """ msg = '' code = '201' + request = context.get('request') + request.session['model_name'] = get_verbose_name(view=context.get('view')) + if isinstance(ex, AuthenticationFailed): code = 401 msg = ex.detail diff --git a/dvadmin-backend/apps/vadmin/utils/request_util.py b/dvadmin-backend/apps/vadmin/utils/request_util.py index eb46195..f67b564 100644 --- a/dvadmin-backend/apps/vadmin/utils/request_util.py +++ b/dvadmin-backend/apps/vadmin/utils/request_util.py @@ -172,3 +172,21 @@ def get_login_location(request, *args, **kwargs): content = r.content.decode('GBK') return content.replace('\r', '').replace('\n', '') return "" + + +def get_verbose_name(queryset=None, view=None, model=None): + """ + 获取 verbose_name + :param request: + :param view: + :return: + """ + if queryset and hasattr(queryset, 'model'): + model = queryset.model + elif view and hasattr(view.get_queryset(), 'model'): + model = view.get_queryset().model + elif view and hasattr(view.get_serializer(), 'Meta') and hasattr(view.get_serializer().Meta, 'model'): + model = view.get_serializer().Meta.model + if model: + return getattr(model, '_meta').verbose_name + return "" diff --git a/dvadmin-ui/src/api/vadmin/system/operationlog.js b/dvadmin-ui/src/api/vadmin/system/operationlog.js new file mode 100755 index 0000000..7971fc7 --- /dev/null +++ b/dvadmin-ui/src/api/vadmin/system/operationlog.js @@ -0,0 +1,35 @@ +import request from '@/utils/request' + +// 查询操作日志列表 +export function list(query) { + return request({ + url: '/admin/system/operation_log/', + method: 'get', + params: query + }) +} + +// 删除操作日志 +export function delOperationLog(operId) { + return request({ + url: '/admin/system/operation_log/' + operId + '/', + method: 'delete' + }) +} + +// 清空操作日志 +export function cleanOperationLog() { + return request({ + url: '/admin/system/operation_log/clean/', + method: 'delete' + }) +} + +// 导出操作日志 +export function exportOperationLog(query) { + return request({ + url: '/admin/system/operation_log/export/', + method: 'get', + params: query + }) +} diff --git a/dvadmin-ui/src/router/index.js b/dvadmin-ui/src/router/index.js index e13fbab..5087fa1 100755 --- a/dvadmin-ui/src/router/index.js +++ b/dvadmin-ui/src/router/index.js @@ -98,6 +98,18 @@ export const constantRoutes = [ meta: { title: '字典数据', icon: '' } } ] + },{ + path: '/operlog', + component: Layout, + hidden: false, + children: [ + { + path: 'log', + component: (resolve) => require(['@/views/vadmin/system/operlog'], resolve), + name: 'Data', + meta: { title: '操作日志', icon: '' } + } + ] } ] diff --git a/dvadmin-ui/src/utils/ruoyi.js b/dvadmin-ui/src/utils/ruoyi.js index b48a642..6d61cba 100755 --- a/dvadmin-ui/src/utils/ruoyi.js +++ b/dvadmin-ui/src/utils/ruoyi.js @@ -56,16 +56,13 @@ export function resetForm(refName) { // 添加日期范围 export function addDateRange(params, dateRange, propName) { var search = params; - search.params = {}; if (null != dateRange && '' != dateRange) { - if (typeof(propName) === "undefined") { - search.params["beginTime"] = dateRange[0]; - search.params["endTime"] = dateRange[1]; - } else { - search.params["begin" + propName] = dateRange[0]; - search.params["end" + propName] = dateRange[1]; - } + // create_datetime__range = this.dateRange + var dateTime=new Date(); + + search.as = JSON.stringify({create_datetime__range : dateRange}); } + console.log(11,search) return search; } diff --git a/dvadmin-ui/src/views/vadmin/system/operlog/index.vue b/dvadmin-ui/src/views/vadmin/system/operlog/index.vue new file mode 100755 index 0000000..4feac0e --- /dev/null +++ b/dvadmin-ui/src/views/vadmin/system/operlog/index.vue @@ -0,0 +1,292 @@ + + + +