☰
Node.js+Vue+ThinkPHP房屋租赁系统全栈开发实战解析
2026/10/7 17:20:59 网站建设 项目流程

又到一年课设季,我在不少技术群里看到有人把“Node.js + Vue + ThinkPHP”的大学生房屋租赁系统当成三个后端技术来学,开口第一句就是“Node.js和ThinkPHP到底谁才是主后端”。其实把这个题目翻译成人话就是:Vue负责做网页界面,ThinkPHP负责写接口、操作数据库,Node.js负责在开发阶段把前端项目跑起来、把源码编译打包。三个技术各有分工,谁也不抢谁的活儿。这篇文章就以这个典型的课设题目为例,把整套系统从环境搭建、数据库设计、接口开发、前端页面到打包部署讲一遍,适合正在做毕业设计、课程设计,或者第一次接触前后端分离架构的开发者参考。

1. 标题拆解:Node.js、Vue和ThinkPHP,各管哪一段

1.1 为什么一个系统要同时出现三个技术栈

先说结论:大学生房屋租赁系统本身是一个典型的业务闭环系统,它需要有一个用户可见的界面,也需要有一组能增删改查数据的接口,更需要一个数据库来存房源、用户、订单、收藏这些信息。在这个结构下,Vue是前端框架,负责渲染页面;ThinkPHP是后端框架,负责处理请求、读写MySQL;Node.js则不是后端主角,而是整个前端工程链的运行时环境——装依赖、启动开发服务器、构建打包,全部依赖Node.js。

很多人对这个题目产生误解,是因为标题把三个技术名平铺在一起,看起来像“三选一”或者“三层各用一门语言”。实际上这个组合非常常见,尤其是校园项目里:Vue写页面,ThinkPHP写API,前后端通过JSON数据通信。Node.js在这里更像是“水电煤”,没有它,Vue工程根本跑不起来。

这种架构的好处也在这套组合里体现得很明显:前端可以单独开发、单独调试,后端接口只要保证规范返回JSON,前端随时可以对接,互不阻塞。对于时间紧、又要答辩演示的课设项目来说,这是效率最高的组织方式。

1.2 技术选型是拍脑袋还是按需要来的

有人会问,为什么不干脆用Vue + 一个纯Java后端,或者直接用ThinkPHP渲染模板?原因很简单:这类项目的核心考核点通常在于“前后端分离”和“完整业务流”,而不是某一门语言的深度。用ThinkPHP做后端,原因是它上手快、文档全、自带模板引擎和ORM,非常适合短周期项目。用Vue做前端,是因为组件化开发能快速搭出房源列表、详情页、用户中心这些重复性较高的界面。Node.js的价值则在工程化:Vue CLI或Vite都要在Node.js之上运行,npm也是依赖管理的入口。

所以这个题目本身不是让三者在“后端”打架,而是让它们各自归位。你在答辩时如果能讲清楚“Node.js承担的是前端工程化工具链,ThinkPHP承担的是API服务,Vue承担的是SPA页面渲染”,这一句话就能让评委明白你不是在乱套技术栈。

2. 环境准备:Node.js和Vue项目从零跑起来

2.1 Node.js安装与npm镜像配置

Node.js的安装本身不复杂,去官网下载LTS版本安装包,一路下一步就行。安装完成后打开命令行验证:

node -v npm -v

能正常输出版本号,说明Node.js环境已经就位。但国内环境下,npm下载依赖经常慢到怀疑人生,所以第一步应该是把npm源切到国内镜像:

npm config set registry https://registry.npmmirror.com

验证是否生效:

npm config get registry

这一步做完,后面npm install的速度会快非常多。我在带毕设学生时经常发现,前端工程半天装不完依赖,十有八九是没换镜像源。

2.2 npm.ps1脚本权限报错的处理

在Windows上装完Node.js,很多人执行npm -v或者npm install时报错:

npm : 无法加载文件 C:\Program Files\nodejs\npm.ps1,因为在此系统上禁止运行脚本。

这个报错看着吓人,原因其实很简单:Windows PowerShell的默认执行策略是Restricted,不允许运行本地脚本文件。npm本质上是一个.ps1脚本,所以被执行策略拦住了。

解决办法有两种。第一种,用管理员身份打开PowerShell,执行:

Set-ExecutionPolicy RemoteSigned

输入Y确认,再重新执行npm命令就正常了。第二种,如果不想改系统策略,直接改用Windows Terminal里的cmd来跑npm命令,cmd不会受PowerShell执行策略影响。

这是整个环境搭建阶段出现频率最高的坑,十个人里至少有六七个会被卡在这里。知道原因之后就能一眼看出来:不是npm坏了,只是系统默认禁止了脚本执行。

2.3 创建Vue项目并安装依赖

现在创建Vue项目,推荐使用Vite方式。

npm create vue@latest

按提示输入项目名称,选择需要的特性。作为房屋租赁系统,建议勾选:

  • TypeScript可以暂时不选,节外生枝;
  • Router必须选,页面切换要靠它;
  • Pinia建议选,用户登录状态、收藏列表都要放到全局状态里;
  • ESLint建议选,写代码时能顺手检查低级错误。

进入项目目录并安装依赖:

cd house-rental-web npm install npm run dev

看到终端输出Local: http://localhost:5173/,就说明Vue项目已经跑起来了。这里有一点要强调:Vite开发服务器默认跑在5173端口,后端ThinkPHP默认跑在8000端口,两个服务同时开着,前端页面才能跨端口请求后端接口。

2.4 ThinkPHP本地运行与连接数据库

后端这边,ThinkPHP的安装推荐用Composer:

composer create-project topthink/think tp

进入目录后,先修改.env文件,配置数据库连接:

[DATABASE] HOSTNAME = 127.0.0.1 DATABASE = house_rental USERNAME = root PASSWORD = 123456 HOSTPORT = 3306 CHARSET = utf8mb4

然后启动内置开发服务器:

php think run

默认监听http://127.0.0.1:8000,访问能看到ThinkPHP的欢迎页或自定义首页,说明后端环境也通了。

这个环节最容易出现的问题,一是PHP环境变量没配好,php命令在命令行里找不到;二是数据库还没创建,.env里写的库名在MySQL中不存在,导致接口一调就报连接错误。建议提前用Navicat或命令行创建好数据库,再启动ThinkPHP。

3. 后端核心:ThinkPHP怎么把租房业务写成API

3.1 数据库设计:六张表帮你把业务模型闭环

大学生房屋租赁系统的核心业务链是:用户注册登录 -> 房东发布房源 -> 租客浏览房源 -> 收藏/预约看房 -> 线下成交。围绕这条链路,数据库不需要设计得太复杂,但每一张表都得待在自己的位置上。

我常用的表结构如下:

表名用途说明关键字段
users用户表,区分租客与房东id, username, password, phone, role, status
houses房源表,出租信息的核心id, user_id, title, cover, area, address, price, status
images房源图片表,支持多图轮播id, house_id, url, sort
favorites收藏表,租客收藏房源id, user_id, house_id
appointments预约表,记录看房申请id, user_id, house_id, visit_time, phone, status
leases租赁订单表,记录成交结果id, user_id, house_id, start_date, end_date, price

这六张表把业务闭环串起来了。users表里的role字段用来区分普通租客和房东,租客能收藏、预约,房东能发布、下架房源。houses表里的status字段用来标记房源状态,1表示上架,0表示下架,后台管理员可以审核房源,审核通过的房源才允许上架展示。appointments表的status字段管理预约流程,用户提交预约后,房东可以确认或拒绝。

有个设计细节值得单独说:图片单独建一张images表,而不是在houses表里塞一个字符串字段。原因很简单,房源详情页要做轮播图,一套房源往往有多张图,单独表存多条记录,查询和排序都清晰,以后要加封面图、楼层图也方便扩展。

3.2 控制器与模型的分层写法

ThinkPHP的推荐写法是控制器只做参数接收和结果返回,具体SQL和业务逻辑放模型里。以房源列表接口为例,控制器代码大概长这样:

<?php declare(strict_types=1); namespace app\controller; use app\BaseController; use app\model\House; use think\facade\Request; class HouseController extends BaseController { public function index() { $page = Request::param('page', 1); $limit = Request::param('limit', 10); $keyword = Request::param('keyword', ''); $area = Request::param('area', ''); $where = [['status', '=', 1]]; if ($keyword) { $where[] = ['title', 'like', "%{$keyword}%"]; } if ($area) { $where[] = ['area', 'like', "%{$area}%"]; } $list = House::where($where) ->with(['images']) ->order('create_time', 'desc') ->paginate(['list_rows' => $limit, 'page' => $page]); return json(['code' => 0, 'msg' => 'ok', 'data' => $list]); } }

这里有几个关键点:

  • where用数组拼接,避免SQL注入,也方便动态追加条件;
  • with(['images'])是用关联模型一次性把图片查出来,避免前端拿到房源数据后还要再去请求图片接口;
  • paginate直接返回分页对象,前端能拿到total、per_page、current_page这些字段,分页组件直接对接。

模型里定义关联的方式:

<?php namespace app\model; use think\Model; class House extends Model { protected $name = 'houses'; public function images() { return $this->hasMany(Image::class, 'house_id'); } }

用这种分层方式写代码,到了答辩的时候也很好讲:数据查询在模型里,业务逻辑在控制器里,页面展示在Vue里,每一层的职责边界清清楚楚。

3.3 登录注册与Token鉴权

房屋租赁系统里,发布房源、收藏、预约这类操作必须区分用户身份,所以不能不做鉴权。简单可靠的方案是登录成功后返回一个Token,前端保存起来,后续每次请求在Authorization头里带上,后端解析Token识别用户。

ThinkPHP里实现Token鉴权,可以直接用官方插件think-jwt,也可以自己写一个简单的Token生成逻辑:

public function login() { $username = Request::param('username'); $password = Request::param('password'); $user = User::where('username', $username)->find(); if (!$user || !password_verify($password, $user->password)) { return json(['code' => 1, 'msg' => '用户名或密码错误']); } $token = md5($user->id . time() . uniqid()); cache('token_' . $token, $user->id, 7200); return json(['code' => 0, 'data' => ['token' => $token, 'user_info' => $user]]); }

这里用缓存存Token和用户ID的映射,有效期两个小时。前端请求带Token时,后端通过cache('token_' . $token)查出用户ID,不管查列表还是写数据,都能知道当前操作者是谁。

判断当前用户是不是房源发布者、收藏的房源是不是自己的,这类鉴权逻辑都可以抽成一个公共方法放到BaseController里,每个需要登录的控制器继承它,代码复用率会高很多。

4. 前端核心:Vue页面怎么把数据串起来

4.1 页面路由与整体结构

Vue前端的结构可以按功能拆成四个区域:公共页面、用户中心、管理员后台、通用组件。路由表规划下来大概是这个样子:

const routes = [ { path: '/', name: 'home', component: () => import('@/views/Home.vue') }, { path: '/detail/:id', name: 'detail', component: () => import('@/views/Detail.vue') }, { path: '/login', name: 'login', component: () => import('@/views/Login.vue') }, { path: '/register', name: 'register', component: () => import('@/views/Register.vue') }, { path: '/user', name: 'user', component: () => import('@/views/user/Layout.vue'), children: [ { path: 'publish', name: 'publish', component: () => import('@/views/user/Publish.vue') }, { path: 'favorites', name: 'favorites', component: () => import('@/views/user/Favorites.vue') }, { path: 'appointments', name: 'appointments', component: () => import('@/views/user/Appointments.vue') }, ] }, { path: '/admin', name: 'admin', component: () => import('@/views/admin/Layout.vue'), children: [ { path: 'houses', name: 'adminHouses', component: () => import('@/views/admin/HouseManage.vue') }, { path: 'users', name: 'adminUsers', component: () => import('@/views/admin/UserManage.vue') }, ] } ]

注意这里使用了路由懒加载,每个页面组件都是独立的异步块,首屏只加载当前页面的JS,数据量大了以后页面打开速度会更稳。这种细节在答辩演示时反而是加分项。

用户中心和管理后台都用嵌套路由加一个Layout组件,左侧是菜单,右侧是内容区,这种布局对课设来说已经很够用。

4.2 axios封装、Vite代理与接口对接

前端所有和后端的通信都建议走封装好的axios实例,不要每个页面直接import axios去写。统一封装的好处是:请求头自动带Token、后端返回的报错信息自动弹提示、统一处理登录过期。

import axios from 'axios' import { ElMessage } from 'element-plus' import router from '@/router' const request = axios.create({ baseURL: '/api', timeout: 10000 }) request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = token } return config }) request.interceptors.response.use( response => { const res = response.data if (res.code !== 0) { ElMessage.error(res.msg) return Promise.reject(new Error(res.msg)) } return res.data }, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token') router.push('/login') } ElMessage.error(error.message || '请求失败') return Promise.reject(error) } )

这段封装是很多Vue项目的“标准答案”,核心就是把鉴权、异常提示这些公共逻辑集中到拦截器里。开发环境中,/api这个前缀必须交给Vite代理转发到ThinkPHP的8000端口,否则直接请求/api/house会打到5173上,自然404。

// vite.config.js server: { proxy: { '/api': { target: 'http://127.0.0.1:8000', changeOrigin: true } } }

4.3 房源列表、详情、收藏和预约的交互实现

房源列表页是整站的流量入口,需要展示搜索栏、筛选条件、房源卡片、分页。每个房源卡片用通用组件HouseCard来写,因为首页、搜索结果页、我的收藏页面都用到一模一样的卡片结构。

这里有个很实用的点:卡片组件里用了Vue插槽,可以让不同页面在底部插入不同的操作按钮。首页的卡片放“查看详情”,收藏页的卡片放“取消收藏”,这样一套组件多处复用,不用为每个页面单独写一版。

<template> <div class="house-card" @click="goDetail"> <el-image :src="house.cover" fit="cover" /> <div class="house-info"> <h3>{{ house.title }}</h3> <p>{{ house.area }} · {{ house.address }}</p> <span class="price">¥{{ house.price }}/月</span> <div class="card-actions"> <slot name="actions" /> </div> </div> </div> </template>

详情页是交互最密集的地方。需要展示房源图片轮播、基本信息、房东信息、收藏按钮、预约按钮。收藏的逻辑非常简单:点击时判断用户是否已登录,未登录跳登录页,已登录就调收藏接口。

预约会稍微复杂一点,因为要带一个时间字段。我建议用el-date-picker让用户选期望看房时间,提交时把house_id、visit_time、phone三个字段一起交给后端。这里的前端校验是必不可少的:选的时间不能是过去,手机号格式要正确,否则后端也会拒。

用户中心的“我的房源”“我的收藏”“我的预约”三个页面,看起来是独立的,但数据来源都是同一个接口体系,区别只在于传不同的参数。比如“我的收藏”其实就是请求/api/favorites并返回关联的房源信息,列表直接用HouseCard渲染,插槽放“取消收藏”按钮。

5. 联调与部署:开发环境跑通之后的事

5.1 跨域问题:代理能解决九成烦恼

前后端分离项目逃不掉跨域问题。开发阶段,Vite代理已经处理好了,前端所有的/api请求都会被转发到后端,浏览器看到的还是同域请求。

但如果后端接口直接被其他程序调用,或者你跳过了前端代理直接访问后端,就需要ThinkPHP这边开启跨域支持。在中间件里加一段响应头:

header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS'); header('Access-Control-Allow-Headers: Content-Type, Authorization');

把这个处理放到ThinkPHP的全局中间件里,所有接口都会自动带上跨域响应头。我实习项目里遇到过前端代理配好了但接口始终不通的情况,最后发现是后端没接收OPTIONS预检请求,加上这段逻辑就好。

5.2 打包上线:dist目录怎么和ThinkPHP共存

开发完成后,前端要执行:

npm run build

生成的dist目录里是静态HTML、CSS和JS。部署方案有两种,取决于服务器环境。

第一种方案:把dist目录里的文件复制到ThinkPHP的public目录下,入口文件是index.html,后端接口路径/api由ThinkPHP路由解析,静态资源直接由Nginx或Apache提供。这是最简单的部署方式,一个目录搞定前端后端。

第二种方案:Nginx分别配置前端站点和后端接口。

server { listen 80; server_name your.domain.com; root /var/www/house_rental/dist; index index.html; location /api { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; } location / { try_files $uri $uri/ /index.html; } }

这里的try_files配置必须加上,原因是Vue Router如果使用createWebHistory模式,刷新/detail/5这种深层路径时,服务器不知道路由到底是前端管理的还是后端管理的,需要它把所有找不到的路径都回退到index.html,交给前端路由接管。如果不想配置这个,可以直接把路由模式改成createWebHashHistory,URL变成/#/detail/5的写法,刷新不404,代价是URL不够好看。

5.3 答辩/演示前容易被问到的细节

打包部署做完,系统已经能跑起来了,但答辩演示阶段还有一些细节容易被评委追问,提前准备好答案能省很多尴尬。

  • 权限控制是怎么做的?答案:后端每个需要登录的接口都通过Token鉴权,管理员接口会额外校验用户的role字段。
  • 图片上传存储在哪里?本地public目录下的storage文件夹,开发阶段够用。如果有条件和时间,可以改成OSS对象存储。
  • 搜索功能支持哪些条件?当前实现了关键字搜索和区域筛选,如果要优化可以加价格排序、户型和朝向筛选。
  • 如果同一房源被多人同时下单怎么办?这个问题问出来的概率不低,标准回答是:在下单事务里对房源状态加锁或加条件更新,update ... where status = 1,影响行数为0就说明已被抢。

另外,演示前一定要把MySQL服务、ThinkPHP服务、Vite开发服务器或者Nginx服务全部提前启动好,不要等评委坐在面前了才在那敲命令。准备一份带假数据的数据库备份文件,现场恢复数据比手工录入要快得多。

6. 写在最后的个人经验

这类全栈课设项目,最大的坑不是某个技术不会用,而是三个技术栈之间的关系一开始没想清楚,结果哪一个都没做透。我见过太多人先去啃ThinkPHP的模板引擎,转头又去研究Vue的源码,到最后前后端接口对不上,演示的时候当场翻车。其实这个项目的正确打开方式是:先把六张数据表建好,用ThinkPHP把数据接口一个个测通,再用Vue把页面搭起来,通过接口把数据串起来,最后做打包部署。按这条路径推进,每一步都能看到阶段性成果,越做越有信心。

一个小技巧分享给你:我在做这类前后端分离项目时,习惯先在前端写一份mock数据列表,页面结构先跑起来,后端接口好了以后把mock数据源换成真实请求。这样一来前端不用干等后端,后端也不用被前端催着改字段,两边并行开发效率特别高。房屋租赁系统这种业务结构清晰、模块边界明确的题目,非常适合用这个方式推进。

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

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

立即咨询