Django房源系统开发实战:数据模型设计、Admin后台与性能优化指南

Django房源系统开发实战:数据模型设计、Admin后台与性能优化指南 简介这是一份基于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 django4.2.16 # 验证安装 python -m django --version逻辑说明venv是把依赖隔离在本项目目录下的标准做法比直接往系统Python里pip install安全得多——因为一个项目装了pymysql另一个没装互不干扰。激活后命令行前面会出现(venv)看到这个再执行后续命令才生效。固定Django版本号很重要“最新版”不一定“最稳定”Django每次大版本发布都会调整某些API的默认行为毕业论文写到一半被升级打断很麻烦。参数说明django4.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数据库并连接DjangoSQLite虽然零配置就能跑但数据库文件跟着源码走提交到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在服务管理器找MySQL80Linux执行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关掉CtrlC接着配置路由分发。项目根路由负责“把带什么前缀的请求交给哪个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, nameindex), ] # house/views.py 添加一个最简单的视图函数 def index_view(request): return render(request, house/index.html)参数说明path第一个参数是URL表达式空字符串代表域名根路径admin/是精确前缀匹配。nameindex给这个路由起了名字模板里用{% 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_length50) sort_order models.IntegerField(排序权重, default0) 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_length200) category models.ForeignKey(HouseCategory, on_deletemodels.CASCADE, verbose_name所属分类) rent_type models.CharField(租售类型, max_length10, choicesRENT_TYPE_CHOICES, defaultrent) price models.DecimalField(价格, max_digits10, decimal_places2) area models.DecimalField(面积(㎡), max_digits8, decimal_places2) bedroom_count models.IntegerField(室, default1) living_room_count models.IntegerField(厅, default1) bathroom_count models.IntegerField(卫, default1) address models.CharField(详细地址, max_length255) description models.TextField(房源描述, blankTrue) cover_image models.ImageField(封面图, upload_tohouse_cover/%Y/%m/, blankTrue, nullTrue) publisher models.ForeignKey(User, on_deletemodels.CASCADE, verbose_name发布人) status models.BooleanField(是否上架, defaultTrue) view_count models.IntegerField(浏览次数, default0) created_at models.DateTimeField(发布时间, auto_now_addTrue) updated_at models.DateTimeField(更新时间, auto_nowTrue) 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_digits10表示总位数10位decimal_places2表示小数位2位最大支持 99,999,999.99房源价格这个量级完全覆盖。rent_type用CharField加choices参数存储的是字符串常量。有人会把这种字段设计成IntegerField加0/1映射但这样写在Django Admin下拉框里显示的是数字不直观而且字符串的可读性在生成报表和API返回时好得多。注意choices枚举值要写语义化的短字符串rent/sell不要写1/2后期维护时一眼能看懂含义。外键category关联HouseCategory表on_deletemodels.CASCADE表示分类被删时该分类下的房源一并删除。也可以用PROTECT保护分类不被误删但毕业设计场景CASCADE更省事不会出现“试图删除分类但被外键约束挡住”的报错。3.2 预约看房和用户收藏的关联表设计房源表只解决了“有什么房子”的问题一个完整系统还要有“用户行为”数据。预约看房表记录用户哪天看了哪套房收藏表记录用户喜欢哪套房。这两张表都要和House以及User做关联。# house/models.py 继续追加 class HouseAppointment(models.Model): 预约看房表 house models.ForeignKey(House, on_deletemodels.CASCADE, verbose_name预约房源) user models.ForeignKey(User, on_deletemodels.CASCADE, verbose_name预约用户) appointment_date models.DateField(预约日期) appointment_time models.TimeField(预约时间段) contact_name models.CharField(联系人, max_length50) contact_phone models.CharField(联系电话, max_length20) remark models.CharField(备注, max_length500, blankTrue) is_visited models.BooleanField(是否已看房, defaultFalse) created_at models.DateTimeField(预约时间, auto_now_addTrue) 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_deletemodels.CASCADE, verbose_name用户) house models.ForeignKey(House, on_deletemodels.CASCADE, verbose_name房源) created_at models.DateTimeField(收藏时间, auto_now_addTrue) 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 PillowPillow是Python的图像处理库ImageField内部依赖它做图片格式验证和尺寸获取。不装Pillowmigrate不会报错但一旦后台提交含图片的表单就会报错。装好之后顺带确认一下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(statusTrue) # 按筛选条件逐层过滤 if category_id: houses houses.filter(category_idcategory_id) if keyword: # icontains表示不区分大小写的模糊匹配 houses houses.filter(title__icontainskeyword) if rent_type: houses houses.filter(rent_typerent_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(statusTrue)得到的QuerySet是惰性的——数据库查询并不会立即执行只有真正遍历时才会触发生成SQL。所以在filter链上不断叠加条件不会造成多次查询最终只执行一条带WHERE条件组合的SQL。category_id从GET参数里取出来时是字符串而filter里的category_id是字段名加后缀注意写法是category_id而不是category前者直接对应数据库外键字段后者需要Django额外做JOIN解析。Paginator(houses, 6)里的6是每页条数毕业设计截图时每页显示6条卡片式房源正好两行三列视觉效果好也不拥挤。page参数从URL的?page2里取值PageNotAnInteger和EmptyPage这两个异常类分别处理“页码不是数字”和“页码超出范围”两种场景第一种兜底回第一页第二种兜底到最后一页保证用户怎么乱输URL都不会看到报错页。问题在于模板。模板里要显示分页链接通常用的是Django内置的分页导航条但默认样式朴素。如果想做出“上一页、下一页、页码数字”的常规分页组件可以手动在模板循环输出页码判断当前页高亮。!-- house/templates/house/house_list.html 分页部分 -- nav classpagination span classstep-links {% if page_obj.has_previous %} a href?page1{% 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 classcurrent 第 {{ 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如果只带page2筛选条件就丢了。解决方式就是把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, idhouse_id, statusTrue) # 用session记录当前用户已浏览过的房源ID列表防止刷新刷量 viewed_houses request.session.get(viewed_houses, []) if house_id not in viewed_houses: # F表达式避免并发下数值覆盖 House.objects.filter(idhouse_id).update(view_countmodels.F(view_count) 1) viewed_houses.append(house_id) request.session[viewed_houses] viewed_houses # 推荐相同分类下的其他房源 related_houses House.objects.filter(categoryhouse.category, statusTrue).exclude(idhouse.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(idhouse.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 }}/p4.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{} stylewidth: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_hierarchycreated_at会生成一个按日期钻取筛选的导航条对时间跨度数据很多的后台特别实用。readonly_fields里的字段不能为空且不在表单的可编辑区域出现view_count是系统自动累计的不允许管理员手改成任意数。cover_preview这个自定义方法是Admin里展示图片缩略图的标准做法format_html负责把图片标签安全格式化。登录Admin后台后界面的“站点管理”标题默认是英文。如果论文截图需要中文界面在settings.py里设置# jiaju/settings.py LANGUAGE_CODE zh-hans TIME_ZONE Asia/ShanghaiLANGUAGE_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朝阳, statusTrue) print(houses.query) # 打印实际执行的SQL print(houses.count()) # 验证价格区间筛选5000到8000元的出租房源 result House.objects.filter(rent_typerent, price__gte5000, price__lte8000) print(result.query)打印的query属性会输出Django生成的原始SQL语句肉眼检查WHERE子句是否符合预期。这是调试ORM最直接的武器——页面显示结果不对时先用Shell确认数据层正确再排查视图和模板的问题问题定位效率提高一大截。5. Django版本差异、N1查询和性能优化系统功能跑通之后答辩前最值得做的优化有两类一类是查询性能优化一类是生产环境部署。这两类工作在论文的“系统测试与优化”章节里通常是必写内容而且面试时也常被追问。5.1 select_related解决列表页查询爆炸问题在房源列表页中模板要显示每条房源的分类名称和发布人用户名这两个字段分别在HouseCategory和User表里。默认情况下每次访问house.category.name都会触发一次新的数据库查询——列出20条房源就会产生至少21条SQL。这个现象在Django里叫N1查询问题。解决的方案是ORM的select_related方法。# house/views.py 修改上方列表查询 houses House.objects.select_related(category, publisher).filter(statusTrue)逻辑说明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(statusTrue) 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_strDjango 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 替换为随机生成的长字符串当DebugFalse后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_rootsettings.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 doesnt 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 cryptographyMySQL 8.0默认的认证插件是caching_sha2_password旧版pymysql不支持这种认证方式。install cryptography后pymysql就能用它来完成安全连接握手错误随之消失。这也解释了为什么很多教程里让“安装mysqlclient而不是pymysql”——mysqlclient的原生C库直接支持所有MySQL认证插件但安装它需要编译环境。Windows上如果没有装过Visual C Build Tools安装mysqlclient会卡在编译环节所以纯Python的pymysql加cryptography是最少折腾的组合。本文还有配套的精品资源点击获取