Spring Boot+Vue+MyBatis民宿租赁系统:从数据库设计到部署上线全解析
说个挺常见的场景身边有朋友做了几年民宿生意订单靠微信聊天记录和 Excel 表格轮记节假日来了两拨客人同时订同一间房撞车之后赔钱又赔口碑。他找到我的时候问能不能搞一套系统我给他落地了一套前后端分离的民宿租赁系统——就是标题里这套 Spring Boot Vue MyBatis MySQL 的完整方案从数据库设计到前端页面再到服务器上跑起来一共两周时间。今天把这套系统的设计思路、核心实现和部署细节完整拆开写一遍源码结构和部署命令可以直接抄适合正在学前后端分离项目的 Java 同学、准备课程设计/毕业设计的人以及确实有小民宿管理系统需求的朋友参考。1. 民宿租赁系统的业务拆解与技术选型依据1.1 这个系统到底解决什么业务问题先别急着看代码做系统之前得把业务边界理清楚。民宿租赁和传统酒店预订有一个很大的区别民宿通常是小规模、多房源、价格灵活房东身兼运营、客服、保洁调度多重角色。这套系统面向的用户分两类一类是普通访客他们要浏览房源、查看房型详情、选定入住和离店日期、线上下单另一类是民宿管理员负责上下架房源、管理订单状态、处理退改签。核心流程可以压缩成一条线访客选房 - 提交入住单 - 确认价格 - 下单支付 - 房东接单 - 到期入住 - 退房评论。支付环节在实际小民宿里往往走线下微信或者支付宝转账系统在订单状态上做一个“待支付 - 已支付”的标记即可不需要真的接第三方支付 SDK。这么设计不是偷懒而是避免引入商户号申请、回调验签等一系列对学习项目来说过重的环节同时保持业务闭环完整。清楚了这条业务线数据库表和接口设计就都有了依据。整套系统的功能拆成三块游客端的房源浏览与下单、用户中心的订单与收藏管理、后台管理端的房源和订单维护。三块之间通过角色权限和登录状态隔离开这就是前后端分离项目最典型的权限模型。1.2 技术栈为什么是 Spring Boot Vue MyBatis MySQL选这套组合有几个非常现实的原因。后端用 Spring Boot看中的是它的自动配置和内嵌 Tomcat一个mvn spring-boot:run或者java -jar就能起来不要求你懂复杂的容器配置同时 Spring Boot 的生态太成熟了拦截器、参数校验、全局异常处理都有现成方案踩坑容易找到答案。前端用 Vue 而不选 React主要考虑两点一是 Vue 的中文资料和学习曲线对国内开发者更友好二是 Element UI 这类组件库能让后台管理页面快速成型。民宿系统的重点是“快速、稳定、够用”不需要承担太重的前端交互复杂度。MyBatis 出现在这里而不是 JPA/Spring Data JPA是因为民宿系统的查询场景里有大量动态条件——按城市、按价格区间、按入住日期过滤房源这种 SQL 用 MyBatis 的 XML 写起来最直观你完全能掌控 SQL 的执行计划。加上 MyBatis 本身学习成本不高面试也常问用它做一个项目能同时喂饱课程设计和求职展示两个需求。MySQL 的选择不需要多解释开源、免费、普及率高学校和企业都在用。至此整套技术栈没有任何冷门组件任何人拿到源码都容易复现。1.3 项目整体目录规划动手之前先约定好目录和模块边界这是前后端分离项目最容易乱的地方。我的后端工程叫homestay-server前端工程叫homestay-web两者完全独立只通过 HTTP 接口通信。后端的包结构这么分com.example.homestay ├── config # 跨域、WebMvc、拦截器注册 ├── controller # 接口层只做参数接收和结果返回 ├── service # 业务层事务边界在这里 ├── mapper # MyBatis 的 Mapper 接口 ├── entity # 数据库实体 ├── dto # 接口传输对象避免把实体直接暴露出去 ├── common # 统一返回结果、状态码、全局异常 └── util # JWT 工具类等这个结构看起来传统但对教学和毕设非常实用。评论区不少人和我讨论过“按技术层分包”和“按业务模块分包”哪个更好我的看法是业务逻辑复杂度上来之后按模块分包更清晰但民宿这种规模的项目按技术层分包配合 controller 尽量轻薄、复杂逻辑下沉到 service 的约束反而是最好维护的。别为了架构而架构项目边界和团队经验匹配最重要。2. 数据库建模四张核心表与时间冲突判断2.1 用户、房源、订单、评论的表结构设计数据库是整个系统的地基表字段设计错了后面处处别扭。民宿系统最核心的几张表是用户表、房源表、房源图片表、订单表、评论表和收藏表其中我特别想把house和order的设计拿出来讲透。用户表tb_user的字段不复杂但密码存储方式我要多说一句绝不能明文存密码后端用 BCrypt 做加密注册时encoder.encode(password)登录时encoder.matches(rawPassword, 加密后密码)做校验。表里加一个role字段区分普通用户和管理员值为USER或ADMIN后续做权限拦截就是一条判断语句的事。CREATE TABLE tb_user ( id bigint NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 登录名, password varchar(100) NOT NULL COMMENT BCrypt加密后的密码, phone varchar(20) DEFAULT NULL, avatar varchar(255) DEFAULT NULL, role varchar(20) NOT NULL DEFAULT USER COMMENT USER/ADMIN, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;房源表tb_house是信息最密集的一张表民宿和酒店的差别在“个性化”所以除了价格、城市这些常规字段我还加了房型、可住人数、床位数、面积这些特色描述字段让前端可以按这些维度做筛选展示。CREATE TABLE tb_house ( id bigint NOT NULL AUTO_INCREMENT, owner_id bigint DEFAULT NULL COMMENT 房东用户id后台录入时可先为空, title varchar(100) NOT NULL COMMENT 房源标题, description text COMMENT 房源详情描述, city varchar(50) NOT NULL, address varchar(255) DEFAULT NULL, price_per_night decimal(10,2) NOT NULL COMMENT 每晚价格, area decimal(10,2) DEFAULT NULL COMMENT 面积单位平方米, room_count int DEFAULT 1 COMMENT 卧室数, bed_count int DEFAULT 1 COMMENT 床位数, max_guests int DEFAULT 2 COMMENT 可住人数, cover_image varchar(255) DEFAULT NULL COMMENT 封面图URL, status tinyint NOT NULL DEFAULT 1 COMMENT 0下架 1上架, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_city_status (city, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;注意我在city和status上建了联合索引。民宿列表页最常见的查询就是“某城市 上架中的房源”这个联合索引能直接覆盖。这里有一个新手特别容易犯的错误只在status上建索引结果城市过滤还是全表扫。订单表是整个系统业务约束最密集的表也是民宿系统能不能“用起来”的关键。CREATE TABLE tb_order ( id bigint NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 业务订单号, user_id bigint NOT NULL, house_id bigint NOT NULL, check_in_date date NOT NULL COMMENT 入住日期, check_out_date date NOT NULL COMMENT 离店日期, days int NOT NULL COMMENT 入住天数, total_price decimal(10,2) NOT NULL COMMENT 订单总价, status tinyint NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2已入住 3已完成 4已取消, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, pay_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_id (user_id), KEY idx_house_status_date (house_id, status, check_in_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;订单状态机很关键待支付 - 已支付 - 已入住 - 已完成这是一个正向链路待支付和已支付都可以进入已取消。订单创建时默认待支付只有确认支付后才算“占用”了房间的日期段。这个状态定义直接影响下一节“时间段冲突判断”的 SQL。评论和收藏表逻辑相对简单评论表关联用户、房源、订单三张表保证“订过房的人才能评论”收藏表加一个唯一索引(user_id, house_id)防止重复收藏。这里不多展开源码里有完整 SQL。2.2 为什么不用数据库外键约束这个设计可能让一些学校老师不满意但做真实项目的人都知道这是主流。我在tb_order里关联user_id、house_id但没有加任何FOREIGN KEY约束原因有三个。第一物理外键会影响业务变更的灵活性。比如说订单表以后要支持“删除用户时把订单标记为注销用户”有物理外键约束就得先处理子表或者改约束策略没有外键的话应用层自己控制代码更直接。第二物理外键在并发写入场景下会增加额外的锁开销对于民宿这种量级虽然感觉不到但习惯要从一开始养成。第三MyBatis 的查询本来就是按需 join物理外键能带来的引用完整性保护应用层的事务和业务校验完全能替代。引用完整性由谁保证答案是在 service 层做校验。创建订单前先检查用户是否存在、房源是否上架、再检查日期段是否冲突这三个条件在一个事务里完成效果等同于外键约束但代码逻辑一目了然。2.3 时间段冲突判断民宿系统的核心难点民宿预订和商品购物最大的区别在于商品库存是离散的卖一件少一件民宿的“库存”是一段时间区间两个订单的入住日期发生了部分重叠就意味着房间被重复售卖。这里我给出一个非常经典也足够高效的冲突判定 SQL它是整套系统价值最高的片段之一。判断一个房源在[checkIn, checkOut)时间段内是否已被占用核心条件是select idcountConflictOrders resultTypeint SELECT COUNT(*) FROM tb_order WHERE house_id #{houseId} AND status IN (1, 2, 3) AND check_in_date lt; #{checkOut} AND check_out_date gt; #{checkIn} /select这个 SQL 的逻辑来自区间重叠的数学判定两个区间[a, b)和[c, d)有交集当且仅当a d且c b。套到订房场景里已有订单的区间是[check_in_date, check_out_date)新订单想订的区间是[#{checkIn}, #{checkOut})两者重叠的条件就是上面这条 SQL 的写法。很多新手容易写反写成check_in_date #{checkIn} AND check_out_date #{checkOut}意思是“已有订单完全包含在新订单内”这只覆盖了一种情况。考虑完整的话新订单可能完全包含已有订单、部分重叠在左侧、部分重叠在右侧、完全被包含一共四种位置关系。用起点小于对方终点 且 终点大于对方起点这个公式一次就能命中所用重叠场景你花十分钟推一遍就永远不会忘了。2.4 价格计算与事务边界价格通过入住天数算出来days (check_out_date - check_in_date) / 86400000在 Java 里用ChronoUnit.DAYS.between(checkIn, checkOut)更稳妥然后totalPrice pricePerNight.multiply(days)。这里特别注意pricePerNight和totalPrice要用BigDecimal不能动不动就用double涉及金额的地方精度错了是要出大事的。下单整个流程包在一个Transactional事务里生成订单号、插入订单记录、扣减后续判断的可订余量这里的业务设计是不需要单独做库存表的靠冲突查询保证。事务边界放在 service 层这样 controller 里调用一个方法就能完成整个下单动作如果任意一步失败订单记录自动回滚不会出现支出半截订单的脏数据。3. 后端实现Spring Boot 项目骨架与 MyBatis 配置实战3.1 初始化工程和基础依赖后端工程我用 Spring Boot 2.7.xJava 8。为什么不用 Spring Boot 3如果读者用 JDK 17 且不在乎旧生态3.x 确实没问题但对于课程设计和大部分公司的存量项目Spring Boot 2.7 JDK 8 的组合兼容性最好网上能搜到的各类报错方案也都是围绕这个组合来的。别图新版本项目重点是跑通业务而不是陪框架踩升级坑。pom.xml里核心依赖就这几样spring-boot-starter-web、mybatis-spring-boot-starter2.2.2 版本、mysql-connector-java8.0.x用runtime范围、lombok减少样板代码额外加jjwt做登录令牌、spring-boot-starter-validation做参数校验。application.yml里最关键的配置是数据源和 MyBatis 三件套spring: datasource: url: jdbc:mysql://localhost:3306/homestay_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.homestay.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImplmap-underscore-to-camel-case: true是 MyBatis 最容易忽略也最实用的一个配置项。数据库字段create_time会自动映射到实体类的createTime没有它你的实体类要么字段命名改成下划线风格要么在 SQL 里写别名非常难受。log-impl配置成 StdOutImpl 后SQL 语句和执行参数会直接打到控制台开发阶段排查动态 SQL 极其有用生产环境记得关掉。3.2 Mapper 接口与 XML 绑定最常见的报错MyBatis 报Invalid bound statement (not found)这个错的排查路径我太熟了多半是三件事之一XML 文件没有放在mapper-locations指定的路径下XML 的 namespace 和 Mapper 接口路径不匹配UserMapper接口没有被 Spring 扫描到。我的做法是 Mapper 接口用Mapper注解标注这样不需要在启动类上额外加MapperScan就能被 Spring 管理然后 XML 文件统一放在src/main/resources/mapper/目录下和Mapper.java包名保持一致。还有一点XML 里的resultMap不要滥用实体字段命名规范加上驼峰映射配置之后绝大多数查询都不需要手写 resultMap直接resultType就行。3.3 登录认证与拦截器设计民宿系统的用户中心、订单接口都不能裸奔需要登录校验。我的方案是 JWT HandlerInterceptor这也是前后端分离项目最主流的登录方案。用户登录成功时后端签发一个 token通过jjwt生成过期时间一周token 里存userId和usernamepublic class JwtUtil { private static final String SECRET homestay-secret-key-please-change; private static final long EXPIRE 7 * 24 * 60 * 60 * 1000L; public static String generate(Long userId, String username) { return Jwts.builder() .setSubject(username) .claim(userId, userId) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() EXPIRE)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Claims parse(String token) { return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody(); } }这里有两个坑必须说。第一jjwt0.9.1 依赖了javax.xml.bind相关类JDK 8 没问题但 JDK 9 以上运行时会报ClassNotFoundException解决方式是加javax.xml.bind:jaxb-api依赖或者直接升级 jjwt 0.11.x 并改用新版 Builder API。第二SECRET 放在代码里只是为了演示真实项目要放到配置中心或者环境变量里。拦截器的注册要通过WebMvcConfigurer完成Configuration public class WebConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns(/api/**) .excludePathPatterns( /api/user/login, /api/user/register, /api/house/**, /api/city/** ); } }/api/house/**开放是因为游客要看房源列表和详情订单相关接口不在这里自然就被拦截住了。后端的权限拦截还有一个细节管理员接口要额外校验role ADMIN我在AdminInterceptor里做了一级判断只有放行的请求才会进入 Controller。3.4 统一返回结果与全局异常处理前后端分离项目最怕接口返回的数据结构五花八门今天返回{success: true}明天返回{code: 200}前端写的解析代码一遍遍改。我直接定义了一个ResultT泛型类public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.code 200; result.message success; result.data data; return result; } public static T ResultT error(Integer code, String message) { ResultT result new Result(); result.code code; result.message message; return result; } }配合全局异常处理器RestControllerAdvice业务异常、参数校验异常、兜底异常各返回对应的 code 和 message前端拿到非 200 的 code 就弹错误提示。这样接口文档的约定非常干净联调阶段少扯皮。3.5 业务层核心实现下单接口的完整流程把上面这些串起来下单接口的 service 代码大概是Override Transactional public Long createOrder(OrderCreateRequest req, Long userId) { House house houseMapper.selectById(req.getHouseId()); if (house null || house.getStatus() ! 1) { throw new BusinessException(房源不存在或已下架); } LocalDate checkIn req.getCheckInDate(); LocalDate checkOut req.getCheckOutDate(); if (!checkIn.isBefore(checkOut)) { throw new BusinessException(入住日期必须早于离店日期); } int conflict orderMapper.countConflictOrders(req.getHouseId(), checkIn, checkOut); if (conflict 0) { throw new BusinessException(该时间段已被预订请更换日期); } Order order new Order(); order.setOrderNo(generateOrderNo()); order.setUserId(userId); order.setHouseId(house.getId()); order.setCheckInDate(checkIn); order.setCheckOutDate(checkOut); order.setDays((int) ChronoUnit.DAYS.between(checkIn, checkOut)); order.setTotalPrice(house.getPricePerNight().multiply(BigDecimal.valueOf(order.getDays()))); order.setStatus(0); orderMapper.insert(order); return order.getId(); }这段代码就是一个标准的“先检查后写入”事务模板新手可以直接照着这个节奏写其他业务比如收藏、评论模式完全一致。4. 前端实现Vue 环境搭建、页面结构与组件复用4.1 Vue 环境搭建的常见坑前端工程我用 Vue 2.6 Element UI这个组合的配套资料最全vue create homestay-web创建项目Manually select features 勾选 Router、Vuex然后一路确认。整套环境搭建里最容易出问题的两个点一个是 Node 版本一个是 npm 镜像。Vue 2 项目搭配 Node 14 或 16 都很稳如果电脑是 Node 18 以上的高版本安装依赖时出现Error: error:0308010C:digital envelope routines::unsupported这类报错是因为 Webpack 4 和 OpenSSL 新版的哈希算法不兼容。解决办法是执行export NODE_OPTIONS--openssl-legacy-provider再重新构建或者装nvm切回 Node 16。这个坑在热搜词里反复出现我身边不下十个人被它卡过。依赖下载慢或者失败多半是 npm 默认镜像网络问题。先配镜像再装依赖顺序不要反npm config set registry https://registry.npmmirror.com npm install安装完npm run serve起来浏览器访问 8080Vue 默认端口前端骨架就跑起来了。4.2 三种角色的页面路线按访客、普通用户、管理员三种角色组织路由页面逻辑会清晰很多。访客能访问的页面是首页、房源详情、登录注册登录后的普通用户额外进入个人中心看到我的订单、我的收藏管理员账号登录后走独立的/admin路由模块访问后台管理界面。路由前置守卫里做两件事一是判断页面是否需要登录二是判断当前用户角色是否匹配router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next(/login) return } if (to.meta.role to.meta.role ! store.state.user.role) { next(/) return } next() })这个守卫逻辑写完之后前端的页面权限和后端拦截器形成双重校验。注意前端守卫只是体验优化真正的安全校验永远在后端前端路由可以从控制台被玩坏但接口层的拦截器挡得住。4.3 房源列表、详情和下单页的组件拆解前端开发的重头在组件复用。民宿首页是房源卡片流我抽了一个HouseCard组件传入house对象就能渲染一张卡片包含封面、标题、地址、价格和评分列表页和搜索结果页都可以复用同一个组件首页轮播图和筛选栏则是元件级别的拆分。详情页重点解决两个问题图片展示和日期选择。图片区用 Element UI 的el-carousel做轮播日期选择用el-date-picker配typedaterange。这里有一个细节地方活动日历需要禁用已经被人预订的日期为此前端在进入详情页时调后端接口拿到该房源已占用的日期段列表然后把每一天映射成disabledDate让用户从一开始就选不出冲突区间。这套逻辑要和后端的时间冲突 SQL 配合使用前端做一个用户体验层的预拦截后端做最终确认两层都不可少。订单提交页的核心是回显价格选中日期区间后自动计算天数并显示总价。价格计算要用整数天数(离店日期 - 入住日期) / 86400000日期字符串传给后端时统一用yyyy-MM-dd格式避免时区偏移。这里有一个非常常见的 bug在 JavaScript 里new Date(2025-06-01)在不同浏览器解析结果可能差 8 个小时因此我建议直接用dayjs来处理日期对象别用原生 Date 的字符串构造。4.4 Vuex 里放什么、不放什么Vuex 我只用来存登录令牌和用户信息这两个字段的影响范围是全局性的路由守卫要读、所有请求要带、退出登录要清空。不放任何业务数据进 Vuex房源列表和订单数据都在各自页面里维护这样各页面之间的状态不会互相污染。axios 封装时自定义一个拦截器请求前从 Vuex 读取 token 塞进 header响应后统一拦截401发现登录过期就清除用户状态并跳回登录页。这是项目里小改动大收益的一个点只写一遍全站接口都受益。4.5 管理后台的表格和表单管理后台核心就是两张表房源管理、订单管理。房源管理页面用el-table展示房源列表配“上架/下架”的el-switch操作订单管理页面展示订单流用el-tag按状态显示不同颜色已支付订单可以点击“确认入住”。后台的实现难度不高但它是检验“组件化思维”的好场景表格列配置、分页组件、状态标签都可以抽成公共组件下次加一个“评论管理”页面复制粘贴加改造半小时搞定。5. 前后端联调跨域、代理与接口规范5.1 开发环境的代理配置解决跨域前端在 8080后端在 8081我开发时后端改了端口避免和前端冲突浏览器的同源策略会拦截跨域请求。联调阶段的最终解法是在vue.config.js里配置devServer.proxymodule.exports { devServer: { port: 3000, proxy: { /api: { target: http://localhost:8081, changeOrigin: true } } } }配置之后前端请求/api/house/list开发服务器会把请求转发给http://localhost:8081/api/house/list浏览器看到的都是同源请求跨域问题自然消失。这就是前后端分离项目开发阶段最常见的解决方案不需要动后端一行代码。5.2 生产环境的跨域处理前端构建完成后静态文件放进 Nginx如果 Nginx 把/api反向代理给后端那么整个系统都处于同一个域名下不存在跨域。但如果是前后端分开放到不同服务器比如后端单跑一台机器前端调接口必须走 CORS。后端开启 CORS 的配置如下Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }注意 Spring Boot 2.4 以后allowedOrigins(*)和allowCredentials(true)不能共存必须用allowedOriginPatterns(*)才能配合开启凭证请求。这是联调阶段很容易踩到的细节控制台报错信息看起来是在说跨域配置不对实际是版本校验规则变了。5.3 接口路径规范与统一状态码接口路径全部以/api开头模块名放在后面/api/house/list、/api/house/{id}、/api/order/create、/api/user/login。所有响应体统一走第一节的ResultT结构前端 axios 拦截器里统一处理code字段非 200 就弹Message.error(response.data.message)业务代码完全不用到处判断成功失败。6. 部署上线从源码打包到 Nginx 反向代理6.1 MySQL 5.7 的安装与初始化部署前服务器上必须先有可用的 MySQL。Windows 上装 MySQL 5.7 最省事的方式是下载 ZIP 包解压后手动初始化不要想着靠安装向导省事因为 5.7 的安装向导经常在最后启动服务步骤失败手动流程反而全程可控。解压后在根目录新建my.ini[mysqld] basedirC:/mysql-5.7.44-winx64 datadirC:/mysql-5.7.44-winx64/data port3306 character-set-serverutf8mb4然后以管理员身份打开命令行依次执行mysqld --initialize-insecure mysqld install net start mysql--initialize-insecure会生成一个空密码的 root 账号登录后立刻执行ALTER USER rootlocalhost IDENTIFIED BY 你的密码;。如果执行mysqld时提示缺少MSVCP120.dll去微软官网装一下 VC 2013 运行库这是 5.7 在 Windows 上最经典的报错。Linux 服务器上用apt install mysql-server或者yum install mysql-server都行装完同样要执行mysql_secure_installation设置密码。8.0 和 5.7 的驱动类名通用但 URL 里必须带serverTimezoneAsia/Shanghai否则 JDBC 驱动会报时区错误。6.2 后端打包运行后端打包进入项目根目录执行mvn clean package -DskipTeststarget 目录下生成homestay-server-0.0.1-SNAPSHOT.jar然后拷贝到服务器用一个最小化的启动命令nohup java -jar homestay-server-0.0.1-SNAPSHOT.jar --spring.datasource.password你的密码 /opt/homestay/run.log 21 配置文件里我建议把数据库账号密码等容易变的内容通过--spring.datasource.username这种参数形式覆盖而不是每次改 jar 包里的配置文件。看启动日志用tail -f /opt/homestay/run.log出现Started HomestayApplication字样就说明启动成功。6.3 前端构建与 Nginx 配置前端打包前先改好vue.config.js里的publicPath我习惯设为./这样打包后的静态资源走相对路径不管部署在域名根路径还是子路径都不会出现资源 404。然后执行npm run build生成的dist目录就是全部静态文件。Nginx 配置是整个部署环节最核心的部分。我的方案是 Nginx 托管前端静态文件同时把/api反代到后端 Java 进程server { listen 80; server_name your-domain.com; root /var/www/homestay-web/dist; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8081; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }try_files $uri $uri/ /index.html;这一行是 Vue Router history 模式必须配置的。如果没有它刷新/house/3这种详情页Nginx 会去找服务器上并不存在的真实文件返回 404。加上这一行后所有到不了真实文件的请求都回到 index.html由前端路由接管。这是一条部署必踩坑必须写进配置里。配置检查用nginx -t通过后nginx -s reload生效。访问首页能打开页面、能登录下单、能管理房源整套系统就算正式上线了。6.4 部署阶段的三类常见事故排查我是经历过“本地好好的服务器上废了”的经典画面的排查顺序一般是第一类后端进程起来就挂。先看run.log最常见的是数据库连不上——检查 MySQL 是否启动、账号密码是否正确、端口是否放行。云服务器的话优先检查安全组是否放行了 3306 和 8081 端口。第二类页面能开但接口全部 500。优先看后端run.log里的 SQL 报错多半是数据库里没有表或者表结构不对重新执行一遍docs/sql/init.sql即可。第三类接口 404。Nginx 反向代理路径没有对上/api/house/list被代理后后端无法匹配或者proxy_pass结尾少了一个/导致路径拼接错位。改完后nginx -t验证再 reload。7. 部署完成后的运维心得与后续扩展方向系统上线不代表项目结束代码的可观测性和后续维护同样重要。一个真实的体会是要在后端加一个简单请求日志过滤器把每次接口调用的用户、路径、耗时打到日志文件里之后线上排查问题会轻松很多。另外数据库要养成定期备份的习惯MySQL 备份一行命令能搞定mysqldump -u root -p homestay_db backup_$(date %Y%m%d).sql项目跑通以后往下扩展的方向也很多。如果民宿规模扩大可以讨论引入 Redis 存储热点房源信息与占用日期缓存、增加 RabbitMQ 处理订单创建后的短信通知、Spring Security 替代手写拦截器。不过所有这些优化都有一个前提现有系统的业务边界要清晰、代码结构要规整否则越优化越乱。最后分享一个做这类项目最大的心得项目能不能“立住”就看核心业务逻辑是不是经得起推敲。民宿系统的核心就是日期冲突判断那一条 SQL 和下单事务的完整性把它写透、反复测试边界条件比堆一百个冗余功能都有价值。网上有很多博客会把简单项目包装得很复杂但真正的开发经验往往来自你亲手排掉的那几个报错——比如 Vue 的 OpenSSL 报错、MyBatis 的 Invalid bound statement、Nginx 的刷新 404。这些坑我都替你踩过一轮了你现在遇到的时候直接照上面的方案处理就行。