Django+Vue前后端分离租房管理系统开发全攻略

Django+Vue前后端分离租房管理系统开发全攻略 简介这是一份基于Python、Django与Vue.js开发并整合成的租房管理系统完整项目主要面向毕业设计、课程设计等学习场景集中解决房源信息发布、在线查询、房屋管理和房东租客交互等核心问题。后端依托Django框架的ORM、模板、身份验证、URL路由与表单处理能力前端采用Vue声明式渲染、组件化、虚拟DOM、指令系统和Vuex状态管理能够帮助学习者掌握前后端分离开发的全栈链路。压缩包共418个文件总体积约25.05MB其中包含164张jpeg、35张jpg和29张png图片素材用于界面展示与数据演示43个vue组件与48个ts文件构成前端核心逻辑31个py文件承担Django后端模型、视图、路由和配置另有js、less、json、md等文件用于静态资源、样式、配置和说明。目录结构明了便于按模块阅读从数据库建模到接口联调、页面交互均有完整示例。目前已有145人学习下载适合需要快速搭建租房类系统、理解数据库建模与接口设计或希望通过完整项目提升工程能力的开发者参考。1. 用 Django Vue 拆解租房管理系统的第一性原理从开发视角看租房管理系统在毕业设计里出现频率极高因为它天然覆盖了三类用户房东、租客、管理员、两类核心流程发布房源、预约/签约和一类典型管理场景订单和状态流转。很多人拿到“pythondjangovue开发”这个组合后第一反应是先去写代码但实际落地时最花时间的却是另一件事把房源、租约、订单、收藏这些对象的关系理清楚。前后端分离不是把页面拆成两块就完事而是先让数据模型能同时支撑 Vue 页面的展示颗粒度和 Django 后台的管理粒度。记住一个经验建模阶段多花一天联调阶段就能省三天。下面按我自己的实现顺序展开后端先行、接口随后、Vue 页面再跟上最后讲部署和答辩演示。2. 后端建模与接口把房源、租客、订单变成可查询的数据表2.1 Django Model 设计先画清楚业务边界再动手建表在做租房系统时不要一上来就写models.py先画一张业务关系图。租客搜索房源、房东发布房源、租客发起预约/订单、后台管理所有信息至少需要四张核心表房屋信息表 House、用户表Django 自带 User 扩展、订单表 RentOrder、收藏表 Favorite。常见的错误是“一间房屋只对应一个租客”一旦出现多段租期你就会被迫加字段来补救最终生成的 SQL 结构很难看。正确的做法是把房屋、租客、订单解耦把“当前是否可租”作为房屋的状态字段而不是通过订单是否存在来推导。下面是一份可直接运行的房屋模型# apps/house/models.py from django.db import models from django.contrib.auth.models import User class House(models.Model): STATUS_CHOICES ( (available, 待租), (rented, 已租), (offline, 已下架), ) title models.CharField(房源标题, max_length128) cover models.ImageField(封面图, upload_tohouse/%Y/%m/, blankTrue) price models.DecimalField(月租金, max_digits8, decimal_places2) area models.FloatField(面积/m², default0) house_type models.CharField(户型, max_length32, default1室1厅) address models.CharField(地址, max_length255) description models.TextField(房源描述, blankTrue) status models.CharField(状态, max_length12, choicesSTATUS_CHOICES, defaultavailable) landlord models.ForeignKey(User, on_deletemodels.CASCADE, related_namehouses, verbose_name房东) created_at models.DateTimeField(auto_now_addTrue) updated_at models.DateTimeField(auto_nowTrue) class Meta: ordering [-created_at] def __str__(self): return f{self.title}{self.get_status_display()}这段代码里有两个关键点related_namehouses让user.houses.all()可以直接拿到该用户发布的全部房源避免额外查询choices字段让状态在 Django Admin 里自动变成下拉选择同时get_status_display()能直接拿到中文描述前端接口里展示状态文本时不需要再维护一份映射。订单表不能直接引用 House 快照因为房屋价格可能会改。常见做法是把成交时价格冗余一份在订单里class RentOrder(models.Model): ORDER_STATUS ( (pending, 待支付), (paid, 已支付), (done, 已结束), (cancelled, 已取消), ) house models.ForeignKey(House, on_deletemodels.CASCADE, related_nameorders) tenant models.ForeignKey(User, on_deletemodels.CASCADE, related_namerent_orders) rental_price models.DecimalField(成交月租金, max_digits8, decimal_places2) start_date models.DateField(起租日期) end_date models.DateField(结束日期) status models.CharField(订单状态, max_length12, choicesORDER_STATUS, defaultpending) created_at models.DateTimeField(auto_now_addTrue)价格冗余的原因很直白订单代表一次已经发生的交易如果后续房东修改房源价格历史订单的金额不能跟着变。同理地址、户型这类信息在订单里不必重复存储通过house外键实时取即可但金额这种“合同约定项”必须冗余。这也是答辩时容易被导师追问的细节提前想清楚比临时解释更有说服力。收藏表用联合唯一约束避免同一个用户对同一房源反复收藏class Favorite(models.Model): house models.ForeignKey(House, on_deletemodels.CASCADE, related_namefavorites) user models.ForeignKey(User, on_deletemodels.CASCADE, related_namefavorites) created_at models.DateTimeField(auto_now_addTrue) class Meta: unique_together (house, user)unique_together是数据库层的约束配合后面的get_or_create使用并发请求下也不会产生重复数据。2.2 DRF 序列化器与视图集接口字段如何对齐前端需求模型建好后下一步是把模型实例变成 JSON。推荐继续用 Django REST frameworkDRF因为视图集和路由生成器能让接口层代码量压到最小。序列化器最简单的写法是直接引用模型字段但这样会把landlord、created_at这些内部字段暴露给前端。租房系统的接口面是给用户看的应当返回房东昵称、房源封面 URL、状态的中文文本。# apps/house/serializers.py from rest_framework import serializers from .models import House class HouseListSerializer(serializers.ModelSerializer): landlord_name serializers.CharField(sourcelandlord.username, read_onlyTrue) status_text serializers.CharField(sourceget_status_display, read_onlyTrue) cover_url serializers.SerializerMethodField() class Meta: model House fields [id, title, cover_url, price, area, house_type, address, status_text, landlord_name] def get_cover_url(self, obj): request self.context.get(request) if obj.cover and request: return request.build_absolute_uri(obj.cover.url) return Nonesource参数用于指定模型实例上的属性路径get_status_display是 Django 为 choices 字段自动生成的方法SerializerMethodField则适合做封面 URL 拼装。注意这里没有使用read_only_fields而是直接将字段写进fields并在字段上声明read_onlyTrue两种写法等价但后者更直观适合课程设计阶段逐字段核对。视图集推荐按“列表/详情”拆成两个序列化器避免列表页因为嵌套详情而变慢。查询逻辑要处理好权限边界房东只能改自己的房源# apps/house/views.py from rest_framework import viewsets, permissions from .models import House from .serializers import HouseListSerializer, HouseDetailSerializer class HouseViewSet(viewsets.ModelViewSet): def get_queryset(self): # 列表页只展示待租房源详情页不做状态限制 queryset House.objects.select_related(landlord) if self.action list: queryset queryset.filter(statusavailable) return queryset def get_serializer_class(self): if self.action list: return HouseListSerializer return HouseDetailSerializer def get_permissions(self): if self.action in (create, update, partial_update, destroy): return [permissions.IsAuthenticated()] return []这里写了get_queryset和get_permissions两个钩子已经解决三个问题列表页自动过滤掉已租和下架的房源发布、修改、删除需要登录列表用select_related避免每行数据都查询一次房东用户名。如果你后面加“同小区推荐”需求只需在retrieve里根据地址关键字做一次filter(address__contains...)不需要改动视图集结构。2.3 Django Admin 后台用最小配置支撑“房屋出租管理”毕业设计答辩时评委往往会点开后台看你能不能管理数据。Django Admin 是这里最稳的底牌几行配置就能让整个系统的管理端成型。# apps/house/admin.py admin.register(House) class HouseAdmin(admin.ModelAdmin): list_display (title, landlord, price, status, created_at) list_filter (status, house_type) search_fields (title, address, landlord__username) list_editable (status,) date_hierarchy created_at后台功能配置项效果说明列表展示list_display后台列表页直接显示房源标题、房东、价格、状态左侧筛选list_filter按状态、户型快速过滤适合演示时筛选已租房源搜索框search_fields支持标题、地址、房东用户名的模糊查询行内编辑list_editable不用进入详情页就能切换上下架状态时间层级date_hierarchy按日期快速浏览新建房源date_hierarchy在房源数量较多时特别好用它会在列表页顶部生成按年份、月份下钻的导航条比手动排序直观。想要把后台界面做得更耐看可以考虑接入 SimpleUI 这类主题包但要注意与 Django 版本的兼容性装好后先在本地跑一遍迁移再提交到演示环境。3. Vue3 前端实现从零搭建租房门户的页面骨架3.1 工程初始化与路由设计页面不再是静态模板前端用 Vue3 Vite 而不是 Vue CLI因为 Vite 依赖安装速度更快Vue3 组合式 API 在面试环节也更好讲。初始化命令npm create vitelatest rental-web -- --template vue cd rental-web npm install npm install axios vue-router4 npm run dev拿到工程后的第一个动作不是写页面而是把路由规划好。租房系统的前端路由按访客视角和登录视角拆成两层未登录用户可访问首页、房源列表、房源详情、登录注册登录后用户才能看到“我的预约”“我的收藏”“发布房源”。Vue Router 的beforeEach守卫承担这一职责// src/router/index.js import { createRouter, createWebHistory } from vue-router const routes [ { path: /, component: () import(/views/Home.vue) }, { path: /houses, component: () import(/views/HouseList.vue) }, { path: /house/:id, component: () import(/views/HouseDetail.vue) }, { path: /login, component: () import(/views/Login.vue) }, { path: /publish, component: () import(/views/PublishHouse.vue), meta: { requiresAuth: true } }, ] const router createRouter({ history: createWebHistory(), routes, }) router.beforeEach((to) { const token localStorage.getItem(access_token) if (to.meta.requiresAuth !token) { return { path: /login, query: { redirect: to.fullPath } } } })路由路径页面组件访问控制/Home.vue公开/housesHouseList.vue公开/house/:idHouseDetail.vue公开动态路由参数 id/loginLogin.vue公开/publishPublishHouse.vue需登录未登录跳转登录页并携带 redirect用meta.requiresAuth标记受保护页面的做法比在组件里逐个判断localStorage清晰。query里的redirect参数会让登录页认证成功后跳回原页面体验更接近真实网站。使用createWebHistory后本地开发一切正常但打包部署到 Nginx 时必须追加try_files配置否则刷新详情页 404这一坑在第 5 章专门展开。3.2 axios 封装与 API 对接把后端数据渲染到卡片与后端通讯前先做一个统一请求模块。租房系统接口访问频率不高但 token 失效、后端 500 这类问题必须由拦截器统一处理否则每个页面都要写一遍错误提示。// src/utils/request.js import axios from axios import router from /router const request axios.create({ baseURL: /api, timeout: 10000, }) request.interceptors.request.use(config { const token localStorage.getItem(access_token) if (token) config.headers.Authorization Bearer ${token} return config }) request.interceptors.response.use( res res.data, err { if (err.response?.status 401) { localStorage.removeItem(access_token) router.push({ path: /login }) } return Promise.reject(err) } ) export default requestbaseURL: /api对应后端 Django 配置的 URL 前缀。前端开发模式用 Vite 代理把/api转发到http://127.0.0.1:8000生产环境由 Nginx 转发前后端各管一段。列表页拿到数据后渲染卡片!-- src/views/HouseList.vue -- script setup import { ref, onMounted } from vue import request from /utils/request const houses ref([]) const loading ref(false) const loadHouses async () { loading.value true try { const data await request.get(/houses/, { params: { status: available, page: 1 } }) houses.value data.results || data } finally { loading.value false } } onMounted(loadHouses) /script这个示例传了两个参数status过滤可租房源page配合下一章的分页配置使用。前端不必关心分页算法只需要把data.results当成当前页数据渲染data.count是总数。显示房源卡片时价格、面积、户型这类高频字段直接展示详情入口用router-link跳转。后续要增加“按价格排序”或“地图定位”功能都只需在列表页加按钮和参数。3.3 房源详情页与收藏功能动态路由与登录态配合详情页通过useRoute()拿id再调用后端GET /houses/{id}/获取完整信息。比信息展示更有代表性的是“收藏”功能它要求前端同时处理登录态、按钮状态和接口调用失败三个逻辑。script setup import { ref } from vue import { useRoute } from vue-router import request from /utils/request const route useRoute() const houseId route.params.id const isFavorite ref(false) const toggleFavorite async () { if (!localStorage.getItem(access_token)) { alert(请先登录再收藏) return } try { const res await request.post(/houses/${houseId}/favorite/) isFavorite.value res.is_favorite } catch (e) { alert(e.response?.data?.detail || 操作失败) } } /script示例用alert是为了减少组件库依赖如果项目里已装 Element Plus 或 Naive UI换成 message 组件即可。收藏操作放在POST /houses/{id}/favorite/这种自定义 action 上后端在HouseViewSet增加装饰器方法from rest_framework.decorators import action from rest_framework.response import Response action(detailTrue, methods[post]) def favorite(self, request, pkNone): house self.get_object() fav, created Favorite.objects.get_or_create(househouse, userrequest.user) if not created: fav.delete() return Response({is_favorite: not created})这个接口把收藏与取消收藏合并成一个请求get_or_create返回的created为 True 表示新增收藏为 False 表示已存在并执行删除。前端只需在响应后切换按钮样式不需要自己维护“是否已收藏”的状态刷新页面后也不会丢失。联调时遇到detail参数报错或 action 路由不生效先确认视图集继承了viewsets.ModelViewSet只有detailTrue的 action 才能通过/houses/3/favorite/方式访问。4. DjangoVue 联调必须跨过的四道坎CORS、JWT、分页与图片上传4.1 CORS 跨域配置前后端分离的第一道击破点前后端分离项目本地联调时最大的障碍是浏览器同源策略。Vite 开发服务器默认跑在 5173 端口Django 跑在 8000 端口端口不同即跨域。开发环境优先用 Vite 代理浏览器看到的请求仍是同源的// vite.config.js server: { proxy: { /api: { target: http://127.0.0.1:8000, changeOrigin: true } } }生产环境若前后端不在同一 Nginx 端口下则需在 Django 装django-cors-headers并配置允许来源# settings.py INSTALLED_APPS [corsheaders, ...] MIDDLEWARE [corsheaders.middleware.CorsMiddleware, ...] CORS_ALLOWED_ORIGINS [http://localhost:5173, https://your-domain.com]CorsMiddleware必须放在CommonMiddleware之前位置错了会导致预检请求失败。联调时接口通但浏览器报 CORS 错误优先检查这两项配置。4.2 JWT 认证登录注册与接口权限控制租房系统的登录认证推荐用djangorestframework-simplejwt。接入成本低、无状态、不依赖 Session适合前后端分离。安装后在settings.py配置默认认证类根路由补充登录和刷新 token 的端点# settings.py REST_FRAMEWORK { DEFAULT_AUTHENTICATION_CLASSES: ( rest_framework_simplejwt.authentication.JWTAuthentication, ), } # urls.py from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView urlpatterns [ path(api/token/, TokenObtainPairView.as_view()), path(api/token/refresh/, TokenRefreshView.as_view()), ]前端在 axios 拦截器里统一加 Authorization登录后把返回的access和refresh分别存入 localStorage 即可。提示不要把access有效期设太长。SIMPLE_JWT里把ACCESS_TOKEN_LIFETIME设为 60 分钟足够支撑整场答辩REFRESH_TOKEN_LIFETIME设为 1 天即可。4.3 分页与搜索参数的约定比实现更重要DRF 默认不开启分页但租房列表页一旦房源超过几十条不分页就会明显卡顿。在settings.py中开启全局分页REST_FRAMEWORK { DEFAULT_PAGINATION_CLASS: rest_framework.pagination.PageNumberPagination, PAGE_SIZE: 10, }开启后接口返回{count: 32, next: ..., previous: null, results: [...]}。前端列表页用params: { page: currentPage }传页码返回next为 null 表示没有下一页可以停用“加载更多”。筛选交给django-filter省去手写一堆if request.query_params.get(...)# settings.py INSTALLED_APPS [django_filters] REST_FRAMEWORK { DEFAULT_FILTER_BACKENDS: (django_filters.rest_framework.DjangoFilterBackend,), } # filters.py from django_filters import rest_framework as filters from .models import House class HouseFilter(filters.FilterSet): min_price filters.NumberFilter(field_nameprice, lookup_exprgte) max_price filters.NumberFilter(field_nameprice, lookup_exprlte) kw filters.CharFilter(field_nametitle, lookup_expricontains) class Meta: model House fields [house_type, status]前端请求/api/houses/?kw地铁min_price1500max_price3000page1后端自动完成过滤后分页。注意field_name必须是模型真实字段名lookup_expr是 Django ORM 查询表达式icontains表示不区分大小写模糊匹配。4.4 图片上传接口参数不是 JSON 而是 FormData房源必须有图但图片上传是前后端联调最头疼的一环。Django 端先配置媒体文件地址MEDIA_URL /media/ MEDIA_ROOT BASE_DIR / media根urls.py里加开发环境媒体路由。前端上传的关键错误在于把图片文件用 JSON 序列化传给后端。图片是二进制数据JSON 无法直接承载必须使用FormDataconst formData new FormData() formData.append(title, form.title) formData.append(price, form.price) formData.append(area, form.area) formData.append(cover, form.coverFile) const res await request.post(/houses/, formData, { headers: { Content-Type: multipart/form-data } })Content-Type交由浏览器自动生成手动指定容易漏掉boundary参数。后端报The submitted data was not a file检查request.FILES是否存在以及前端input的typefile是否真被选中文件。这个报错在毕设答辩中几乎必现提前在本地模拟一遍“选择文件→提交→后台看到图片”的流程能省掉现场调试的尴尬。功能方法路径认证说明房源列表GET/api/houses/否分页与筛选房源详情GET/api/houses/{id}/否完整房源信息发布房源POST/api/houses/是使用 FormData收藏/取消POST/api/houses/{id}/favorite/是切换收藏状态登录POST/api/token/否返回 access 与 refresh创建预约POST/api/appointments/是提交看房时间5. 从开发机到演示环境DjangoVue 部署、避坑与答辩演示技巧5.1 部署前必须处理的 Django 配置项本地开发跑通只是第一步演示环境通常是笔记本或实验室服务器资源不稳定提前把两件事做掉DEBUG False ALLOWED_HOSTS [localhost, 127.0.0.1, 你的服务器IP]DEBUGFalse之后媒体文件访问会失效这是最常见的问题。需要在根urls.py中显式告诉 Django 处理媒体文件from django.conf import settings from django.conf.urls.static import static urlpatterns static(settings.MEDIA_URL, document_rootsettings.MEDIA_ROOT)数据库环节提前确认。开发时默认用的 SQLite 文件拷贝到其他机器演示时路径对不上会导致启动失败。如果使用 MySQL记得装好mysqlclient并在DATABASES里正确配置HOST、PORT否则启动即报错。用宝塔面板部署的话Nginx 配置里的try_files写法与裸装一致区别只在面板站点设置界面里填写原理相同。5.2 前端打包后的经典布局与路由异常Vue 项目执行npm run build后dist/目录生成assets/下的 JS/CSS 文件。直接打开dist/index.html往往白屏或只有文字没有样式原因是静态资源路径写死为根路径。把vite.config.js里的base设为./让资源以相对路径加载export default defineConfig({ base: ./, plugins: [vue()], })路由用createWebHistory刷新/house/3时若 Nginx 没有回退到 index.html 就会 404location / { root /var/www/rental-web/dist; try_files $uri $uri/ /index.html; }这一步解决 SPA 路由的“刷新死链”问题面试被问到前端部署答得上这条就能体现真实上线经验。5.3 答辩演示时的数据准备与故障预案演示效果一半取决于数据。房源列表准备 12-15 条覆盖不同户型、价格区间和状态含 2-3 条已租、1 条下架让筛选有内容可展示。演示顺序固定为首页展示 → 列表筛选 → 详情页 → 登录 → 收藏 → 后台管理。后台先展示房源列表和筛选状态再点进一条房源说明字段设计比直接念代码更有说服力。故障预案准备两套第一后端接口超时先看 Django 终端日志最后几行确认是数据库问题还是路由问题路由报错时用resolve(/api/houses/1/)快速确认视图匹配是否正常。第二前端白屏时按 F12 看 Console重点看网络请求失败还是 JS 语法报错。现场演示时把 Django 开发服务器和 Vite 日志窗口都打开评委看到请求被成功转发本身就是一种可信度展示。本文还有配套的精品资源点击获取