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

Django双类型用户模型实现及基类选型最佳实践咨询

Django多角色用户模型实现最佳实践

你的核心需求是两类用户共享认证体系、分设独立档案、做权限隔离,先直接给选型结论:

一、核心选型:优先选AbstractUser,不要用AbstractBaseUser

两个基类的适用场景边界非常清晰:

  • AbstractUser是Django做好的完整默认用户实现,自带姓名、邮箱、密码、权限标记、组关联、会话管理全套开箱即用的逻辑,你只需要加自己需要的额外字段就行,90%的多角色用户场景都用这个,维护成本极低。
  • AbstractBaseUser只提供最基础的密码哈希、登录认证核心方法,所有字段、权限逻辑、管理器都要从零写,只有当你要完全推翻Django默认用户设计(比如彻底取消用户名、用手机号作为唯一登录凭证、完全不用内置权限体系)的时候才选,你的场景完全没必要承担这个额外开发量。

二、现有代码的问题和优化方案

你设计的User+两个独立OneToOne档案模型的思路完全符合行业通用实践,就是细节上有几个需要修正的点:
用户模型关系示意图

1. 基础User模型修正

你现在继承AbstractBaseUser的写法缺了Django用户体系必备的权限字段、登录标记、管理器配置,直接用会导致admin后台、权限校验全链路报错。直接改成继承AbstractUser即可,它已经自带了你写的first_name/last_name/email字段,不用重复定义:

from django.contrib.auth.models import AbstractUser
from django.db import models

class User(AbstractUser):
    # 用Django3.0+内置的TextChoices替代硬编码元组,避免写错字符串
    class UserType(models.TextChoices):
        EMPLOYEE = 'employee', '员工'
        CUSTOMER = 'customer', '客户'

    phone_number = models.CharField(max_length=20, verbose_name="联系电话")
    user_type = models.CharField(max_length=8, choices=UserType.choices, verbose_name="用户类型")

注意:自定义用户模型必须在项目第一次执行数据库迁移前配置,在settings.py里添加AUTH_USER_MODEL = '你的app名.User',已经生成过迁移表再改用户模型会触发数据库结构冲突。

2. 档案模型修正

你现在的档案模型有两个小问题:一是员工角色枚举里manger是拼写错误,二是__str__方法直接返回关联的User对象会触发类型错误,另外加related_name方便你从User对象反向查询对应的档案,不用记默认的小写类名:

class EmployeeProfile(models.Model):
    class EmployeeRole(models.TextChoices):
        DRIVER = 'driver', '配送司机'
        PRODUCTION = 'production', '生产岗'
        MANAGER = 'manager', '运营管理员'

    user = models.OneToOneField(
        User, 
        on_delete=models.CASCADE, 
        related_name='employee_profile'
    )
    role = models.CharField(max_length=12, choices=EmployeeRole.choices, verbose_name="内部岗位")

    def __str__(self):
        return f"员工:{self.user.get_full_name()} - {self.get_role_display()}"


class CustomerProfile(models.Model):
    user = models.OneToOneField(
        User, 
        on_delete=models.CASCADE, 
        related_name='customer_profile'
    )
    company = models.CharField(max_length=100, verbose_name="所属企业")
    address = models.CharField(max_length=100, verbose_name="配送地址")
    address_2 = models.CharField(max_length=100, blank=True, default='', verbose_name="地址补充")
    city = models.CharField(max_length=50, verbose_name="城市")
    state = models.CharField(max_length=2, help_text="州缩写,例:OH", verbose_name="州")
    zipcode = models.CharField(max_length=5, verbose_name="邮编")

    def __str__(self):
        return f"客户:{self.user.get_full_name()} - {self.company}"

这里把address_2加了blank=True,毕竟不是所有客户都有第二个配送地址,不用强制必填。

3. 自动创建档案的实现

你提到的Django信号是可行的,用post_save做兜底即可,不用写太复杂的逻辑:

from django.db.models.signals import post_save
from django.dispatch import receiver

@receiver(post_save, sender=User)
def create_matching_profile(sender, instance, created, **kwargs):
    # 只有用户第一次创建的时候才生成对应档案,后续更新不触发
    if created:
        if instance.user_type == User.UserType.EMPLOYEE:
            EmployeeProfile.objects.create(user=instance)
        elif instance.user_type == User.UserType.CUSTOMER:
            CustomerProfile.objects.create(user=instance)

如果要做的更严谨,可以自定义UserManager,在create_user方法里强制要求传入user_type参数,从源头避免创建出没有类型的脏用户数据。

4. 视图权限控制

不要在业务代码里到处硬写if user.user_type == 'xxx'做判断,直接复用Django自带的组+权限体系:

  • 给3种员工角色、客户分别创建对应的权限组,提前配置好每个组能访问的接口/视图权限
  • 用户创建后自动加到对应组里,视图层直接用内置的@login_required、@permission_required装饰器或者DRF权限类做校验,后续加角色、改权限不用改业务代码。

三、什么场景才需要换AbstractBaseUser?

如果后续你的需求出现以下变化,再考虑切换到AbstractBaseUser:

  • 完全取消用户名字段,用手机号/邮箱作为唯一登录凭证
  • 90%的Django默认用户字段你都用不上,需要做极致精简
  • 完全不用Django自带的admin后台、权限体系,要自己从零实现
    除此之外,AbstractUser完全能满足你的配送系统需求,也是目前行业内同类项目的通用选型。

内容的提问来源于stack exchange,提问作者Thast16

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.03 10:36:39