from adrf.views import APIView from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework import status from asgiref.sync import sync_to_async from drf_yasg.utils import swagger_auto_schema from drf_yasg import openapi from user.qr_auth import generate_qr_token, get_qr_status, scan_qr, confirm_qr, cancel_qr from user.models import FUser from user.serializers.user_serializers import UserSerializer from user.services import create_login_record from utils.response_codes import ResponseCode, create_standardized_response, create_standardized_error_response from rest_framework_simplejwt.tokens import RefreshToken from chunyu_project.common_schemas import success_response, error_response class QRTokenView(APIView): permission_classes = [AllowAny] @swagger_auto_schema( tags=['认证'], operation_summary='获取扫码登录二维码', operation_description='生成唯一的 QR Token,返回 token 供前端生成二维码', responses={200: success_response}, ) async def post(self, request): token = await sync_to_async(generate_qr_token)() return create_standardized_response(data={ "token": token, "expires_in": 300, }) class QRStatusView(APIView): permission_classes = [AllowAny] @swagger_auto_schema( tags=['认证'], operation_summary='查询扫码状态', operation_description='前端轮询此接口获取扫码登录状态', manual_parameters=[ openapi.Parameter('token', openapi.IN_PATH, description='QR Token', type=openapi.TYPE_STRING, required=True), ], responses={200: success_response}, ) async def get(self, request, token): # get_qr_status 走同步 cache,线程池兜底 data = await sync_to_async(get_qr_status)(token) response_data = { "status": data.get("status", "expired"), "username": data.get("scan_username"), } if data.get("status") == "confirmed": scan_user_id = data.get("scan_user_id") user = await FUser.objects.filter(id=scan_user_id).afirst() if user: refresh = await sync_to_async(RefreshToken.for_user)(user) response_data["auth"] = { "user": await sync_to_async(lambda: UserSerializer(user).data)(), "refresh": str(refresh), "access": str(refresh.access_token), "token_type": "bearer", "expires_at_timestamp": refresh.access_token.payload['exp'], } return create_standardized_response(data=response_data) class QRScanView(APIView): permission_classes = [IsAuthenticated] @swagger_auto_schema( tags=['认证'], operation_summary='APP扫码上报', operation_description='APP扫描WEB端二维码后,上报token和用户信息', request_body=openapi.Schema( type=openapi.TYPE_OBJECT, required=['token'], properties={ 'token': openapi.Schema(type=openapi.TYPE_STRING, description='扫描到的QR Token'), }, ), responses={200: success_response, 400: error_response, 401: error_response}, ) async def post(self, request): token = request.data.get("token") if not token: return create_standardized_error_response( code=ResponseCode.PARAMETER_ERROR, message="缺少 token", status_code=status.HTTP_400_BAD_REQUEST, ) user = request.user # scan_qr 走同步 cache,线程池兜底 success = await sync_to_async(scan_qr)(token, user.id, user.username) if not success: return create_standardized_error_response( code=ResponseCode.VALIDATION_ERROR, message="二维码已过期或无效", status_code=status.HTTP_400_BAD_REQUEST, ) return create_standardized_response(message="扫码成功,等待确认") class QRConfirmView(APIView): permission_classes = [IsAuthenticated] @swagger_auto_schema( tags=['认证'], operation_summary='APP确认登录', operation_description='APP端用户确认登录WEB端', request_body=openapi.Schema( type=openapi.TYPE_OBJECT, required=['token'], properties={ 'token': openapi.Schema(type=openapi.TYPE_STRING, description='QR Token'), }, ), responses={200: success_response, 400: error_response, 401: error_response}, ) async def post(self, request): token = request.data.get("token") if not token: return create_standardized_error_response( code=ResponseCode.PARAMETER_ERROR, message="缺少 token", status_code=status.HTTP_400_BAD_REQUEST, ) # confirm_qr 走同步 cache,线程池兜底 data = await sync_to_async(confirm_qr)(token) if not data: return create_standardized_error_response( code=ResponseCode.VALIDATION_ERROR, message="确认失败,请重新扫码", status_code=status.HTTP_400_BAD_REQUEST, ) # create_login_record 内含 ORM 写入,线程池兜底 await sync_to_async(create_login_record)(request, request.user, 'success') return create_standardized_response(message="登录确认成功") class QRCancelView(APIView): permission_classes = [IsAuthenticated] @swagger_auto_schema( tags=['认证'], operation_summary='APP取消登录', operation_description='APP端用户取消登录WEB端', request_body=openapi.Schema( type=openapi.TYPE_OBJECT, properties={ 'token': openapi.Schema(type=openapi.TYPE_STRING, description='QR Token'), }, ), responses={200: success_response, 401: error_response}, ) async def post(self, request): token = request.data.get("token") if token: # cancel_qr 走同步 cache,线程池兜底 await sync_to_async(cancel_qr)(token) return create_standardized_response(message="已取消")