📢 阅读提示

感谢你阅读本章。本文所有代码和注释均来自真实项目,为了能让更多认真学习的读者持续获得优质内容,本文将在7天后转为“粉丝可见”。
如果你觉得本文有价值,欢迎收藏并关注我,后续的模型设计、权限函数、导入导出以及层级自动计算的实现等章节将继续保持同样详细的注释风格。
感谢理解与支持!

一、业务场景

在我们这套保卫部内控系统中,用户单位是一家大型国企,其组织架构是标准的四级管理体系:

层级 名称 示例
1 一级公司级 XX有限公司
2 二级单位 保卫部、人事部、XX分公司
3 三级大队 办公室、武装部、技术研发中心
4 四级小队 740818队、生产服务队

系统需要完整还原这个树形结构,并支持部门增删改查、实现数据隔离(上级看下级)、做到权限提升(主管部门看全公司)等业务。

二、业务要求

在这里插入图片描述

  1. 部门层级最多支持4级(符合国企实际,且可配置限制)。
  2. 每个部门必须知道它的上级部门(根部门无上级)。
  3. 用户只选择上级,系统自动计算当前部门属于第几级,不允许手动填写层级。
  4. 能快速获取某个部门及其所有下级部门的ID列表,用于权限过滤(例如:保卫部主任能看到所有下级部门在本项目中上报的所有信息)。
  5. 不能出现循环引用(A的上级是B,B的上级是A)。
  6. 同一父级下不能有两个同名部门(已删除的除外)。
  7. 部门撤并不物理删除,只标记删除(软删除),并记录删除人、删除时间、删除原因(本章只介绍字段,详细逻辑见后续章节)。

三、模型设计(代码 + 逐行注释)

# organization/models.py
from django.db import models
from django.core.exceptions import ValidationError
from django.contrib.auth.models import User

class Department(models.Model):
    """
    部门模型:用自关联外键实现树形结构,用 level 字段记录层级深度。
    
    设计目标:
      1. 用户只选父部门,系统自动算出当前是第几级,避免人工填错。
      2. 能够快速查询某个部门下的所有子孙部门(用于权限判断)。
      3. 防止循环引用(例如 A 父部门是 B,B 父部门是 A)。
    """
    
    # ------------------------------------------------------------
    # 1. 基本字段
    # ------------------------------------------------------------
    name = models.CharField(
        max_length=100,
        verbose_name="部门名称"
    )
    # 部门名称,例如“治安科”“保卫部”。必填,最长100字符。
    
    code = models.CharField(
        max_length=20,
        unique=True,
        verbose_name="部门代码"
    )
    # 部门代码,例如“ZAK”“BWB”。唯一,用于程序识别(方便导出、接口对接)。
    
    description = models.TextField(
        blank=True,
        verbose_name="部门描述"
    )
    # 部门描述,可选。例如“负责治安巡逻、案件处理”。
    
    # ------------------------------------------------------------
    # 2. 树形结构字段(核心)
    # ------------------------------------------------------------
    parent = models.ForeignKey(
        'self',                     # 指向自己这个模型,表示上级部门也是一个部门对象
        on_delete=models.CASCADE,   # 如果父部门被物理删除,子部门也级联删除(配合软删除使用,实际影响不大)
        null=True,                  # 根部门没有上级,所以允许为空
        blank=True,                 # 表单中可以不填(根部门)
        verbose_name="上级部门",
        related_name='children'     # 反向访问名:通过 department.children 可以获取所有直接下级
    )
    # 解释:
    #   - 每个部门指向它的父部门,形成单向链表。
    #   - 根部门(公司级)的 parent = None。
    #   - related_name='children' 是方便写法:parent_department.children.all() 拿到所有子部门。
    #   - on_delete=CASCADE 物理删除时级联,但我们实际只用软删除,所以这个设置影响不大。
    
    # ------------------------------------------------------------
    # 3. 层级字段(性能优化)
    # ------------------------------------------------------------
    level = models.IntegerField(
        default=1,
        editable=False,              # 不在Admin表单中显示,由系统自动计算,不允许用户修改
        verbose_name="部门层级"
    )
    # 为什么需要 level?
    #   如果只有 parent 外键,要查询某个部门下的所有子孙部门,必须递归查询数据库多次。
    #   而有了 level 字段,我们可以先用 level > 当前level 缩小范围,再配合 parent 链精确过滤,
    #   大幅减少数据库查询次数。虽然本例递归深度小,但这个字段为未来优化留了空间。
    # 层级规则:
    #   - 根部门(parent=None) → level = 1
    #   - 子部门 → level = 父部门.level + 1
    #   - 我们限制最大 level = 4(符合国企四级管理)。
    
    # ------------------------------------------------------------
    # 4. 业务标记字段
    # ------------------------------------------------------------
    is_active = models.BooleanField(
        default=True,
        verbose_name="是否激活"
    )
    # 控制部门是否启用(临时停用)。停用后,在业务数据中不能再选这个部门。
    
    is_main_department = models.BooleanField(
        default=False,
        verbose_name="是否为主管部门",
        help_text="标记该部门是否为主管部门(如保卫部),权限可提升到父级"
    )
    # 这是本项目权限设计的一个特点:
    #   - 普通部门:用户只能看本部门及下级数据。
    #   - 主管部门(如保卫部):用户权限提升到父级(公司级),可以看到所有子单位数据。
    #   - 我们在后续权限函数 get_viewable_departments 中会用到这个标记。
    
    # ------------------------------------------------------------
    # 5. 软删除字段(本章只定义,详细逻辑在第六章《软删除和审计字段》中阐述)
    # ------------------------------------------------------------
    is_deleted = models.BooleanField(
        default=False,
        verbose_name="是否已删除"
    )
    # 软删除标记。True 表示已删除(逻辑删除),False 表示正常。
    # 业务代码中默认查询只取 is_deleted=False 的记录。
    
    deleted_at = models.DateTimeField(
        null=True,
        blank=True,
        verbose_name="删除时间"
    )
    # 记录删除的时间,用于审计和定期清理。
    
    deleted_by = models.ForeignKey(
        User,
        on_delete=models.SET_NULL,   # 如果删除操作者的账号被删了,这里保留 NULL,不级联删除记录
        null=True,
        blank=True,
        related_name='deleted_departments',
        verbose_name="删除操作者"
    )
    # 谁删的?记录 user id,便于追溯责任。
    
    delete_reason = models.TextField(
        blank=True,
        verbose_name="删除原因"
    )
    # 为什么删?业务要求删除时填写原因(例如“部门撤销”),便于审计。
    
    # ------------------------------------------------------------
    # 6. 审计字段(自动记录创建和更新时间)
    # ------------------------------------------------------------
    created_at = models.DateTimeField(
        auto_now_add=True,
        verbose_name="创建时间"
    )
    # 第一次保存时自动设为当前时间,之后不再改变。
    
    updated_at = models.DateTimeField(
        auto_now=True,
        verbose_name="更新时间"
    )
    # 每次调用 save() 都会自动更新为当前时间。
    
    # ------------------------------------------------------------
    # 7. 管理器(默认管理器,后续会换成自定义的 SoftDeleteManager)
    # ------------------------------------------------------------
    objects = models.Manager()
    # 默认管理器。在第六章《软删除和审计字段》我们会用 SoftDeleteManager 覆盖,让其自动过滤 is_deleted=True。
    
    # ------------------------------------------------------------
    # 8. 元数据(排序、约束)
    # ------------------------------------------------------------
    class Meta:
        ordering = ['level', 'code']
        # 列表页默认排序:先按层级升序(一级在前),再按代码升序。
        
        verbose_name = "部门"
        verbose_name_plural = "部门"
        
        constraints = [
            # 约束1:同一父级下,未删除的部门名称必须唯一
            models.UniqueConstraint(
                fields=['parent', 'name'],
                condition=models.Q(is_deleted=False),
                name='unique_name_per_parent'
            ),
            # 约束2:同一父级下,只能有一个主管部门(is_main_department=True)
            models.UniqueConstraint(
                fields=['parent'],
                condition=models.Q(is_main_department=True, is_deleted=False),
                name='unique_main_department_per_parent'
            ),
        ]
        # 这两个约束会在数据库层面强制保证,比在代码里用 filter 检查更可靠。

四、核心方法详解

  1. save() 方法:自动计算层级 + 限制深度
def save(self, *args, **kwargs):
    """
    重写 save 方法,实现:
      1. 根据 parent 自动计算 level。
      2. 限制最大层级(本例为4级)。
    """
    # ---------- 步骤1:计算当前部门层级 ----------
    if self.parent:
        # 如果有父部门,当前层级 = 父部门层级 + 1
        # 注意:父部门可能已被软删除,此处是否需要检查?可根据业务决定。
        # 我们这里加一个友好提示,避免用户选已删除的部门作为上级。
        if self.parent.is_deleted:
            raise ValidationError("上级部门已被删除,不能关联")
        self.level = self.parent.level + 1
    else:
        # 没有父部门,说明是根部门(公司级),层级为1
        self.level = 1
    
    # ---------- 步骤2:限制最大层级 ----------
    MAX_LEVEL = 4   # 业务上最多4级,可根据实际情况调整
    if self.level > MAX_LEVEL:
        raise ValidationError(f"部门层级不能超过{MAX_LEVEL}级")
    
    # ---------- 步骤3:调用父类的 save 方法,真正写入数据库 ----------
    super().save(*args, **kwargs)
    
    # 注意:如果业务允许修改 parent(移动部门),上面的代码只更新了当前部门的 level,
    # 但其子孙部门的 level 不会自动更新。解决方案:
    #   - 在 save 中检测 self.pk 是否存在(更新操作)且 parent 字段值发生了变化,
    #     然后递归更新所有下级的 level。但因为递归更新较复杂且部门移动极少发生,
    #     我们暂不实现,在项目使用文档中说明这个限制。在实际操作中,硬删除权限在配置时不对外开放。有兴趣的朋友也可在此基础上迭代完善。
  1. clean() 方法:防止循环引用
def clean(self):
    """
    模型级验证,在调用 full_clean() 时自动执行。
    主要目的:防止循环引用(例如 A 的上级是 B,B 的上级是 A)。
    """
    if not self.parent:
        # 没有上级,无需检查
        return
    
    # 验证1:不能指向自己
    if self.parent == self:
        raise ValidationError({"parent": "部门不能作为自己的上级"})
    
    # 验证2:向上查找祖先,如果遇到自己,说明形成了循环
    ancestor = self.parent
    while ancestor:
        if ancestor == self:
            raise ValidationError({"parent": "检测到循环引用,请重新选择上级"})
        ancestor = ancestor.parent
    
    # 注意:clean() 不会自动在 save() 中调用。
    # 如果通过 Django Admin 保存,Admin 会自动调用 full_clean() 进而触发 clean()。
    # 如果直接调用 model.save(),需要在 save() 里手动调用 self.full_clean()。
  1. get_descendant_ids() 方法:获取所有下级ID
def get_descendant_ids(self, include_self=True, include_deleted=False):
    """
    递归获取当前部门及其所有下级部门的 ID 列表。
    
    参数:
      include_self: 是否包含当前部门自身的 ID(默认 True)
      include_deleted: 是否包含已软删除的部门(默认 False,正常业务不包含)
    
    返回:
      一个列表,如 [10, 11, 12, ...]
    
    用途:
      在权限判断时,可以用这些 ID 去过滤数据:
        viewable_dept_ids = user_dept.get_descendant_ids(include_self=True)
        Employee.objects.filter(department_id__in=viewable_dept_ids)
    """
    ids = []
    
    # 1. 如果需要包含自身,先把当前部门 ID 加入列表
    if include_self:
        ids.append(self.id)
    
    # 2. 获取直接子部门(根据是否包含已删除决定过滤)
    if include_deleted:
        children = self.children.all()          # children 是 related_name 定义的反向关系
    else:
        children = self.children.filter(is_deleted=False)
    
    # 3. 递归遍历每个子部门,把它们的子孙 ID 也加入列表
    for child in children:
        ids.extend(child.get_descendant_ids(include_self=True, include_deleted=include_deleted))
    
    return ids

    # 性能说明:因为层级最多4级,递归深度很小,每次递归会查询一次数据库。
    # 如果部门数量非常大(几千个),可以考虑用 level 字段 + 前缀树方式优化,
    # 但本例够用,保持代码简单。

五、注意事项(踩坑经验)

  1. 层级上限:虽然硬编码 MAX_LEVEL = 4,但如果实际业务调整了组织架构,该值也是可以调整的。
  2. 移动部门:本例没有实现移动部门后自动更新子孙层级的逻辑。如果业务需要,可以在 save() 中检测 parent 字段是否变化,然后递归更新子孙的 level。但因为递归更新可能涉及大量数据且部门移动极低频,我们暂不实现,留作扩展。
  3. 软删除部门不可作为上级:在 save() 中已经检查了 parent.is_deleted,会抛出异常。更优雅的方式是在 formfield_for_foreignkey 中直接过滤掉已删除的部门,让用户根本选不到(本项目考虑在后续实现这个功能迭代)。
  4. 循环引用检测:递归向上查找时,如果树很深可能会效率低,但我们限制4级,完全可接受。如果未来扩展为多级,可以考虑在 clean() 中使用集合记录已访问节点。
  5. 约束的 condition 参数:需要考虑Django版本和数据库版本的支持差异。

六、小结

本章我们用详细的代码注释,逐行解释了部门模型的每一部分:

· 自关联 parent 形成树形结构。
· level 字段自动计算层级,避免递归查询性能问题。
· clean() 防止循环引用。
· get_descendant_ids() 快速获取所有下级,为权限过滤提供数据基础。

代码注释详细是本系列博客文章的最大特色,后续每一章都会保持这个标准:每行代码都有解释,每个方法都争取讲清楚思路和用途。

下一章我们将在本章基础上,继续探讨软删除与审计字段 的完整实现,让数据可恢复、操作可追溯。

互动问题:你在实际项目中,有没有因为循环引用导致过递归死循环?或者因为没加 level 字段而导致查询性能很差?欢迎评论区分享你的故事。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐