基于Django+Vue3的校园租房系统全栈开发实战

发布时间:2026/9/10 1:21:05

基于Django+Vue3的校园租房系统全栈开发实战 做这个Python Vue3的校园租房系统前前后后花了差不多三周时间。中途踩了不少坑也推翻过几次方案最后落地的一套前后端分离架构我觉得挺有代表性的——既有校园业务场景的典型功能又把Django后端和Vue3前端的关键技术点都串起来了。这篇文章就把我整个实现过程、技术选型思路和避坑记录完整写出来希望能帮到正在做类似毕设或项目练手的朋友。先说下这套系统最终长什么样学生用户租客可以浏览房源、按校区或价格筛选、收藏房源、在线发起租房订单房东可以是个人也可以是校内宿管或周边房东能发布房源、管理房源上下架、处理租客订单管理员在后端可以审核房源、管理用户和统计平台数据。技术栈上后端用的是Python 3.10 Django 4 Django REST Framework MySQL认证方案用的JWTsimplejwt前端用Vue 3.2 Vite Pinia Vue Router 4 Ant Design Vue 3.x。整套系统完全前后端分离开发时通过Vite代理联调部署时用Nginx托管前端并反向代理后端接口。如果你是刚开始接触前后端分离项目或者正在准备自己的课程设计、毕业设计这篇文章可以当一份完整参考。下面按我的实际开发顺序来梳理从整体思路、后端实现、前端实现、接口联调到最后的高频报错排查一步一步说清楚。1. 项目定位与整体设计思路1.1 校园租房场景有什么特殊需求校园租房和市面上通用的租房平台最大的区别在于“人群固定、周期短、信任成本高”。学生租房的核心场景无非这几种考研复习需要在校区附近短租几个月、毕业实习需要租半年、寒暑假留校备考或者外地同学来学校交流需要临时住宿。这些场景决定了系统不能像贝壳、自如那样做长租逻辑而是需要能表达“短租周期”、“按床位出租”、“距离校区距离”这类校园特有要素。我在设计的时候把用户分为租客和房东两类还允许同一账号在两类身份之间切换——因为在校园场景里很多房东本身也是教职工甚至高年级学生他们既可能出租房源也可能出去租房。这个身份切换的需求一开始没做是在画原型的时候和一个做宿管的老师聊过之后才决定必须支持否则后期数据模型改起来非常痛苦。另外校园租房的信任机制很重要。小程序、微信群里的租房信息最大问题是真假难辨所以系统里必须要有“用户认证”概念比如学生认证、教职工认证。虽然最终我实现的是简版后台手动审核但在表设计上给后续接入学生证上传、学号验证预留了字段。这一点建议大家在设计阶段就想清楚不然后面加字段做数据迁移虽然不复杂但如果有线上数据就麻烦了。1.2 为什么选Python Vue3而不是其他组合后端选Python而不是Java或者Node原因有三点。第一Django带Admin后台做一个校园级别的管理系统管理员页面几乎不用额外开发Django Admin改改配置就能管理用户、审核房源这个效率是Spring Boot没法比的。第二Django的ORM写起来比MyBatis直观太多尤其适合一个人开发全栈项目可以在很短时间内把数据模型建好。第三Python的生态里做爬虫、数据分析都很方便如果后期想抓取周边房源数据做价格分析或者对订单数据做统计报表直接写Python脚本就行不需要跨语言。前端选Vue3而不用React最主要的原因是Vue的上手曲线更平缓模板语法对从没接触过前端框架的人更友好。而选Vue3而不是Vue2则是考虑到Pinia比Vuex更简洁、Composition API对组件逻辑复用更友好而且Vite的构建速度比webpack快一个量级开发体验好很多。组件库方面我用了Ant Design Vue而不是Element Plus纯粹是个人偏好Ant Design的表单校验和表格组件在管理端场景更好用两个库其实都能胜任不必纠结。这套组合的另一个优势是社区资料极其丰富。Django的DRF教程、Vue3的教学视频、前后端分离的实战案例随便一搜一大把遇到问题很容易找到解决方案。对于新手来说基本不太需要啃源码靠搜索就能解决百分之八十的问题。1.3 系统功能模块如何划分我把整个系统拆成租客端、房东端和管理后台三个视角来设计功能而不是直接按传统的前台、后台划分。这样做的原因是租客端和房东端虽然共用前端工程但功能入口、业务逻辑完全不同如果混在一起写代码会非常杂乱。租客端核心模块包括房源浏览与搜索按校区、价格区间、面积、户型筛选、房源详情多图轮播、房东信息、收藏按钮、在线下单选择租期、提交订单、订单管理待支付、待入住、已入住、已退租、个人中心基本信息、我的收藏。房东端核心模块包括房源管理发布房源、编辑、上下架、订单处理接单、拒绝、确认入住、确认退租、收益统计简单订单金额汇总。管理后台则通过Django Admin实现主要做用户管理、房源审核和平台数据概览。业务流转是整个系统的核心。我设计了这样一条主流程租客浏览房源→提交租房订单状态为待房东接单→房东接单状态变为待付款→租客付款状态变为待入住校园场景一般是线下付款所以这里付款是确认付款→租客入住状态变为已入住→租客申请退租/房东确认退租状态变为已结束。这个状态机看似简单但实际实现时最大的坑是状态字段的类型和流转校验我在2.2节会详细说。2. 后端核心实现Django DRF JWT2.1 环境准备与项目初始化Python环境的坑我在Windows上踩过不少。先说结论建议用Python 3.10或3.11不要用最新的3.12/3.13因为部分依赖包尤其是django-cors-headers和一些图像处理库的编译版在新版本上可能还没适配。安装Python时一定要勾选“Add Python to PATH”不然装完以后在终端敲python提示找不到命令只能手动配置环境变量徒增麻烦。安装完Python后我用venv创建了独立的虚拟环境避免和系统全局Python包冲突。项目目录结构如下campus-rent/ ├── backend/ # Django 后端 │ ├── manage.py │ ├── config/ # 项目配置目录 │ ├── apps/ │ │ ├── users/ # 用户模块 │ │ ├── houses/ # 房源模块 │ │ └── orders/ # 订单模块 │ └── requirements.txt ├── frontend/ # Vue3 前端 │ ├── src/ │ ├── package.json │ └── vite.config.js └── README.md创建虚拟环境并安装依赖的操作很简单cd backend python -m venv venv # Windows激活虚拟环境 venv\Scripts\activate # macOS/Linux 激活虚拟环境 # source venv/bin/activate pip install django djangorestframework djangorestframework-simplejwt django-cors-headers mysqlclient pillow依赖安装完成后创建Django项目和应用django-admin startproject config . python manage.py startapp users python manage.py startapp houses python manage.py startapp orders这里有个小习惯把apps统一放在backend/apps/目录下而不是放在根目录。这样做的原因是后续如果项目变大可以方便地用Django的app目录指定也便于把公共组件抽出来。如果你的项目规模不大直接放根目录也没问题。2.2 数据模型设计从需求到表的完整拆解数据模型是一套系统的地基设计错了后面改起来特别费劲。我设计了5张核心表用户表、房源表、房源图片表、订单表、收藏表。下面直接给出我最终落地的模型定义并解释每张表设计时考虑的关键点。用户表没有直接用Django的默认User表而是扩展了一个UserProfile表来存用户类型、手机号、学校、学生证号等业务字段。原因是Django自带的User表字段固定直接改源码权限模型不划算用OneToOne扩展最稳妥。用户类型用一个整数字段表示1代表租客2代表房东3代表既是租客又是房东这样设计比存字符串更节省存储且查询效率更高。房源表设计时几个关键决策租金使用IntegerField而不是DecimalField因为校园房源的租金基本都是整数用IntegerField可以避免前后端浮点运算的精度问题前端传99.99后端得到的可能是99.9899999面积用DecimalField(max_digits5, decimal_places1)保留一位小数状态字段用SmallIntegerField0待审核、1已上架、2已下架、3已驳回。户型字段我用了CharField存“一室一厅”、“四室两卫”这类字符串而不是拆分出室、厅、卫三个字段因为校园房源户型种类少字符串查询也能满足筛选需求不要过度设计。房源图片单独建一张HouseImage表而不是在House表里存一个JSON字段。这样设计的好处是第一多图上传时前端可以逐张上传并返回图片ID第二以后如果要做图片排序、图片懒加载有独立表操作更灵活第三Django Admin里管理图片也方便。订单表的设计是整个系统最核心的部分。关键字段包括订单号用时间戳随机数生成不用自增ID做展示用编号避免被人遍历、关联房源、租客、房东、租期起止日期、租金快照、押金、总金额和状态字段。这里特别要注意“租金快照”这个字段——用户在提交订单那一刻的房价必须存到这个字段里而不能在订单详情时再去读House表当前租金。原因是房东可能随时改价如果订单详情实时读当前价会出现用户看到的价格和支付的价格不一致这在业务上是不可接受的。下面的代码是订单模型的核心片段重点是状态字段和几个约束class Order(models.Model): 租房订单 STATUS_CHOICES ( (0, 待房东接单), (1, 待付款), (2, 待入住), (3, 已入住), (4, 已退租), (5, 已取消), (6, 已拒绝), ) order_no models.CharField(max_length32, uniqueTrue, verbose_name订单编号) house models.ForeignKey(House, on_deletemodels.CASCADE, related_nameorders, verbose_name房源) tenant models.ForeignKey(UserProfile, on_deletemodels.CASCADE, related_nametenant_orders, verbose_name租客) landlord models.ForeignKey(UserProfile, on_deletemodels.CASCADE, related_namelandlord_orders, verbose_name房东) start_date models.DateField(verbose_name入住日期) end_date models.DateField(verbose_name退租日期) rent_price models.DecimalField(max_digits8, decimal_places2, verbose_name租金快照) deposit models.DecimalField(max_digits8, decimal_places2, default0, verbose_name押金) total_amount models.DecimalField(max_digits10, decimal_places2, verbose_name订单总金额) status models.SmallIntegerField(choicesSTATUS_CHOICES, default0, verbose_name订单状态) created_at models.DateTimeField(auto_now_addTrue, verbose_name创建时间) class Meta: db_table rent_order ordering [-created_at]收藏表比较简单就是用户和房源的多对多关系但注意要加UniqueConstraint保证同一用户不能重复收藏同一房源class Favorite(models.Model): user models.ForeignKey(UserProfile, on_deletemodels.CASCADE, verbose_name用户) house models.ForeignKey(House, on_deletemodels.CASCADE, verbose_name房源) created_at models.DateTimeField(auto_now_addTrue) class Meta: db_table rent_favorite constraints [ models.UniqueConstraint(fields[user, house], nameunique_user_house) ]表设计完成后执行makemigrations和migrate。如果在迁移时报依赖错误多半是app的注册顺序或者外键引用路径写错了检查一下INSTALLED_APPS里的app顺序以及ForeignKey中是否写了完整的“app_label.ModelName”。2.3 DRF与JWT认证登录鉴权的完整实现接口鉴权我用的djangorestframework-simplejwt选它而不是django-rest-knox或者自己写Token理由很简单JWT无状态、天然适合前后端分离前端拿到token后存到localStorage请求时放到Authorization头里不需要后端维护会话记录。对于并发量不大的校园系统来说JWT的性能和实现复杂度都更合适。需要注意JWT的一个经典问题token一旦签发在过期之前无法从服务端撤销。所以我在用户修改密码或者被管理员封禁时会强制要求重新登录前端拦截401后跳转登录页。简单粗暴但有效如果追求更精细的控制可以引入token黑名单机制但校园场景没有必要。配置JWT只需要在settings.py里加几行# config/settings.py from datetime import timedelta SIMPLE_JWT { ACCESS_TOKEN_LIFETIME: timedelta(days1), REFRESH_TOKEN_LIFETIME: timedelta(days7), AUTH_HEADER_TYPES: (Bearer,), }然后配置DRF的认证类和权限类REST_FRAMEWORK { DEFAULT_AUTHENTICATION_CLASSES: ( rest_framework_simplejwt.authentication.JWTAuthentication, ), DEFAULT_PERMISSION_CLASSES: ( rest_framework.permissions.IsAuthenticated, ), }这里注意一个坑如果全局配置了IsAuthenticated那么登录接口和注册接口必须单独配置AllowAny权限否则会出现“未认证用户无法调登录接口”的尴尬。解决办法是在对应的类视图上写permission_classes [AllowAny]。登录和注册接口的序列化器我直接用了DRF的Serializer自定义了validate逻辑。注册时校验两次密码一致、手机号唯一密码用Django的make_password加密后保存。登录接口直接使用simplejwt提供的TokenObtainPairView但需要自定义序列化器把用户ID和昵称也返回给前端因为前端一进来就要显示用户信息# apps/users/views.py from rest_framework_simplejwt.views import TokenObtainPairView from .serializers import CustomTokenObtainPairSerializer class CustomTokenObtainPairView(TokenObtainPairView): serializer_class CustomTokenObtainPairSerializer# apps/users/serializers.py class CustomTokenObtainPairSerializer(TokenObtainPairSerializer): def validate(self, attrs): data super().validate(attrs) data[user_id] self.user.id data[nickname] self.user.profile.nickname data[user_type] self.user.profile.user_type return data自定义身份验证后端也要写一下因为simplejwt默认通过USERNAME_FIELD默认是username认证但业务流程里我希望能用手机号或学号登录。这个改造不算难但必须在settings.py里配置AUTH_USER_MODEL或者自定义认证后端。我直接创建了apps/users/backends.pyfrom django.contrib.auth.backends import ModelBackend from django.contrib.auth import get_user_model from django.db.models import Q User get_user_model() class EmailOrPhoneBackend(ModelBackend): def authenticate(self, request, usernameNone, passwordNone, **kwargs): try: user User.objects.get(Q(usernameusername) | Q(phoneusername)) except User.DoesNotExist: return None if user.check_password(password) and self.user_can_authenticate(user): return user return None然后在settings.py里配置AUTHENTICATION_BACKENDS指向这个类。如果不配置你会发现用手机号登录永远提示“用户名或密码错误”这个坑我印象很深。2.4 核心接口实现房源、订单、权限控制房源模块的接口我按照DRF的ModelViewSet来写借助DRF自带的Router注册路由再用一个自定义的过滤器处理搜索和筛选条件。典型代码如下# apps/houses/views.py from rest_framework import viewsets from rest_framework.permissions import IsAuthenticatedOrReadOnly from django_filters.rest_framework import DjangoFilterBackend from rest_framework.filters import SearchFilter, OrderingFilter from .models import House from .serializers import HouseListSerializer, HouseDetailSerializer class HouseViewSet(viewsets.ModelViewSet): permission_classes [IsAuthenticatedOrReadOnly] filter_backends [DjangoFilterBackend, SearchFilter, OrderingFilter] filterset_fields [community, bedroom, status] search_fields [title, address, description] ordering_fields [price, created_at] def get_queryset(self): queryset House.objects.filter(status1) # 只返回已上架房源 price_min self.request.query_params.get(price_min) price_max self.request.query_params.get(price_max) if price_min: queryset queryset.filter(price__gteprice_min) if price_max: queryset queryset.filter(price__lteprice_max) return queryset def get_serializer_class(self): if self.action retrieve: return HouseDetailSerializer return HouseListSerializer def perform_create(self, serializer): serializer.save(landlordself.request.user.profile, status0)这个实现有几个关键点值得细说。get_queryset里面强制过滤了status1确保未上架的房源不会出现在列表接口中这是数据层面上的权限隔离比全部返回后让前端过滤更安全。get_serializer_class区分列表和详情列表接口只返回摘要字段详情接口才返回完整描述和房东信息这样可以显著减少列表页的数据传输量移动端弱网环境加载会快很多。perform_create里自动把当前登录用户设为房东避免前端传一个假的身份。房源图片上传接口单独放在/houses/upload/使用Django的FileUploadParser或自定义基于DRF的APIView。前端用Ant Design Vue的Upload组件拿到图片后逐张上传后端返回图片URL前端再把URL拼到房源数据里一起提交。订单接口的实现是另一个重头戏。订单创建时后端要先做三项校验房源必须存在且状态为上架租客不能是房源房东本人自己租自己的房子业务上不允许租期日期必须合法开始日期不能早于今天退租日期必须晚于入住日期。校验通过后计算总金额总金额 租金快照 × 租期天数 押金。这里租期的天数计算方式也有讲究我用的方式是(end_date - start_date).days即按自然日计算。但要注意退租当天通常不算居住所以实际收费天数应该是(end_date - start_date).days不需要额外减一因为学生租房一般当天退房当天走不存在多算一天的问题。订单状态的流转我用了一个简单的状态机校验函数确保接口不允许跳过状态# apps/orders/utils.py ORDER_TRANSITIONS { 0: [1, 5, 6], # 待房东接单 - 待付款 / 已取消 / 已拒绝 1: [2, 5], # 待付款 - 待入住 / 已取消 2: [3, 5], # 待入住 - 已入住 / 已取消 3: [4], # 已入住 - 已退租 } def can_transition(current_status, new_status): return new_status in ORDER_TRANSITIONS.get(current_status, [])每次状态变更的接口里都调用这个函数不允许用户把已取消的订单改成已入住。这也是整个系统里最值得写的业务逻辑之一。3. 前端核心实现Vue3 全家桶3.1 Vue3工程初始化与目录规划前端工程我用了Vite 4.x创建命令很简单npm create vitelatest frontend -- --template vue cd frontend npm install npm install vue-router4 pinia axios ant-design-vue4创建完成后我把src目录重新规划了一下src/ ├── api/ # 所有接口请求封装按模块拆分 ├── assets/ # 静态资源 ├── components/ # 公共组件 ├── router/ # 路由配置 ├── store/ # Pinia状态管理 ├── views/ │ ├── home/ # 首页、房源列表、房源详情 │ ├── order/ # 订单相关页面 │ ├── landlord/ # 房东端页面 │ └── user/ # 个人中心、登录注册 ├── utils/ # 工具函数axios实例等 └── App.vue目录规划得好不好直接影响后期维护效率。我在第一次写这个项目时把所有页面都放在views下平铺后期找文件找得想哭。第二次重构后按业务域划分子目录文件定位快了很多。这个习惯建议从一开始就养成。3.2 axios二次封装统一处理token和错误axios封装是前后端分离项目里必不可少的一层。没有这层封装每个页面都要重复写请求拦截、错误提示、token失效处理代码会膨胀得很厉害。我的封装思路是创建axios实例时配置基础URL和超时时间请求拦截器里从Pinia读取token并加到Authorization头响应拦截器里统一处理HTTP状态码和业务码。这里我想特别强调一个坑token的获取方式。如果直接在其他模块里import store文件可能会遇到初始化顺序问题因为Pinia store可能在axios模块加载时尚未初始化。我用的方案是在请求拦截器里从localStorage直接读取token因为Pinia里的token本质上也是localStorage的镜像读localStorage不会有初始化顺序问题// src/utils/request.js import axios from axios import { message } from ant-design-vue import router from ../router const request axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || /api, timeout: 15000 }) request.interceptors.request.use(config { const token localStorage.getItem(access_token) if (token) { config.headers.Authorization Bearer ${token} } return config }) request.interceptors.response.use( response response.data, error { if (error.response) { if (error.response.status 401) { localStorage.removeItem(access_token) localStorage.removeItem(user_info) router.push(/login) message.warning(登录已过期请重新登录) } else if (error.response.status 403) { message.error(没有权限执行该操作) } else if (error.response.status 404) { message.error(请求的资源不存在) } else { const msg error.response.data.detail || error.response.data.message || 请求失败 message.error(msg) } } else { message.error(网络异常请检查网络连接) } return Promise.reject(error) } ) export default request接口封装我按模块拆分到api目录下比如house.js里放房源相关的所有接口// src/api/house.js import request from ../utils/request export function getHouseList(params) { return request.get(/houses/, { params }) } export function getHouseDetail(id) { return request.get(/houses/${id}/) } export function createHouse(data) { return request.post(/houses/, data) } export function uploadHouseImage(file) { const formData new FormData() formData.append(file, file) return request.post(/houses/upload/, formData, { headers: { Content-Type: multipart/form-data } }) }3.3 路由与权限守卫页面访问控制路由配置上我分成三个层级公开路由、需要登录的路由、房东专属路由。公开路由包括首页、房源列表、房源详情和登录注册页需要登录的路由包括订单管理、个人中心和收藏房东专属路由包括房源管理、订单处理。路由守卫是前后端分离项目里控制页面的核心。它的作用有两层第一层没有token的用户不能访问需要登录的页面直接跳转到登录页第二层登录用户如果访问了和自己身份不匹配的页面比如租客访问房东管理页要跳转到首页并给出提示。第二层控制很容易被忽略但如果不加用户手动输入/landlord就可以看到房东端接口了。我的路由配置和守卫代码如下// src/router/index.js import { createRouter, createWebHistory } from vue-router const routes [ { path: /, name: Home, component: () import(../views/home/Index.vue) }, { path: /houses/:id, name: HouseDetail, component: () import(../views/home/HouseDetail.vue) }, { path: /login, name: Login, component: () import(../views/user/Login.vue) }, { path: /order, name: OrderList, component: () import(../views/order/OrderList.vue), meta: { requiresAuth: true } }, { path: /landlord/houses, name: LandlordHouses, component: () import(../views/landlord/LandlordHouses.vue), meta: { requiresAuth: true, requiresLandlord: true } }, ] const router createRouter({ history: createWebHistory(), routes }) router.beforeEach((to, from, next) { const token localStorage.getItem(access_token) const userInfo JSON.parse(localStorage.getItem(user_info) || {}) if (to.meta.requiresAuth !token) { next({ path: /login, query: { redirect: to.fullPath } }) return } if (to.meta.requiresLandlord userInfo.user_type 1) { next(/) return } next() })这里用了路由懒加载每个页面按需加载首屏性能会好很多。注意meta配置requiresAuth表示需要登录requiresLandlord表示必须是房东身份。组件内可以通过$route.meta判断当前页面权限来渲染不同的按钮或区块。3.4 登录注册与Pinia用户状态管理登录流程是整个前端的基础。用户在登录页输入手机号和密码调用后端登录接口拿到access_token和refresh_token然后把用户信息保存到localStorage和Pinia中。之后所有请求自动带上token路由守卫放行。Pinia的store设计很简单核心是一个useUserStore管理token、用户信息和登录/登出动作// src/store/user.js import { defineStore } from pinia export const useUserStore defineStore(user, { state: () ({ token: localStorage.getItem(access_token) || , userInfo: JSON.parse(localStorage.getItem(user_info) || {}) }), getters: { isLogin: (state) !!state.token, isLandlord: (state) state.userInfo.user_type ! 1 }, actions: { setLoginData({ token, userInfo }) { this.token token this.userInfo userInfo localStorage.setItem(access_token, token) localStorage.setItem(user_info, JSON.stringify(userInfo)) }, logout() { this.token this.userInfo {} localStorage.removeItem(access_token) localStorage.removeItem(user_info) } } })登录页的表单校验用了Ant Design Vue的Form组件手机号和密码都有各自的校验规则。提交成功后调用userStore.setLoginData然后根据登录来源跳转如果有redirect参数就跳回来源页否则根据user_type跳转租客去首页房东去房源管理页。有一个细节容易踩坑登录成功后用户刷新页面Pinia会重置但localStorage里的token和userInfo还在。所以我在store初始化的state里就直接从localStorage读数据保证刷新后用户状态不丢失。这比在main.js里做额外初始化简单得多。3.5 核心页面实现要点房源列表页是用户进入系统的第一个主要界面我设计成左侧筛选栏右侧卡片列表。筛选栏包括校区下拉框数据不来自后端是前端写死的枚举因为校园校区的数量非常有限、价格区间两个数字输入框、户型单选按钮。筛选条件变化时触发getHouseList重新请求请求参数通过URL参数传递这样刷新页面后筛选条件还会保留在URL中可以分享链接给别人。房源详情页有更多交互细节。顶部是图片轮播用了Ant Design Vue的Carousel组件点击缩略图切换大图。右侧是核心信息标题、租金大字加粗显示、面积、户型、地址、房东信息头像、昵称、认证标识、入住退租日期选择器、下单按钮。下单按钮点击后弹出一个确认弹窗展示租期天数和总价用户确认后调用创建订单接口。这里有个交互细节日期选择器需要设置禁止选择过去的日期否则用户可以选到昨天后端虽然会校验但前端的及时校验体验更好。收藏功能我做了防重复点击处理用户点击收藏时先判断isLogin未登录跳转登录页已登录则调用收藏接口如果已收藏则取消收藏按钮状态实时切换。为了避免用户在接口还没返回时连续点击导致重复请求我用了loading状态做按钮禁用这是一个很常见的用户体验细节。房东端页面相对简单主要是房源管理列表和订单处理。房源管理列表用Ant Design Vue的Table组件每一行显示房源标题、图片缩略图、价格、状态标签和操作按钮。操作按钮根据状态变化待审核的房源只能下架已上架的可以下架和编辑已下架的可以重新上架。订单处理页面侧重点在于展示订单状态流转房东可以通过点击按钮把订单状态变更为下一个状态按钮的状态根据当前订单状态动态渲染。4. 前后端联调与部署落地4.1 跨域问题的两种解法跨域CORS是前后端分离项目一定会遇到的问题但它的解法很固定搞懂原理后就不会再被困扰。所谓跨域简单说就是浏览器出于安全策略默认不允许前端网页去请求不同源的接口。这里的“源”由协议、域名、端口三部分组成只要有任何一个不同就构成跨域。开发环境最推荐的方式是Vite代理在vite.config.js里配置proxy让前端请求转发到后端浏览器看到的请求都在同一个源下从根本上避免跨域。配置如下// vite.config.js export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { /api: { target: http://127.0.0.1:8000, changeOrigin: true } } } })这意味着前端请求/api/houses/实际上被Vite转发到了后端http://127.0.0.1:8000/api/houses/。这个方案的好处是开发时不需要后端开启CORS也不需要在请求里写死完整域名。生产环境则用Nginx做反向代理把前端静态文件和/api开头的请求都放在同一个域名下。Nginx的配置片段如下server { listen 80; server_name your-domain.com; root /var/www/campus-rent/dist; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location /media/ { alias /var/www/campus-rent/media/; } }Nginx配置里最容易踩的坑是location /的try_files配置。如果缺少这行用户直接访问子路由比如刷新/order页面会返回404因为前端路由是history模式Nginx默认会去找/order这个真实文件找不到就404。try_files的作用就是当找不到对应文件时回退到index.html让Vue Router接管路由。除了以上两种方式还有一个方案是后端用django-cors-headers开启CORS。如果你一定要在开发环境直接请求后端地址可以这样配置pip install django-cors-headers然后在INSTALLED_APPS中添加corsheaders在MIDDLEWARE中尽量靠前添加CorsMiddleware最后设置CORS_ALLOWED_ORIGINS [ http://localhost:5173, ]我个人的建议是开发环境用Vite代理彻底绕开跨域生产环境用Nginx代理也保持同源。django-cors-headers只在特殊场景下才需要比如小程序、移动App直接请求后端日常前端项目的跨域问题用代理方案就已经解决了。4.2 图片上传与文件存储图片上传是这类系统里绕不开的功能。我在后端实现了两个上传入口一个是房源图片上传一个是用户头像上传。上传逻辑其实很简单接收入参文件校验类型和大小然后保存到MEDIA_ROOT目录返回文件的URL。settings.py中的配置MEDIA_URL /media/ MEDIA_ROOT BASE_DIR / media上传接口的实现关键点是要限制文件类型和大小。我只允许jpg、jpeg、png三种格式最大5MB。这个限制在前端和后端都要做不能只做前端因为接口可以直接被curl调用绕过前端限制。后端校验代码import os from rest_framework.views import APIView from rest_framework.parsers import MultiPartParser from rest_framework.response import Response class HouseImageUploadView(APIView): parser_classes [MultiPartParser] def post(self, request): file request.FILES.get(file) if not file: return Response({detail: 未接收到文件}, status400) ext os.path.splitext(file.name)[1].lower() if ext not in [.jpg, .jpeg, .png]: return Response({detail: 仅支持jpg/jpeg/png格式}, status400) if file.size 5 * 1024 * 1024: return Response({detail: 图片大小不能超过5MB}, status400) house_image HouseImage.objects.create( imagefile, uploaderrequest.user.profile ) return Response({url: house_image.image.url})前端上传组件我直接用Ant Design Vue的Upload配置了自定义上传逻辑因为默认上传方式是提交表单我需要用封装好的axios实例带token上传template a-upload :file-listfileList :custom-requesthandleUpload list-typepicture-card accept.jpg,.jpeg,.png div v-iffileList.length 5 PlusOutlined / div上传图片/div /div /a-upload /template script setup const handleUpload async (options) { const { file, onSuccess, onError } options try { const res await uploadHouseImage(file) imageUrls.value.push(res.url) onSuccess(res) } catch (err) { onError(err) } } /script关于文件存储如果是在云服务器上做演示或小型部署本地存储完全够用。但如果要上线到正式环境建议用OSS/S3对象存储把图片放到云存储上Django只存图片URL。原因有两个一是本地存储会占用服务器磁盘空间图片多了以后备份和迁移都很头疼二是对象存储自带的CDN加速能让图片加载更快。不过考虑到很多校园项目其实是课程设计、毕设级别我用本地存储定期备份就够了。4.3 从开发到上线的完整流程前后端联调完成后部署流程我走了一遍记录下来供参考。前端项目先构建生成静态文件到dist目录cd frontend npm run build然后把dist目录里的所有文件复制到服务器的/webroot/campus-rent/dist目录下同时把前端的环境变量里VITE_API_BASE_URL改成正式环境的值。构建时如果没有配置环境变量axios的baseURL会使用vite代理配置但代理只在开发服务器下生效生产环境必须依赖Nginx把/api转发到后端所以构建结果中的请求路径必须是/api开头的相对路径而不是localhost:8000这种绝对地址。后端代码部署到服务器上安装依赖然后执行迁移和收集静态文件pip install -r requirements.txt python manage.py migrate python manage.py collectstatic --noinput python manage.py runserver 0.0.0.0:8000生产环境我用了Gunicorn替代runserverpip install gunicorn gunicorn config.wsgi:application --bind 0.0.0.0:8000 --workers 3最后配置Nginx反向代理和媒体文件路径重启Nginx整个系统就可以通过域名访问了。部署过程中的一个坑Django的ALLOWED_HOSTS必须加上服务器域名或IP否则访问会报400 Bad Request。另一个坑DEBUGFalse以后Django不再自动托管静态文件和媒体文件必须用collectstatic收集静态文件交给Nginx托管媒体文件需要在Nginx配置location /media/映射。5. 高频报错与排查技巧实录5.1 后端常见问题数据库驱动安装失败是Windows环境下的重灾区。我使用MySQL需要在Windows上安装mysqlclient但mysqlclient在Windows下需要预编译的wheel包或者Visual C编译环境否则pip install会直接报错。笨办法是安装Visual C Build Tools聪明一点的办法是直接到网站下载对应Python版本的whl文件安装或者改用pymysql并在manage.py里加一行pymysql.install_as_MySQLdb()。pymysql的性能略低于mysqlclient但对校园系统的小并发完全够用。迁移文件冲突也是常见问题。多人协作开发时经常出现两个人同时改了同一个模型然后都生成了迁移文件导致makemigrations报依赖冲突。解决方案是协商好谁先合并谁后合并后合并的人删除自己生成的迁移文件后重新生成一次。一个人开发时基本不会遇到这个问题但如果用了Git分支开发不同模块也要注意。5.2 前端常见问题跨域错误是出现频率最高的前端问题。表现是浏览器控制台出现“Access to XMLHttpRequest at ... from origin ... has been blocked by CORS policy”。看到这行报错时先别急着配置后端CORS要分场景。开发环境下首先确认Vite代理配置是否正确请求路径是否真的走了代理生产环境则确认Nginx是否已经把/api转发到后端。刷新页面404的问题我已经在4.1节提到过了这是Vue Router history模式的经典问题。解决方法是Nginx配置try_files或者改用hash模式路由。hash模式的URL长这样/campus-rent/#/order不太美观但对部署最简单的场景够用。我推荐用history模式try_files因为URL更符合现代网站的观感。接口返回401但用户确实已经登录了这个问题的排查思路要清晰。第一步打开浏览器开发者工具的Network面板看请求头里有没有Authorization字段第二步看Authorization前缀是不是“Bearer ”比如“Bearer eyJhbGciOi...”。如果前缀写错了比如写成了“JWT eyJhbGci...”simplejwt默认不认识就会401。我封装axios时专门排查过这个问题最终确认了写法是Bearer ${token}。5.3 独家避坑建议版本锁定是我吃了大亏后养成的习惯。Django 4.x和3.x的部分API有细微差异DRF的版本更新也可能导致序列化器行为变化Vue3的Vite版本也经常升级。建议在requirements.txt里锁定所有依赖版本号在package.json里固定版本而不是用^符号否则半年后重新部署会发现项目跑不起来或者行为诡异。比如我项目的requirements.txt是这样的Django4.2.7 djangorestframework3.14.0 djangorestframework-simplejwt5.3.0 django-cors-headers4.3.0 pymysql1.1.0 Pillow10.1.0 gunicorn21.2.0前后端字段命名的一致性也很关键。后端序列化器返回的字段名和前端Vue组件里使用的字段名必须完全一致。因为Python习惯用下划线命名比如user_typeJavaScript习惯用驼峰命名比如userType如果前后端各自选择了不同的命名风格联调时就会出现“接口返回了user_type前端却取userType”的问题。最简单的做法是统一用下划线命名前端拿到数据后直接使用不做转换。Django模型默认的字段名就是下划线风格所以前端配合使用下划线风格的变量名最省事。日期时间的序列化格式也要提前约定。DRF默认会把DateTimeField序列化成ISO 8601格式比如“2024-06-15T14:30:0008:00”前端拿到这个字符串直接显示会很丑。我全局配置了日期格式REST_FRAMEWORK { DATETIME_FORMAT: %Y-%m-%d %H:%M:%S, DATE_FORMAT: %Y-%m-%d, }这样后端直接返回“2024-06-15 14:30:00”前端不用做任何格式化就能展示。注意如果你需要在前端做时间运算比如计算租期天数建议还是返回时间戳或ISO格式因为字符串格式做运算很麻烦。我的做法是租期天数由后端计算并返回前端不参与任何时间计算。5.4 搜索功能中的一个小优化搜索是用户高频使用的功能但很多教程里的搜索实现都没有做“防抖”。用户在搜索框里连续输入“阳光小区”如果每次输入都触发一次请求会发送“阳”、“阳光”、“阳光小”、“阳光小区”四次请求既浪费带宽又可能造成后端压力。我在搜索框组件里加了300毫秒的防抖import { ref, watch } from vue const keyword ref() let timer null watch(keyword, () { clearTimeout(timer) timer setTimeout(() { fetchHouseList() }, 300) })这只是个小优化但从用户体验和代码规范角度看是很加分的细节。面试官如果问到搜索功能怎么优化能说出防抖、节流、请求取消用AbortController这些点印象分会高不少。6. 测试与性能优化要点6.1 接口测试postman还是drf自带文档后端接口写完以后我习惯先用Postman做一轮冒烟测试确认每个接口都能正确返回。但等到接口数量多了以后维护Postman里的请求集合也变得麻烦。DRF自带的接口文档页面开启DEFAULT_SCHEMA_CLASS后访问/api/docs/能直接展示所有接口支持在线调试对前后端联调和给外部人员演示都非常方便。需要注意的一点在settings.py里如果全局配置了权限为IsAuthenticated接口文档页也需要登录才能访问。如果希望文档可以公开访问可以在URL配置里单独设置AllowAny。6.2 性能优化除了加缓存还能做什么校园租房系统的并发量不会很高所以在性能优化上我没有做太复杂的操作。但几个基本层面的优化还是做了第一房源列表接口加了django-filter的复杂查询时确保所有筛选字段都有数据库索引用explain查看执行计划确认没有全表扫描第二列表接口使用select_related和prefetch_related预加载外键数据避免N1查询问题第三前端对房源列表做了分页每页12条而不是一次性返回几百条数据。index字段的添加也值得说。我在House表的外键字段和常用筛选字段上建立了索引class Meta: indexes [ models.Index(fields[price]), models.Index(fields[status, created_at]), ]订单表的status字段也建了索引因为管理后台会按状态筛选订单。这些索引对大数据量场景来说很有必要但对小数据量项目来说收益不明显。做决策时可以有一个理性预期校园系统数据量到了一万条以上索引优化才开始有明显效果。6.3 代码规范与提交规范一个人开发项目时最容易忽略代码规范但代码规范其实是给自己看的。我用的Python代码格式化工具是Black前端用Prettier两个工具都能在保存时自动格式化代码。统一代码风格的好处不是“给别人看”而是自己在一个月以后回来看代码时不用额外花精力去理解当时是怎么写的。Git提交信息我用的是这种格式feat(模块): 描述、fix(模块): 描述。虽然不是严格遵循Conventional Commits规范但能保证从提交历史里快速定位到某个模块的改动。好的提交习惯对回滚某个功能、协作者评审代码都很有帮助。7. 最后的实操心得做这个项目的过程中零零散散踩了太多次坑。有些坑看着很小但找问题就能花掉一下午。比如Vite代理配置里少了“/api”前缀后转发会404前端在响应拦截器里把data直接给到调用方导致分页对象被拆开后端因为忘了加权限装饰器导致匿名用户可以删除房源等等。这些经验写出来是想告诉大家前后端分离项目调试一个接口时排查链路的先后顺序很重要先看浏览器Network面板请求是否发出、请求头是否正确、后端是否收到、返回什么状态码再去看代码不要一上来就怀疑代码逻辑。还要特别提一点做这类系统时一定要有“数据安全”的意识。虽然校园系统的用户量不大但用户真实姓名、手机号、学号都属于个人敏感信息。我在实现里没有做过度设计但至少做到密码使用Django自带的PBKDF2加密数据库不开公网端口Nginx层面限制了上传文件大小后端接口对用户权限做了严格校验。最后想说的是技术方案永远是为业务场景服务的。校园租房这个场景如果放到商业公司里做可能要接入支付、信用体系、电子合同但在校园项目里最核心的其实是把信息和信任的问题解决了。能通过技术手段把“找房、带看、签约、入住”这条链路在线上打通让数据沉淀下来就已经算成功了。我这个项目的源码和部署文档已经整理好放在GitHub上评论区留了链接。如果你也在做类似的系统欢迎直接拿去参考更欢迎在评论区交流你踩过的坑。如果我有时间后面还可以写一篇关于这个项目如何接入校园统一认证登录的具体教程。
延伸阅读

更多相关文章

2026/9/10 1:21:05

WPS多维表格Vibe Coding:意图驱动的低代码业务建模实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/10 1:21:05

C++与人工智能框架:从训练到部署的实战指南

先问个问题:你能想到的AI应用,从手机里的实时人脸解锁,到工厂流水线上的缺陷检测,再到自动驾驶里那个每秒钟都在判断“前面是行人还是纸箱”的系统,这些模型真正跑起来的时候,底层是什么语言?很…

2026/9/10 1:21:05

拼音打字练习软件实测:打字侠深度评测与14天提速指南

打字这回事,很多人可能觉得,都2026年了,谁还不会打字?但说实话,我在给企业做办公效率培训时见过太多真实案例:有人每天要写大量报告,却还在用两根食指戳键盘,眼睛在屏幕和键盘之间来…

2026/9/10 2:16:12

FPGA开发中的Verilog POC实战:快速验证概念,避免返工

简介:面向数字逻辑与FPGA初学者的Verilog POC概念验证代码包,以最小可运行工程演示顶层CPU的波形模拟验证流程,帮助读者理解双向端口驱动规则与三态门OE使能逻辑,掌握ModelSim/Vivado等工具下的激励设置与边界测试思路。资源共77个…

2026/9/10 2:16:12

ResNet人脸表情识别实战:从微表情建模到边缘部署

简介:本资源是一套基于ResNet架构的人脸表情识别完整实现方案,面向计算机视觉初学者、本科毕业设计及课程设计学生,解决从数据预处理、模型构建、训练验证到实时视频识别的全流程实践问题。压缩包共16个文件,含3个核心Python脚本&…

2026/9/10 2:16:12

Simulink混合型谐波抑制仿真:PPF+APF协同控制与调参全解析

搞谐波抑制仿真,尤其是MATLAB/Simulink里要做“无源PPF有源APF混合型”方案的同学和工程师,大概率是已经在网上搜过一圈了。搜索结果里要么是纯理论PPT,要么是模块截图看不清楚参数,要么给了模型但没讲为什么这么接、为什么效果出…

2026/9/9 13:11:35

超人会飞不算本事:系统稳定依赖清晰规则与边界设计

开头先不绕弯子。“#斯坦李吐槽dc 所以超人是无缘无故会飞的嘛哈哈哈哈哈哈哈锤哥真是技术人才啊!#雷神 #复联”这类调侃式短标题,第一波冲击力在于它把两个宇宙的角色塞进同一个吐槽箱里,但细想一下就能发现,它真正碰到的根本不是…

2026/9/8 7:15:15

超人VS蜘蛛侠:拆解超级IP的影响力与传播方法论

把“蜘蛛侠 vs 超人”放在 CSDN 上聊,可能很多人第一反应是走错片场了。但如果把这两个角色看成“两个持续运营了 80 多年的文化产品”,你会发现,这场比较本质上是两个不同 IP 策略的长期结果对比:超人赢在定义了整个超级英雄题材…

2026/9/9 16:31:09

基于CNN的调制信号识别:MATLAB实现时频图分类实战

简介:本资源是一套面向通信工程与信号处理方向学习者、研究者的深度学习实践方案,聚焦调制信号自动检测与识别这一典型无线通信任务,解决传统方法依赖人工特征、低信噪比下性能下降等痛点。压缩包共12个文件(10.73MB)&…

2026/9/10 0:00:55

目录对比去重实战:用哈希算法精准清理重复文件

我电脑里现在还有一块换了三次机的“数据墓地”硬盘,里面存着2016年以前所有旧笔记本的完整备份。平时不觉得有什么,直到前阵子想把它整理归档,发现同一个安装包、同一批照片、同一份论文草稿,在几个不同的备份目录里反复出现。更…

2026/9/10 0:00:55

Leaflet离线地图完整Demo合集:内网部署与坐标纠偏实战

简介:这是一份面向Web GIS开发者的LeafLet离线地图示例合集,帮助开发者快速掌握离线地图从搭建到交互的完整流程。压缩包共723个文件,大小14.06MB,以319个js脚本、175个html页面和29个css样式文件为主体,配合png/svg图…

2026/9/10 0:00:55

MATLAB读取Rinex 3.02观测文件:多系统GNSS数据解析实战

简介:基于MATLAB开发的Rinex3.02版观测文件(o文件)读取代码包,面向卫星定位导航方向的学习者与研究人员,用于解决新版观测文件的数据解析、历元提取与时间转换问题。压缩包共4个文件,包含两个m脚本、一个19…

2026/9/7 16:23:03

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/7 22:46:00

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/9 10:21:54

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
咨询二维码