尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

SpringBoot+Vue民宿预订系统源码解析:从数据库设计到部署排障

SpringBoot+Vue民宿预订系统源码解析:从数据库设计到部署排障 做毕设和课设的同学翻来找去最后多半会选择这类项目SpringBootVue的民宿在线预定平台管理平台源码。原因很简单——它既不是纯增删改查的图书管理系统那样单薄也不像电商秒杀系统那样过于复杂业务链路恰好卡在难度和价值感的最佳位置。本文就基于这个标题下的项目源码从技术选型、数据库设计、后端接口、前端页面到部署排障完整拆一遍把代码背后那些值得讲的“为什么”也一并说清楚。1. 项目整体设计与技术选型思路1.1 这个项目到底在做什么先把这个系统的业务模型看清楚。典型的民宿预订平台至少包含三类角色普通用户负责搜民宿、看房型、下单、支付民宿主负责维护自己的民宿信息、房态、订单平台管理员负责审核民宿、管理用户、查看经营统计数据。当然很多课设版本把三个角色简化成管理员和普通用户两种但核心链路是一样的游客注册登录 → 按城市/日期搜索民宿 → 查看房型 → 提交订单 → 模拟支付 → 民宿主接单/确认入住 → 退房结算。对比其他毕设题目这个项目的优势在于业务完整性。它有“浏览、下单、支付、后台管理”这四条线能覆盖SpringBoot的接口开发、Vue的前后端交互、MySQL的表结构设计、权限控制、文件上传、数据统计这些常见考点。你不用担心做完之后答辩没东西讲因为随便抽出一个模块都能展开不少内容。而且民宿行业本身贴近生活演示给老师看的时候也容易理解。很多人拿到源码第一件事就是急着跑起来但我不建议这样。第一步应该是拆解需求把角色、功能点、状态流列出来。你可以自己画一个简单的用例表格例如模块功能点涉及角色用户模块注册、登录、个人信息维护所有角色民宿模块民宿列表、详情、搜索用户、民宿主房型模块房型管理、价格配置民宿主订单模块下单、取消、支付、入住用户、民宿主后台模块用户审核、民宿审核、图表统计管理员有了这张表再去对照源码里的表结构和接口你会发现代码不是一团乱麻而是有主线的。1.2 为什么选SpringBootVueMySQL这个组合几乎是当前Java技术栈在校园项目里的“标准答案”。SpringBoot简化了项目构建内嵌Tomcat做REST API非常顺手Vue的组件化开发对前端交互友好的场景很合适社区资料也多MySQL作为开源关系型数据库完全够用而且毕业设计、课程设计的环境基本都是它。更重要的是前后端分离这种开发模式和当下企业的项目结构是一致的。你答辩时可以讲前端通过请求调用后端接口后端返回JSON数据前端用Vue Router控制页面跳转、Vuex/Pinia管理登录状态这种“静态资源与业务逻辑分离”的思路本身就很有讨论空间。如果你的源码里还用了JWT做无状态认证那就更有的聊了老师一听就知道你不是照搬模板而是理解了状态怎么传递。1.3 拿到源码后先看项目结构一份结构清晰的项目源码通常长这样project-root ├─backend │ ├─src/main/java/com/example/... │ │ ├─controller # 控制层 │ │ ├─service # 业务层 │ │ ├─mapper # MyBatis-Plus/Mapper层 │ │ ├─entity # 实体类 │ │ └─config # 配置类 │ └─src/main/resources/application.yml ├─frontend │ ├─src │ │ ├─api # 请求接口封装 │ │ ├─views # 页面组件 │ │ ├─router # 路由 │ │ ├─store # 前端状态管理 │ │ └─utils/request.js # axios封装 │ └─package.json └─sql └─minihouse.sql # 初始化脚本我见过太多同学一上手就跑pm install结果报错一堆然后到处找教程。其实源码拿到手先别碰代码找数据库脚本导入数据库再用接口测试工具Apifox或Postman先看看后端能返回什么数据最后再启动前端页面。这个顺序能帮你快速搞清楚数据流后面改BUG心里也有底。2. 数据库设计与核心业务逻辑拆解2.1 核心表结构设计民宿预定平台的表设计核心是四张表用户表、民宿表、房型表、订单表。下面给出一版简化但能支撑完整业务的DDL作为参考。CREATE TABLE user ( id bigint(20) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL, password varchar(100) NOT NULL, role tinyint(4) NOT NULL DEFAULT 0 COMMENT 0用户 1民宿主 2管理员, phone varchar(20) DEFAULT NULL, avatar varchar(255) DEFAULT NULL, create_time datetime DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE house ( id bigint(20) NOT NULL AUTO_INCREMENT, owner_id bigint(20) NOT NULL COMMENT 民宿主用户ID, name varchar(100) NOT NULL, city varchar(50) NOT NULL, address varchar(200) DEFAULT NULL, description text, cover_url varchar(255) DEFAULT NULL, status tinyint(4) DEFAULT 0 COMMENT 0待审核 1上架 2下架, create_time datetime DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE room ( id bigint(20) NOT NULL AUTO_INCREMENT, house_id bigint(20) NOT NULL, title varchar(100) DEFAULT NULL, price decimal(10,2) NOT NULL COMMENT 每晚价格, stock int(11) NOT NULL DEFAULT 1 COMMENT 同类型房间数量, max_people int(11) DEFAULT 2, cover_url varchar(255) DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE booking ( id bigint(20) NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL, user_id bigint(20) NOT NULL, house_id bigint(20) NOT NULL, room_id bigint(20) NOT NULL, check_in_date date NOT NULL, check_out_date date NOT NULL, nights int(11) DEFAULT 0, total_price decimal(10,2) DEFAULT 0.00, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2已入住 3已完成 4已取消 5已退款, create_time datetime DEFAULT NULL, PRIMARY KEY (id), KEY idx_room_date (room_id, check_in_date, check_out_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;几点说明用户表里的role字段用数字比用字符串节省空间判断也方便。要注意密码存的是BCrypt或MD5的加密结果不是明文。民宿表和房型表是分开的。一个民宿可以拥有多个不同类型的房间比如大床房、双床房、家庭套房。有的简易版本会把房型直接塞进民宿表但这个设计在后续扩展时很痛苦。booking表里的house_id其实可以不做冗余但实际查询时经常要按民宿统计订单保留冗余字段能少一次JOIN。在课设数据量不大的场景下这种取舍是可以接受的。订单表要加复合索引尤其是room_id和日期字段因为后面做日期冲突查询会频繁用到。2.2 订单状态与日期冲突处理订单一单涉及的状态可以这样梳理待支付 → 已支付 → 已入住 → 已完成。待支付状态可以取消已支付状态可以申请退款/取消已入住后只能正常退房。有的系统还有“待民宿主确认”的状态但毕设版本可以先简化成几大状态。状态判断看起来简单实际很容易出BUG的地方是日期冲突。我们要防止同一个人、同一个房间在重叠日期里被重复下单。判断逻辑用一条SQL就能搞定SELECT COUNT(*) FROM booking WHERE room_id #{roomId} AND status IN (0, 1, 2) AND check_in_date #{newEndDate} AND check_out_date #{newStartDate};这里的关键是已有订单的开始日期小于新订单的结束日期并且已有订单的结束日期大于新订单的开始日期。举个例子已有订单是1号入住、3号离店新订单是3号入住、5号离店那么已有订单的check_out_date3新订单check_in_date33 3不成立说明这两个订单不冲突符合民宿行业里“离店当天中午腾房”的习惯。价格计算则更直接先算入住天数再乘每晚单价。Java里可以用ChronoUnit.DAYS.between(checkInDate, checkOutDate)来计算。建单前必须校验离店日期晚于入住日期这个校验既要在后端做也不能只依赖前端。2.3 并发扣库存的思路这就涉及到“为什么不能只查库存再减库存”的问题。如果两个用户同时下单都查到库存还剩1然后都执行stockstock-1库存就变成-1了这就是超卖。很多毕设源码为了省事直接扣减前不做保护老师一旦问起来就露馅。最简单的方案是使用乐观锁在房型表加一个version字段UPDATE room SET stock stock - 1, version version 1 WHERE id #{id} AND stock 0;影响行数为0就说明库存不足下单失败。这行SQL在并发场景下也能保证不会扣成负数。还可以配合Transactional把“校验日期 扣减库存 生成订单”放到一个事务里保证原子性。如果项目里引入了Redis还可以用分布式锁进一步优化。但对毕设来说把上面这条SQL的作用讲清楚已经能证明你具备最基本的并发意识了。3. 后端SpringBoot实操与功能模块实现3.1 工程构建与版本选择后端建议用SpringBoot 2.7.x不要一上来就选3.x。因为很多稳定版的MyBatis-Plus、依赖插件对SpringBoot 3的适配不完全而且3.x强制JDK17对学校机房或老电脑不太友好。SpringBoot 2.7.18兼容JDK8基本“开箱即用”。pom.xml里的关键依赖如下parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies注意mybatis-plus-boot-starter的版本要和SpringBoot 2.7.x搭配这一点很容易踩坑。如果用的是SpringBoot 3MyBatis-Plus就要换3.5.5以上且依赖坐标也会不同。application.yml是另一个容易出问题的地方。MySQL 8连接串需要带时区参数否则启动报错。参考配置server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/minihouse?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 servlet: multipart: max-file-size: 10MB max-request-size: 10MB mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: automap-underscore-to-camel-case打开后数据库里的create_time字段会自动映射到实体类的createTime属性不用写一堆resultMap。3.2 JWT登录认证与权限控制毕设项目一般不用复杂的Spring Security用JWT HandlerInterceptor就够了。整体流程是用户提交用户名密码后端校验通过后生成Token返回前端前端后续请求带上Authorization: Bearer token后端写一个拦截器统一解析Token如果没有或解析失败就直接返回401。Token工具类核心方法public static String generateToken(Long userId, Integer role) { return Jwts.builder() .claim(userId, userId) .claim(role, role) .setExpiration(new Date(System.currentTimeMillis() 7L * 24 * 3600 * 1000)) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); } public static Claims parseToken(String token) { return Jwts.parser() .setSigningKey(SECRET_KEY) .parseClaimsJws(token) .getBody(); }这里要提醒一点如果使用依赖jjwt 0.9.1SECRET_KEY默认会调用JDK的API直接指定一个长度足够字符串就行。在SpringBoot 2.7环境里没问题。如果是SpringBoot 3还得处理模块化限制这也是我不建议新手用SpringBoot 3做毕设的原因之一。拦截器里的角色鉴权可以这么写在方法上定义自定义注解或者把路由后缀按角色区分。毕设场景下最直观的方式是给每个请求加一个请求头字段或者在Controller方法里获取当前用户角色判断。示例如下Component public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } String auth request.getHeader(Authorization); if (auth ! null auth.startsWith(Bearer )) { Claims claims JwtUtil.parseToken(auth.substring(7)); request.setAttribute(userId, claims.get(userId)); request.setAttribute(userRole, claims.get(role)); return true; } response.setStatus(401); response.setContentType(application/json;charsetutf-8); response.getWriter().write({\code\:401,\msg\:\未登录或登录已过期\}); return false; } }在这个拦截器里request.getAttribute(userId)可以拿到当前登录用户的ID。如果某个接口只允许管理员操作可以再写一个RoleInterceptor或者在Controller里用RequireRole类似的注解统一处理。源码里不一定要写得像生产系统那么完善但至少要体现出你有“权限粒度控制”的意识。3.3 民宿列表分页与多条件查询民宿搜索是前台最核心的接口。需要支持按城市、关键词、价格区间、可住人数筛选还要分页返回。用MyBatis-Plus的LambdaQueryWrapper写起来非常清爽Override public PageHouse queryHouse(HouseQueryDto dto) { LambdaQueryWrapperHouse wrapper new LambdaQueryWrapper(); wrapper.eq(StringUtils.hasText(dto.getCity()), House::getCity, dto.getCity()) .like(StringUtils.hasText(dto.getKeyword()), House::getName, dto.getKeyword()) .eq(dto.getStatus() ! null, House::getStatus, dto.getStatus() null ? 1 : dto.getStatus()) .orderByDesc(House::getCreateTime); return houseMapper.selectPage(new Page(dto.getPage(), dto.getPageSize()), wrapper); }StringUtils.hasText是Spring的一个工具方法比直接判断null和空串更稳。这里还隐含了一个逻辑前台只展示状态为“上架”的民宿后台管理则可以根据不同状态筛选。很多源码在写分页时容易漏掉一件事总条数。MyBatis-Plus的selectPage会自动帮我们查总记录数返回的Page对象包含records、total、current、size前端也能直接用。如果是手写SQL要记得SELECT COUNT(*)单独查询别用list.size()当总数。3.4 下单接口的事务边界下单是整个系统里最值得深挖的接口。写一个比较可靠的实现逻辑事务放在Service层Transactional(rollbackFor Exception.class) public Booking createOrder(BookingCreateDto dto, Long userId) { Room room roomMapper.selectById(dto.getRoomId()); if (room null) { throw new BusinessException(房间不存在); } LocalDate checkIn dto.getCheckInDate(); LocalDate checkOut dto.getCheckOutDate(); if (!checkOut.isAfter(checkIn)) { throw new BusinessException(离店日期必须晚于入住日期); } // 日期冲突校验 Long conflictCount bookingMapper.countConflict(room.getId(), checkIn, checkOut); if (conflictCount ! null conflictCount 0) { throw new BusinessException(该房型在所选日期内已被预订); } // 库存扣减使用乐观锁 int rows roomMapper.deductStock(room.getId()); if (rows 0) { throw new BusinessException(该房型已满房); } // 生成订单 long nights ChronoUnit.DAYS.between(checkIn, checkOut); Booking booking new Booking(); booking.setOrderNo(generateOrderNo()); booking.setUserId(userId); booking.setHouseId(room.getHouseId()); booking.setRoomId(room.getId()); booking.setCheckInDate(checkIn); booking.setCheckOutDate(checkOut); booking.setNights(nights); booking.setTotalPrice(room.getPrice().multiply(BigDecimal.valueOf(nights))); booking.setStatus(0); bookingMapper.insert(booking); return booking; }注意几点Transactional(rollbackFor Exception.class)不要漏掉rollbackFor。默认情况下Spring只在遇到RuntimeException时才回滚而很多业务异常是自己封装的RuntimeException所以写了更保险。deductStock那条SQL不能先查再减要用带条件的更新语句UPDATE room SET stock stock - 1 WHERE id #{roomId} AND stock 0订单号不建议用数据库自增ID最好自己生成比如yyyyMMddHHmmss 4位随机数防止订单号泄露业务数据量。3.5 管理端统计报表SQL民宿管理平台后台一般要展示平台的总订单数、总营收、热门民宿等。统计逻辑写在SQL里比在Java内存里循环计算高效得多。一个常见的Top10热门民宿查询SELECT b.house_id, h.name AS house_name, COUNT(*) AS order_count, SUM(b.total_price) AS total_amount FROM booking b LEFT JOIN house h ON b.house_id h.id WHERE b.status IN (1, 2, 3) AND b.create_time #{startTime} AND b.create_time #{endTime} GROUP BY b.house_id, h.name ORDER BY order_count DESC LIMIT 10;统计口径要先定义清楚是“订单数”还是“完成入住数”是“支付金额”还是“退款后的净营收”源码里如果统计逻辑混乱答辩时很容易被问住。建议直接在后端定义一个统计返回对象字段包括orderCount、totalAmount和date方便前端图表直接渲染。4. 前端Vue项目从搭建到页面联动4.1 初始化与目录规划前端部分我建议用Vite初始化项目不要再用慢慢吞吞的Webpack版Vue CLI至少在开发体验上会舒服很多。执行npm create vitelatest frontend -- --template vue然后安装Vue Router、Pinia、Element Plus和Axiosnpm install vue-router4 pinia axios element-plus目录结构这样规划src/ ├─api/ # 每个业务模块的请求封装 ├─components/ # 通用组件 ├─router/ # 路由配置和守卫 ├─store/ # Pinia状态管理 ├─utils/ # axios实例、工具函数 └─views/ # 页面组件 ├─front # 用户端页面 └─admin # 后台管理页面这里有一个设计要点不要把所有页面混在views里。前台首页、民宿详情、订单结算是一类后台民宿管理、用户管理、数据统计是另一类。分开以后路由守卫也好写代码也清晰。4.2 axios封装与路由守卫axios实例会在项目里被反复使用务必封装好。核心是请求拦截器和响应拦截器import axios from axios import { ElMessage } from element-plus import router from /router const service axios.create({ baseURL: /api, timeout: 10000 }) // 请求拦截带上token service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) // 响应拦截统一处理错误 service.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.msg || 请求失败) return Promise.reject(new Error(res.msg)) } return res }, error { if (error.response error.response.status 401) { localStorage.removeItem(token) router.push(/login) } ElMessage.error(error.message || 网络异常) return Promise.reject(error) } ) export default service这里把baseURL设成/api开发时通过Vite代理转发到http://localhost:8080后面讲打包部分会再展开。路由守卫用来判断页面是否需要登录、是否需要管理员权限。Vue Router 4的写法router.beforeEach((to, from, next) { const token localStorage.getItem(token) const userInfo JSON.parse(localStorage.getItem(userInfo) || {}) if (to.meta.requiresAuth !token) { next(/login) return } if (to.meta.role userInfo.role ! to.meta.role) { next(/403) return } next() })在路由配置里给页面加meta{ path: /admin/house, component: () import(/views/admin/HouseManage.vue), meta: { requiresAuth: true, role: 2 } }这样做的好处是以后新增一个管理页面只需要在路由配置里声明权限不用每个页面自己判断。4.3 民宿检索页面与Vue插槽使用民宿列表页是整个前台交互的重点。页面包含城市选择、入住/离店日期选择、关键词搜索和分页列表。城市下拉框一般不写死而是从后端接口拉取已有民宿城市列表这样更灵活。列表页面的一个细节是查询条件应该同步到URL的query参数。比如选中城市为“杭州”后URL变成/search?city杭州checkIn2025-06-01checkOut2025-06-03。这样用户刷新页面或分享链接时条件不会丢。实现方式是在点击搜索时router.push({ path: /search, query: { city: this.searchForm.city, checkIn: this.searchForm.checkInDate, checkOut: this.searchForm.checkOutDate, keyword: this.searchForm.keyword || } })然后在created生命周期里从this.$route.query读参数。Vue插槽这个知识点在组件化开发里很常用也是热词之一。拿民宿卡片组件举例卡片底部按钮可能在不同页面有不同含义首页是“立即预订”管理页面可能是“编辑/下架”。如果用组件直接写死按钮复用性很差。这时可以暴露插槽div classhouse-card img :srchouse.coverUrl alt h3{{ house.name }}/h3 p{{ house.city }} · {{ house.address }}/p slot namefooter/slot /div使用方HouseCard v-foritem in list :keyitem.id :houseitem template #footer el-button typeprimary clickgoDetail(item.id)查看详情/el-button el-button typesuccess clickbook(item.id)立即预订/el-button /template /HouseCard后台管理表格里的“操作”列也是最典型的插槽场景。比如Element Plus表格列el-table-column label操作 width200 template #default{ row } el-button sizesmall clickopenEdit(row)编辑/el-button el-popconfirm title确认删除该民宿 confirmdeleteHouse(row.id) template #reference el-button sizesmall typedanger删除/el-button /template /el-popconfirm /template /el-table-column#default{ row }这种作用域插槽写法等于把当前行数据交给了操作按钮使用非常方便。4.4 日期日历禁订与订单提交民宿详情页要展示哪些日期已经被订通常做法是后端返回一个已订日期数组前端在日期选择器里禁用这些日期。如果只用简单的日期数组只能禁用单个点如果要禁用整段区间前端也可以处理const disabledDate (date) { const time date.getTime() return bookedRanges.some(range { const start new Date(range.checkInDate).getTime() const end new Date(range.checkOutDate).getTime() return time start time end }) }注意这里的判断是“入住日期落在已订区间内就禁用”符合民宿预订的逻辑。如果你用的是Element Plus的el-date-picker把disabledDate绑上去就行。这个功能虽然代码量不大但视觉效果很直观答辩时演示一下能给老师留下不错的印象。订单提交页需要展示房间信息、入住天数、单价和总价。总价在前端可以计算但下单接口返回的totalPrice才是真正写入数据库的金额。前端展示的数据一般只作参考最终以后端计算结果为准这样也避免有人改前端参数“薅羊毛”。提交按钮点击后调用createOrder接口成功后跳转到“我的订单”页面。5. 运行环境、部署打包与常见问题排雷5.1 本地环境配置要点先说说数据库。MySQL从下载到安装最常见的坑是字符集和时区。安装时如果选择默认很可能用了latin1编码导致中文乱码。建库时最好显式指定字符集CREATE DATABASE minihouse DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;连接配置里的characterEncodingutf8和serverTimezoneAsia/Shanghai一定要加上否则你在本地跑源码时启动就会报“The server time zone value”错误。MySQL 8.x的驱动是com.mysql.cj.jdbc.DriverMySQL 5.7可以用也可以不用但换成统一的8.0驱动最省事。后端启动前确认三个东西JDK版本、Maven配置、依赖是否下载成功。Maven默认镜像在国外经常下载很慢。建议在settings.xml里配置阿里云镜像或者直接把本地仓库清空重新下载。SpringBoot项目启动时报“无法加载主类”的多半是IDE缓存坏了执行mvn clean再重新导入即可。前端开发环境要保证Node.js版本不是太老。Vite 4/5要求Node 14.18以上建议用16或18。npm install如果卡住换淘宝镜像npm config set registry https://registry.npmmirror.com5.2 本地跨域代理与生产部署开发环境的前后端不同端口必然产生跨域。最简单的方案是Vite代理前端的/api请求转发到后端地址。在vite.config.js里配置export default defineConfig({ server: { port: 3000, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: path path.replace(/^\/api/, ) } } } })这样前端代码里的axios请求/api/house/list时实际上被代理到http://localhost:8080/house/list后端接口不需要带/api前缀。生产部署则建议用Nginx。后端打包成jarmvn clean package -DskipTests java -jar minihouse-0.0.1-SNAPSHOT.jar前端打包成静态文件npm run buildNginx配置如下server { listen 80; server_name your.domain.com; # 前端静态文件 location / { root /opt/minihouse/dist; try_files $uri $uri/ /index.html; } # 反向代理后端接口 location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }注意proxy_pass结尾的/很关键它表示把/api前缀去掉再转发到后端。加上之后前端的/api/house/list会变成后端的/house/list和开发环境的路径保持一致。5.3 高频问题排查速查表我在这类项目上被问得最多的问题整理成一个表格大家遇到可以直接对照。现象可能原因解决方案后端启动报“Could not create connection to database server”MySQL没启动、端口不对、时区缺失检查MySQL服务确认url端口加serverTimezoneAsia/Shanghai前端口所有请求都404Vite代理没生效或后端接口路径不匹配检查baseURL和代理配置用Apifox直接调后端接口定位中文保存到数据库变成问号数据库连接字符集或表字符集不对确保库表和连接串都是utf8mb4登录后刷新页面就退出Token存在内存变量里把token存到localStorage并初始化Store前端展示的用户ID/订单号精度丢失后端Long转JSON精度超过JS安全整数全局把Long序列化成String页面首次打开白屏报“Cannot read properties of undefined”接口返回null前端未做空值判断给组件加v-if或默认值处理SpringBoot 3.x与MyBatis-Plus不兼容依赖版本过旧改用SpringBoot 2.7.x或升级MyBatis-Plus到3.5.5文件上传后重启服务图片丢失静态文件直接存在项目目录上传到独立目录Nginx代理或配置资源映射其中“Long转String精度丢失”这个问题最容易踩。因为MyBatis-Plus默认使用雪花算法生成长整型ID一位ID有19位数字而JavaScript的Number只能精确表示16位以内的整数一旦超过就会尾数变0。解决方式是在SpringBoot配置里加一个Jackson序列化器Bean public Jackson2ObjectMapperBuilderCustomizer longToStringCustomizer() { return builder - builder.serializerByType(Long.class, ToStringSerializer.instance); }这样所有Long类型字段返回给前端时都变成字符串前端拿到的是完整ID不会再出现“我的订单号最后几位变成0”这种莫名其妙的问题。5.4 用源码学习而不是直接抄说了这么多最后回到“源码”本身。我的建议是不要急着把源码当成最终交付物而是把它当成一个可以跑通的参考骨架。拿到了源码先跑通再尝试改需求。比如给民宿增加一个“设施标签”字段给订单增加“民宿主确认”环节把普通用户和管理员抽成更清晰的角色权限模型。只要你能改出其中任何一个点答辩时就是亮点。如果实在没时间改也要把核心代码看熟。老师大概率会问订单日期冲突怎么处理的并发扣库存怎么解决图片上传存在哪里统计报表的数据怎么来的这几个问题只要答得顺畅项目分就不会低。我带着学生跑通过几次这类项目总结下来最耗时间的往往不是代码本身而是环境。MySQL装半天、依赖拉不下来、SpringBoot版本不匹配、前端跨域被拦截……这些坑每个都能写一篇文章。但反过来想把这些坑都踩一遍你对前后端部署的理解会比只背面试题扎实得多。这个项目放到简历上写的不是一个“XX平台源码”而是“一套前后端分离的民宿预订系统”面试官感兴趣你也有真实可聊的细节。这就是它作为毕业设计、课程设计乃至平时练手项目的最大价值。
返回列表