简介:面向计算机专业毕业设计场景,这份压缩包提供基于Python与Django构建的在线家谱查询录入系统完整源码,适合正在学习Web开发或在准备毕设选题的学生。项目涵盖用户注册登录、个人认证、家谱信息录入、亲属关系建模、查询记录、前端展示等模块,代码可直接运行并继续扩展。压缩包共246个文件、36.6MB,包含23个py源码、23个html模板、21个js与15个css样式、29个pyc依赖文件,另有sqlite3数据库和说明文档,目录清晰,便于分模块对照学习。已有304人学习下载。通过这套代码,可以掌握Django的MVC分层、ORM数据库操作、自带Admin后台配置,以及URL路由、模板渲染和权限控制等常用开发技巧;数据库表设计和部署思路也一并呈现,对完成课程设计或毕业答辩很有帮助。
1. 一个多数人忽略的家谱建模切入点
如果把家谱做成一张关系网,最麻烦的往往不是录入多少个人,而是怎么表示“他是谁、他和别人什么关系”。这个基于Python+Django的在线家谱查询录入系统,让我重新理解了一次模型设计在Web开发里的分量。它不是一个花哨的社交树,而是把用户注册、成员档案、亲属关系、查询历史串成一条完整业务链的毕设级项目。对刚做完基础教程的人来说,它最值得拆的地方在于:Django的ORM怎么表达亲缘层级,auth模块怎么隔离不同用户的数据,以及查询和录入两条路径怎么互相影响。下文按从模型到部署的顺序,把每一步的关键代码、参数和踩坑点过一遍,场景上可以直接映射到家族史整理、校友关系管理这类纵深层级查询需求。
2. 数据模型:用Django ORM还原家族关系网络
2.1 先拆表:用户、成员档案、亲属关系、查询历史
开始动手前,先想清楚一件事:家谱系统里的“关系”不能只靠外键自关联解决。常见做法是拆成四张核心表:用户表保存登录凭证,家谱成员表保存每个人本身的属性,关系表保存“父子”“配偶”这类边,查询历史表为后续的个性化推荐留数据。这样拆的好处是,一个人可以有多个父亲角色(生父、养父、继父)而不会污染成员表结构。
Django的ORM在这里的价值,是让你用Python类定义表结构,而不是手写SQL。先看模型代码的核心部分:
from django.db import models from django.contrib.auth.models import User class FamilyMember(models.Model): GENDER_CHOICES = [('M', '男'), ('F', '女'), ('O', '其他')] owner = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='所属用户') name = models.CharField(max_length=50, verbose_name='姓名') gender = models.CharField(max_length=1, choices=GENDER_CHOICES, verbose_name='性别') birth_date = models.DateField(null=True, blank=True, verbose_name='出生日期') death_date = models.DateField(null=True, blank=True, verbose_name='去世日期') biography = models.TextField(blank=True, verbose_name='生平简介') created_at = models.DateTimeField(auto_now_add=True) class Meta: ordering = ['-id'] verbose_name = '家族成员' def __str__(self): return self.name这段代码定义了家谱成员表的核心字段。owner是外键,指向Django内置的User表,用来做数据隔离——每个用户只能看到自己录入的成员。birth_date和death_date都允许为空,因为族谱里很多祖辈信息并不完整,强制必填会在录入时制造大量阻力。on_delete=models.CASCADE表示用户被删除时,其名下成员自动清空,避免遗留孤儿数据。
再建关系表,把成员之间的边独立出来:
class Relationship(models.Model): RELATION_TYPES = [ ('father', '父亲'), ('mother', '母亲'), ('spouse', '配偶'), ('son', '儿子'), ('daughter', '女儿'), ('sibling', '兄弟姐妹'), ] from_member = models.ForeignKey(FamilyMember, on_delete=models.CASCADE, related_name='relations_from') to_member = models.ForeignKey(FamilyMember, on_delete=models.CASCADE, related_name='relations_to') rel_type = models.CharField(max_length=20, choices=RELATION_TYPES, verbose_name='关系类型') created_at = models.DateTimeField(auto_now_add=True) class Meta: unique_together = ('from_member', 'to_member', 'rel_type') verbose_name = '亲属关系' def __str__(self): return f'{self.from_member} - {self.rel_type} - {self.to_member}'related_name参数非常关键。它决定了反向查询时用哪个名字:从某个成员出发,member.relations_from.all()能查到以他为起点的所有关系,member.relations_to.all()则查到他作为终点的关系。unique_together用来防止同一条有向关系被重复录入,比如“甲-父亲-乙”不能存两条。这一步不加约束,前端再多的校验都挡不住并发请求造成的脏数据。
2.2 树形层级与复杂亲属关系的两种建模方式
处理家谱时,你一定会遇到“隔代查询”和“旁系亲属”这类问题。第一种思路是给FamilyMember加一个parent = models.ForeignKey('self', null=True, blank=True),这就是常见的自关联树模型。它写起来最简单,递归查某个人的后代很直观。但代价是“配偶”“兄弟姐妹”这类非父子关系没法表达,并且树的深度一旦超过五六层,ORM的递归查询会变成性能灾难。
这个项目更合适的是第二种,也就是上面用到的“关系表”方案。它把家谱从一棵树变成一张图,两个人之间的关系由边的类型决定。要查某人的全部儿子,一条查询就够:
sons = FamilyMember.objects.filter( relations_to__from_member=person, relations_to__rel_type='son', gender='M' )注意这里用了一次反向查询,relations_to是Relationship外键指向FamilyMember时被指定的related_name。这样写比在Python里循环判断父子关系要快得多,数据库层面用一次JOIN就完成了。你还可以用同样的方式把某个分支的所有成员捞出来,再在内存里组合成树形JSON,供前端渲染关系图谱。
2.3 用migration落地数据库变更
表结构定好后,接下来要执行Django迁移命令。这个步骤新手容易忽略的点是:改了models.py之后,先makemigrations再migrate,顺序不能反。
python manage.py makemigrations genealogy python manage.py migrate第一条命令会根据models.py的变化生成迁移文件,你可以打开genealogy/migrations/下的文件检查字段类型。第二条命令把迁移真正执行到数据库。如果项目里用了MySQL数据库,跑迁移前必须确保mysqlclient已经装好,否则会直接报ModuleNotFoundError。另外,makemigrations --dry-run可以只预览变更而不生成文件,在多人协作时用来做变更预审很实用。
3. 注册登录与权限控制:不要让家谱裸奔
3.1 用户模型:内置User够不够用
Django自带的auth.User已经包含用户名、密码、邮箱、权限组等字段,对大多数毕业设计够用。但如果你想存用户的头像、昵称、世系偏好,推荐做一个Profile模型一对一关联,而不是直接改User表。改内置表会导致后续Django升级时迁移冲突,维护成本直线上升。
from django.db import models from django.contrib.auth.models import User class Profile(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile') nickname = models.CharField(max_length=30, blank=True) family_name = models.CharField(max_length=30, blank=True, verbose_name='姓氏') avatar = models.ImageField(upload_to='avatars/', blank=True) def __str__(self): return self.nickname or self.user.usernameOneToOneField确保一个用户只有一份扩展资料。访问用户信息时用request.user.profile.nickname,没有Profile的话会报RelatedObjectDoesNotExist。常见做法是在注册逻辑里同时创建User和Profile,或者用Django的信号机制自动创建。我更推荐后者,因为手动创建容易漏,一旦忘掉,后面所有取user.profile的视图都会崩。
3.2 用类视图实现注册和登录
视图函数写法简单,但类视图能把“GET展示表单”和“POST处理提交”拆开,代码更干净。下面这段注册视图是实际项目中我会保留的骨架:
from django.views import View from django.shortcuts import render, redirect from django.contrib.auth.forms import UserCreationForm from django.contrib.auth import login class RegisterView(View): def get(self, request): form = UserCreationForm() return render(request, 'register.html', {'form': form}) def post(self, request): form = UserCreationForm(request.POST) if form.is_valid(): user = form.save() Profile.objects.create(user=user) login(request, user) return redirect('home') return render(request, 'register.html', {'form': form})UserCreationForm自带密码确认和基本强度校验,省得自己写两遍密码比对逻辑。Profile.objects.create(user=user)在注册成功后立刻初始化扩展资料,这一步如果放到单独的方法里,要记得加事务保证一致性。登录可以用Django内置的LoginView,URL配置指向它即可:
from django.contrib.auth.views import LoginView from django.urls import path urlpatterns = [ path('login/', LoginView.as_view(template_name='login.html'), name='login'), ]template_name如果不指定,默认会去找registration/login.html。这里显式指定模板,让目录结构更可控。登录成功后默认跳转到/accounts/profile/,通常要再设置LOGIN_REDIRECT_URL = '/',否则会撞上自带的默认路由,返回404。
3.3 权限校验与数据隔离:防止越权查看家谱
Django的@login_required装饰器解决了“没登录不能访问”,但解决不了“登录了A用户却能看B用户的家谱”。数据隔离必须靠ORM层面的条件过滤来实现。看这条查找某个成员详情的视图:
from django.contrib.auth.decorators import login_required from django.shortcuts import get_object_or_404, render @login_required def member_detail(request, member_id): member = get_object_or_404( FamilyMember, id=member_id, owner=request.user ) relation_list = Relationship.objects.filter( models.Q(from_member=member, to_member__owner=request.user) | models.Q(to_member=member, from_member__owner=request.user) ) return render(request, 'member_detail.html', { 'member': member, 'relations': relation_list, })核心在第二行:get_object_or_404里除了传入id,还加了owner=request.user。这样别的用户即使手工拼接URL,也只会得到404而不是403。403会暴露“资源存在但没有权限”的信息,404则直接隐藏数据存在与否,安全性更好。关系查询里的to_member__owner=request.user用了跨表查询,确保关联到的成员也属于当前用户。
4. 查询录入核心链路:从表单到数据库再到模板
4.1 成员录入表单的字段校验与错误反馈
录入表单的作用不只是收集数据,更要在前端把脏数据挡住。Django Form层负责数据清洗和校验,模板负责回显错误信息。先看表单定义:
from django import forms from .models import FamilyMember class MemberForm(forms.ModelForm): class Meta: model = FamilyMember fields = ['name', 'gender', 'birth_date', 'death_date', 'biography'] widgets = { 'birth_date': forms.DateInput(attrs={'type': 'date'}), 'death_date': forms.DateInput(attrs={'type': 'date'}), } def clean(self): cleaned_data = super().clean() birth = cleaned_data.get('birth_date') death = cleaned_data.get('death_date') if birth and death and death < birth: self.add_error('death_date', '去世日期不能早于出生日期') return cleaned_datawidgets里指定日期输入框类型,浏览器会弹出原生日历选择器,减少用户手输格式错误。clean方法里做跨字段校验,死亡日期早于出生日期这种逻辑错误,ModelForm默认不会查,必须自己写。add_error会把错误信息绑定到death_date字段,模板里直接用{{ form.death_date.errors }}就能展示。
视图层处理提交时,要把owner字段自动填入当前用户,而不是让用户在表单里选:
def member_add(request): if request.method == 'POST': form = MemberForm(request.POST) if form.is_valid(): member = form.save(commit=False) member.owner = request.user member.save() return redirect('member_detail', member_id=member.id) else: form = MemberForm() return render(request, 'member_form.html', {'form': form})commit=False是ModelForm的常用技巧:先不写数据库,把owner赋值后再保存。如果不这样,外键字段会因为缺失而直接报错。注意在表单类里不要引入owner字段,否则用户可以通过伪造POST请求把家谱挂到别人名下。
4.2 多条件家谱查询:Q对象的组合查询
家谱查询和普通的列表查询最大的区别,是条件组合很多:按姓名模糊搜、按性别筛选、按年龄区间、还可能要查某一年代出生的所有成员。Django的Q对象让这些条件能以逻辑表达式的方式组合。
from django.db.models import Q def search_members(request): keyword = request.GET.get('keyword', '').strip() gender = request.GET.get('gender', '') start_year = request.GET.get('start_year', '') end_year = request.GET.get('end_year', '') members = FamilyMember.objects.filter(owner=request.user) if keyword: members = members.filter( Q(name__icontains=keyword) | Q(biography__icontains=keyword) ) if gender: members = members.filter(gender=gender) if start_year: members = members.filter(birth_date__year__gte=start_year) if end_year: members = members.filter(birth_date__year__lte=end_year) members = members.select_related('owner').order_by('birth_date') return render(request, 'member_list.html', {'members': members})name__icontains会生成SQL里的LIKE '%keyword%',且忽略大小写。birth_date__year__gte是Django对日期字段的跨表查询语法,表示出生年份大于等于指定值。select_related('owner')在这里会JOIN用户表,避免在模板里遍历成员时每次都查一次owner,这是列表页性能优化的第一级。
4.3 前端Bootstrap表格与分页渲染
页面层用的Bootstrap在后台目录里已经有完整资源。列表页用表格展示成员信息,配合Django内置分页器。分页必须放在视图里做,不能在模板里用切片,否则数据量大时会把所有记录一次性查出来。
from django.core.paginator import Paginator def member_list(request): members = FamilyMember.objects.filter(owner=request.user).order_by('id') paginator = Paginator(members, 10) page_number = request.GET.get('page') page_obj = paginator.get_page(page_number) return render(request, 'member_list.html', {'page_obj': page_obj})模板渲染时,表格每一行放姓名、性别、生卒年,以及“详情/编辑”操作按钮。底部分页链接保持GET参数,因为查询条件也通过GET传递:
<nav> <ul class="pagination"> {% if page_obj.has_previous %} <li><a href="?page={{ page_obj.previous_page_number }}&keyword={{ request.GET.keyword }}">上一页</a></li> {% endif %} <li><a>第 {{ page_obj.number }} / {{ page_obj.paginator.num_pages }} 页</a></li> {% if page_obj.has_next %} <li><a href="?page={{ page_obj.next_page_number }}&keyword={{ request.GET.keyword }}">下一页</a></li> {% endif %} </ul> </nav>链接里手动拼接keyword参数,是为了保证翻页后搜索条件不会丢失。如果只是?page=2,用户点上一页时关键词就没了,这是分页搜索里最常见的体验问题。Bootstrap的栅格系统直接用于布局,表单加上form-control、form-group类之后,原生的Django表单渲染也能获得一致的外观。
4.4 用REST Framework暴露查询接口
如果后续要做小程序端或App端,建议直接把查询能力做成REST API。Django REST Framework(DRF)在这里比手写JSONResponse更省事,它自带序列化、认证、分页。一个精简的序列化器如下:
from rest_framework import serializers from .models import FamilyMember class MemberSerializer(serializers.ModelSerializer): age = serializers.SerializerMethodField() class Meta: model = FamilyMember fields = ['id', 'name', 'gender', 'birth_date', 'death_date', 'biography', 'age'] def get_age(self, obj): if obj.birth_date: return (date.today() - obj.birth_date).days // 365 return None视图集配合IsAuthenticated权限类,可以保证接口没有被公开暴露:
from rest_framework import viewsets, permissions from .models import FamilyMember from .serializers import MemberSerializer class MemberViewSet(viewsets.ModelViewSet): serializer_class = MemberSerializer permission_classes = [permissions.IsAuthenticated] def get_queryset(self): return FamilyMember.objects.filter(owner=self.request.user)注意SerializerMethodField会自动调用get_age方法,输出一个动态计算字段。DRF的ModelViewSet默认提供增删改查全套接口,如果你只给前端看功能,可以直接用ReadOnlyModelViewSet,减少误删除的风险。
5. 部署与验证:从本地到Linux服务器的完整闭环
5.1 用Nginx + Gunicorn把Django项目跑起来
本地开发用python manage.py runserver没问题,但生产环境必须换应用服务器。常见选择是Gunicorn配合Nginx,一个跑Python进程,一个处理静态文件和反向代理。你如果用的是宝塔面板部署Django,流程同样基于这套逻辑。
pip install gunicorn mysqlclient python manage.py collectstatic gunicorn family_tree.wsgi:application -w 3 -b 127.0.0.1:8000family_tree是项目根目录的包名,wsgi:application指定入口。-w 3表示3个工作进程,CPU核数加一通常是经验值,别在1核小机器上开8个进程,内存会先被吃满。collectstatic负责把Django的静态文件统一收集到STATIC_ROOT目录,Nginx配置里要把它映射出去,否则页面上的Bootstrap和font-awesome样式全部加载不了。
Nginx侧只需要代理动态请求:
location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location /static/ { alias /var/www/family_tree/static/; }这里alias的路径必须和settings.py里的STATIC_ROOT一致。常见的坑是浏览器返回200但样式全无,打开开发者工具看到CSS文件404,那就是Nginx的alias路径写错了,或者忘了执行collectstatic。
5.2 Admin后台:用一行代码搞定管理界面美化
项目自带Django Admin,但默认的深蓝色背景确实不够专业。美化方式不需要前端框架,直接在admin.py里配置就能改布局和操作逻辑:
from django.contrib import admin from .models import FamilyMember, Relationship @admin.register(FamilyMember) class FamilyMemberAdmin(admin.ModelAdmin): list_display = ('name', 'gender', 'birth_date', 'owner') list_filter = ('gender', 'owner') search_fields = ('name', 'biography') list_per_page = 20list_display决定后台列表显示哪些字段,list_filter在右侧生成筛选器,search_fields把顶部的搜索框关联到指定字段。修改完这些属性,后台管理成员数据的效率翻倍,不用点进详情页就能快速定位记录。如果你想把关联关系一起展示,可以进一步用inline组件,在成员详情页直接增删改名下的亲属关系,这也是这个系统管理端最实用的一个点。
5.3 验证查询删除对象时的坑
部署完成后,建议手动执行一遍Django原生的查询与删除操作来验证数据完整性。常见误区是直接调用QuerySet.delete()时忽略级联行为。比如想删掉一个成员,但他的配偶关系还挂在关系表里,外键约束会同时把关系一起级联删除:
from genealogy.models import FamilyMember member = FamilyMember.objects.get(pk=1) member.delete()这条delete会触发CASCADE,把Relationship里from_member或to_member指向这个成员的所有边一起删掉。如果项目里有些关系数据是想保留的,必须在模型里改成on_delete=models.SET_NULL加null=True,让删除成员后关系记录仍然存在但指向为空。验证方法是删完以后立刻执行Relationship.objects.count(),看数量变化是否符合预期。
最后建议在settings.py里打开DEBUG = False后再跑一次完整流程,你会发现错误信息从详细堆栈变成了通用404/500页,这是生产环境该有的状态。如果遇到500,先看gunicorn的错误日志,大多数问题集中在静态文件路径、数据库连接配置和ALLOWED_HOSTS没有加上服务器IP这三类。
本文还有配套的精品资源,点击获取