☰
Spring Boot+Vue+MyBatis民宿租赁系统:从数据库设计到部署上线全解析
2026/10/9 8:35:37 网站建设 项目流程

说个挺常见的场景:身边有朋友做了几年民宿生意,订单靠微信聊天记录和 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

房源表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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

注意我在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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

订单状态机很关键:待支付 -> 已支付 -> 已入住 -> 已完成,这是一个正向链路;待支付和已支付都可以进入已取消。订单创建时默认待支付,只有确认支付后才算“占用”了房间的日期段。这个状态定义直接影响下一节“时间段冲突判断”的 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 id="countConflictOrders" resultType="int"> 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.x,Java 8。为什么不用 Spring Boot 3?如果读者用 JDK 17 且不在乎旧生态,3.x 确实没问题;但对于课程设计和大部分公司的存量项目,Spring Boot 2.7 + JDK 8 的组合兼容性最好,网上能搜到的各类报错方案也都是围绕这个组合来的。别图新版本,项目重点是跑通业务而不是陪框架踩升级坑。

pom.xml里核心依赖就这几样:spring-boot-starter-web、mybatis-spring-boot-starter(2.2.2 版本)、mysql-connector-java(8.0.x,用runtime范围)、lombok减少样板代码,额外加jjwt做登录令牌、spring-boot-starter-validation做参数校验。

application.yml里最关键的配置是数据源和 MyBatis 三件套:

spring: datasource: url: jdbc:mysql://localhost:3306/homestay_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true 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.StdOutImpl

map-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和username:

public 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},前端写的解析代码一遍遍改。我直接定义了一个Result<T>泛型类:

public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.code = 200; result.message = "success"; result.data = data; return result; } public static <T> Result<T> error(Integer code, String message) { Result<T> 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起来,浏览器访问 8080(Vue 默认端口),前端骨架就跑起来了。

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配type="daterange"。这里有一个细节,地方活动日历需要禁用已经被人预订的日期,为此前端在进入详情页时调后端接口,拿到该房源已占用的日期段列表,然后把每一天映射成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.proxy:

module.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。所有响应体统一走第一节的Result<T>结构,前端 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] basedir=C:/mysql-5.7.44-winx64 datadir=C:/mysql-5.7.44-winx64/data port=3306 character-set-server=utf8mb4

然后以管理员身份打开命令行,依次执行:

mysqld --initialize-insecure mysqld install net start mysql

--initialize-insecure会生成一个空密码的 root 账号,登录后立刻执行ALTER USER 'root'@'localhost' 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 里必须带serverTimezone=Asia/Shanghai,否则 JDBC 驱动会报时区错误。

6.2 后端打包运行

后端打包:进入项目根目录执行mvn clean package -DskipTests,target 目录下生成homestay-server-0.0.1-SNAPSHOT.jar,然后拷贝到服务器,用一个最小化的启动命令:

nohup java -jar homestay-server-0.0.1-SNAPSHOT.jar --spring.datasource.password=你的密码 > /opt/homestay/run.log 2>&1 &

配置文件里我建议把数据库账号密码等容易变的内容通过--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。这些坑我都替你踩过一轮了,你现在遇到的时候直接照上面的方案处理就行。

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

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

立即咨询