
又到一年课设季我在不少技术群里看到有人把“Node.js Vue ThinkPHP”的大学生房屋租赁系统当成三个后端技术来学开口第一句就是“Node.js和ThinkPHP到底谁才是主后端”。其实把这个题目翻译成人话就是Vue负责做网页界面ThinkPHP负责写接口、操作数据库Node.js负责在开发阶段把前端项目跑起来、把源码编译打包。三个技术各有分工谁也不抢谁的活儿。这篇文章就以这个典型的课设题目为例把整套系统从环境搭建、数据库设计、接口开发、前端页面到打包部署讲一遍适合正在做毕业设计、课程设计或者第一次接触前后端分离架构的开发者参考。1. 标题拆解Node.js、Vue和ThinkPHP各管哪一段1.1 为什么一个系统要同时出现三个技术栈先说结论大学生房屋租赁系统本身是一个典型的业务闭环系统它需要有一个用户可见的界面也需要有一组能增删改查数据的接口更需要一个数据库来存房源、用户、订单、收藏这些信息。在这个结构下Vue是前端框架负责渲染页面ThinkPHP是后端框架负责处理请求、读写MySQLNode.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 vuelatest按提示输入项目名称选择需要的特性。作为房屋租赁系统建议勾选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的安装推荐用Composercomposer 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怎么把租房业务写成API3.1 数据库设计六张表帮你把业务模型闭环大学生房屋租赁系统的核心业务链是用户注册登录 - 房东发布房源 - 租客浏览房源 - 收藏/预约看房 - 线下成交。围绕这条链路数据库不需要设计得太复杂但每一张表都得待在自己的位置上。我常用的表结构如下表名用途说明关键字段users用户表区分租客与房东id, username, password, phone, role, statushouses房源表出租信息的核心id, user_id, title, cover, area, address, price, statusimages房源图片表支持多图轮播id, house_id, url, sortfavorites收藏表租客收藏房源id, user_id, house_idappointments预约表记录看房申请id, user_id, house_id, visit_time, phone, statusleases租赁订单表记录成交结果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_types1); 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 classhouse-card clickgoDetail el-image :srchouse.cover fitcover / div classhouse-info h3{{ house.title }}/h3 p{{ house.area }} · {{ house.address }}/p span classprice¥{{ house.price }}/月/span div classcard-actions slot nameactions / /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交给前端路由接管。如果不想配置这个可以直接把路由模式改成createWebHashHistoryURL变成/#/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数据源换成真实请求。这样一来前端不用干等后端后端也不用被前端催着改字段两边并行开发效率特别高。房屋租赁系统这种业务结构清晰、模块边界明确的题目非常适合用这个方式推进。