在题库系统的开发过程中,题目分类是基础且核心的功能模块,很多场景下分类需要支持可选的子类型,比如一级分类为语文时可能没有子类型,而一级分类为数学时可以包含代数、几何等子类型。这种结构如果设计不当,很容易出现数据冗余或者查询逻辑复杂的问题,下面介绍一种优雅的实现方案。

核心设计思路
我们可以采用自关联的树形结构来设计分类模型,通过父级分类字段来实现一级分类和子类型的关联,同时子类型字段设置为可选,这样既可以支持多层级的分类结构,也能满足部分分类没有子类型的需求。
模型定义实现
首先定义题目分类的基础模型,包含分类名称、父级分类、排序权重等字段,父级分类设置为自关联,且允许为空,这样没有父级分类的就是一级分类,有父级分类的就是子类型。
from django.db import models
class QuestionCategory(models.Model):
# 分类名称
name = models.CharField(max_length=50, verbose_name="分类名称")
# 父级分类,自关联,允许为空,为空时表示一级分类
parent = models.ForeignKey(
'self',
on_delete=models.CASCADE,
null=True,
blank=True,
related_name='children',
verbose_name="父级分类"
)
# 排序权重,数值越小越靠前
sort_weight = models.IntegerField(default=0, verbose_name="排序权重")
# 分类描述
description = models.TextField(null=True, blank=True, verbose_name="分类描述")
# 创建时间
created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
class Meta:
# 按排序权重和创建时间排序
ordering = ['sort_weight', 'created_at']
verbose_name = "题目分类"
verbose_name_plural = "题目分类"
def __str__(self):
return self.name
@property
def is_top_category(self):
"""判断是否为一级分类"""
return self.parent is None
@property
def has_children(self):
"""判断是否包含子类型"""
return self.children.exists()
题目模型关联设计
题目模型需要同时关联一级分类和可选的子类型,这样查询时可以快速筛选对应分类下的题目,不需要每次都遍历分类的层级关系。
from django.db import models
from .category import QuestionCategory
class Question(models.Model):
# 题目内容
content = models.TextField(verbose_name="题目内容")
# 关联一级分类,通过分类的parent字段为空的过滤条件保证是一级分类
top_category = models.ForeignKey(
QuestionCategory,
on_delete=models.CASCADE,
related_name='top_questions',
verbose_name="一级分类",
limit_choices_to={'parent__isnull': True}
)
# 关联子类型,可选,限制只能选择属于当前一级分类的子类型
sub_category = models.ForeignKey(
QuestionCategory,
on_delete=models.CASCADE,
null=True,
blank=True,
related_name='sub_questions',
verbose_name="子类型",
limit_choices_to={'parent__isnull': False}
)
# 题目难度
difficulty = models.CharField(
max_length=20,
choices=[('easy', '简单'), ('medium', '中等'), ('hard', '困难')],
verbose_name="难度"
)
# 创建时间
created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
class Meta:
ordering = ['-created_at']
verbose_name = "题目"
verbose_name_plural = "题目"
def __str__(self):
return f"{self.top_category.name}-{self.content[:20]}"
def save(self, *args, **kwargs):
"""保存时校验子类型是否属于对应的一级分类"""
if self.sub_category and self.sub_category.parent != self.top_category:
raise ValueError("子类型必须属于对应的一级分类")
super().save(*args, **kwargs)
常用操作示例
创建分类数据
首先创建一级分类,再为需要子类型的一级分类创建对应的子类型分类。
# 创建一级分类 math_category = QuestionCategory.objects.create(name="数学", sort_weight=1) chinese_category = QuestionCategory.objects.create(name="语文", sort_weight=2) # 为数学分类创建子类型 QuestionCategory.objects.create(name="代数", parent=math_category, sort_weight=1) QuestionCategory.objects.create(name="几何", parent=math_category, sort_weight=2) # 语文分类不需要子类型,不创建子分类即可
查询分类和题目
可以通过模型提供的属性和方法快速查询需要的分类和题目数据。
# 获取所有一级分类 top_categories = QuestionCategory.objects.filter(parent__isnull=True) # 获取数学分类下的所有子类型 math_sub_categories = QuestionCategory.objects.filter(parent__name="数学") # 获取数学分类下所有的题目,包含有子类型和没有子类型的 math_questions = Question.objects.filter(top_category__name="数学") # 获取代数子类型下的所有题目 algebra_questions = Question.objects.filter(sub_category__name="代数")
设计优势总结
- 结构灵活:支持任意一级分类可选子类型,不需要为不同的分类单独设计表结构
- 查询高效:题目模型直接关联一级分类和子类型,不需要多次联表查询分类层级
- 扩展性强:如果后续需要支持三级分类,只需要调整父级关联的逻辑即可,不需要修改整体结构
- 数据一致:通过save方法的校验和ForeignKey的limit_choices_to限制,保证子类型一定属于对应的一级分类
注意事项
在后台管理页面配置时,需要为Question模型的sub_category字段添加动态限制,保证选择子类型时只能选择当前选中一级分类下的子类型,可以自定义表单来实现这个逻辑。另外如果分类层级较多,可以考虑添加递归方法获取所有子分类,方便批量操作。