ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于Django的在线家谱系统:关系建模与部署实践

基于Django的在线家谱系统:关系建模与部署实践 简介面向计算机专业毕业设计场景这份压缩包提供基于Python与Django构建的在线家谱查询录入系统完整源码适合正在学习Web开发或在准备毕设选题的学生。项目涵盖用户注册登录、个人认证、家谱信息录入、亲属关系建模、查询记录、前端展示等模块代码可直接运行并继续扩展。压缩包共246个文件、36.6MB包含23个py源码、23个html模板、21个js与15个css样式、29个pyc依赖文件另有sqlite3数据库和说明文档目录清晰便于分模块对照学习。已有304人学习下载。通过这套代码可以掌握Django的MVC分层、ORM数据库操作、自带Admin后台配置以及URL路由、模板渲染和权限控制等常用开发技巧数据库表设计和部署思路也一并呈现对完成课程设计或毕业答辩很有帮助。1. 一个多数人忽略的家谱建模切入点如果把家谱做成一张关系网最麻烦的往往不是录入多少个人而是怎么表示“他是谁、他和别人什么关系”。这个基于PythonDjango的在线家谱查询录入系统让我重新理解了一次模型设计在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_deletemodels.CASCADE, verbose_name所属用户) name models.CharField(max_length50, verbose_name姓名) gender models.CharField(max_length1, choicesGENDER_CHOICES, verbose_name性别) birth_date models.DateField(nullTrue, blankTrue, verbose_name出生日期) death_date models.DateField(nullTrue, blankTrue, verbose_name去世日期) biography models.TextField(blankTrue, verbose_name生平简介) created_at models.DateTimeField(auto_now_addTrue) class Meta: ordering [-id] verbose_name 家族成员 def __str__(self): return self.name这段代码定义了家谱成员表的核心字段。owner是外键指向Django内置的User表用来做数据隔离——每个用户只能看到自己录入的成员。birth_date和death_date都允许为空因为族谱里很多祖辈信息并不完整强制必填会在录入时制造大量阻力。on_deletemodels.CASCADE表示用户被删除时其名下成员自动清空避免遗留孤儿数据。再建关系表把成员之间的边独立出来class Relationship(models.Model): RELATION_TYPES [ (father, 父亲), (mother, 母亲), (spouse, 配偶), (son, 儿子), (daughter, 女儿), (sibling, 兄弟姐妹), ] from_member models.ForeignKey(FamilyMember, on_deletemodels.CASCADE, related_namerelations_from) to_member models.ForeignKey(FamilyMember, on_deletemodels.CASCADE, related_namerelations_to) rel_type models.CharField(max_length20, choicesRELATION_TYPES, verbose_name关系类型) created_at models.DateTimeField(auto_now_addTrue) 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, nullTrue, blankTrue)这就是常见的自关联树模型。它写起来最简单递归查某个人的后代很直观。但代价是“配偶”“兄弟姐妹”这类非父子关系没法表达并且树的深度一旦超过五六层ORM的递归查询会变成性能灾难。这个项目更合适的是第二种也就是上面用到的“关系表”方案。它把家谱从一棵树变成一张图两个人之间的关系由边的类型决定。要查某人的全部儿子一条查询就够sons FamilyMember.objects.filter( relations_to__from_memberperson, relations_to__rel_typeson, genderM )注意这里用了一次反向查询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_deletemodels.CASCADE, related_nameprofile) nickname models.CharField(max_length30, blankTrue) family_name models.CharField(max_length30, blankTrue, verbose_name姓氏) avatar models.ImageField(upload_toavatars/, blankTrue) 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(useruser) login(request, user) return redirect(home) return render(request, register.html, {form: form})UserCreationForm自带密码确认和基本强度校验省得自己写两遍密码比对逻辑。Profile.objects.create(useruser)在注册成功后立刻初始化扩展资料这一步如果放到单独的方法里要记得加事务保证一致性。登录可以用Django内置的LoginViewURL配置指向它即可from django.contrib.auth.views import LoginView from django.urls import path urlpatterns [ path(login/, LoginView.as_view(template_namelogin.html), namelogin), ]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, idmember_id, ownerrequest.user ) relation_list Relationship.objects.filter( models.Q(from_membermember, to_member__ownerrequest.user) | models.Q(to_membermember, from_member__ownerrequest.user) ) return render(request, member_detail.html, { member: member, relations: relation_list, })核心在第二行get_object_or_404里除了传入id还加了ownerrequest.user。这样别的用户即使手工拼接URL也只会得到404而不是403。403会暴露“资源存在但没有权限”的信息404则直接隐藏数据存在与否安全性更好。关系查询里的to_member__ownerrequest.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(commitFalse) member.owner request.user member.save() return redirect(member_detail, member_idmember.id) else: form MemberForm() return render(request, member_form.html, {form: form})commitFalse是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(ownerrequest.user) if keyword: members members.filter( Q(name__icontainskeyword) | Q(biography__icontainskeyword) ) if gender: members members.filter(gendergender) if start_year: members members.filter(birth_date__year__gtestart_year) if end_year: members members.filter(birth_date__year__lteend_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(ownerrequest.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 classpagination {% if page_obj.has_previous %} lia href?page{{ page_obj.previous_page_number }}keyword{{ request.GET.keyword }}上一页/a/li {% endif %} lia第 {{ page_obj.number }} / {{ page_obj.paginator.num_pages }} 页/a/li {% if page_obj.has_next %} lia href?page{{ page_obj.next_page_number }}keyword{{ request.GET.keyword }}下一页/a/li {% endif %} /ul /nav链接里手动拼接keyword参数是为了保证翻页后搜索条件不会丢失。如果只是?page2用户点上一页时关键词就没了这是分页搜索里最常见的体验问题。Bootstrap的栅格系统直接用于布局表单加上form-control、form-group类之后原生的Django表单渲染也能获得一致的外观。4.4 用REST Framework暴露查询接口如果后续要做小程序端或App端建议直接把查询能力做成REST API。Django REST FrameworkDRF在这里比手写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(ownerself.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(pk1) member.delete()这条delete会触发CASCADE把Relationship里from_member或to_member指向这个成员的所有边一起删掉。如果项目里有些关系数据是想保留的必须在模型里改成on_deletemodels.SET_NULL加nullTrue让删除成员后关系记录仍然存在但指向为空。验证方法是删完以后立刻执行Relationship.objects.count()看数量变化是否符合预期。最后建议在settings.py里打开DEBUG False后再跑一次完整流程你会发现错误信息从详细堆栈变成了通用404/500页这是生产环境该有的状态。如果遇到500先看gunicorn的错误日志大多数问题集中在静态文件路径、数据库连接配置和ALLOWED_HOSTS没有加上服务器IP这三类。本文还有配套的精品资源点击获取
返回列表