from django.contrib.auth import authenticate from django.contrib.auth.models import User from django.db import transaction from django.db.models import Q from django.utils import timezone from django_filters.rest_framework import DjangoFilterBackend from rest_framework import permissions, serializers, status, viewsets from rest_framework.authtoken.models import Token from rest_framework.decorators import action from rest_framework.exceptions import PermissionDenied from rest_framework.response import Response from rest_framework.views import APIView from .models import Member from .phone import normalize_phone from .serializers import ( MemberPublicSerializer, MemberSerializer, StaffUserSerializer, ) def _validation_detail(exc): detail = getattr(exc, "detail", None) if isinstance(detail, (list, tuple)) and detail: return str(detail[0]) if isinstance(detail, dict): for value in detail.values(): if isinstance(value, (list, tuple)) and value: return str(value[0]) return str(value) return str(detail or exc) class IsStaff(permissions.BasePermission): def has_permission(self, request, view): return bool(request.user and request.user.is_authenticated and request.user.is_staff) class LoginView(APIView): """后台登录,返回 Token。""" permission_classes = [permissions.AllowAny] def post(self, request): username = request.data.get("username") or request.data.get("account") password = request.data.get("password") or request.data.get("pwd") user = authenticate(username=username, password=password) if not user or not user.is_staff: return Response( {"detail": "用户名或密码错误,或无后台权限"}, status=status.HTTP_400_BAD_REQUEST, ) token, _ = Token.objects.get_or_create(user=user) return Response( { "token": token.key, "username": user.username, "roles": ["admin"] if user.is_superuser else ["staff"], } ) class MeView(APIView): permission_classes = [permissions.IsAuthenticated] def get(self, request): user = request.user return Response( { "username": user.username, "roles": ["admin"] if user.is_superuser else ["staff"], "is_staff": user.is_staff, } ) class RegisterView(APIView): """仅 staff 可创建其他 staff(替代开放注册)。""" permission_classes = [IsStaff] def post(self, request): serializer = StaffUserSerializer(data=request.data) serializer.is_valid(raise_exception=True) user = serializer.save() token, _ = Token.objects.get_or_create(user=user) return Response( { "token": token.key, "username": user.username, "roles": ["admin"] if user.is_superuser else ["staff"], }, status=status.HTTP_201_CREATED, ) class StaffUserViewSet(viewsets.ModelViewSet): """系统人员:Django User 且 is_staff=True。""" serializer_class = StaffUserSerializer permission_classes = [IsStaff] filter_backends = [DjangoFilterBackend] filterset_fields = ["is_active", "is_superuser"] def get_queryset(self): return User.objects.filter(is_staff=True).order_by("id") def perform_destroy(self, instance): # 禁止删除自己;软禁用更安全,这里改为 is_active=False if instance.pk == self.request.user.pk: raise PermissionDenied("不能删除当前登录账号") instance.is_active = False instance.save(update_fields=["is_active"]) class MemberViewSet(viewsets.ModelViewSet): """前台用户管理。""" queryset = Member.objects.all() serializer_class = MemberSerializer permission_classes = [IsStaff] filter_backends = [DjangoFilterBackend] filterset_fields = ["is_vip", "is_disabled"] def get_queryset(self): qs = super().get_queryset() q = self.request.query_params.get("search") or self.request.query_params.get("q") if q: qs = qs.filter(Q(name__icontains=q) | Q(phone__icontains=q)) return qs @action(detail=False, methods=["post"]) def batch_vip(self, request): ids = request.data.get("ids") or [] is_vip = bool(request.data.get("is_vip")) if not ids: return Response({"detail": "ids 必填"}, status=status.HTTP_400_BAD_REQUEST) updated = Member.objects.filter(id__in=ids).update(is_vip=is_vip) return Response({"updated": updated, "is_vip": is_vip}) def _member_login_response(member): if not member.user_id: return None token, _ = Token.objects.get_or_create(user=member.user) member.last_login_at = timezone.now() member.save(update_fields=["last_login_at", "updated_at"]) return { "token": token.key, "member": MemberPublicSerializer(member).data, } class MemberLoginView(APIView): """前台会员登录(手机号 + 密码)。""" permission_classes = [permissions.AllowAny] def post(self, request): raw_phone = request.data.get("phone") or request.data.get("username") or "" password = request.data.get("password") or request.data.get("pwd") or "" try: phone = normalize_phone(raw_phone, required=True) except serializers.ValidationError as exc: return Response( {"detail": _validation_detail(exc)}, status=status.HTTP_400_BAD_REQUEST, ) if not password: return Response( {"detail": "请输入手机号和密码"}, status=status.HTTP_400_BAD_REQUEST, ) member = Member.objects.filter(phone=phone).select_related("user").first() # 兼容尚未迁移的国内 11 位旧数据 if not member and phone.startswith("+86") and len(phone) == 14: member = ( Member.objects.filter(phone=phone[3:]) .select_related("user") .first() ) if not member or member.is_disabled or not member.user_id: return Response( {"detail": "手机号或密码错误"}, status=status.HTTP_400_BAD_REQUEST, ) user = authenticate(username=member.user.username, password=password) if not user or user.is_staff: return Response( {"detail": "手机号或密码错误"}, status=status.HTTP_400_BAD_REQUEST, ) payload = _member_login_response(member) return Response(payload) class MemberRegisterView(APIView): """前台会员注册。""" permission_classes = [permissions.AllowAny] @transaction.atomic def post(self, request): try: phone = normalize_phone(request.data.get("phone"), required=True) except serializers.ValidationError as exc: return Response( {"detail": _validation_detail(exc)}, status=status.HTTP_400_BAD_REQUEST, ) data = { "name": (request.data.get("name") or "").strip(), "phone": phone, "organize": (request.data.get("organize") or "").strip(), "password": request.data.get("password") or "", "is_vip": False, "is_disabled": False, } if not data["name"]: return Response({"detail": "请输入姓名"}, status=status.HTTP_400_BAD_REQUEST) if len(data["password"]) < 6: return Response( {"detail": "密码至少 6 位"}, status=status.HTTP_400_BAD_REQUEST, ) serializer = MemberSerializer(data=data) serializer.is_valid(raise_exception=True) member = serializer.save() payload = _member_login_response(member) return Response(payload, status=status.HTTP_201_CREATED) class MemberMeView(APIView): """当前前台会员信息。""" permission_classes = [permissions.IsAuthenticated] def get(self, request): user = request.user if user.is_staff: return Response( {"detail": "当前为后台账号,请使用前台会员登录"}, status=status.HTTP_403_FORBIDDEN, ) member = getattr(user, "member_profile", None) if not member or member.is_disabled: return Response( {"detail": "会员不存在或已禁用"}, status=status.HTTP_403_FORBIDDEN, ) return Response(MemberPublicSerializer(member).data)