Initial commit: React + Django 城市手册项目
- Django 4.2 + DRF + JWT + GraphQL - React 18 + MobX + styled-components - PostgreSQL 数据库 - Docker + Docker Compose + Nginx - 完整的功能模块(用户、版块、文章、服务、交互、版主管理) - 完整的文档(需求、部署、测试)
This commit is contained in:
1
backend/apps/users/__init__.py
Normal file
1
backend/apps/users/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Users app
|
||||
6
backend/apps/users/apps.py
Normal file
6
backend/apps/users/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class UsersConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'apps.users'
|
||||
45
backend/apps/users/models.py
Normal file
45
backend/apps/users/models.py
Normal file
@@ -0,0 +1,45 @@
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.db import models
|
||||
|
||||
|
||||
class User(AbstractUser):
|
||||
"""Custom user model extending AbstractUser."""
|
||||
|
||||
ROLE_CHOICES = [
|
||||
('user', '普通用户'),
|
||||
('moderator', '版主'),
|
||||
('ai_auditor', 'AI审核员'),
|
||||
('admin', '管理员'),
|
||||
]
|
||||
|
||||
STATUS_CHOICES = [
|
||||
('active', '正常'),
|
||||
('disabled', '禁用'),
|
||||
]
|
||||
|
||||
email = models.EmailField(unique=True)
|
||||
first_name = models.CharField(max_length=150)
|
||||
last_name = models.CharField(max_length=150)
|
||||
avatar = models.ImageField(upload_to='avatars/', null=True, blank=True)
|
||||
role = models.CharField(max_length=20, choices=ROLE_CHOICES, default='user')
|
||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='active')
|
||||
|
||||
USERNAME_FIELD = 'email'
|
||||
REQUIRED_FIELDS = ['username', 'first_name']
|
||||
|
||||
class Meta:
|
||||
db_table = 'users'
|
||||
verbose_name = 'User'
|
||||
verbose_name_plural = 'Users'
|
||||
|
||||
def __str__(self):
|
||||
return self.email
|
||||
|
||||
def is_moderator(self):
|
||||
return self.role == 'moderator'
|
||||
|
||||
def is_admin(self):
|
||||
return self.role == 'admin'
|
||||
|
||||
def is_ai_auditor(self):
|
||||
return self.role == 'ai_auditor'
|
||||
38
backend/apps/users/serializers.py
Normal file
38
backend/apps/users/serializers.py
Normal file
@@ -0,0 +1,38 @@
|
||||
from rest_framework import serializers
|
||||
from .models import User
|
||||
|
||||
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for User model (basic info)."""
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ('id', 'username', 'email', 'first_name', 'last_name', 'avatar', 'role', 'status')
|
||||
read_only_fields = ('id', 'role', 'status')
|
||||
|
||||
|
||||
class UserDetailSerializer(serializers.ModelSerializer):
|
||||
"""Detailed serializer for User model."""
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = '__all__'
|
||||
read_only_fields = ('id', 'date_joined', 'last_login')
|
||||
|
||||
|
||||
class UserUpdateSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for updating user profile."""
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ('first_name', 'last_name', 'avatar')
|
||||
|
||||
|
||||
class UserStatsSerializer(serializers.Serializer):
|
||||
"""Serializer for user statistics."""
|
||||
articles_count = serializers.IntegerField()
|
||||
services_count = serializers.IntegerField()
|
||||
comments_count = serializers.IntegerField()
|
||||
likes_count = serializers.IntegerField()
|
||||
favorites_count = serializers.IntegerField()
|
||||
ratings_count = serializers.IntegerField()
|
||||
10
backend/apps/users/urls.py
Normal file
10
backend/apps/users/urls.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from .views import UserViewSet
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r'users', UserViewSet, basename='user')
|
||||
|
||||
urlpatterns = [
|
||||
path('', include(router.urls)),
|
||||
]
|
||||
153
backend/apps/users/views.py
Normal file
153
backend/apps/users/views.py
Normal file
@@ -0,0 +1,153 @@
|
||||
from rest_framework import viewsets, permissions, status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
from django.db.models import Count, Q
|
||||
from .models import User
|
||||
from .serializers import (
|
||||
UserSerializer,
|
||||
UserDetailSerializer,
|
||||
UserUpdateSerializer,
|
||||
UserStatsSerializer
|
||||
)
|
||||
|
||||
|
||||
class UserViewSet(viewsets.ModelViewSet):
|
||||
"""ViewSet for User model."""
|
||||
|
||||
queryset = User.objects.all()
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action == 'retrieve' and self.kwargs.get('pk') == 'me':
|
||||
return UserDetailSerializer
|
||||
elif self.action in ['update', 'partial_update'] and self.kwargs.get('pk') == 'me':
|
||||
return UserUpdateSerializer
|
||||
return UserSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
# Only admins can see all users
|
||||
if self.request.user.is_admin():
|
||||
return User.objects.all()
|
||||
# Regular users can only see themselves
|
||||
return User.objects.filter(id=self.request.user.id)
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
"""Only admins can list all users."""
|
||||
if not request.user.is_admin():
|
||||
return Response(
|
||||
{'detail': 'You do not have permission to perform this action.'},
|
||||
status=status.HTTP_403_FORBIDDEN
|
||||
)
|
||||
return super().list(request, *args, **kwargs)
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
"""Get user details (me for current user)."""
|
||||
if kwargs.get('pk') == 'me':
|
||||
self.kwargs['pk'] = request.user.id
|
||||
return super().retrieve(request, *args, **kwargs)
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
"""Update user details (only me for regular users)."""
|
||||
if kwargs.get('pk') == 'me':
|
||||
self.kwargs['pk'] = request.user.id
|
||||
elif not request.user.is_admin() and str(kwargs.get('pk')) != str(request.user.id):
|
||||
return Response(
|
||||
{'detail': 'You can only update your own profile.'},
|
||||
status=status.HTTP_403_FORBIDDEN
|
||||
)
|
||||
return super().update(request, *args, **kwargs)
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def me(self, request):
|
||||
"""Get current user details."""
|
||||
serializer = self.get_serializer(request.user)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=True, methods=['get'])
|
||||
def stats(self, request, pk=None):
|
||||
"""Get user statistics."""
|
||||
if pk == 'me':
|
||||
pk = request.user.id
|
||||
user = self.get_object()
|
||||
|
||||
if str(user.id) != str(request.user.id) and not request.user.is_admin():
|
||||
return Response(
|
||||
{'detail': 'You do not have permission to view this user\'s stats.'},
|
||||
status=status.HTTP_403_FORBIDDEN
|
||||
)
|
||||
|
||||
from apps.articles.models import Article
|
||||
from apps.featured_services.models import FeaturedService
|
||||
from apps.interactions.models import Comment, Like, Favorite, Rating
|
||||
|
||||
return Response({
|
||||
'articles_count': Article.objects.filter(author=user).count(),
|
||||
'services_count': FeaturedService.objects.filter(submitter=user).count(),
|
||||
'comments_count': Comment.objects.filter(author=user).count(),
|
||||
'likes_count': Like.objects.filter(user=user).count(),
|
||||
'favorites_count': Favorite.objects.filter(user=user).count(),
|
||||
'ratings_count': Rating.objects.filter(user=user).count(),
|
||||
})
|
||||
|
||||
@action(detail=True, methods=['get'])
|
||||
def favorites(self, request, pk=None):
|
||||
"""Get user's favorites."""
|
||||
if pk == 'me':
|
||||
pk = request.user.id
|
||||
user = self.get_object()
|
||||
|
||||
if str(user.id) != str(request.user.id):
|
||||
return Response(
|
||||
{'detail': 'You can only view your own favorites.'},
|
||||
status=status.HTTP_403_FORBIDDEN
|
||||
)
|
||||
|
||||
from apps.interactions.serializers import FavoriteSerializer
|
||||
favorites = Favorite.objects.filter(user=user).select_related()
|
||||
serializer = FavoriteSerializer(favorites, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=True, methods=['get'])
|
||||
def ratings(self, request, pk=None):
|
||||
"""Get user's ratings."""
|
||||
if pk == 'me':
|
||||
pk = request.user.id
|
||||
user = self.get_object()
|
||||
|
||||
if str(user.id) != str(request.user.id):
|
||||
return Response(
|
||||
{'detail': 'You can only view your own ratings.'},
|
||||
status=status.HTTP_403_FORBIDDEN
|
||||
)
|
||||
|
||||
from apps.interactions.serializers import RatingSerializer
|
||||
ratings = Rating.objects.filter(user=user).select_related()
|
||||
serializer = RatingSerializer(ratings, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def search(self, request):
|
||||
"""Search users (admin only)."""
|
||||
if not request.user.is_admin():
|
||||
return Response(
|
||||
{'detail': 'Only admins can search users.'},
|
||||
status=status.HTTP_403_FORBIDDEN
|
||||
)
|
||||
|
||||
query = request.query_params.get('q', '')
|
||||
if query:
|
||||
users = User.objects.filter(
|
||||
Q(username__icontains=query) |
|
||||
Q(email__icontains=query) |
|
||||
Q(first_name__icontains=query)
|
||||
)
|
||||
else:
|
||||
users = User.objects.all()
|
||||
|
||||
page = self.paginate_queryset(users)
|
||||
if page is not None:
|
||||
serializer = self.get_serializer(page, many=True)
|
||||
return self.get_paginated_response(serializer.data)
|
||||
|
||||
serializer = self.get_serializer(users, many=True)
|
||||
return Response(serializer.data)
|
||||
Reference in New Issue
Block a user