基于SpringBoot+Vue的民宿租赁系统实战开发与部署全解析

发布时间:2026/10/2 12:23:38
基于SpringBoot+Vue的民宿租赁系统实战开发与部署全解析 做民宿租赁这类系统最难的不是某个功能写不写得出来而是“前后端怎么拆、拆完之后怎么合、部署之后怎么调”。我前后用 SpringBoot Vue MyBatis MySQL 这套组合做了一个完整的民宿租赁系统从需求分析、数据库设计、接口开发、前端页面到打包部署全部跑通。这篇文章就把整个项目的设计思路、核心代码、部署过程和踩过的坑一次性讲清楚适合正在学前后端分离项目实战的同学也适合毕设选题是民宿、租房、酒店管理方向的人直接参考。先说结论这套系统骨架完全可以复用。民宿租赁本质上是“房源信息管理 订单状态流转 用户权限控制”的组合把这三个核心搞定换一个业务场景比如会议室预约、二手租赁、工位预订只是改字段和页面的事。下面我按一个完整项目的推进顺序把每一步拆给你看。1. 项目整体设计与技术选型思路1.1 为什么一定要做前后端分离很多人第一次做课程设计或者毕设习惯用 Thymeleaf 或者 JSP 在服务端直接渲染页面。那种方式在小项目里确实省事但一旦涉及多端复用、多人协作、独立迭代问题就来了。民宿租赁系统这个项目我一开始就定了前后端分离原因有三个第一业务天然分角色。用户端要的是一个体验流畅的浏览预订界面管理端要的是表格密集、操作高频的后台界面这两种界面如果混在一套服务端模板里代码会越写越乱。前后端分离之后用户端和管理端可以是两套 Vue 项目也可以是一个项目里做路由区分后端只提供 JSON 接口互不干扰。第二后端接口可以被多端调用。同一个查询房源的接口用户端在用管理端在用后续如果要做小程序端接口可以直接复用不用重新写服务端逻辑。第三部署和排障更清晰。前端打包成静态文件交给 Nginx后端打成 Jar 包独立运行。前端挂了不影响后端后端重启也不影响前端静态资源接口报错时直接看 Network 面板就能定位是前端的问题还是后端的问题。1.2 技术栈选型为什么是 SpringBoot Vue MyBatis MySQL这套组合是当前 Java 领域最稳的入门到实战路线没有之一。SpringBoot解决了传统 SSM 项目里大量 XML 配置的问题。以前写一个 SpringMVC 项目要配 web.xml、springmvc.xml、applicationContext.xmlSpringBoot 一个启动类全部搞定内嵌 Tomcat 也让部署变得极其简单java -jar一条命令就能跑起来。Vue选择它的理由很直接组件化开发在后台管理系统和展示型页面上的效率确实比 jQuery 那套高太多。我用的 Vue 2 Vue Router Vuex虽然现在 Vue 3 已经普及但考虑到很多学校课程和现有教程还以 Vue 2 为主这套项目的生态资料最全遇到问题搜起来最快。MyBatis是用 Java 做持久层绕不开的选择。相比 JPA/Hibernate 的全自动映射MyBatis 的半自动模式在复杂查询上更好控制SQL 写什么样执行就是什么样出现性能问题可以精准优化。后面我会详细讲我封装的那些动态 SQL那是这个项目里最有含金量的部分。MySQL没什么好犹豫的开源免费、资料多、云服务器上装一个很方便。民宿租赁这种量级的并发MySQL 配上合理索引和连接池完全够用。1.3 功能模块怎么划分拿到需求先别急着写代码先把角色和模块理清楚。这个系统我分成三种角色角色核心诉求对应模块游客浏览房源、查看详情房源列表、房源详情注册用户预订民宿、管理订单、发表评价在线预订、订单中心、评价管理管理员管理房源信息、处理订单、管理用户房源管理、订单管理、用户管理、数据统计功能模块就是三块主线房源模块发布、上下架、搜索、详情、图片、订单模块创建订单、支付模拟、取消、确认入住、退房结算、用户模块注册登录、个人信息、我的订单、我的评价。我特意把“支付”做成模拟支付因为接入真实支付宝/微信支付需要企业资质和商户号个人项目通常不具备条件。订单状态机设计成合理闭环就行真实支付网关留出接口位置以后有条件直接替换。2. 数据库设计与核心表结构2.1 六张核心表的字段设计数据库设计直接决定后面写代码的心情。我强烈建议动手建表前先画一遍 E-R 图哪怕用纸笔画都行。这个项目我总共设计了六张表用户表、民宿信息表、民宿图片表、订单表、评价表、收藏表。用户表t_user关键字段是用户名、密码、手机号、角色标识和头像。密码一定不能明文存我用了 BCrypt 加密。这里有个细节注册时前端把密码传过来后端在 Service 层加密后再入库千万不要在 SQL 里写死加密逻辑。民宿表t_homestay是这个项目的核心字段比较多民宿名称、简介、地址、所属城市、价格按晚计费、床位数、户型、设施标签、封面图、状态0下架 1上架 2审核中、房东ID、创建时间。城市字段单独拎出来建索引因为搜索场景里“按城市找民宿”是最高频操作。图片表单独拆出来而不是直接存字符串拼接的路径这是我最推荐的做法。一张民宿多张图片单独建表可以用homestay_id img_url sort三个字段查的时候ORDER BY sort后续如果要做图片懒加载、首图轮播数据处理都更灵活。订单表t_order的字段要特别注意订单编号用时间戳随机数生成、民宿ID、用户ID、入住日期、离店日期、入住人数、订单金额、状态、创建时间。金额我用的DECIMAL(10,2)绝对不用float或double浮点数的精度问题在涉及钱的地方会吃大亏。评价表和收藏表结构相对简单评价表关联订单ID、民宿ID、用户ID、评分、内容收藏表做联合唯一索引(user_id, homestay_id)防止重复收藏。2.2 订单状态流转的细节设计订单是这个系统里逻辑最复杂的部分状态流转设计的核心代码如下CREATE TABLE t_order ( id BIGINT NOT NULL AUTO_INCREMENT, order_no VARCHAR(32) DEFAULT NULL, user_id BIGINT DEFAULT NULL, homestay_id BIGINT DEFAULT NULL, check_in_date DATE DEFAULT NULL, check_out_date DATE DEFAULT NULL, guest_count INT DEFAULT NULL, total_amount DECIMAL(10, 2) DEFAULT NULL, status TINYINT DEFAULT NULL, create_time DATETIME DEFAULT NULL, pay_time DATETIME DEFAULT NULL, PRIMARY KEY (id), KEY idx_user_id (user_id), KEY idx_homestay_id (homestay_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;订单状态我用一个 TINYINT 字段配合常量类管理定义如下状态值含义可操作动作0待付款取消订单、去支付1已付款待入住取消订单需申请退款2已入住不可取消3已退房发表评价4已取消无状态流转的逻辑集中在 Service 层用一个OrderStatusEnum枚举来管理每个业务方法只允许特定的状态转换。比如支付方法只接受 status0 的订单取消方法只接受 status0 或 status1 的订单。这样写虽然有代码量但比“随意 update status”安全得多线上项目最怕状态乱跳后面写统计报表的时候你会发现状态机清晰有多重要。金额计算我单独抽了一个PriceCalculator工具类输入民宿单价、入住日期和离店日期计算总共多少晚。这里要注意check_out_date是离店日期不能算进费用里比如 7 月 1 日入住、7 月 3 日离店是 2 晚代码里直接用DateUtils算天数差就行。权限校验也要做用户只能查看和操作自己的订单管理端接口要校验管理员角色这个绝对不能省。2.3 索引设计与查询优化民宿列表页的查询条件通常是“城市 入住日期 离店日期 关键词”对应的 SQL 是SELECT h.*, (SELECT MIN(price) FROM t_homestay WHERE city h.city) AS min_price FROM t_homestay h WHERE h.city #{city} AND h.status 1 ORDER BY h.create_time DESC LIMIT #{offset}, #{pageSize}这个查询在city和status上建联合索引idx_city_status (city, status)之后性能提升非常明显。我在测试数据里插了 5000 条假数据不加索引的查询在本地要 300ms 左右加索引之后直接降到 10ms 以内。记住最左前缀原则联合索引(city, status)中 city 必须在前面如果你单独查 status这个索引是用不上的。日期查询我推荐用Date类型存储不要存字符串。字符串比较大小虽然也能用但遇到格式不统一比如 2024-07-1 和 2024-07-01就会出问题。入库前统一用LocalDate.parse()或者SimpleDateFormat格式化。3. 后端核心接口与 MyBatis 实现3.1 SpringBoot 项目结构与 RESTful 接口规范后端项目的包结构长这样com.example.homestay ├── controller # 接收请求返回统一结果 ├── service # 业务逻辑 ├── mapper # MyBatis Mapper 接口 ├── entity # 数据库实体类 ├── dto # 前端交互的数据对象 ├── vo # 视图对象 ├── config # 跨域、拦截器等配置 └── common # 统一返回结果、异常处理Controller 层只做三件事接收参数、调用 Service、返回统一结果。我定义了一个ResultT统一返回类结构是{code, message, data}code 为 200 表示成功其他为业务错误码。这样前端 Axios 拦截器只用判断 code 就能统一处理错误不用每个接口单独写一套。接口设计遵循 RESTful 风格查询用 GET新增用 POST修改用 PUT删除用 DELETE。比如民宿相关的接口RestController RequestMapping(/api/homestay) public class HomestayController { GetMapping(/list) public ResultPageResultHomestayVO list(RequestParam Integer page, RequestParam Integer size, RequestParam(required false) String city) { return Result.success(homestayService.pageQuery(page, size, city)); } GetMapping(/{id}) public ResultHomestayDetailVO detail(PathVariable Long id) { return Result.success(homestayService.getDetail(id)); } }这里有个实际经验分页参数必须给默认值而且要做上限保护。我见过很多项目前端传一个 size9999 就把全部数据拉出来了内存和带宽全浪费。我在配置类里统一处理page 最小为 1size 最大不超过 100。3.2 MyBatis 动态 SQL 的实战用法这个项目里 MyBatis 的动态 SQL 是重头戏。搜索接口往往有多个可选条件城市可能传也可能不传关键词可能传也可能不传价格区间可能传也可能不传。用if标签组合即可select idpageQuery resultTypecom.example.homestay.entity.Homestay SELECT * FROM t_homestay where if testcity ! null and city ! AND city #{city} /if if testkeyword ! null and keyword ! AND (name LIKE CONCAT(%, #{keyword}, %) OR address LIKE CONCAT(%, #{keyword}, %)) /if if testminPrice ! null AND price gt; #{minPrice} /if if testmaxPrice ! null AND price lt; #{maxPrice} /if AND status 1 /where ORDER BY create_time DESC LIMIT #{offset}, #{pageSize} /select几个细节要认真说。第一where标签会自动去掉第一个多余的AND比手动写WHERE 11优雅得多。第二和在 XML 里必须转义成gt;和lt;否则 XML 解析直接报错这个是新手最容易踩的坑。第三CONCAT(%, #{keyword}, %)这样做模糊查询在前缀匹配的情况下还能走索引。MyBatis 的 Mapper 接口和 XML 绑定我用的方式是接口方法名和 XML 的id一致然后用Mapper注解扫描。项目里尽量不要用Select注解写复杂 SQLXML 文件能改、能统一管理、能直接复制到 Navicat 里调试在生产维护阶段这个优势会被无限放大。3.3 文件上传与静态资源处理民宿封面图和轮播图的上传我用的是本地磁盘存储不往数据库存。上传接口把文件保存到服务器指定目录然后往数据库里记录的是访问 URL例如/upload/2024/07/xxx.jpg。这样做的好处是后续迁移到 OSS、MinIO 或者 Nginx 托管都非常容易只改上传接口的实现表结构不用动。SpringBoot 里做文件上传很简单Controller 接收MultipartFile校验文件大小和后缀名PostMapping(/upload) public ResultString upload(RequestParam(file) MultipartFile file) { if (file.isEmpty()) { return Result.fail(文件不能为空); } String originalFilename file.getOriginalFilename(); String ext originalFilename.substring(originalFilename.lastIndexOf(.)); // 只允许图片类型 ListString allowedExt Arrays.asList(.jpg, .jpeg, .png, .gif, .webp); if (!allowedExt.contains(ext.toLowerCase())) { return Result.fail(不支持的图片格式); } String newFileName UUID.randomUUID().toString().replace(-, ) ext; // 按日期分目录存储 String datePath new SimpleDateFormat(yyyy/MM).format(new Date()); String savePath uploadDir / datePath / newFileName; file.transferTo(new File(savePath)); return Result.success(/upload/ datePath / newFileName); }这里有两个很实际的经验。文件名一定用 UUID 重命名不要用用户上传的原始文件名否则会有两个问题中文文件名乱码、同名文件互相覆盖。目录按年月分一年后文件多了方便清理也避免一个目录下文件数过多导致文件系统性能下降。静态资源映射需要在配置类里加WebMvcConfigurer把/upload/**映射到磁盘物理路径。跨域配置也放在这里允许前端开发服务器的地址访问Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceHandler(file: uploadDir /); }4. Vue 前端实现要点4.1 路由与登录拦截前端我用的 Vue Router。路由配置分成两块不需要登录就能访问的路由首页、房源列表、房源详情、登录注册页和需要登录才能访问的路由订单中心、个人中心、民宿管理后台。登录拦截用 Vue Router 的全局前置守卫实现router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (to.meta.requiresAuth !token) { next({ path: /login, query: { redirect: to.fullPath } }); } else { next(); } });登录成功后把 token 存在localStorage同时存一份用户基本信息在 Vuex 里。路由守卫只做前端拦截真正的安全校验在后端后端有拦截器统一检查请求头里的 token解析失败直接返回 401。记住一句话前端拦截是用户体验后端拦截才是安全底线。动态路由我一开始也考虑过——根据用户角色动态生成可访问的路由表比如管理员才能访问民宿管理页面。后来实际做的时候发现这个项目角色就两类直接静态配置路由 菜单用v-ifisAdmin控制显隐更简单可靠。动态路由适合那种权限粒度很细的管理系统这里过度设计反而增加维护成本。4.2 Axios 封装与跨域联调Axios 封装是前端项目的基建我统一建了一个request.jsimport axios from axios; const request axios.create({ baseURL: /api, timeout: 10000 }); // 请求拦截器自动携带 token 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 200) { return res.data; } if (res.code 401) { localStorage.removeItem(token); router.push(/login); } return Promise.reject(new Error(res.message || 请求失败)); }, error { return Promise.reject(error); } ); export default request;baseURL写成/api而不是完整的http://localhost:8080这是前后端分离联调最优雅的处理方式。开发环境在 Vue 的vue.config.js里配置 devServer 代理把/api转发到后端地址devServer: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }生产环境则由 Nginx 把/api反向代理到后端服务。这样前端代码里不需要区分环境开发和生产用同一套接口路径避免出现“本地调试没问题部署到服务器就 404”的尴尬。4.3 核心页面组件拆解前端页面我按组件化的思路拆。房源列表页是重点拆成SearchBar搜索栏、HomestayCard民宿卡片、Pagination分页三个组件。HomestayCard接收一个homestay对象展示封面图、名称、地址、价格、评分点击跳转到详情页。房源详情页用了动态路由路径配置为/homestay/:id页面加载时通过this.$route.params.id调接口拿详情数据。图片区域我做了简单的轮播数据来自/api/homestay/{id}返回的图片列表字段。订单提交流程是这样用户在详情页选择入住日期、离店日期、入住人数前端根据民宿单日价格实时计算预估价展示给用户用户点击“立即预订”后跳转到确认页提交订单。前端计算价格只是为了体验最终价格必须以后端计算为准防止有人篡改请求参数绕过价格校验。我这里后端在创建订单时重新算了一遍金额并把剩余的库存/可订状态也重新校验了一次这个双保险非常必要。订单列表页用了el-tabs按状态分“全部 / 待付款 / 待入住 / 已入住 / 已完成 / 已取消”几个 Tab每个 Tab 调对应的接口参数。表格用的el-table操作列根据订单状态动态渲染按钮待付款显示“去支付”和“取消”已退房显示“去评价”。5. 完整部署流程从本地到云服务器5.1 环境准备与项目初始化部署之前先把环境准备好。我用的云服务器是 2 核 4G 配置系统 CentOS 7。需要装的东西有JDK 8、MySQL 5.7、Nginx、Maven只要本地打包用服务器上不一定要装、Git用来拉取代码。MySQL 安装我建议用 rpm 包方式比编译安装省事得多。装完后第一件事就是改 root 密码然后创建业务数据库和专用账号CREATE DATABASE homestay DEFAULT CHARACTER SET utf8mb4; CREATE USER homestay% IDENTIFIED BY StrongPassword123; GRANT ALL PRIVILEGES ON homestay.* TO homestay%; FLUSH PRIVILEGES;用专用账号而不是 root 连接应用这是数据库安全的基本操作也方便以后做权限回收。utf8mb4 必选因为民宿描述里可能有 emoji 表情utf8 存不了。后端配置文件application.yml里数据库连接用如下配置spring: datasource: url: jdbc:mysql://localhost:3306/homestay?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: homestay password: StrongPassword123 driver-class-name: com.mysql.cj.jdbc.DriverMySQL 8.x 的驱动类是com.mysql.cj.jdbc.Driver5.x 是老版本的com.mysql.jdbc.Driver这里版本对应错了启动就直接报错。serverTimezoneAsia/Shanghai必须加否则时区问题会导致日期数据错了 8 个小时。5.2 后端 Maven 打包与运行后端打包我用 Maven前提是本地能连上 Maven 中央仓库下载依赖。如果网络受限配置阿里云镜像源能大幅提速在settings.xml的 mirrors 节点加镜像即可。打包命令mvn clean package -DskipTests-DskipTests跳过测试因为很多项目的单元测试依赖本地数据库环境在打包机上跑会失败导致整个构建中断。打包完成后target目录下会生成homestay-0.0.1-SNAPSHOT.jar。上传到服务器之后用nohup后台启动nohup java -jar homestay-0.0.1-SNAPSHOT.jar --server.port8080 homestay.log 21 启动日志会写到homestay.log里排bug全靠它。查看启动是否成功用tail -f homestay.log看到Started Application in x.x seconds说明启动成功。然后curl http://localhost:8080/api/homestay/list简单验证接口通不通。这里有个很关键的细节不要用 root 用户直接运行 jar 包。我一开始图省事直接 root 跑后来发现安全隐患很大。建议创建一个普通用户把 jar 文件放到/opt/homestay目录下目录属主改成普通用户再启动。5.3 前端构建与 Nginx 反向代理配置前端构建前先检查vue.config.js里的publicPath。默认是/如果你的系统要部署在域名的子路径下比如http://ip/homestay/就要改成./或者具体路径。我这次是直接部署在根路径保持默认即可。npm install npm run build构建成功的标志是dist目录生成。把dist里所有文件上传到服务器的/usr/share/nginx/html目录。Nginx 配置是前后端分离部署最关键的一环我的配置长这样server { listen 80; server_name your_domain_or_ip; location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://localhost:8080/api/; 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 /upload/ { proxy_pass http://localhost:8080/upload/; } }try_files $uri $uri/ /index.html;这行是 Vue Router history 模式的核心配置。不加这行用户在详情页刷新比如/homestay/3时Nginx 会去找服务器上真实的homestay/3文件找不到就 404。加了这行所有匹配不到静态文件的请求都会回退到index.html由 Vue Router 接管路由。/api前缀的请求全部转发给后端前端代码里的baseURL: /api和这个配置完美对应。/upload图片请求也要转发否则详情页图片全挂。配置完成后nginx -t检查语法然后nginx -s reload重载配置。到这里整站就跑通了浏览器访问 IP 看到首页登录、搜索、下单、上传图片、后台管理全链路可用。6. 常见问题与排查记录6.1 跨域问题报错还是报错前后端分离第一个拦路虎必然是跨域。开发环境我建议直接走代理不要用后端CrossOrigin注解硬开跨域。代理方案下浏览器看到的请求是同源的不会触发 CORS 机制。生产环境有 Nginx 反向代理更不存在跨域问题。如果你确实要用跨域方式调试后端可以写一个全局 CORS 配置但注意allowedOriginPatterns要写具体来源不要用*放任所有来源。我见过有人直接用http://localhost:9528这种前端开发地址上线前忘了删结果外域请求也能访问接口这是安全隐患。6.2 MySQL 连接踩坑记录我这次部署遇到最恶心的问题是 MySQL 8 的 SSL 连接报错。本地开发没问题一到服务器就报javax.net.ssl.SSLHandshakeException。查了一圈原因是 MySQL 8 默认开启 SSL而服务器上的 JDK 在 SSL 握手时出了兼容问题。解决办法在连接 URL 里显式关掉 SSLuseSSLfalse同时不加verifyServerCertificate参数。如果你用的数据库驱动版本和 MySQL 服务端版本相差太大还会遇到Public Key Retrieval is not allowed的报错同样在 URL 后面加allowPublicKeyRetrievaltrue解决。这两个参数我建议直接写进application.yml省得每次新环境都要排查一遍。6.3 MyBatis 映射常见坑MyBatis 项目里最常见的问题是“查询结果全是 null但 SQL 在 Navicat 里跑得好好的”。原因九成是实体类字段和数据库列名映射不上。MySQL 的字段命名习惯是check_in_dateJava 实体类习惯是checkInDate如果不做配置MyBatis 按驼峰自动映射的能力默认是关闭的。解决办法有两种一是写resultMap手动映射每一列字段多了非常繁琐二是在application.yml里打开驼峰映射mybatis: configuration: map-underscore-to-camel-case: true我强烈建议用第二种一个配置项解决所有映射问题。前提是数据库字段命名规范统一用下划线Java 字段统一用驼峰否则映射仍然不生效。还有一个坑是WHERE条件里传了null导致动态 SQL 漏掉条件查出了全表数据。排查方法是开启 MyBatis 的 SQL 日志在配置里加上logging: level: com.example.homestay.mapper: debug日志里会打印完整的 SQL 和参数值一眼就能看出是哪个条件被漏掉了。这个日志在生产环境不要开会刷屏而且暴露 SQL 细节只在需要排查的时候临时打开。6.4 Vue 打包部署的典型问题前端部署遇到的第一个问题是刷新 404这个我在上文已经说过try_files解决。第二个问题是静态资源路径不对CSS 和 JS 加载 404检查publicPath和实际部署路径是否匹配。第三个问题是接口地址错误。前端请求打到了 Nginx 的 80 端口但后端根本没被代理到——检查nginx.conf里location /api/是否配置正确可以用curl http://localhost:8080/api/...在服务器上直连测试先确认后端活着再排查代理。第四个问题比较隐蔽前端修改了一个小按钮文案部署上去却发现页面没变化。这是因为浏览器缓存了旧的 JS 文件。解决办法是在 Vue 构建时开启文件指纹或者部署后用版本号参数强制刷新。我用的是最土但最有效的方法构建、上传、然后nginx -s reload如果还是旧的就 CtrlF5 强刷再不行排查 CDN 缓存或浏览器缓存。我实际开发中还遇到过一次很小但很典型的失误前端npm run build时报内存溢出查了资料是因为 webpack 打包时默认内存上限不够解决办法是给 Node 增加内存参数NODE_OPTIONS--max-old-space-size4096 npm run build这个小坑在处理大项目的时候很容易碰到建议直接写进项目的package.json的 scripts 里。最后说几句实在话。这个民宿租赁系统虽然功能不算特别复杂但五脏俱全用户体系、权限拦截、复杂条件查询、订单状态机、文件上传、图片管理、前后端分离部署这些恰恰是真实企业项目里最高频的能力组合。我建议拿到这套源码的同学不要只停留在“跑起来”这一步试着改一个功能感受一下连锁反应比如给民宿增加一个“可预订日期”的库存字段、给订单增加一个真实支付回调接口、把本地文件存储替换成 MinIO任何一个改动都能让你的理解上一个台阶。我个人的体会是毕设和项目实战最大的差别不在于功能多少而在于你有没有真正搞懂每一行代码为什么这么写。把状态机、动态 SQL、路由守卫、反向代理这四个点吃透这个项目就真正属于你自己了。后续如果想扩展可以往多房东入驻、优惠券系统、消息通知这几个方向做表结构和接口扩展空间我在设计之初都留好了位置改起来不会伤筋动骨。