ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Vue+Node.js+元宇宙:房屋租赁系统全栈开发实践

Vue+Node.js+元宇宙:房屋租赁系统全栈开发实践 上个月在做一个房屋租赁系统时朋友问了我一句“元宇宙跟租房有什么关系”我当时正把一套房源的3D户型图加载进前端随口答了一句“能让你躺在床上就把房子逛了。”这句玩笑话后来成了项目的核心卖点。这个项目定位很明确用Vue搭建包含后台管理、房源展示、在线预约与合同管理的Web端用Node.js做RESTful API同时在前端引入全景看房和虚拟漫游能力让用户不是对着几张平面图猜空间而是像站在房间里一样环视周围。对新手和中小团队来说这套系统在技术选型和功能落地上都比较平衡既没有盲目追新也没有停留在传统的CRUD层面。今天就把我的完整思路、数据库设计、关键实现和踩过的坑都整理出来希望能帮到正在做类似项目的朋友。1. 项目定位与技术选型为什么是vuenodejs元宇宙概念1.1 传统房屋租赁系统的痛点传统房屋租赁管理系统的痛点我相信做过这类项目的朋友都有体会信息不透明照片与实际情况差距大租客为了看一套房反复跑现场管理员又要手动维护房源状态合同流程全靠线下跑。我自己接手过的需求里最常见的一句话是“能不能让客户少跑几趟”这才是隐藏的核心需求。所以我在设计这个项目时没有把它做成一个“信息发布后台增删改查”的普通系统而是把看房环节做重。用户打开房源详情不再只是看图片轮播而是可以直接进入一个“虚拟房间”通过鼠标拖拽查看天花板、地板、窗户朝向甚至可以点击房间里的热点标注查看家电尺寸和租金说明。这样既减少了无效带看也提升了房源信息的可信度。另外租赁业务本身存在角色差异租客、房东、平台管理员需要的功能完全不同。租客关注搜索、收藏、预约看房房东关注房源发布、租约状态、账单管理员关注审核、数据统计和异常订单。如果前端不分角色、后端不做权限控制后面维护起来就是一场灾难。1.2 技术栈选择背后的考量为什么选Vue而不是React其实两个都能做但我个人更习惯Vue的模板语法和渐进式引入方式。这个项目里有大量表单、列表、状态联动Vue的双向绑定能少写很多DOM操作代码。再加上Vue Router和Pinia的配合小团队在拆分页面和状态管理时成本很低。更重要的是Vue中文资料多遇到问题好搜后接手的人上手快。后端选Node.js原因也很实际整个团队已经会JavaScript不需要额外引入Java或Go的人。Node.js的生态里有Express、Koa、NestJS这些成熟框架做一个小型租赁系统的API服务绰绰余。配合JWT做认证、MySQL做数据存储、PM2做进程守护从开发到部署一条线都能自己搞定。数据库我选了MySQL而非MongoDB。虽然MongoDB对字段变化的容忍度高但租赁系统里合同、账单、用户信息都是结构化很强的数据牵扯到金额和时间范围查询SQL的约束和事务处理更可靠。我自己早期用过非关系库做订单后来统计报表时写聚合特别别扭这次直接用MySQL反而省心。1.3 “元宇宙平台”在项目里到底指什么“元宇宙”这个词这两年已经被说烂了但在实际开发中它完全可以落到具体技术上。我这里实现的“元宇宙平台”是指基于浏览器端的沉浸式三维空间展示与虚拟漫游系统不依赖VR头显也不搞区块链那一套。具体落地选了两个层次第一层是全景图展示房源上传一组环绕拍的鱼眼图前端用Three.js或者Pannellum把照片映射到球体上用户拖拽鼠标就能360度看房。第二层是轻量级3D户型漫游用Three.js加载GLB格式的户型模型用户可以像玩游戏一样在房间内移动走到客厅、卧室、阳台查看每个区域的标注信息。很多人一听到3D就觉得开发成本高其实关键在于取舍。全景图方案开发量小照片采集也相对容易真3D模型方案需要建模成本但展示效果更好。我最后采用“全景图为主、3D模型为辅”的混合模式大户型用全景小户型用3D这样既控制了成本又能让平台宣传时有底气说“元宇宙看房”。2. 系统总体设计与数据建模2.1 核心业务模块划分整个系统按业务域分开前端页面和后端接口都围绕这些模块来组织避免后期代码像一团乱麻。用户模块租客、房东、平台管理员三类角色包含注册登录、实名认证、个人中心。房源模块房源发布、审核、上下架、搜索筛选、详情展示、收藏与浏览记录。看房模块全景看房、3D户型漫游、线上预约看房申请、经纪人确认时间。合同模块电子合同生成、租金计算、押金记录、租约状态变更、续租退租流程。消息模块站内信、看房提醒、合同到期提醒、系统通知。管理后台用户管理、房源审核、数据报表、系统配置。模块之间不是孤立的。比如房源模块的状态变化会触发消息模块的通知合同模块生成后又会影响房源的可租状态。我在后端设计时把这些联动逻辑统一放在Service层而不是散落在各个Controller里这样测试和维护都更容易。2.2 数据库表结构与关键字段数据库是整个系统最核心的部分设计时不仅要满足当前需求还要给后续扩展留余地。我拆了几张主表用户表、房源表、房源媒体表、预约看房表、合同表、收藏表。用户表比较简单核心字段如下字段名类型说明idbigint主键phonevarchar手机号登录passwordvarcharbcrypt加密后的密码roletinyint0租客 1房东 2管理员real_namevarchar实名姓名id_cardvarchar身份证号statustinyint正常/禁用房源表需要多花心思。除了基础信息还要存经纬度、租赁类型、押付方式、状态等。字段名类型说明idbigint主键landlord_idbigint房东用户IDtitlevarchar房源标题cover_urlvarchar封面图addressvarchar详细地址longitudedecimal经度latitudedecimal纬度rent_pricedecimal月租金deposit_typevarchar押付方式如押一付三roomstinyint几室hallstinyint几厅areadecimal面积statustinyint待审核/已上架/已下架/已出租房源媒体表单独拆出来是为了支持一套房源多张图片、多个全景链接。字段包括媒体类型、URL、排序权重、关联房源ID。点进详情页时按权重从小到大的顺序加载图片和全景图标。合同表我加了一个合同编号字段方便前端生成PDF和在后台搜索。租金总额、押金、起止时间每一条都做严格约束合同状态字段包含待签约、生效中、已退租、已到期。创建的时候同时更新房源状态这里一定要在事务里做否则可能会出现合同建好了房源还挂着“可租”。2.3 后端API设计与权限控制后端接口我采用RESTful风格路径上的资源名称用复数操作由HTTP方法和路径共同决定。下面是最核心的几个接口POST /api/users/register注册POST /api/users/login登录GET /api/houses?city上海priceMin1000page1房源列表GET /api/houses/:id房源详情POST /api/houses房东发布房源PUT /api/houses/:id/status房源上下架POST /api/appointments预约看房GET /api/appointments/mine我发起的预约POST /api/contracts创建合同GET /api/contracts/:id合同详情权限控制我用的是JWT加中间件。用户在登录成功后后端返回一个包含角色信息的token前端存到localStorage然后每次请求在header里携带。Node.js中间件里先校验token再把角色和路由权限做匹配。管理员接口和房东接口区分开比如/api/admin/users只能由role 2的用户访问。要特别说明的是后端不能完全相信前端传过来的角色字段必须从JWT解析出的用户ID去查数据库拿到当前用户的真实角色再做判断。这个很多人容易忽略结果别人把请求体里的角色改成管理员就拿到了后台权限这是高危漏洞。3. 实现细节从Node.js后端到Vue前端3.1 Node.js后端项目搭建与目录结构后端我用的Express框架因为它轻量、中间件生态丰富适合这种规模的项目。目录结构大致是这样server/ ├── app.js ├── config/ │ ├── db.js │ └── env.js ├── routes/ │ ├── user.js │ ├── house.js │ ├── appointment.js │ └── contract.js ├── controllers/ │ ├── houseController.js │ └── userController.js ├── services/ │ └── houseService.js ├── middlewares/ │ ├── auth.js │ └── errorHandler.js ├── models/ │ ├── House.js │ └── User.js └── utils/ └── response.js这个结构的核心思想是分层路由层只做参数校验和调用ControllerController负责调用ServiceService里写业务逻辑和数据库操作Models定义数据模型。我见过很多新手把所有代码堆在路由回调里一个接口几百行出了问题根本没法定位。分层虽然前期多写几个文件但对维护来说是值得的。数据库连接我用的mysql2原因是支持Promise不依赖回调嵌套。启动时在app.js里初始化连接池设置连接超时和最大连接数。// config/db.js const mysql require(mysql2/promise); const pool mysql.createPool({ host: process.env.DB_HOST, user: process.env.DB_USER, password: process.env.DB_PASS, database: process.env.DB_NAME, waitForConnections: true, connectionLimit: 10, charset: utf8mb4, }); module.exports pool;3.2 核心接口实现与关键代码房源列表接口是系统里被调用最多的接口之一既要支持筛选、分页又要保证SQL效率。我一开始使用拼接字符串的方式动态生成SQL发现过滤器多的时候代码又乱又有注入风险。后来改成参数化查询代码清晰很多。下面是一个简化版本包含城市、租金范围、页码和分页逻辑// services/houseService.js async function getHouseList({ city, priceMin, priceMax, page, pageSize }) { const offset (page - 1) * pageSize; const conditions []; const params []; if (city) { conditions.push(city ?); params.push(city); } if (priceMin) { conditions.push(rent_price ?); params.push(priceMin); } if (priceMax) { conditions.push(rent_price ?); params.push(priceMax); } // 默认只查上架的房源 conditions.push(status ?); params.push(1); const whereSql conditions.length ? WHERE ${conditions.join( AND )} : ; const [countRows] await pool.query( SELECT COUNT(*) AS total FROM houses ${whereSql}, params ); const [list] await pool.query( SELECT id, title, cover_url, rent_price, address, area, rooms, halls FROM houses ${whereSql} ORDER BY create_time DESC LIMIT ? OFFSET ?, [...params, pageSize, offset] ); return { total: countRows[0].total, list, page, pageSize, }; }这里有几个要点。第一分页参数必须用LIMIT和OFFSET但不要把用户传来的page直接拼进SQL一定要转成数字或参数化。第二查询列表时不要SELECT *只取列表页需要的字段减少网络传输。第三总数查询和列表查询分开执行虽然多次查询但逻辑清晰得多。创建合同时我额外加了事务处理。因为合同创建涉及两张表合同表插入新记录房源表更新状态为已出租。如果第一步插入成功第二步更新失败就会出现数据不一致。用事务可以保证两个操作要么都成功要么都回滚。const conn await pool.getConnection(); try { await conn.beginTransaction(); await conn.query( INSERT INTO contracts (contract_no, house_id, tenant_id, landlord_id, start_date, end_date, rent, deposit, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?), [contractNo, houseId, tenantId, landlordId, startDate, endDate, rent, deposit, 0] ); await conn.query( UPDATE houses SET status ? WHERE id ? AND status ?, [3, houseId, 1] ); await conn.commit(); } catch (err) { await conn.rollback(); throw err; } finally { conn.release(); }状态字段里的3代表已出租前台的status 1代表可出租。更新时加上AND status 1这个条件可以防止并发下同一套房被反复出租。3.3 Vue前端路由与状态管理前端我用Vue 3加Vite构建比Vue CLI快很多配置文件也更简洁。路由用Vue Router 4项目里有两种路由公共路由和需要登录后才能访问的路由。我引入了动态路由的概念。用户登录后后端返回角色信息前端再根据角色注册对应路由。比如管理员才显示“用户管理”和“数据报表”房东才显示“我的房源”和“发布房源”。这样做的好处是用户无法直接通过修改URL访问无权限的页面因为对应路由根本没被注册。路由守卫是另一个重点。我在beforeEach里检查目标路由是否需要认证如果需要就判断本地有没有token没有就跳到登录页并且带上来源路径登录成功后自动跳回原页面。router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (to.meta.requiresAuth !token) { next({ path: /login, query: { redirect: to.fullPath } }); } else { next(); } });状态管理我选了Pinia而不是Vuex。Pinia的API更简单没有mutations这一层对新手更友好。我建了三个store用户状态、房源筛选条件、预约流程状态。用户状态里存token、用户信息和角色房源筛选条件用于在列表页和详情页之间保持搜索参数不丢失预约流程状态则记录从选择时间到支付定金如果有的过程。3.4 3D看房与虚拟漫游的落地方式这是“元宇宙平台”的核心功能也是最容易做出差异化的地方。我实际做的混合方案可以分两种情况说明。第一种是全景图看房。前端使用Pannellum这个开源库加载环绕拍摄的鱼眼图它会自动映射到球体内部。用户可以通过鼠标拖拽和滚轮缩放查看房间各个角落。实现并不复杂只要在Vue组件里初始化一个pannellum.viewer对象。template div refpanoContainer classpano-container/div /template script setup import { ref, onMounted, watch } from vue; const props defineProps({ imageUrl: String, autoRotate: { type: Boolean, default: false, }, }); const panoContainer ref(null); let viewer null; function initPano(url) { if (viewer) { viewer.destroy(); } if (!url) return; viewer window.pannellum.viewer(panoContainer.value, { type: equirectangular, panorama: url, autoLoad: true, autoRotate: props.autoRotate ? -1 : 0, showZoomCtrl: true, showFullscreenCtrl: true, }); } onMounted(() initPano(props.imageUrl)); watch(() props.imageUrl, (newUrl) initPano(newUrl)); /script这个组件接收一个图片地址创建后可以自动旋转适合作为房源详情页的沉浸式入口。但需要注意全景图文件体积可能很大我一般在后端生成一个压缩版本同时给用户提示“正在加载全景首次打开请稍等”。第二种是3D户型漫游。我用Three.js加载后端上传的GLB模型。GLB是二进制格式体积比glTF小加载快。模型里面包含了户型墙体、家具、门和窗户。用户按WASD键在房间里移动鼠标控制视角方向。这个部分的代码很多我就不全部贴出来了。关键是控制移动速度和碰撞检测。没有碰撞检测的话用户会直接穿墙体验非常出戏。我做了一个简化方案导入模型时自动提取墙体坐标作为障碍物移动前检查目标位置是否会在墙体内如果是就阻止移动。功能上线后我发现一个问题很多用户第一次用鼠标控制视角会晕。后来我在界面上加了一个“俯视图模式”切换到2D平面图这样至少能保证所有用户都能看明白户型结构。这个妥协很有必要别为了炫技牺牲了易用性。4. 开发环境配置与踩坑实录4.1 Node.js与Vue环境安装这个项目的开发环境是整个团队协作的基础但环境问题却最常让人头大。我推荐使用Node.js LTS版本不要为了尝鲜装最新版。实测下来LTS版本的npm生态兼容性明显更好很多原生模块在奇数版本上编译会报错。安装完Node.js后npm会自带。但国内网络环境下直接执行npm install经常慢到怀疑人生。我一般先把镜像源切到国内有两种方式临时使用npm install --registryhttps://registry.npmmirror.com或者全局配置npm config set registry https://registry.npmmirror.com。前端项目创建时可以用Vite的官方脚手架或者使用npm create vuelatest。这里有个细节新版脚手架会问是否安装Vue Router、Pinia、ESLint等建议全部选Yes省得后面自己补。创建完成后再安装额外的异步组件库和三个依赖比如three、pannellum和axios。4.2 npm脚本无法加载的解决方案新手在Windows上运行Node.js项目时大概率会遇到下面这一条搜索次数极高npm : 无法加载文件 C:\Program Files\nodejs\npm.ps1因为在此系统上禁止运行脚本。这个报错本质上不是Node.js的问题而是Windows PowerShell的执行策略限制了.ps1脚本运行。npm是一个.ps1文件在受限的PowerShell环境里会被拦截。解决办法很简单用管理员身份打开PowerShell。执行Set-ExecutionPolicy RemoteSigned。当提示是否要更改执行策略时输入Y并回车。关闭PowerShell重新打开终端再执行npm命令。这里解释一下RemoteSigned的含义本地脚本允许运行从互联网下载的脚本需要有可信签名才能执行。这已经能满足日常开发需求不会对电脑安全产生大影响。如果你只想对当前用户设置可以加-Scope CurrentUser参数。我在项目初始化时也遇到过这个问题当时直接用了管理员设置后来发现在普通终端运行npm依然报错。原来是因为PowerShell窗口没有重启策略没有刷新。所以完成设置后一定要重开终端这是很多人忽略的细节。4.3 开发中常见的报错与排查开发过程中我记录了几类高频问题整理成一个排查列表方便以后再遇到时快速定位。现象可能原因排查思路前端请求接口报404后端路由没注册先确认后端启动没有再验证路径和参数是否匹配跨域错误前后端端口不同后端配置CORS或前端配置Vite代理npm install卡死镜像源问题切换镜像源检查网络上传图片后加载不出来静态资源路径没配确认Nginx或Express静态目录指向正确看房全景图黑屏图片CORS限制给静态资源服务加Access-Control-Allow-Origin跨域是我在这个项目里踩得最深的坑之一。一开始我图省事在后端直接设置app.use(cors())允许了所有来源。后来发现带token的请求始终不成功排查半天才发现是浏览器在处理OPTIONS预检请求时对Authorization头做了限制。正确做法是显式允许指定源和请求头不要图省事全开。另外一个容易忽略的问题是前端请求时axios没有设置withCredentials或者不携带token。我在封装请求实例时统一从localStorage读取token放到请求头里。如果发现接口返回401先不要怀疑后端token验证逻辑先确认Header真的带上了。调试这类问题我会在浏览器开发者工具的Network面板看请求详情比后端打日志快得多。5. 项目优化与部署上线5.1 性能优化手段系统开发完成后我第一轮优化就集中在首屏加载速度上。Vue的打包文件默认比较大如果不处理用户打开首页可能白屏好几秒。我做了以下几件事路由懒加载把详情页、后台管理页等非首屏页面拆成单独chunk首屏只加载首页相关的代码。组件库按需引入我采用的UI库支持按需引入按钮、表单、弹窗单独打包而不是全量引入。全量引入会让包体多出将近1MB。图片懒加载房源列表页的图片统一加v-lazy指令等图片进入可视区域再加载。后端也做了一部分缓存优化。房源列表页的筛选结果变化频率不高我用Redis缓存了热门城市的前三页数据缓存时间设为5分钟。用户再次搜索相同关键词时直接从Redis返回结果数据库压力明显降低。这个优化在房源数量超过十万条时效果非常明显。数据库索引也是不能忽视的一环。我在houses表的city、status、rent_price字段上建了联合索引在查询预约记录时给user_id建了索引。不要在一开始就建一堆索引要根据实际查询条件来。索引建多了一方面占用磁盘空间另一方面插入和更新都会变慢。我习惯是先用慢查询日志观察一段时间再补充索引。5.2 前后端打包部署前端打包用Vite执行npm run build输出到dist目录。后端部署我用PM2来维护进程因为Node.js进程崩溃后PM2会自动重启不用半夜爬起来手动拉起服务。PM2启动命令大概是这样的pm2 start app.js --name rental-api --env production pm2 save pm2 startup生产环境里我建议把后端服务挂在Nginx后面由Nginx统一处理静态文件、反向代理和HTTPS证书。前端打包出来的静态文件交给Nginx直接托管接口请求则转发到Node.js进程。Nginx配置核心部分如下server { listen 80; server_name your-domain.com; root /var/www/rental-frontend/dist; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }这里有个重要细节location /里必须加上try_files $uri $uri/ /index.html否则用户直接访问某个前端路由路径时Nginx返回404。因为前端是SPA只有index.html一个入口文件刷新页面时需要把请求重写到入口。5.3 安全加固要点安全方面我踩过大大小小的坑这里把最关键的几点列出来。首先是密码存储。绝对不能用明文或简单的MD5我使用的是bcrypt加盐哈希。bcrypt的特点是每次生成的哈希值都不同即使两个用户密码相同存储值也不一样。校验时调用bcrypt.compare方法而不是自己写字符串判断。const bcrypt require(bcrypt); const saltRounds 10; async function createUser(phone, password) { const hash await bcrypt.hash(password, saltRounds); // 将 phone 和 hash 存入数据库 }其次是JWT的使用。签发的token要设置过期时间短一点更安全比如2小时。用户在登录后前端定时刷新token或者做一个刷新接口。如果在一个被盗用的token未过期前发现异常可以通过维护一个黑名单列表来阻止访问虽然增加复杂度但对管理后台这类敏感系统是必要的。最后是SQL注入和XSS。SQL注入的防范核心是参数化查询我前面已经提过。XSS则要从前端和后端两边防御前端在渲染用户提交的内容时用文本插值而不是v-html后端对用户上传的内容做转义和校验。我在房源标题和公告栏两个地方吃过XSS的亏后来统一改为富文本白名单过滤只允许加粗、链接、列表等安全标签其余标签一律剔除。部署时还应该加上HTTPS推荐使用Let’s Encrypt证书或云厂商免费证书。没加HTTPS之前用户登录信息在HTTP明文传输如果网络被监听账号密码就完全暴露了这个风险一定要在正式上线前消除。最后再分享一点我个人的体会。这个项目最让我意外的不是技术难点而是用户对“元宇宙看房”的接受度。一开始我做3D漫游觉得所有功能都要酷炫后来给中介试用了几天反馈最多的反而是“加载太快了”和“里面能不能放个卷尺”他们真正关心的是效率和信息量而不是炫技。所以如果你也想做类似的系统建议先从全景看房这个轻量功能切入把选房、签约、账单管理这些核心流程做稳再逐步叠加3D模型、视频带看、智能推荐。这个项目后续我还打算加上基于用户浏览行为的推荐算法以及房东和租客的在线消息沟通。技术与业务做到平衡系统的价值才能真正体现出来。
返回列表