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

