〔五〕企业内保系统组织机构模型实战:好的组织机构树是会“自已成长”的——自关联与层级自动计算,告别递归噩梦

〔五〕企业内保系统组织机构模型实战:好的组织机构树是会“自已成长”的——自关联与层级自动计算,告别递归噩梦 阅读提示感谢你阅读本章。本文所有代码和注释均来自真实项目为了能让更多认真学习的读者持续获得优质内容本文将在7天后转为“粉丝可见”。如果你觉得本文有价值欢迎收藏并关注我后续的模型设计、权限函数、导入导出以及层级自动计算的实现等章节将继续保持同样详细的注释风格。感谢理解与支持一、业务场景在我们这套保卫部内控系统中用户单位是一家大型国企其组织架构是标准的四级管理体系层级名称示例1一级公司级XX有限公司2二级单位保卫部、人事部、XX分公司3三级大队办公室、武装部、技术研发中心4四级小队740818队、生产服务队系统需要完整还原这个树形结构并支持部门增删改查、实现数据隔离上级看下级、做到权限提升主管部门看全公司等业务。二、业务要求部门层级最多支持4级符合国企实际且可配置限制。每个部门必须知道它的上级部门根部门无上级。用户只选择上级系统自动计算当前部门属于第几级不允许手动填写层级。能快速获取某个部门及其所有下级部门的ID列表用于权限过滤例如保卫部主任能看到所有下级部门在本项目中上报的所有信息。不能出现循环引用A的上级是BB的上级是A。同一父级下不能有两个同名部门已删除的除外。部门撤并不物理删除只标记删除软删除并记录删除人、删除时间、删除原因本章只介绍字段详细逻辑见后续章节。三、模型设计代码 逐行注释# organization/models.pyfromdjango.dbimportmodelsfromdjango.core.exceptionsimportValidationErrorfromdjango.contrib.auth.modelsimportUserclassDepartment(models.Model): 部门模型用自关联外键实现树形结构用 level 字段记录层级深度。 设计目标 1. 用户只选父部门系统自动算出当前是第几级避免人工填错。 2. 能够快速查询某个部门下的所有子孙部门用于权限判断。 3. 防止循环引用例如 A 父部门是 BB 父部门是 A。 # ------------------------------------------------------------# 1. 基本字段# ------------------------------------------------------------namemodels.CharField(max_length100,verbose_name部门名称)# 部门名称例如“治安科”“保卫部”。必填最长100字符。codemodels.CharField(max_length20,uniqueTrue,verbose_name部门代码)# 部门代码例如“ZAK”“BWB”。唯一用于程序识别方便导出、接口对接。descriptionmodels.TextField(blankTrue,verbose_name部门描述)# 部门描述可选。例如“负责治安巡逻、案件处理”。# ------------------------------------------------------------# 2. 树形结构字段核心# ------------------------------------------------------------parentmodels.ForeignKey(self,# 指向自己这个模型表示上级部门也是一个部门对象on_deletemodels.CASCADE,# 如果父部门被物理删除子部门也级联删除配合软删除使用实际影响不大nullTrue,# 根部门没有上级所以允许为空blankTrue,# 表单中可以不填根部门verbose_name上级部门,related_namechildren# 反向访问名通过 department.children 可以获取所有直接下级)# 解释# - 每个部门指向它的父部门形成单向链表。# - 根部门公司级的 parent None。# - related_namechildren 是方便写法parent_department.children.all() 拿到所有子部门。# - on_deleteCASCADE 物理删除时级联但我们实际只用软删除所以这个设置影响不大。# ------------------------------------------------------------# 3. 层级字段性能优化# ------------------------------------------------------------levelmodels.IntegerField(default1,editableFalse,# 不在Admin表单中显示由系统自动计算不允许用户修改verbose_name部门层级)# 为什么需要 level# 如果只有 parent 外键要查询某个部门下的所有子孙部门必须递归查询数据库多次。# 而有了 level 字段我们可以先用 level 当前level 缩小范围再配合 parent 链精确过滤# 大幅减少数据库查询次数。虽然本例递归深度小但这个字段为未来优化留了空间。# 层级规则# - 根部门parentNone → level 1# - 子部门 → level 父部门.level 1# - 我们限制最大 level 4符合国企四级管理。# ------------------------------------------------------------# 4. 业务标记字段# ------------------------------------------------------------is_activemodels.BooleanField(defaultTrue,verbose_name是否激活)# 控制部门是否启用临时停用。停用后在业务数据中不能再选这个部门。is_main_departmentmodels.BooleanField(defaultFalse,verbose_name是否为主管部门,help_text标记该部门是否为主管部门如保卫部权限可提升到父级)# 这是本项目权限设计的一个特点# - 普通部门用户只能看本部门及下级数据。# - 主管部门如保卫部用户权限提升到父级公司级可以看到所有子单位数据。# - 我们在后续权限函数 get_viewable_departments 中会用到这个标记。# ------------------------------------------------------------# 5. 软删除字段本章只定义详细逻辑在第六章《软删除和审计字段》中阐述# ------------------------------------------------------------is_deletedmodels.BooleanField(defaultFalse,verbose_name是否已删除)# 软删除标记。True 表示已删除逻辑删除False 表示正常。# 业务代码中默认查询只取 is_deletedFalse 的记录。deleted_atmodels.DateTimeField(nullTrue,blankTrue,verbose_name删除时间)# 记录删除的时间用于审计和定期清理。deleted_bymodels.ForeignKey(User,on_deletemodels.SET_NULL,# 如果删除操作者的账号被删了这里保留 NULL不级联删除记录nullTrue,blankTrue,related_namedeleted_departments,verbose_name删除操作者)# 谁删的记录 user id便于追溯责任。delete_reasonmodels.TextField(blankTrue,verbose_name删除原因)# 为什么删业务要求删除时填写原因例如“部门撤销”便于审计。# ------------------------------------------------------------# 6. 审计字段自动记录创建和更新时间# ------------------------------------------------------------created_atmodels.DateTimeField(auto_now_addTrue,verbose_name创建时间)# 第一次保存时自动设为当前时间之后不再改变。updated_atmodels.DateTimeField(auto_nowTrue,verbose_name更新时间)# 每次调用 save() 都会自动更新为当前时间。# ------------------------------------------------------------# 7. 管理器默认管理器后续会换成自定义的 SoftDeleteManager# ------------------------------------------------------------objectsmodels.Manager()# 默认管理器。在第六章《软删除和审计字段》我们会用 SoftDeleteManager 覆盖让其自动过滤 is_deletedTrue。# ------------------------------------------------------------# 8. 元数据排序、约束# ------------------------------------------------------------classMeta:ordering[level,code]# 列表页默认排序先按层级升序一级在前再按代码升序。verbose_name部门verbose_name_plural部门constraints[# 约束1同一父级下未删除的部门名称必须唯一models.UniqueConstraint(fields[parent,name],conditionmodels.Q(is_deletedFalse),nameunique_name_per_parent),# 约束2同一父级下只能有一个主管部门is_main_departmentTruemodels.UniqueConstraint(fields[parent],conditionmodels.Q(is_main_departmentTrue,is_deletedFalse),nameunique_main_department_per_parent),]# 这两个约束会在数据库层面强制保证比在代码里用 filter 检查更可靠。四、核心方法详解save() 方法自动计算层级 限制深度defsave(self,*args,**kwargs): 重写 save 方法实现 1. 根据 parent 自动计算 level。 2. 限制最大层级本例为4级。 # ---------- 步骤1计算当前部门层级 ----------ifself.parent:# 如果有父部门当前层级 父部门层级 1# 注意父部门可能已被软删除此处是否需要检查可根据业务决定。# 我们这里加一个友好提示避免用户选已删除的部门作为上级。ifself.parent.is_deleted:raiseValidationError(上级部门已被删除不能关联)self.levelself.parent.level1else:# 没有父部门说明是根部门公司级层级为1self.level1# ---------- 步骤2限制最大层级 ----------MAX_LEVEL4# 业务上最多4级可根据实际情况调整ifself.levelMAX_LEVEL:raiseValidationError(f部门层级不能超过{MAX_LEVEL}级)# ---------- 步骤3调用父类的 save 方法真正写入数据库 ----------super().save(*args,**kwargs)# 注意如果业务允许修改 parent移动部门上面的代码只更新了当前部门的 level# 但其子孙部门的 level 不会自动更新。解决方案# - 在 save 中检测 self.pk 是否存在更新操作且 parent 字段值发生了变化# 然后递归更新所有下级的 level。但因为递归更新较复杂且部门移动极少发生# 我们暂不实现在项目使用文档中说明这个限制。在实际操作中硬删除权限在配置时不对外开放。有兴趣的朋友也可在此基础上迭代完善。clean() 方法防止循环引用defclean(self): 模型级验证在调用 full_clean() 时自动执行。 主要目的防止循环引用例如 A 的上级是 BB 的上级是 A。 ifnotself.parent:# 没有上级无需检查return# 验证1不能指向自己ifself.parentself:raiseValidationError({parent:部门不能作为自己的上级})# 验证2向上查找祖先如果遇到自己说明形成了循环ancestorself.parentwhileancestor:ifancestorself:raiseValidationError({parent:检测到循环引用请重新选择上级})ancestorancestor.parent# 注意clean() 不会自动在 save() 中调用。# 如果通过 Django Admin 保存Admin 会自动调用 full_clean() 进而触发 clean()。# 如果直接调用 model.save()需要在 save() 里手动调用 self.full_clean()。get_descendant_ids() 方法获取所有下级IDdefget_descendant_ids(self,include_selfTrue,include_deletedFalse): 递归获取当前部门及其所有下级部门的 ID 列表。 参数 include_self: 是否包含当前部门自身的 ID默认 True include_deleted: 是否包含已软删除的部门默认 False正常业务不包含 返回 一个列表如 [10, 11, 12, ...] 用途 在权限判断时可以用这些 ID 去过滤数据 viewable_dept_ids user_dept.get_descendant_ids(include_selfTrue) Employee.objects.filter(department_id__inviewable_dept_ids) ids[]# 1. 如果需要包含自身先把当前部门 ID 加入列表ifinclude_self:ids.append(self.id)# 2. 获取直接子部门根据是否包含已删除决定过滤ifinclude_deleted:childrenself.children.all()# children 是 related_name 定义的反向关系else:childrenself.children.filter(is_deletedFalse)# 3. 递归遍历每个子部门把它们的子孙 ID 也加入列表forchildinchildren:ids.extend(child.get_descendant_ids(include_selfTrue,include_deletedinclude_deleted))returnids# 性能说明因为层级最多4级递归深度很小每次递归会查询一次数据库。# 如果部门数量非常大几千个可以考虑用 level 字段 前缀树方式优化# 但本例够用保持代码简单。五、注意事项踩坑经验层级上限虽然硬编码 MAX_LEVEL 4但如果实际业务调整了组织架构该值也是可以调整的。移动部门本例没有实现移动部门后自动更新子孙层级的逻辑。如果业务需要可以在 save() 中检测 parent 字段是否变化然后递归更新子孙的 level。但因为递归更新可能涉及大量数据且部门移动极低频我们暂不实现留作扩展。软删除部门不可作为上级在 save() 中已经检查了 parent.is_deleted会抛出异常。更优雅的方式是在 formfield_for_foreignkey 中直接过滤掉已删除的部门让用户根本选不到本项目考虑在后续实现这个功能迭代。循环引用检测递归向上查找时如果树很深可能会效率低但我们限制4级完全可接受。如果未来扩展为多级可以考虑在 clean() 中使用集合记录已访问节点。约束的 condition 参数需要考虑Django版本和数据库版本的支持差异。六、小结本章我们用详细的代码注释逐行解释了部门模型的每一部分· 自关联 parent 形成树形结构。· level 字段自动计算层级避免递归查询性能问题。· clean() 防止循环引用。· get_descendant_ids() 快速获取所有下级为权限过滤提供数据基础。代码注释详细是本系列博客文章的最大特色后续每一章都会保持这个标准每行代码都有解释每个方法都争取讲清楚思路和用途。下一章我们将在本章基础上继续探讨软删除与审计字段的完整实现让数据可恢复、操作可追溯。互动问题你在实际项目中有没有因为循环引用导致过递归死循环或者因为没加 level 字段而导致查询性能很差欢迎评论区分享你的故事。