1. 项目概述
在Web应用开发中,用户资料管理是最基础也最核心的功能之一。作为Python生态中最流行的Web框架,Django配合其强大的REST Framework扩展包,为开发者提供了构建RESTful API的高效工具集。今天我要分享的是在DRF(Django REST Framework)中实现用户资料更新功能的完整实践方案。
这个功能看似简单,但实际开发中会遇到不少细节问题:如何设计合理的序列化器?如何处理密码修改等敏感操作?如何保证数据验证的严谨性?如何优化API性能?这些都是我在多个生产项目中积累的经验总结。
2. 环境准备与基础配置
2.1 项目初始化
首先确保你已经创建了Django项目并安装了必要依赖:
bash复制pip install django djangorestframework
在settings.py中添加REST Framework配置:
python复制INSTALLED_APPS = [
...
'rest_framework',
'rest_framework.authtoken', # 如果需要token认证
]
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.TokenAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
]
}
2.2 用户模型设计
Django自带的User模型通常不能满足实际需求,我们需要扩展它:
python复制from django.contrib.auth.models import AbstractUser
from django.db import models
class User(AbstractUser):
avatar = models.ImageField(upload_to='avatars/', null=True, blank=True)
bio = models.TextField(max_length=500, blank=True)
phone = models.CharField(max_length=20, blank=True)
def __str__(self):
return self.username
别忘了在settings.py中指定自定义用户模型:
python复制AUTH_USER_MODEL = 'your_app.User'
3. 序列化器设计与实现
3.1 基础用户序列化器
创建serializers.py文件:
python复制from rest_framework import serializers
from .models import User
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'username', 'email', 'first_name',
'last_name', 'avatar', 'bio', 'phone']
extra_kwargs = {
'username': {'read_only': True},
'email': {'required': False}
}
3.2 密码修改序列化器
密码修改需要单独处理:
python复制class PasswordChangeSerializer(serializers.Serializer):
old_password = serializers.CharField(required=True)
new_password = serializers.CharField(required=True, min_length=8)
def validate_old_password(self, value):
user = self.context['request'].user
if not user.check_password(value):
raise serializers.ValidationError("旧密码不正确")
return value
3.3 头像上传处理
对于文件上传需要特殊处理:
python复制class AvatarUpdateSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['avatar']
def validate_avatar(self, value):
# 验证文件大小
if value.size > 2*1024*1024:
raise serializers.ValidationError("头像文件不能超过2MB")
# 验证文件类型
if not value.name.lower().endswith(('.jpg', '.jpeg', '.png')):
raise serializers.ValidationError("只支持JPG/PNG格式图片")
return value
4. 视图层实现
4.1 用户资料视图
在views.py中创建视图:
python复制from rest_framework import generics, permissions
from .models import User
from .serializers import UserSerializer, PasswordChangeSerializer
class UserProfileView(generics.RetrieveUpdateAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
permission_classes = [permissions.IsAuthenticated]
def get_object(self):
return self.request.user
4.2 密码修改视图
python复制from rest_framework.response import Response
from rest_framework import status
class PasswordChangeView(generics.GenericAPIView):
serializer_class = PasswordChangeSerializer
permission_classes = [permissions.IsAuthenticated]
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = request.user
user.set_password(serializer.validated_data['new_password'])
user.save()
return Response({"detail": "密码已更新"}, status=status.HTTP_200_OK)
4.3 头像上传视图
python复制class AvatarUpdateView(generics.UpdateAPIView):
serializer_class = AvatarUpdateSerializer
permission_classes = [permissions.IsAuthenticated]
def get_object(self):
return self.request.user
5. 路由配置
在urls.py中配置路由:
python复制from django.urls import path
from .views import UserProfileView, PasswordChangeView, AvatarUpdateView
urlpatterns = [
path('profile/', UserProfileView.as_view(), name='user-profile'),
path('profile/password/', PasswordChangeView.as_view(), name='password-change'),
path('profile/avatar/', AvatarUpdateView.as_view(), name='avatar-update'),
]
6. 高级功能实现
6.1 部分更新(PATCH)支持
DRF默认支持部分更新,但我们需要确保序列化器正确处理:
python复制class UserSerializer(serializers.ModelSerializer):
# ... 之前的代码 ...
def update(self, instance, validated_data):
# 确保email不为空时是唯一的
email = validated_data.get('email', None)
if email and User.objects.filter(email=email).exclude(pk=instance.pk).exists():
raise serializers.ValidationError({'email': '该邮箱已被使用'})
return super().update(instance, validated_data)
6.2 多字段验证
添加自定义验证逻辑:
python复制class UserSerializer(serializers.ModelSerializer):
# ... 之前的代码 ...
def validate(self, data):
# 确保至少提供姓名或姓氏中的一个
if not any(key in data for key in ['first_name', 'last_name']):
if not self.instance or (not self.instance.first_name and not self.instance.last_name):
raise serializers.ValidationError("必须提供姓氏或名字")
return data
6.3 性能优化
对于频繁访问的接口,可以添加缓存和查询优化:
python复制from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_page
class UserProfileView(generics.RetrieveUpdateAPIView):
# ... 之前的代码 ...
@method_decorator(cache_page(60*5)) # 缓存5分钟
def get(self, request, *args, **kwargs):
return super().get(request, *args, **kwargs)
def get_queryset(self):
return super().get_queryset().select_related('profile')
7. 测试策略
7.1 单元测试
创建tests.py文件:
python复制from django.urls import reverse
from rest_framework.test import APITestCase
from django.contrib.auth import get_user_model
User = get_user_model()
class UserProfileTests(APITestCase):
def setUp(self):
self.user = User.objects.create_user(
username='testuser',
email='test@example.com',
password='testpass123'
)
self.client.force_authenticate(user=self.user)
def test_retrieve_profile(self):
url = reverse('user-profile')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['username'], 'testuser')
def test_update_profile(self):
url = reverse('user-profile')
data = {'first_name': 'Test', 'last_name': 'User'}
response = self.client.patch(url, data)
self.assertEqual(response.status_code, 200)
self.user.refresh_from_db()
self.assertEqual(self.user.first_name, 'Test')
7.2 集成测试
python复制class PasswordChangeTests(APITestCase):
def setUp(self):
self.user = User.objects.create_user(
username='testuser',
email='test@example.com',
password='oldpassword'
)
self.client.force_authenticate(user=self.user)
def test_password_change(self):
url = reverse('password-change')
data = {
'old_password': 'oldpassword',
'new_password': 'newpassword123'
}
response = self.client.post(url, data)
self.assertEqual(response.status_code, 200)
self.assertTrue(User.objects.get(pk=self.user.pk).check_password('newpassword123'))
def test_wrong_old_password(self):
url = reverse('password-change')
data = {
'old_password': 'wrongpassword',
'new_password': 'newpassword123'
}
response = self.client.post(url, data)
self.assertEqual(response.status_code, 400)
8. 安全注意事项
8.1 敏感字段处理
确保敏感字段不会被意外更新:
python复制class UserSerializer(serializers.ModelSerializer):
# ... 之前的代码 ...
def get_fields(self):
fields = super().get_fields()
request = self.context.get('request', None)
if request and request.method == 'GET':
# 返回所有字段用于读取
return fields
else:
# 更新时排除敏感字段
restricted_fields = ['is_staff', 'is_superuser', 'last_login']
for field in restricted_fields:
if field in fields:
del fields[field]
return fields
8.2 速率限制
防止暴力破解密码:
python复制from rest_framework.throttling import UserRateThrottle
class PasswordChangeThrottle(UserRateThrottle):
scope = 'password_change'
rate = '3/hour'
class PasswordChangeView(generics.GenericAPIView):
# ... 之前的代码 ...
throttle_classes = [PasswordChangeThrottle]
8.3 日志记录
记录重要操作:
python复制import logging
logger = logging.getLogger(__name__)
class PasswordChangeView(generics.GenericAPIView):
# ... 之前的代码 ...
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = request.user
user.set_password(serializer.validated_data['new_password'])
user.save()
logger.info(f"用户 {user.username} 修改了密码")
return Response({"detail": "密码已更新"}, status=status.HTTP_200_OK)
9. 前端集成示例
9.1 使用Fetch API更新资料
javascript复制async function updateProfile(data) {
const response = await fetch('/api/profile/', {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCookie('csrftoken'),
},
body: JSON.stringify(data)
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.detail || '更新失败');
}
return await response.json();
}
// 使用示例
updateProfile({
first_name: '张',
last_name: '三',
bio: '全栈开发者'
}).then(data => {
console.log('更新成功', data);
}).catch(error => {
console.error('更新失败:', error.message);
});
9.2 头像上传处理
javascript复制async function uploadAvatar(file) {
const formData = new FormData();
formData.append('avatar', file);
const response = await fetch('/api/profile/avatar/', {
method: 'PUT',
headers: {
'X-CSRFToken': getCookie('csrftoken'),
},
body: formData
});
if (!response.ok) {
throw new Error('头像上传失败');
}
return await response.json();
}
10. 部署注意事项
10.1 静态文件配置
确保头像等媒体文件正确配置:
python复制# settings.py
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
# urls.py
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
# ... 其他路由 ...
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
10.2 生产环境安全设置
python复制# settings.py
# 限制上传文件类型
FILE_UPLOAD_MAX_MEMORY_SIZE = 2 * 1024 * 1024 # 2MB
DATA_UPLOAD_MAX_MEMORY_SIZE = 2 * 1024 * 1024 # 2MB
# 添加安全中间件
MIDDLEWARE = [
...
'django.middleware.security.SecurityMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
# HTTPS设置
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
10.3 性能调优
python复制# 使用更高效的JSON解析器
REST_FRAMEWORK = {
'DEFAULT_PARSER_CLASSES': [
'rest_framework.parsers.JSONParser',
# 其他解析器...
],
'DEFAULT_RENDERER_CLASSES': [
'rest_framework.renderers.JSONRenderer',
# 其他渲染器...
]
}
# 数据库优化
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'mydatabase',
'USER': 'mydatabaseuser',
'PASSWORD': 'mypassword',
'HOST': 'localhost',
'PORT': '5432',
'CONN_MAX_AGE': 60 * 5, # 5分钟连接池
}
}
11. 常见问题解决方案
11.1 跨域问题处理
安装django-cors-headers并配置:
python复制# settings.py
INSTALLED_APPS = [
...
'corsheaders',
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
...
]
CORS_ALLOWED_ORIGINS = [
"https://yourdomain.com",
"http://localhost:8080",
]
11.2 序列化器验证错误
处理验证错误的统一响应:
python复制from rest_framework.views import exception_handler
def custom_exception_handler(exc, context):
response = exception_handler(exc, context)
if response is not None:
customized_response = {}
customized_response['errors'] = []
for key, value in response.data.items():
if isinstance(value, list):
value = value[0]
customized_response['errors'].append(f"{key}: {value}")
response.data = customized_response
return response
# settings.py
REST_FRAMEWORK = {
'EXCEPTION_HANDLER': 'your_app.utils.custom_exception_handler'
}
11.3 性能问题排查
使用Django Debug Toolbar分析查询:
python复制# settings.py
if DEBUG:
INSTALLED_APPS += ['debug_toolbar']
MIDDLEWARE += ['debug_toolbar.middleware.DebugToolbarMiddleware']
INTERNAL_IPS = ['127.0.0.1']
12. 扩展功能思路
12.1 双因素认证集成
python复制# 安装依赖
# pip install django-otp
# settings.py
INSTALLED_APPS = [
...
'django_otp',
'django_otp.plugins.otp_totp',
]
# 视图扩展
from django_otp.plugins.otp_totp.models import TOTPDevice
class TwoFactorSetupView(APIView):
permission_classes = [permissions.IsAuthenticated]
def post(self, request):
device, created = TOTPDevice.objects.get_or_create(user=request.user)
if created:
device.save()
return Response({
'secret': device.config_url,
'qr_code_url': f"otpauth://totp/{request.user.username}?secret={device.key}"
})
12.2 用户活动日志
python复制# models.py
class UserActivity(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
action = models.CharField(max_length=100)
ip_address = models.GenericIPAddressField()
timestamp = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['-timestamp']
# 信号处理
from django.db.models.signals import post_save
from django.dispatch import receiver
@receiver(post_save, sender=User)
def log_user_activity(sender, instance, created, **kwargs):
if created:
action = 'account_created'
else:
action = 'profile_updated'
UserActivity.objects.create(
user=instance,
action=action,
ip_address=get_client_ip()
)
12.3 API文档集成
使用drf-yasg或drf-spectacular生成API文档:
python复制# 安装
# pip install drf-spectacular
# settings.py
INSTALLED_APPS += ['drf_spectacular']
REST_FRAMEWORK = {
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
}
SPECTACULAR_SETTINGS = {
'TITLE': '用户API文档',
'DESCRIPTION': '用户资料管理API文档',
'VERSION': '1.0.0',
}
# urls.py
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
urlpatterns = [
...
path('schema/', SpectacularAPIView.as_view(), name='schema'),
path('docs/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
]
13. 性能监控与优化
13.1 添加性能监控
python复制# 安装
# pip install django-silk
# settings.py
INSTALLED_APPS += ['silk']
MIDDLEWARE += ['silk.middleware.SilkyMiddleware']
# urls.py
urlpatterns += [path('silk/', include('silk.urls', namespace='silk'))]
13.2 查询优化
使用django-querycount监控查询:
python复制# 安装
# pip install django-querycount
# settings.py
MIDDLEWARE += ['querycount.middleware.QueryCountMiddleware']
QUERYCOUNT = {
'THRESHOLDS': {
'MEDIUM': 50,
'HIGH': 200,
'MIN_TIME_TO_LOG': 0,
'MIN_QUERY_COUNT_TO_LOG': 0
},
'IGNORE_REQUEST_PATTERNS': [],
'IGNORE_SQL_PATTERNS': [],
'DISPLAY_DUPLICATES': None,
'RESPONSE_HEADER': 'X-DjangoQueryCount-Count'
}
13.3 缓存策略
使用Redis缓存用户资料:
python复制# settings.py
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://127.0.0.1:6379/1",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
}
}
}
# 视图缓存装饰器
from django.views.decorators.cache import cache_page
from django.utils.decorators import method_decorator
class UserProfileView(generics.RetrieveUpdateAPIView):
# ... 其他代码 ...
@method_decorator(cache_page(60*5, key_prefix='user_profile'))
def get(self, request, *args, **kwargs):
return super().get(request, *args, **kwargs)
14. 国际化支持
14.1 多语言错误消息
python复制# serializers.py
from django.utils.translation import gettext_lazy as _
class PasswordChangeSerializer(serializers.Serializer):
old_password = serializers.CharField(required=True, error_messages={
'required': _('旧密码是必填字段')
})
new_password = serializers.CharField(
required=True,
min_length=8,
error_messages={
'required': _('新密码是必填字段'),
'min_length': _('密码长度至少需要8个字符')
}
)
def validate_old_password(self, value):
user = self.context['request'].user
if not user.check_password(value):
raise serializers.ValidationError(_("旧密码不正确"))
return value
14.2 时区处理
python复制# settings.py
TIME_ZONE = 'Asia/Shanghai'
USE_TZ = True
# 模型字段
from django.utils import timezone
class UserActivity(models.Model):
# ... 其他字段 ...
timestamp = models.DateTimeField(default=timezone.now)
def local_time(self):
return timezone.localtime(self.timestamp)
15. 微服务架构下的用户服务
15.1 JWT认证集成
python复制# 安装
# pip install djangorestframework-simplejwt
# settings.py
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_simplejwt.authentication.JWTAuthentication',
]
}
from datetime import timedelta
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=30),
'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
'ROTATE_REFRESH_TOKENS': True,
'BLACKLIST_AFTER_ROTATION': True,
}
# urls.py
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
)
urlpatterns += [
path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
]
15.2 用户服务API网关
python复制# 使用Django Channels实现WebSocket通知
# pip install channels
# routing.py
from django.urls import re_path
from . import consumers
websocket_urlpatterns = [
re_path(r'ws/profile/updates/$', consumers.ProfileUpdateConsumer.as_asgi()),
]
# consumers.py
import json
from channels.generic.websocket import AsyncWebsocketConsumer
class ProfileUpdateConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.user = self.scope['user']
if self.user.is_anonymous:
await self.close()
else:
self.group_name = f'user_{self.user.id}'
await self.channel_layer.group_add(
self.group_name,
self.channel_name
)
await self.accept()
async def disconnect(self, close_code):
if hasattr(self, 'group_name'):
await self.channel_layer.group_discard(
self.group_name,
self.channel_name
)
async def profile_update(self, event):
await self.send(text_data=json.dumps(event['data']))
16. 自动化测试与CI/CD
16.1 自动化测试配置
python复制# pytest.ini
[pytest]
DJANGO_SETTINGS_MODULE = your_project.settings
python_files = tests.py test_*.py *_tests.py
addopts = --cov=your_app --cov-report=html
16.2 GitHub Actions配置
yaml复制# .github/workflows/django.yml
name: Django CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:12
env:
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.9'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run tests
env:
DATABASE_URL: postgres://postgres:postgres@localhost:5432/postgres
SECRET_KEY: test-secret-key
run: |
python manage.py test
16.3 Docker部署配置
dockerfile复制# Dockerfile
FROM python:3.9-slim
ENV PYTHONUNBUFFERED 1
ENV PYTHONDONTWRITEBYTECODE 1
RUN apt-get update && apt-get install -y \
build-essential \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "your_project.wsgi:application"]
17. 监控与告警
17.1 Sentry错误监控
python复制# 安装
# pip install sentry-sdk
# settings.py
import sentry_sdk
from sentry_sdk.integrations.django import DjangoIntegration
sentry_sdk.init(
dsn="YOUR_DSN_HERE",
integrations=[DjangoIntegration()],
traces_sample_rate=1.0,
send_default_pii=True
)
17.2 性能指标收集
python复制# 安装
# pip install prometheus-client django-prometheus
# settings.py
INSTALLED_APPS += ['django_prometheus']
MIDDLEWARE = ['django_prometheus.middleware.PrometheusBeforeMiddleware'] + \
MIDDLEWARE + \
['django_prometheus.middleware.PrometheusAfterMiddleware']
# urls.py
urlpatterns += [path('metrics/', include('django_prometheus.urls'))]
17.3 自定义健康检查
python复制# views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from django.db import connection
class HealthCheckView(APIView):
def get(self, request):
try:
connection.ensure_connection()
return Response({'status': 'ok'})
except Exception as e:
return Response({'status': 'error', 'detail': str(e)}, status=503)
# urls.py
urlpatterns += [path('health/', HealthCheckView.as_view())]
18. 安全加固措施
18.1 密码策略强化
python复制# settings.py
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
'OPTIONS': {
'min_length': 12,
}
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
18.2 请求限流
python复制# settings.py
REST_FRAMEWORK = {
'DEFAULT_THROTTLE_CLASSES': [
'rest_framework.throttling.AnonRateThrottle',
'rest_framework.throttling.UserRateThrottle'
],
'DEFAULT_THROTTLE_RATES': {
'anon': '100/day',
'user': '1000/day'
}
}
18.3 安全头设置
python复制# settings.py
SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_BROWSER_XSS_FILTER = True
X_FRAME_OPTIONS = 'DENY'
19. 移动端适配
19.1 响应式设计支持
python复制# serializers.py
class MobileUserSerializer(UserSerializer):
avatar = serializers.SerializerMethodField()
class Meta(UserSerializer.Meta):
fields = ['id', 'username', 'avatar', 'first_name', 'last_name']
def get_avatar(self, obj):
request = self.context.get('request')
if obj.avatar:
return {
'original': request.build_absolute_uri(obj.avatar.url),
'thumbnail': request.build_absolute_uri(obj.avatar['thumbnail'].url)
}
return None
# views.py
class MobileUserProfileView(UserProfileView):
def get_serializer_class(self):
if self.request.user_agent.is_mobile:
return MobileUserSerializer
return super().get_serializer_class()
19.2 推送通知集成
python复制# 安装
# pip install pyfcm
# services.py
from pyfcm import FCMNotification
push_service = FCMNotification(api_key="YOUR_API_KEY")
def send_profile_update_notification(user, message):
devices = user.fcmdevice_set.all()
registration_ids = [device.registration_id for device in devices]
if registration_ids:
result = push_service.notify_multiple_devices(
registration_ids=registration_ids,
message_title="资料更新",
message_body=message
)
return result
return None
20. 数据分析与用户行为追踪
20.1 用户行为分析
python复制# models.py
class UserBehavior(models.Model):
ACTION_CHOICES = [
('profile_view', '查看资料'),
('profile_update', '更新资料'),
('password_change', '修改密码'),
]
user = models.ForeignKey(User, on_delete=models.CASCADE)
action = models.CharField(max_length=50, choices=ACTION_CHOICES)
device = models.CharField(max_length=255)
ip_address = models.GenericIPAddressField()
timestamp = models.DateTimeField(auto_now_add=True)
class Meta:
indexes = [
models.Index(fields=['user', 'action']),
models.Index(fields=['timestamp']),
]
# signals.py
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.contrib.auth.signals import user_logged_in
@receiver(post_save, sender=User)
def track_user_behavior(sender, instance, created, **kwargs):
if not created:
request = get_current_request()
if request:
UserBehavior.objects.create(
user=instance,
action='profile_update',
device=request.META.get('HTTP_USER_AGENT', ''),
ip_address=get_client_ip(request)
)
@receiver(user_logged_in)
def track_login(sender, request, user, **kwargs):
UserBehavior.objects.create(
user=user,
action='login',
device=request.META.get('HTTP_USER_AGENT', ''),
ip_address=get_client_ip(request)
)
20.2 数据可视化
python复制# views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from django.db.models import Count
from datetime import datetime, timedelta
class UserActivityStatsView(APIView):
permission_classes = [permissions.IsAdminUser]
def get(self, request):
thirty_days_ago = datetime.now() - timedelta(days=30)
# 按天统计活动
daily_activity = (
UserBehavior.objects
.filter(timestamp__gte=thirty_days_ago)
.extra({'date': "date(timestamp)"})
.values('date')
.annotate(count=Count('id'))
.order_by('date')
)
# 按动作类型统计
action_stats = (
UserBehavior.objects
.filter(timestamp__gte=thirty_days_ago)
.values('action')
.annotate(count=Count('id'))
.order_by('-count')
)
return Response({
'daily_activity': list(daily_activity),
'action_stats': list(action_stats)
})
