Django多用户登录is_online异常 列表接口仅最后登录用户显示在线
你的在线状态逻辑异常是三个代码错误共同导致的:
- 信号接收器写法完全不规范:你把
user_logged_in、user_logged_out、post_save三个信号的接收器直接定义在Profile模型类内部,既没有加@staticmethod装饰器,也没有按Django最佳实践放到单独的signals模块注册,极易出现参数错位、信号重复注册/失效、实例引用异常的问题。 post_save信号配置错误:信号里的sender=user是小写,引用的是类内部定义的OneToOneField字段对象,而不是User模型类,导致该信号完全没有绑定到正确的模型事件上;同时信号里重复编写了Profile创建逻辑,和你RegisterSerializer里的Profile创建逻辑冲突,会触发唯一键约束报错,且每次User保存就强制调用instance.profile.save()的逻辑会无意义刷新Profile实例,很容易覆盖之前设置的字段值。- 登录信号发送顺序错误:你在
CustomTokenObtainPairSerializer里,还没完成用户活跃状态校验就发送了user_logged_in信号,同时视图层的用户列表查询没有加select_related预加载Profile,会产生N+1查询,还可能读到ORM缓存的旧实例数据。
另外你用的JWT认证是无状态的,默认登出逻辑(前端删除token、token加入黑名单)不会主动触发user_logged_out信号,后续也会出现用户关了页面还是一直显示在线的问题。
1. 重构信号逻辑
首先把所有信号从Profile类里移出来,修正sender配置,去掉冗余逻辑。你可以直接把信号写在models.py的Profile类外面,更规范的做法是新建对应app下的signals.py文件:
from django.contrib.auth.models import User from django.db.models.signals import post_save, user_logged_in, user_logged_out from django.dispatch import receiver from .models import Profile @receiver(user_logged_in) def user_got_online(sender, user, request, **kwargs): # 直接用update方法避免触发多余的save信号 Profile.objects.filter(user=user).update(is_online=True) @receiver(user_logged_out) def user_got_offline(sender, user, request, **kwargs): Profile.objects.filter(user=user).update(is_online=False) @receiver(post_save, sender=User) def create_user_profile(sender, instance, created, **kwargs): # 仅在用户首次创建时生成Profile,不要重复save if created: Profile.objects.create(user=instance)
然后在对应app的apps.py里重写ready方法导入信号,确保信号能被正确注册:
from django.apps import AppConfig class UsersConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'users' # 替换成你的实际app名 def ready(self): import users.signals # 导入信号模块触发注册
记得把settings.py里INSTALLED_APPS的对应app配置改成'users.apps.UsersConfig',不要直接写'users'。
2. 调整登录逻辑的信号发送顺序
修改CustomTokenObtainPairSerializer的validate方法,先完成用户合法性校验,再发送登录信号:
class CustomTokenObtainPairSerializer(TokenObtainPairSerializer): def validate(self, attrs): authenticate_kwargs = { self.username_field: attrs[self.username_field], "password": attrs["password"], } try: authenticate_kwargs["request"] = self.context["request"] except KeyError: pass user = authenticate(**authenticate_kwargs) if not user: raise exceptions.AuthenticationFailed( "Username or password is incorrect" ) # 先做认证规则校验,通过了再更新在线状态 if not api_settings.USER_AUTHENTICATION_RULE(user): raise exceptions.AuthenticationFailed( self.error_messages["no_active_account"], "no_active_account", ) token = RefreshToken.for_user(user) # 自定义token payload token['username'] = user.username token['first_name'] = user.first_name token['last_name'] = user.last_name token['country'] = user.profile.country token['city'] = user.profile.city token['bio'] = user.profile.bio token['photo'] = json.dumps(str(user.profile.profile_pic)) # 所有校验通过后再发送登录信号 user_logged_in.send(sender=user.__class__, request=self.context['request'], user=user) return { 'refresh': str(token), 'access': str(token.access_token), }
注意原来认证失败直接返回字典的写法是错的,必须抛异常才能被simplejwt的认证逻辑正确处理。
3. 优化用户列表查询
修改UserViewSet的queryset,加select_related预加载Profile,避免N+1查询和缓存不一致问题:
class UserViewSet(viewsets.ModelViewSet): # 预加载关联的profile数据,一次查询查出所有用户和对应profile queryset = User.objects.select_related('profile').all().order_by('-date_joined') serializer_class = RegisterSerializer @action(detail=True, methods=['POST']) def set_password(self, request, pk=None): user = self.get_object() serializer = PasswordSerializer(data=request.data) if serializer.is_valid(): user.set_password(serializer.validated_data['new_password']) user.save() return Response({'status': 'password set'}) else: return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
4. 补全登出逻辑
因为JWT是无状态认证,你需要在登出接口(作废refresh token的逻辑)里手动发送登出信号,才能正确把用户is_online设为False:
# 登出接口示例 from django.contrib.auth.signals import user_logged_out class LogoutView(APIView): permission_classes = [IsAuthenticated] def post(self, request): try: refresh_token = request.data["refresh"] token = RefreshToken(refresh_token) token.blacklist() # 作废token后手动发送登出信号 user_logged_out.send(sender=request.user.__class__, request=request, user=request.user) return Response(status=status.HTTP_205_RESET_CONTENT) except Exception as e: return Response(status=status.HTTP_400_BAD_REQUEST)
你现在的在线状态逻辑只处理了主动登录、主动登出的场景,没法处理用户直接关闭浏览器、token过期未主动登出的场景,后续如果要做更精准的在线状态,需要加上心跳检测逻辑,定期更新用户最后活跃时间,把超过一定时间没心跳的用户标记为离线。
内容的提问来源于stack exchange,提问作者Emm

