操作日志前端完成

pull/2/head
李强 2021-03-21 11:24:50 +08:00
parent 38e09f556b
commit f2f8fa241e
17 changed files with 475 additions and 33 deletions

View File

@ -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']

View File

@ -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}]')

View File

@ -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):

View File

@ -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'))

View File

@ -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)

View File

@ -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__'

View File

@ -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

View File

@ -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)

View File

@ -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')

View File

@ -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<pk>.*)/', DictDetailsModelViewSet.as_view({'get': 'dict_details_list'})),
re_path('config/configKey/(?P<pk>.*)/', 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<pk>.*)/', 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

View File

@ -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="清空成功")

View File

@ -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

View File

@ -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 ""

View File

@ -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
})
}

View File

@ -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: '' }
}
]
}
]

View File

@ -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;
}

View File

@ -0,0 +1,292 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="系统模块" prop="request_modular">
<el-input
v-model="queryParams.request_modular"
placeholder="请输入系统模块"
clearable
style="width: 240px;"
size="small"
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="操作人员" prop="creator_name">
<el-input
v-model="queryParams.creator_name"
placeholder="请输入操作人员"
clearable
style="width: 240px;"
size="small"
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select
v-model="queryParams.status"
placeholder="操作状态"
clearable
size="small"
style="width: 240px"
>
<el-option
v-for="dict in statusOptions"
:key="dict.dictValue"
:label="dict.dictLabel"
:value="dict.dictValue"
/>
</el-select>
</el-form-item>
<el-form-item label="操作时间">
<el-date-picker
v-model="dateRange"
size="small"
style="width: 240px"
value-format="yyyy-MM-dd HH:mm:ss"
type="daterange"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="['00:00:00', '23:59:59']"
></el-date-picker>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery"></el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery"></el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="el-icon-delete"
size="mini"
:disabled="multiple"
@click="handleDelete"
v-hasPermi="['monitor:operlog:remove']"
>删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="el-icon-delete"
size="mini"
@click="handleClean"
v-hasPermi="['monitor:operlog:remove']"
>清空</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="warning"
plain
icon="el-icon-download"
size="mini"
@click="handleExport"
v-hasPermi="['system:config:export']"
>导出</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="list" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="日志编号" align="center" prop="id" />
<el-table-column label="系统模块" align="center" prop="request_modular" />
<el-table-column label="请求方式" align="center" prop="request_method" />
<el-table-column label="操作人员" align="center" prop="creator_name" />
<el-table-column label="主机" align="center" prop="request_ip" width="130" :show-overflow-tooltip="true" />
<el-table-column label="操作地点" align="center" prop="request_location" :show-overflow-tooltip="true" />
<el-table-column label="操作状态" align="center" prop="status" :formatter="statusFormat" />
<el-table-column label="操作日期" align="center" prop="create_datetime" width="180">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.create_datetime) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button
size="mini"
type="text"
icon="el-icon-view"
@click="handleView(scope.row,scope.index)"
v-hasPermi="['monitor:operlog:query']"
>详细</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total>0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
<!-- 操作日志详细 -->
<el-dialog title="操作日志详细" :visible.sync="open" width="700px" append-to-body>
<el-form ref="form" :model="form" label-width="100px" size="mini">
<el-row>
<el-col :span="12">
<el-form-item label="操作模块:">{{ form.request_modular }} / {{ form.request_msg }}</el-form-item>
<el-form-item
label="登录信息:"
>{{ form.creator_name }} / {{ form.request_ip }} / {{ form.request_location }} / {{ form.request_browser }}</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="请求地址:">{{ form.request_path }}</el-form-item>
<el-form-item label="请求方式:">{{ form.request_method }}</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="请求参数:">{{ form.request_body }}</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="返回参数:">{{ form.json_result }}</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="返回状态码:">{{ form.response_code }}</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="操作状态:">
<div v-if="form.status === true"></div>
<div v-else-if="form.status === false">失败</div>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="操作时间:">{{ parseTime(form.create_datetime) }}</el-form-item>
</el-col>
<!-- <el-col :span="24">-->
<!-- <el-form-item label="异常信息:" v-if="form.status === false">{{ form.json_result }}</el-form-item>-->
<!-- </el-col>-->
</el-row>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="open = false"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import {cleanOperationLog, delOperationLog, exportOperationLog, list} from "@/api/vadmin/system/operationlog";
export default {
name: "Operlog",
data() {
return {
//
loading: true,
//
ids: [],
//
multiple: true,
//
showSearch: true,
//
total: 0,
//
list: [],
//
open: false,
//
statusOptions: [{dictLabel: '成功', dictValue: true}, {dictLabel: '失败', dictValue: false}],
//
dateRange: [],
//
form: {},
//
queryParams: {
pageNum: 1,
pageSize: 10,
request_modular: undefined,
creator_name: undefined,
status: undefined
}
};
},
created() {
this.getList();
},
methods: {
/** 查询登录日志 */
getList() {
this.loading = true;
list(this.addDateRange(this.queryParams, this.dateRange)).then(response => {
this.list = response.data.results;
this.total = response.data.count;
this.loading = false;
}
);
},
//
statusFormat(row, column) {
return this.selectDictLabel(this.statusOptions, row.status);
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1;
this.getList();
},
/** 重置按钮操作 */
resetQuery() {
this.dateRange = [];
this.resetForm("queryForm");
this.handleQuery();
},
//
handleSelectionChange(selection) {
this.ids = selection.map(item => item.id)
this.multiple = !selection.length
},
/** 详细按钮操作 */
handleView(row) {
this.open = true;
this.form = row;
},
/** 删除按钮操作 */
handleDelete(row) {
const ids = row.id || this.ids;
this.$confirm('是否确认删除日志编号为"' + ids + '"的数据项?', "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(function() {
return delOperationLog(ids);
}).then(() => {
this.getList();
this.msgSuccess("删除成功");
})
},
/** 清空按钮操作 */
handleClean() {
this.$confirm('是否确认清空所有操作日志数据项?', "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(function() {
return cleanOperationLog();
}).then(() => {
this.getList();
this.msgSuccess("清空成功");
})
},
/** 导出按钮操作 */
handleExport() {
const queryParams = this.queryParams;
this.$confirm('是否确认导出所有操作日志数据项?', "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(function() {
return exportOperationLog(queryParams);
}).then(response => {
this.download(response.data.file_url,response.data.name);
})
}
}
};
</script>