Django房源系统开发实战:数据模型设计、Admin后台与性能优化指南
2026/9/14 11:48:06 网站建设 项目流程

简介:这是一份基于Django框架开发的佳居房源系统毕业设计完整资料包,面向计算机相关专业的在校学生、教师及企业开发者,适用于毕业设计、课程设计、项目初期立项演示或自学进阶。资源包含项目所有源码、使用说明、数据库脚本及配套文档,核心代码已经过测试运行成功,可帮助读者快速理解房源信息管理、用户认证与权限、数据交互等核心模块的设计思路,并在此基础上进行功能扩展。压缩包共2000个文件,以1317个Python源码文件为主体,辅以HTML模板、CSS样式、JavaScript脚本、国际化翻译文件(mo/po)以及SQL数据库脚本,整体大小约29.42MB,文件分类清晰、目录结构合理,便于按模块查阅。目前已有99人学习下载,附带的完整工程与详细说明文档,非常适合需要参考真实项目实现、完成毕设或课设任务以及动手实践的学习者使用。

1. 基于Django的房源系统,先别急着写代码

很多人看到“佳居房源系统”这类毕业设计题目,第一反应是去网上搜整套源码,解压、配库、跑起来就算交差。但真正拿到一个基于Django的房源系统题目时,最该想的不是“从哪复制”,而是“如果让我从零设计,表和表之间的关系怎么定义”。因为这类题目的验收点就三个:能不能完成房源信息的增删改查、能不能把后台管理界面做像样、能不能在论文里说清楚设计思路。这三点恰好都是Django的强项——ORM帮我们省掉SQL手写,Admin后台帮我们省掉前端界面,模板系统帮我们省掉前后端分离的复杂度。对于Python基础停留在语法层面、Django只跑过官方投票教程的同学来说,这个题目其实是最合适的练手级完整项目。

我见过太多翻车案例:有人用SQLite提交上去,老师一换环境就报错;有人在models里用CharField存价格,导致排序全是字符串比较;更常见的是把图片上传功能绕过Django自带机制,结果管理后台一张图都显示不出来。这篇文会按照“项目搭建→数据模型→业务视图→后台优化→部署排错”这条线,把一套能拿去答辩的房源系统完整带出来。每一步都给参数、给命令、给报错定位思路,照着做至少能在自己机器上跑通,换台机器也能十分钟恢复环境。

2. 从零搭建一个可运行的Django房源项目骨架

Django项目的起步动作必须一次做对,因为后期所有模型、视图、静态文件都挂在这个骨架下面。这一步出问题,后面每跑一步都在报错,特别影响心态。

2.1 创建虚拟环境和安装Django

先确认Python版本。Django 4.x LTS要求Python 3.8以上,目前教学楼机房常见的Python 3.10和3.11都能跑,Python 3.12也没问题。但要注意Python 3.13刚发布时对Django 4.2存在兼容性小问题,如果电脑刚装了最新版Python,建议装Django 5.0或直接降级用Python 3.11。

# 创建虚拟环境,venv是Python自带模块,无需额外安装 python -m venv venv # 激活虚拟环境,Windows和Linux命令不同 # Windows: venv\Scripts\activate # Linux/Mac: source venv/bin/activate # 安装Django,这里固定版本避免后续迁移时被新大版本打断 pip install django==4.2.16 # 验证安装 python -m django --version

逻辑说明:venv是把依赖隔离在本项目目录下的标准做法,比直接往系统Python里pip install安全得多——因为一个项目装了pymysql另一个没装,互不干扰。激活后命令行前面会出现(venv),看到这个再执行后续命令才生效。固定Django版本号很重要,“最新版”不一定“最稳定”,Django每次大版本发布都会调整某些API的默认行为,毕业论文写到一半被升级打断很麻烦。参数说明:django==4.2.16中的4.2是LTS长维护版本,官方支持到2026年4月,普通本科毕业设计周期内完全够用。

如果第一步就卡住,先检查python命令是否被识别。新电脑常见问题是在安装Python时没勾选“Add Python to PATH”,导致python命令找不到。可以尝试python3代替python,或者在Windows下用py命令。

2.2 startproject和startapp的职责边界

Django把“项目”和“应用”分开,这个设计是整套框架的核心思想。“项目”是配置中心和路由总入口,“应用”才是写业务代码的地方。一个房源系统理论上可以只建一个app把所有代码塞进去,但为了论文里“系统模块划分”那一段有东西可写,建议拆成两个:accounts管用户和收藏,house管房源和预约。

# 创建项目,命名用简短小写,不要用中文和连字符 django-admin startproject jiaju # 进入项目目录 cd jiaju # 创建两个业务应用 python manage.py startapp house python manage.py startapp accounts # 查看生成的文件结构 tree /F # Windows ls -R # Linux/Mac

逻辑说明:startproject生成的是manage.py和与项目同名的配置目录(settings.py、urls.py等),这个目录是全局总控室。startapp生成的是业务应用目录,里面自动带models.py、views.py、admin.py三个文件,稍后核心代码都写在里面。把代码分散到不同app的好处是模型和视图按业务边界隔离,答辩时老师问“扩展一个功能要改哪些文件”,你能直接说出来改哪里、不影响哪里,这就比把所有逻辑堆在一个views.py里高明得多。注意不要使用app命名Django内置名称(如test、static),会引发命名冲突。

创建完成后还需要做两件事才能真正跑起来:在settings.py的INSTALLED_APPS列表里注册新创建的app,以及在项目根目录urls.py里用include把两个app的路由挂载上去。注册app这一步漏了是Django新手最常见的错误,因为报错信息特别隐蔽,往往是运行迁移时提示“Unknown command: migrate”或者创建超级用户后访问页面404。

# jiaju/settings.py 文件内 INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'house', # 新增 'accounts', # 新增 ] # 数据库连接配置 DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'jiaju_db', 'USER': 'root', 'PASSWORD': '123456', 'HOST': '127.0.0.1', 'PORT': '3306', } }

参数说明:INSTALLED_APPS里的顺序有讲究,Django自带的app放在前面,第三方和自定义app放在后面。这是因为模板和静态文件查找时,最先出现的app优先级更高,放在后面可以避免自定义app的文件意外“遮蔽”Django内置文件。DATABASES配置里的ENGINE换成mysql引擎后,还需要安装连接驱动,这个放在下一步。

2.3 配置MySQL数据库并连接Django

SQLite虽然零配置就能跑,但数据库文件跟着源码走,提交到Git或发给老师后,数据库内容一锅端全暴露了,而且SQLite对并发写支持弱,多窗口操作经常提示“database is locked”。毕业设计一律建议用MySQL,理由有三个:一是答辩现场数据库结构可以直接截图放论文;二是老师如果需要检查数据,用Navicat打开的视觉效果远好于SQLite;三是简历上写“熟练使用MySQL”比“用过SQLite”有说服力。

# 安装MySQL连接驱动,这里用pymysql而不是mysqlclient pip install pymysql # 在项目配置目录下的__init__.py中加入以下代码
# jiaju/__init__.py import pymysql pymysql.install_as_MySQLdb()

逻辑说明:Django默认通过MySQLdb模块连接MySQL,但在Windows环境下MySQLdb几乎无法安装成功,所以用pymysql模拟MySQLdb的接口。在包目录下的__init__.py里执行install_as_MySQLdb(),就是告诉Django“用pymysql来扮演MySQLdb”。这个方法在Django 4.2和pymysql 1.1.0版本下实测稳定。参数说明:pymysql是一个纯Python实现的MySQL客户端库,几乎不依赖编译环境,相比mysqlclient需要本机装C编译器才能编译,省去很多折腾。

连接数据库之前的准备工作两步:第一步启动MySQL服务(Windows在服务管理器找MySQL80,Linux执行service mysql start);第二步建库。建库命令里必须指定字符集为utf8mb4,否则存中文房源标题的时候会报“Incorrect string value”错误。

-- MySQL命令行或Navicat查询窗口执行 CREATE DATABASE jiaju_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

字符集选utf8mb4而不是utf8,是因为utf8mb4是真正的四字节utf8编码,能存emoji和生僻字。房源描述里偶尔会出现特殊符号,用utf8可能直接写不进数据库。COLLATE选择_unicode_ci则是在排序和比较时不区分大小写,搜索房源标题时更宽容。

2.4 路由分发与首页快速验证

写完配置不验证等于白搭。最快的验证方式不是立刻写业务功能,而是先把Django自带页面跑出来确认整个链路通不通。

# 执行数据迁移,这一步会生成Django内置的auth、session等数据表 python manage.py migrate # 启动开发服务器,默认跑在8000端口 python manage.py runserver # 浏览器访问 http://127.0.0.1:8000,看到火箭发射页就说明骨架搭好了

看到“The install worked successfully”页面后,把runserver关掉(Ctrl+C),接着配置路由分发。项目根路由负责“把带什么前缀的请求交给哪个app处理”,app内部路由负责“具体URL指向哪个视图函数”。这个层级关系在论文系统设计图里可以用一张简单的请求流程图画出来。

# jiaju/urls.py 根路由 from django.contrib import admin from django.urls import path, include urlpatterns = [ path('admin/', admin.site.urls), path('', include('house.urls')), # 根路径交给house应用 path('accounts/', include('accounts.urls')), # 用户相关交给accounts ] # house/urls.py 应用内部路由(新建文件) from django.urls import path from . import views urlpatterns = [ path('', views.index_view, name='index'), ] # house/views.py 添加一个最简单的视图函数 def index_view(request): return render(request, 'house/index.html')

参数说明:path第一个参数是URL表达式,空字符串代表域名根路径,'admin/'是精确前缀匹配。name='index'给这个路由起了名字,模板里用{% url 'index' %}反向解析URL时依赖这个名字,不要漏写。这样配置后,访问根路径会去找house应用下名为index的视图函数,视图函数返回渲染的模板。

确认网址能打开后,用命令创建一个超级管理员账号,这是进入Django后台大门的钥匙。

python manage.py createsuperuser # 按提示输入用户名、邮箱、密码,密码至少8位且不能纯数字

然后访问http://127.0.0.1:8000/admin/,能正常登录后台管理页面,说明从浏览器到Django再到数据库的链路全部贯通。到此为止项目骨架拉起来了,下一章的数据模型是整个系统的地基,也是论文里数据表设计章节的素材。

3. Django模型设计:房源信息的核心数据表结构

模型层是房源系统的中枢神经。模型建得好,后续的查询、筛选、后台列表显示都是顺手的事;模型建得差,后面写业务逻辑时每天都要回来补字段、改字段、重新迁移数据库。这一章把房源系统的核心模型完整带一遍,每一个字段都说明为什么这么选。

3.1 从需求反推字段清单

毕业设计题目是“佳居房源系统”,常见功能拆解为:房源管理(出租/出售)、房源搜索筛选、户型分类、用户收藏、预约看房。围绕这些功能反推,至少需要三张核心表:房源表、户型/分类表、预约表。用户相关复用Django内置的auth用户表(User表),不用自己建用户模型,省事且安全。

先来看房源表(House)的设计。这张表的字段规划决定了整个系统的功能上限,宁可一次建全也不要后期反复改。

# house/models.py from django.db import models from django.contrib.auth.models import User class HouseCategory(models.Model): """房源分类表:比如整租、合租、二手房、新房""" name = models.CharField('分类名称', max_length=50) sort_order = models.IntegerField('排序权重', default=0) class Meta: verbose_name = '房源分类' verbose_name_plural = verbose_name def __str__(self): return self.name class House(models.Model): """房源信息主表""" RENT_TYPE_CHOICES = ( ('rent', '出租'), ('sell', '出售'), ) title = models.CharField('房源标题', max_length=200) category = models.ForeignKey(HouseCategory, on_delete=models.CASCADE, verbose_name='所属分类') rent_type = models.CharField('租售类型', max_length=10, choices=RENT_TYPE_CHOICES, default='rent') price = models.DecimalField('价格', max_digits=10, decimal_places=2) area = models.DecimalField('面积(㎡)', max_digits=8, decimal_places=2) bedroom_count = models.IntegerField('室', default=1) living_room_count = models.IntegerField('厅', default=1) bathroom_count = models.IntegerField('卫', default=1) address = models.CharField('详细地址', max_length=255) description = models.TextField('房源描述', blank=True) cover_image = models.ImageField('封面图', upload_to='house_cover/%Y/%m/', blank=True, null=True) publisher = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='发布人') status = models.BooleanField('是否上架', default=True) view_count = models.IntegerField('浏览次数', default=0) created_at = models.DateTimeField('发布时间', auto_now_add=True) updated_at = models.DateTimeField('更新时间', auto_now=True) class Meta: verbose_name = '房源信息' verbose_name_plural = verbose_name ordering = ['-created_at'] def __str__(self): return self.title

逻辑说明:价格和面积都用DecimalField而不是FloatField或IntegerField。FloatField在做范围筛选和排序时会因为浮点数精度问题出现诡异结果,比如1000000.0显示成1000000.00没大问题,但999999.99这类数值用FloatField存储后实际值是999999.9899999999。DecimalField用定点数存储,价格、面积这种需要精确比较的数值必须用它。max_digits=10表示总位数10位,decimal_places=2表示小数位2位,最大支持 99,999,999.99,房源价格这个量级完全覆盖。

rent_type用CharField加choices参数,存储的是字符串常量。有人会把这种字段设计成IntegerField加0/1映射,但这样写在Django Admin下拉框里显示的是数字,不直观;而且字符串的可读性在生成报表和API返回时好得多。注意choices枚举值要写语义化的短字符串(rent/sell),不要写1/2,后期维护时一眼能看懂含义。

外键category关联HouseCategory表,on_delete=models.CASCADE表示分类被删时该分类下的房源一并删除。也可以用PROTECT保护分类不被误删,但毕业设计场景CASCADE更省事,不会出现“试图删除分类但被外键约束挡住”的报错。

3.2 预约看房和用户收藏的关联表设计

房源表只解决了“有什么房子”的问题,一个完整系统还要有“用户行为”数据。预约看房表记录用户哪天看了哪套房,收藏表记录用户喜欢哪套房。这两张表都要和House以及User做关联。

# house/models.py 继续追加 class HouseAppointment(models.Model): """预约看房表""" house = models.ForeignKey(House, on_delete=models.CASCADE, verbose_name='预约房源') user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='预约用户') appointment_date = models.DateField('预约日期') appointment_time = models.TimeField('预约时间段') contact_name = models.CharField('联系人', max_length=50) contact_phone = models.CharField('联系电话', max_length=20) remark = models.CharField('备注', max_length=500, blank=True) is_visited = models.BooleanField('是否已看房', default=False) created_at = models.DateTimeField('预约时间', auto_now_add=True) class Meta: verbose_name = '预约看房' verbose_name_plural = verbose_name ordering = ['-created_at'] def __str__(self): return f"{self.house.title} - {self.contact_name}" class Favorite(models.Model): """房源收藏表""" user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='用户') house = models.ForeignKey(House, on_delete=models.CASCADE, verbose_name='房源') created_at = models.DateTimeField('收藏时间', auto_now_add=True) class Meta: verbose_name = '房源收藏' verbose_name_plural = verbose_name unique_together = ('user', 'house') # 同一用户不能重复收藏同一房源

逻辑说明:unique_together在Django 4.2中仍然有效(它在Django 5.0里被标记为弃用但未移除,4.2不受影响),联合唯一约束保证数据库层面不会出现重复收藏记录。很多人会在views代码里先查重再插入,但数据库层约束才是最后一道防线。appointment_date和appointment_time分开存,比合成一个DateTimeField更灵活——查询“周日上午有哪些预约”时不需要对时间字段做截断。

3.3 数据迁移的完整流程和常见报错

模型定义完只是第一步,真正把表建到MySQL里需要两步操作:先生成迁移文件(记录模型变化),再执行迁移(把变化同步到数据库)。很多刚上手的人分不清makemigrations和migrate的区别,前者是“生成改动说明书”,后者是“按说明书执行”。

python manage.py makemigrations python manage.py migrate

执行makemigrations后,house应用目录下的migrations文件夹里会自动生成一个0001_initial.py文件,打开能看到models.py内容对应的Python数据结构。如果makemigrations提示“No changes detected”,通常是app没有在INSTALLED_APPS里注册,或者models.py文件路径不对。如果migrate提示“Table already exists”,说明之前已经迁移过,可以执行python manage.py migrate house --fake做一次假迁移跳过。

表名默认是“app名_小写模型名”,比如house_house、house_houseappointment。如果想让表名更规范,可以在模型Meta类里加db_table字段指定表名。不过Django自动生成的表名格式在论文里也能说通:“系统各模块数据表统一以模块名前缀区分”。

迁移之后,还需要把ImageField依赖的Pillow库装上,否则访问房源管理页面时Django会直接报错“Cannot write images to the database”或者更隐晦的“module 'PIL' has no attribute”。

pip install Pillow

Pillow是Python的图像处理库,ImageField内部依赖它做图片格式验证和尺寸获取。不装Pillow,migrate不会报错,但一旦后台提交含图片的表单就会报错。装好之后顺带确认一下settings.py里有没有配置MEDIA_ROOT和MEDIA_URL。

# jiaju/settings.py import os MEDIA_URL = '/media/' MEDIA_ROOT = os.path.join(BASE_DIR, 'media')

MEDIA_URL是浏览器访问上传文件的URL前缀,MEDIA_ROOT是文件实际存放的硬盘目录。BASE_DIR是项目根目录,os.path.join把项目根目录和media拼接起来。不配置MEDIA_ROOT时,图片上传会直接写入数据库字段的只是文件名,但文件本身无处存放,后台预览就会裂图。

4. 房源列表、搜索筛选与Admin后台的完整搭建

模型是地基,视图和模板是承重墙。用户看到的所有页面,从房源列表到详情到搜索筛选,全靠这一层支撑。同时Django自带的Admin后台只要正确注册模型并做几个小优化,就能直接当成管理端交给管理员使用。

4.1 首页房源列表与分页的完整套路

房源列表是所有用户访问的第一屏,核心任务是“从数据库取数据、展示到页面、做分页”。一个常见误区是把所有房源一次性取出来直接render,数据量少没事,但房源超过100条后页面加载速度和数据库压力都上来了。

从视图里分页是最标准做法。Django内置的Paginator类负责把查询结果按页切分,只需要在视图里传当前页码给它。

# house/views.py from django.shortcuts import render, get_object_or_404 from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from .models import House, HouseCategory def house_list(request): category_id = request.GET.get('category', '') keyword = request.GET.get('keyword', '').strip() rent_type = request.GET.get('rent_type', '') # 先拿全部上架房源 houses = House.objects.filter(status=True) # 按筛选条件逐层过滤 if category_id: houses = houses.filter(category_id=category_id) if keyword: # icontains表示不区分大小写的模糊匹配 houses = houses.filter(title__icontains=keyword) if rent_type: houses = houses.filter(rent_type=rent_type) # 排序:最新的排前面 houses = houses.order_by('-created_at') # 分页:每页显示6条 paginator = Paginator(houses, 6) page = request.GET.get('page') try: page_obj = paginator.page(page) except PageNotAnInteger: page_obj = paginator.page(1) except EmptyPage: page_obj = paginator.page(paginator.num_pages) categories = HouseCategory.objects.all() context = { 'page_obj': page_obj, 'categories': categories, 'current_category': category_id, 'keyword': keyword, 'rent_type': rent_type, } return render(request, 'house/house_list.html', context)

逻辑说明:最早的houses.objects.filter(status=True)得到的QuerySet是惰性的——数据库查询并不会立即执行,只有真正遍历时才会触发生成SQL。所以在filter链上不断叠加条件不会造成多次查询,最终只执行一条带WHERE条件组合的SQL。category_id从GET参数里取出来时是字符串,而filter里的category_id是字段名加后缀,注意写法是category_id=而不是category=,前者直接对应数据库外键字段,后者需要Django额外做JOIN解析。

Paginator(houses, 6)里的6是每页条数,毕业设计截图时每页显示6条卡片式房源正好两行三列,视觉效果好也不拥挤。page参数从URL的?page=2里取值,PageNotAnInteger和EmptyPage这两个异常类分别处理“页码不是数字”和“页码超出范围”两种场景,第一种兜底回第一页,第二种兜底到最后一页,保证用户怎么乱输URL都不会看到报错页。

问题在于模板。模板里要显示分页链接,通常用的是Django内置的分页导航条,但默认样式朴素。如果想做出“上一页、下一页、页码数字”的常规分页组件,可以手动在模板循环输出页码,判断当前页高亮。

<!-- house/templates/house/house_list.html 分页部分 --> <nav class="pagination"> <span class="step-links"> {% if page_obj.has_previous %} <a href="?page=1{% if keyword %}&keyword={{ keyword }}{% endif %}{% if category %}&category={{ category }}{% endif %}">« 首页</a> <a href="?page={{ page_obj.previous_page_number }}{% if keyword %}&keyword={{ keyword }}{% endif %}{% if category %}&category={{ category }}{% endif %}">上一页</a> {% endif %} <span class="current"> 第 {{ page_obj.number }} / {{ page_obj.paginator.num_pages }} 页 </span> {% if page_obj.has_next %} <a href="?page={{ page_obj.next_page_number }}{% if keyword %}&keyword={{ keyword }}{% endif %}{% if category %}&category={{ category }}{% endif %}">下一页</a> <a href="?page={{ page_obj.paginator.num_pages }}{% if keyword %}&keyword={{ keyword }}{% endif %}{% if category %}&category={{ category }}{% endif %}">末页 »</a> {% endif %} </span> </nav>

这段模板里最容易被忽略的是分页参数保持。如果第一页时用户选择了“分类=整租”这个筛选条件,翻到第二页时URL如果只带page=2,筛选条件就丢了。解决方式就是把keyword和category拼回分页链接的查询字符串里。这也是为什么上面视图代码里把keyword和category_id都放进了context——模板里有值才能拼接URL。

4.2 房源详情页与浏览计数防刷

详情页比列表页简单,只需要根据URL里的房源ID取出对应数据。但这里有一个容易扣分的细节:浏览计数的实现。最笨的做法是每次刷新页面都执行house.view_count += 1再save(),这样自己刷新浏览器十次阅读量就加十次,答辩老师一旦动手试一下就会觉得这个实现太初级。

更合理的方案是——把浏览计数的更新放到ORM的update方法里,配合session做“同一会话不重复计数”的判断。

# house/views.py def house_detail(request, house_id): house = get_object_or_404(House, id=house_id, status=True) # 用session记录当前用户已浏览过的房源ID列表,防止刷新刷量 viewed_houses = request.session.get('viewed_houses', []) if house_id not in viewed_houses: # F表达式避免并发下数值覆盖 House.objects.filter(id=house_id).update(view_count=models.F('view_count') + 1) viewed_houses.append(house_id) request.session['viewed_houses'] = viewed_houses # 推荐相同分类下的其他房源 related_houses = House.objects.filter(category=house.category, status=True).exclude(id=house.id)[:4] context = { 'house': house, 'related_houses': related_houses, } return render(request, 'house/house_detail.html', context)

逻辑说明:request.session是Django默认的session机制,数据存在数据库的django_session表里,每个浏览器对应一个唯一session Key。第一次访问详情页时session里没有这个房源ID,执行浏览数+1并把ID记录到session里;之后同一个浏览器再刷新这个页面就不会重复计数了。这虽然不是完美方案(清cookie后计数仍然能再涨),但应付毕业设计的防刷逻辑演示绰绰有余。使用F('view_count') + 1而不是先取出对象再加1,是因为F表达式直接让数据库执行原子自增,避免“先读后写”在多人同时访问时丢失更新。

related_houses用.exclude(id=house.id)排除当前房源自己,[:4]做切片取出关联房源的前4条。切片会触发独立的SQL查询带上LIMIT 4,不会把全表数据加载进内存,访问压力可控。注意这里不能对切片结果再使用order_by,因为切片后返回的是列表而不是QuerySet,所以order_by要写在切片之前,这是一个典型的坑。

模板中显示浏览数和发布时间格式化时也有一点讲究。Django模板里访问时间字段用{{ house.created_at }},显示出来是“2024-05-20 14:30:00”的格式,如果想显示“2024年05月20日”,要在字段后面加日期格式化参数:

<p>发布时间:{{ house.created_at|date:"Y年m月d日 H:i" }}</p>

4.3 Django Admin后台的美化配置

Admin后台是Django给开发者的礼物,但对于毕业设计来说,默认界面太朴素,直接截图放论文里不够“系统感”。好在Django Admin的定制空间很大,最简单且出效果的三个优化是:列表页显示更多字段、增加筛选器、添加搜索框。

# house/admin.py from django.contrib import admin from .models import House, HouseCategory, HouseAppointment, Favorite @admin.register(House) class HouseAdmin(admin.ModelAdmin): list_display = ('title', 'rent_type', 'price', 'area', 'publisher', 'status', 'created_at') list_filter = ('rent_type', 'category', 'status', 'created_at') search_fields = ('title', 'address', 'description') list_editable = ('status', 'price') list_per_page = 20 date_hierarchy = 'created_at' readonly_fields = ('view_count', 'created_at', 'updated_at') # 后台列表页显示封面缩略图 def cover_preview(self, obj): if obj.cover_image: return format_html('<img src="{}" style="width:80px;height:60px;object-fit:cover"/>', obj.cover_image.url) return '暂无封面' cover_preview.short_description = '封面预览' @admin.register(HouseCategory) class HouseCategoryAdmin(admin.ModelAdmin): list_display = ('name', 'sort_order') @admin.register(HouseAppointment) class HouseAppointmentAdmin(admin.ModelAdmin): list_display = ('house', 'user', 'appointment_date', 'appointment_time', 'contact_name', 'is_visited') list_filter = ('is_visited', 'appointment_date') search_fields = ('contact_name', 'contact_phone', 'house__title') @admin.register(Favorite) class FavoriteAdmin(admin.ModelAdmin): list_display = ('user', 'house', 'created_at')

参数说明:list_display控制列表页显示的列,这里把关键信息全部展示出来,管理员一眼能看到房源状态、价格、发布人。list_editable元组里的字段可以直接在列表页点开编辑,无需进入详情页——把status和price设为可编辑,批量上架下架房源、批量调价时效率提高一大截。但list_editable的字段必须同时出现在list_display里,否则Django会抛异常。search_fields里写house__title这种跨表搜索,双下划线是Django ORM关联查询语法,表示“预约关联的房源的标题”,这个写法在admin、ORM filter、exclude里通用。date_hierarchy='created_at'会生成一个按日期钻取筛选的导航条,对时间跨度数据很多的后台特别实用。

readonly_fields里的字段不能为空且不在表单的可编辑区域出现,view_count是系统自动累计的,不允许管理员手改成任意数。cover_preview这个自定义方法是Admin里展示图片缩略图的标准做法,format_html负责把图片标签安全格式化。

登录Admin后台后,界面的“站点管理”标题默认是英文。如果论文截图需要中文界面,在settings.py里设置:

# jiaju/settings.py LANGUAGE_CODE = 'zh-hans' TIME_ZONE = 'Asia/Shanghai'

LANGUAGE_CODE设为zh-hans把Django Admin、表单验证错误提示、分页组件的默认文案全部变为中文。TIME_ZONE设为Asia/Shanghai让时间字段显示本地时间而不是UTC时间。默认配置下数据库存的是UTC时间,不设时区的话,发布时间会比北京时间晚8小时,这是最容易让答辩现场翻车的细节——老师打开后台看到的时间全是错的。

4.4 搜索筛选完成后如何验证数据正确性

功能写完后最怕的不是没有功能,而是功能有但数据不对。搜索筛选和列表的组合条件比较多,每加一个条件就需要验证一次SQL是否正确。除了在页面上手动提交表单测试外,更可靠的方式是用Django Shell直接验证ORM查询结果:

python manage.py shell
# 在shell交互环境中执行验证 from house.models import House # 验证关键字搜索:标题中含"朝阳"且已上架 houses = House.objects.filter(title__icontains='朝阳', status=True) print(houses.query) # 打印实际执行的SQL print(houses.count()) # 验证价格区间筛选:5000到8000元的出租房源 result = House.objects.filter(rent_type='rent', price__gte=5000, price__lte=8000) print(result.query)

打印的query属性会输出Django生成的原始SQL语句,肉眼检查WHERE子句是否符合预期。这是调试ORM最直接的武器——页面显示结果不对时,先用Shell确认数据层正确,再排查视图和模板的问题,问题定位效率提高一大截。

5. Django版本差异、N+1查询和性能优化

系统功能跑通之后,答辩前最值得做的优化有两类:一类是查询性能优化,一类是生产环境部署。这两类工作在论文的“系统测试与优化”章节里通常是必写内容,而且面试时也常被追问。

5.1 select_related解决列表页查询爆炸问题

在房源列表页中,模板要显示每条房源的分类名称和发布人用户名,这两个字段分别在HouseCategory和User表里。默认情况下,每次访问house.category.name都会触发一次新的数据库查询——列出20条房源就会产生至少21条SQL。这个现象在Django里叫N+1查询问题。解决的方案是ORM的select_related方法。

# house/views.py 修改上方列表查询 houses = House.objects.select_related('category', 'publisher').filter(status=True)

逻辑说明:select_related通过SQL里的JOIN把关联表的数据一次性查出来,对应外键关系(ForeignKey和OneToOneField)有效。加了它之后,上面的列表查询从原来的21条SQL变成1条带JOIN的SQL,页面加载速度会有可感知的提升,更重要的是在数据库监控工具里看不到一堆重复查询记录。

验证前后差异的土办法是使用django.db.connection的queries属性:

from django.test.utils import CaptureQueriesContext from django.db import connection with CaptureQueriesContext(connection) as ctx: houses = House.objects.select_related('category', 'publisher').filter(status=True) for h in houses[:5]: print(h.category.name, h.publisher.username) print(f'查询次数:{len(ctx.captured_queries)}')

如果注释掉select_related,这段代码会产生6条以上SQL;加上后稳定在1到2条。这个优化写在论文里是实打实的技术亮点,老师问起来也能解释清楚原理。

5.2 Django版本升级时三个必查的兼容点

Django从2.x到4.x迭代过程中,有些写法被废弃或改变了,网上很多老教程里的代码直接拷贝到新版本里会报错。答辩前检查自己项目里有没有用到这几个废弃API,能避免现场改代码的尴尬。

第一个是url()函数被path()取代。Django 2.0以后推荐的URL写法是path(),正则路由用re_path()。如果看到老教程里写url(r'^house/$', views.house_list),在Django 4.2里会报TypeError。第二个是force_text变成了force_str,Django 4.0删除了旧的force_text函数,自定义模板过滤器或Admin方法里用到了会直接ImportError。第三个是USE_L10N被移除,Django 4.0起L10N设置合并到了USE_I18N里,留着旧配置不会报错但也没有作用,属于无效配置。

# 快速排查项目里是否有废弃API pip install django-upgrade # 在项目根目录执行自动升级检测 django-upgrade --target-version 4.2 .

django-upgrade会自动扫描整个项目并提示哪些代码可以替换为最新写法,而且可以自动改写。注意执行前做好版本备份,改完之后重新跑一遍功能测试确认没有行为变化。

5.3 部署前Admin所有的防护性操作

系统开发完成后,部署到服务器或交给老师验收前,还有几项收尾操作容易被忽略。第一个是关闭DEBUG模式,settings.py里把DEBUG设为False,同时加ALLOWED_HOSTS配置——不设ALLOWED_HOSTS的话模板里的静态文件会全部失效,而且浏览器访问会被拒绝。第二个是把SECRET_KEY换成一套随机生成的长字符串,部署在公网环境时密钥泄露会给会话伪造留下风险。

# jiaju/settings.py 生产环境关键配置 DEBUG = False ALLOWED_HOSTS = ['*'] # 真实部署时换成自己的域名或IP # 生成随机SECRET_KEY的方式: # python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())" SECRET_KEY = '替换为随机生成的长字符串'

当Debug=False后,Django不再处理静态文件请求,需要在项目根urls.py里追加一段静态文件路由才能让admin后台的CSS正常加载:

# jiaju/urls.py 追加 from django.conf import settings from django.conf.urls.static import static if settings.DEBUG: urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) else: from django.views.static import serve import re urlpatterns += [ # 生产模式下由Django直接服务media文件的临时写法,部署Nginx后可删除 path('media/<path:path>', serve, {'document_root': settings.MEDIA_ROOT}), ]

这段代码的思路是:开发模式下使用Django自带方式服务MEDIA文件,生产模式下临时用serve视图兜底。等真正部署到Nginx后,这段media路由可以删掉,由Nginx的alias指令接管。用这种方式的最小改动即可让系统从开发环境平滑过度到生产验收环境。

5.4 用Sqlite换成MySQL后最容易出现的坑

第2章已经配置了MySQL连接,但开发中途从SQLite切换过来时,有三个地方极容易踩坑。第一个是表数据迁移。Django的migrate只能建表,不能把SQLite里已有的数据导入MySQL。可以用django-db-export这个第三方工具导出JSON再导入,也可以写脚本通过ORM遍历旧表写入新表。

第二个坑是MySQL大小写敏感问题。MySQL在Linux下对表名区分大小写,而Django默认生成的表名都是小写,这个没问题。但如果你在models里的Meta中手动指定了db_table,而且表名带了大写字母,在Linux服务器上就会报“Table doesn't exist”。

第三个坑是pymysql的版本兼容性。pymysql 1.1.0和Django 4.2搭配没问题,但如果用了pymysql的旧版本(比如0.9.x),连接时会报“cryptography package is required for sha256_password or caching_sha2_password”错误。这个错误很常见,处理方式是升级pymysql或安装cryptography库:

pip install cryptography

MySQL 8.0默认的认证插件是caching_sha2_password,旧版pymysql不支持这种认证方式。install cryptography后,pymysql就能用它来完成安全连接握手,错误随之消失。这也解释了为什么很多教程里让“安装mysqlclient而不是pymysql”——mysqlclient的原生C库直接支持所有MySQL认证插件,但安装它需要编译环境。Windows上如果没有装过Visual C++ Build Tools,安装mysqlclient会卡在编译环节,所以纯Python的pymysql加cryptography是最少折腾的组合。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询