You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.26 20:36:29