【健壮性与审计篇】操作日志:前端日志管理界面的设计与实现
前言
在上一篇关于健壮性的文章中,我们重点配置了后端的错误日志和前端的错误捕获。现在,我们将更进一步,实现一个专门的操作日志系统,用于记录用户在平台上的关键操作,并提供一个前端界面供管理员查看这些日志,以达到审计和问题追踪的目的。
这篇文章将分为后端和前端两大部分:
- 后端:
- 创建
OperationLog
模型来结构化地存储操作日志。 - 实现一个工具函数或装饰器,方便在关键的视图操作中记录日志。
- 创建
OperationLogSerializer
和OperationLogViewSet
提供 API。
- 创建
- 前端:
- 创建 API 服务调用后端日志接口。
- 实现操作日志列表页面,支持筛选和分页。
第一部分:后端实现 - 操作日志模型与API
1. 创建 OperationLog 模型
打开 test-platform/backend/api/models.py
,添加 OperationLog
模型:
# test-platform/api/models.py
import uuid
from django.db import models
from django.conf import settings # 导入 settings
from django.contrib.auth.models import User
# ... (其他模型保持不变) ...
class OperationLog(models.Model):
"""
操作日志模型
"""
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL, # 用户删除后,日志中的用户字段设为NULL
null=True,
blank=True,
verbose_name="操作用户"
)
action_time = models.DateTimeField(auto_now_add=True, verbose_name="操作时间")
action_type_choices = [
('CREATE', '创建'), ('UPDATE', '更新'), ('DELETE', '删除'),
('LOGIN', '登录'), ('LOGOUT', '登出'), ('EXECUTE', '执行'),
('VIEW', '查看'), ('OTHER', '其他')
]
action_type = models.CharField(max_length=10, choices=action_type_choices, verbose_name="操作类型")
# 例如:Project, Module, TestCase, User, Role 等
target_resource = models.CharField(max_length=100, verbose_name="操作对象类型")
target_id = models.CharField(max_length=100, null=True, blank=True, verbose_name="操作对象ID") # 有些操作可能没有具体ID
description = models.TextField(verbose_name="操作描述") # 例如 "创建了项目 '项目A'"
# 存储更详细的上下文信息,例如请求参数、修改前后的数据差异等 (可选)
details = models.JSONField(null=True, blank=True, verbose_name="详细信息 (JSON)")
# 记录操作时的 IP 地址
ip_address = models.GenericIPAddressField(null=True, blank=True, verbose_name="IP地址")
class Meta:
verbose_name = "操作日志"
verbose_name_plural = "操作日志列表"
ordering = ['-action_time'] # 按操作时间降序
def __str__(self):
user_str = self.user.username if self.user else "系统操作"
return f"{
self.action_time.strftime('%Y-%m-%d %H:%M:%S')} - {
user_str} {
self.get_action_type_display()} {
self.target_resource}: {
self.description[:50]}"
模型字段解释:
- user: 关联到执行操作的用户。
- action_time: 操作发生的时间。
- action_type: 操作的类型(创建、更新、删除、登录、执行等)。
- target_resource: 操作影响的资源类型(如"项目"、“用户”)。
- target_id: 操作影响的具体资源ID。
- description: 对操作的简短描述。
- details: (可选) JSON格式,存储更详细的上下文或数据快照。
- ip_address: 操作者IP地址。
2. 生成并应用数据库迁移
python manage.py makemigrations api
python manage.py migrate api
3. 创建记录操作日志的工具/装饰器
为了方便在各个视图中记录操作日志,我们可以创建一个工具函数。
a. 创建 api/utils/log_utils.py
:
# test-platform/api/utils/log_utils.py
from ..models import OperationLog # 从父级 models 导入
from django.contrib.auth.models import User
from typing import Optional, Dict, Any
def get_client_ip(request) -> Optional[str]:
"""获取客户端IP地址"""
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = request.META.get('REMOTE_ADDR')
return ip
def record_operation_log(
user: Optional[User],
action_type: str,
target_resource: str,
description: str,
target_id: Optional[Any] = None,
details: Optional[Dict[str, Any]] = None,
request = None # 可选,用于获取 IP
):
"""
记录一条操作日志。
"""
ip_addr = None
if request:
ip_addr = get_client_ip(request)
try:
OperationLog.objects.create(
user=user if user and user.is_authenticated else None,
action_type=action_type,
target_resource=target_resource,
target_id=str(target_id) if target_id is not None else None,
description=description,
details=details,
ip_address=ip_addr
)
except Exception as e:
# 记录日志失败不应影响主业务流程,但应记录错误
import logging
logger = logging.getLogger(__name__) # 或者使用 app 级别的 logger
logger.error(f"记录操作日志失败: {
e}", exc_info=True)
4. 在关键视图操作中调用日志记录函数
现在,我们需要在一些关键的 ModelViewSet 操作 (如 create, update, destroy) 或自定义 action 中调用 record_operation_log
。
修改 ProjectViewSet
(api/views.py
):
# test-platform/api/views.py
from .utils.log_utils import record_operation_log # 导入日志记录函数
# ... 其他导入 ...
class ProjectViewSet(viewsets.ModelViewSet):
queryset = Project.objects.all().order_by('-create_time')
serializer_class = ProjectSerializer
permission_classes = [permissions.IsAuthenticated]
def perform_create(self, serializer):
instance = serializer.save()
record_operation_log(
user=self.request.user,
action_type='CREATE',
target_resource='项目',
target_id=instance.id,
description=f"创建了项目: '{
instance.name}' (ID: {
instance.id})",
request=self.request # 传递 request 对象以获取 IP
)
def perform_update(self, serializer):
instance = serializer.save()
# 可以在 details 中记录修改前后的差异 (更复杂)
record_operation_log(
user=self.request.user,
action_type='UPDATE',
target_resource='项目',
target_id=instance.id,
description=f"更新了项目: '{
instance.name}' (ID: {
instance.id})",
request=self.request
)
def perform_destroy(self, instance):
project_name = instance.name
project_id = instance.id
instance.delete()
record_operation_log(
user=self.request.user,
action_type='DELETE',
target_resource='项目',
target_id=project_id, # instance 在 delete() 后可能没有 id
description=f"删除了项目: '{
project_name}' (ID: {
project_id})",
request=self.request
)
修改 ModuleViewSet
(api/views.py
):