
说实话看见标题里写着【可直接运行】的时候我是有点怀疑的。这些年接手过的遗留系统、拿来改的毕设源码不少十个号称“解压就能跑”的项目起码有八个要先跟环境配置打一架。但这套民宿租赁系统确实是个少见的老实项目后端是主流的Spring Boot前端用Vue数据库扔给MySQL代码结构规整SQL脚本也备得齐全前后端分工非常清晰。把整套流程跑通之后你会发现它覆盖了一个信息管理系统最典型的闭环——房源维护、用户下单、订单状态流转、后台管理、统计查询每一环都有真实业务逻辑而不是那种只会在页面上摆两张表的空壳子。如果你正打算找一套带完整前后端的项目来交作业、做毕设或者想自己动手把一套前后端分离的系统从零跑通这篇内容应该能帮上忙。我会按自己实际跑项目的顺序从技术栈选型、环境配置、项目启动讲到核心业务实现、数据库设计再把那些最容易卡壳的端口、版本、精度问题一并列出来。你照着走一遍基本不会再被“明明能运行却启动失败”这种事折磨。1. 一套能跑通的民宿租赁系统先看清项目全貌1.1 三类角色与业务闭环民宿租赁系统表面上看起来就是个“房源信息管理”项目但真正跑起来会发现它至少把三类用户的使用路径串在了一起。第一类是系统管理员。管理端关心的是平台整体数据民宿入驻了多少家、订单总量、用户注册量、评论内容是否合规。对应的功能包括基本信息管理、民宿审核、订单总览、统计报表、系统用户维护。这套系统里的管理后台通常用Vue搭一个独立的界面用表格展示数据通过后端接口拉取数据并操作。第二类是房东也就是商家的角色。房东需要维护自己的民宿信息比如民宿名称、介绍、封面图、地址、城市还要管理具体房间和房型的价格、库存。在订单流转中房东负责接单、确认入住、处理退订同时需要看到自己的收入情况。第三类是用户也就是住宿的游客。用户侧的业务是从前端页面开始的注册登录、浏览房源、按城市和日期搜索、查看民宿详情、提交订单、支付、查看订单状态、发表评论。这部分交互做得越顺手整套系统看起来就越完整。三类角色合起来组成了民宿租赁的基本业务闭环房东上架民宿和房间用户搜索浏览并下单管理员监督管理全流程订单完成后用户评价房东和管理员都能看到评价内容。这套闭环里包含了基础的CRUD也包含了稍复杂的订单状态切换、库存扣减、日期重叠校验、统计聚合所以它比很多“纯管理后台”项目有嚼头得多。1.2 前端页面与后端接口怎么对应很多初学者拿到一套前后端分离项目会先发懵“前端代码在Vue里后端代码在Spring Boot里两个项目是怎么对上的”其实你只需要抓两条线整栋楼就通了。一条线是页面路由。前端Vue项目里有router配置对应着地址栏的路径例如首页/、民宿详情页/homestay/:id、登录页/login、后台管理页面/admin。每个页面加载时会发请求取数据。另一条线是后端接口。Spring Boot后端通过Controller暴露RESTful API例如GET /api/homestay/list、POST /api/order/create、PUT /api/order/cancel。前端通过axios发请求把返回的JSON渲染到页面上。前后端交互的关键在于接口约定。比如前端提交一个预订请求发送的JSON里有roomId、checkInDate、checkOutDate、contactName后端用对应的实体类或DTO接收校验之后返回订单号、状态码和提示信息。前端拿到结果后决定跳转支付页还是弹窗提示错误。只要这条链路清楚你在读代码时就不会迷路。前后端之间的权限控制也很常见用户登录后拿到token存到localStorageaxios拦截器每次请求自动带上token后端拦截器解析token后再放行接口。整个流程一看就懂也是面试里经常会被追问的地方。2. 技术栈选型Spring Boot Vue MySQL为什么这么搭2.1 后端为什么是Spring Boot而不是老Spring MVC如果是五年前这个项目很可能叫SSMSpring Spring MVC MyBatis整合项目光是配置一堆XML就够折腾半天。Spring Boot把绝大多数配置变成了自动装配和约定优先你引入一个spring-boot-starter-web内嵌的Tomcat就已经准备好了启动类写个main方法就能起服务。我在实际使用中觉得Spring Boot对齐这套民宿租赁系统最大的优势是它让团队成员把注意力放在业务接口上而不是放在环境折腾上。比如你要写一个民宿列表接口直接建Controller、Service、Mapper三层类上打RestController、Service、Mapper注解业务就串起来了。数据库操作Spring Boot也给了两条主流路线MyBatis写SQL灵活可控Spring Data JPA省去大量模板代码。这套系统用的是MyBatis或MyBatis-Plus的话SQL都在mapper.xml里排查问题的时候一眼能看到最终执行的语句。另外Spring Boot的生态太成熟了集成Redis、MQ、定时任务、文件上传都有现成的starter。民宿租赁系统这种体量的项目用Spring Boot以后想扩展也不至于推倒重来。2.2 前端选Vue的务实理由民宿租赁系统的前端如果还拿JSP来渲染会特别痛苦页面里嵌套Java代码前后端工程师没法并行开发交互改起来费劲。Vue作为渐进式框架最大的好处是组件化和数据驱动。你在Vue里写一个民宿卡片组件包含标题、价格、封面图、城市标签数据通过props传进去父组件只要循环数组就能渲染出整个房源列表。页面交互变成了“数据变视图自动变”不用手动操作DOM。这对信息管理系统来说体验提升非常明显因为后台管理页面的公共逻辑——表单校验、弹窗、表格分页、状态标签——都能封装成组件复用。配合Element UI或Element Plus这类组件库后台管理的表格、表单、日期选择器、分页控件都不用手搓开发效率很高。Vue的工程化生态也成熟用Vite或Webpack管理依赖用vue-router管理路由用Pinia或Vuex管理状态整体项目结构清晰新接手的人看一眼目录就知道代码放在哪。2.3 MySQL在这套系统里的角色MySQL在这套系统里承担的是“所有业务数据的最终归宿”。用户信息、民宿和房间资料、订单流水、评论内容全部落库。选中MySQL不是因为它新潮而是因为它足够可靠、免费、文档丰富而且和Spring Boot的配合最顺滑。InnoDB引擎支持事务和外键约束像下单这种“扣库存加订单”的操作必须放在同一个事务里MySQL能保证要么都成功、要么都回滚。数据量在十万级对MySQL来说是轻松活民宿租赁这类垂直领域的业务量单库单表完全够用。有些项目会为了“显得厉害”硬塞Redis做缓存、MongoDB存评论但就这套系统而言没有足够的性能瓶颈之前任何中间件都只是在增加维护成本。先把MySQL用好学会建表、加索引、写聚合SQL这才是后期上任何新组件的基础。3. 从下载到跑通本地环境配置与项目启动全流程3.1 环境版本怎么选别让版本差异成为第一个坑拿到源码的第一件事不是双击打开而是先看版本。我看到太多人卡在环境上的原因只有一个——版本对不上。你先打开后端pom.xml看Spring Boot的parent版本再看前端package.json里的Vue和构建工具版本然后倒推需要的JDK和Node版本。我建议直接按这个组合来组件推荐版本说明JDK8 或 17Spring Boot 2.x用JDK 8Spring Boot 3.x要求JDK 17以上Maven3.6用于后端依赖下载和打包Node.js16 LTS跑Vue项目最稳Vue 3 Vite也可用18 LTSMySQL8.0 或 5.78.0记得调整密码认证方式字符集统一utf8mb4IDEIDEA VSCodeIDEA跑后端VSCode写前端数据库工具Navicat 或 Workbench用来导入SQL脚本、调试数据这里特别提醒不要看到新的就装最新的。Node 17以上运行旧版Webpack项目经常会报OpenSSL错误JDK版本太新跑旧版Spring Boot也会出现一堆不兼容。先按项目实际依赖来装之后再考虑升级。3.2 后端启动细节配置文件与Maven依赖后端项目用IDEA打开等它识别为Maven项目后首次会自动下载依赖。这一步在国内经常很慢甚至失败建议先配置阿里云Maven镜像在maven的settings.xml的mirrors节点里加mirror idaliyunmaven/id mirrorOf*/mirrorOf urlhttps://maven.aliyun.com/repository/public/url /mirror依赖下载完成后找到src/main/resources里的application.yml重点看数据源配置。我第一次跑这种项目都会先确认数据库地址、账号密码是否对得上本地环境。典型的配置是这样的server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/homestay?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver密码改成你自己的数据库密码。如果项目用到Redis或其他中间件配置文件里也会写先确认本机服务有没有启动。然后处理SQL脚本。项目里通常会放一个homestay.sql或database.sql用Navicat新建一个数据库字符集选utf8mb4导入执行。执行完检查一下表是否建立成功、是否已有几条测试数据这些数据能让你启动后立刻看到页面效果。最后找到启动类一般是项目名加Application后缀标着SpringBootApplication点击运行。控制台出现“Started Application in...”说明后端起来了。验证方法很简单浏览器访问 http://localhost:8080 看到错误页也说明端口已经通再访问一个具体的接口路径看看能否返回JSON。3.3 前端启动细节依赖安装与跨域代理前端项目单独放在一个目录里用VSCode打开。先执行依赖安装npm install如果下载慢换成国内镜像npm config set registry https://registry.npmmirror.com安装完成后看package.json里的scripts通常是npm run serve或npm run dev运行即可。Vue 2 Vue CLI项目启动后默认端口是8080容易和后端冲突Vue 3 Vite项目默认通常是5173。前后端联调还有一道重要配置跨域代理。前端页面访问 /api 开头的接口时需要通过代理转发到后端。Vue 2项目在vue.config.js里配module.exports { devServer: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } }Vue 3 Vite项目在vite.config.js里配server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }前端启动后打开页面能注册、能登录、能看到民宿列表说明前后端已经打通整套系统就算真正跑起来了。这个节点非常关键一旦通了后面任何问题你都能通过“是前端调用问题还是后端接口问题”来快速定位。4. 核心业务拆解预订流程的状态流转与关键实现4.1 下单这个动作背后发生了什么一个用户在前端点了“立即预订”看到的反应是“订单创建成功”但后端在这个接口里做了不止一件事。第一步接收参数校验。房间ID、入住日期、离店日期、入住人数、联系人电话这些参数只要有缺漏或格式不对后端会直接拒绝。第二步检查房态和库存。这个房间在目标日期段内是否可订有没有被其他人占用当前剩余可售数量是否大于0。第三步计算价格。单价乘以入住天数得到总价必要时加上清洁费或平台服务费。第四步扣减库存并生成订单。库存减一订单表插入一条待支付记录。第五步返回结果给前端同时启动“超时未支付自动取消”的计时。这个流程里最容易出错的就是第四步因为涉及两个操作扣库存和插订单。如果中间任何一步失败必须保证两边都回滚否则会出现“订单生成了但库存没减超卖”或“库存减了但订单没生成幽灵单”的问题。解决方式就是给方法加事务注解Transactional(rollbackFor Exception.class) public OrderResult createOrder(CreateOrderRequest req) { Room room roomMapper.selectById(req.getRoomId()); if (room null || room.getStock() 0) { throw new BizException(房间不存在或已下架); } int affected roomMapper.deductStockIfEnough(req.getRoomId(), req.getDays()); if (affected 0) { throw new BizException(库存不足请更换日期或房型); } Booking order buildOrder(req, room); bookingMapper.insert(order); return OrderResult.success(order); }代码里那个deductStockIfEnough很有讲究它对应的SQL是“扣减库存但限定库存大于0如果影响行数为0就说明扣减失败”。这种方式比“先查库存再扣库存”更安全避免了并发时两个人同时抢最后一间房的问题。如果是更高并发场景还可以考虑版本号乐观锁或者数据库行锁但对这套民宿系统的体量来说条件更新已经足够。4.2 订单状态机六个状态怎么串起整个流程订单状态是民宿租赁系统里最值得研究的点。这六个状态把用户和房东的每一次操作都串了起来状态码状态触发条件后续动作0待支付用户提交订单锁定库存等待支付1已支付支付回调成功通知房东生成入住凭据2已确认房东后台确认用户收到确认通知3已入住用户办理入住房间占用中4已完成退房结算完成开放评论入口5已取消用户取消或超时释放库存并退款状态机的核心是“不允许非法跳转”。比如已取消的订单不能变成已支付已完成的订单不能再次入住。很多项目在状态流转上只用if-else硬写订单一多就变成一团乱麻。更清晰的做法是维护一张“状态到状态”的流转表private static final MapInteger, SetInteger ALLOWED_TRANSITIONS new HashMap(); static { ALLOWED_TRANSITIONS.put(0, new HashSet(Arrays.asList(1, 5))); ALLOWED_TRANSITIONS.put(1, new HashSet(Arrays.asList(2, 5))); ALLOWED_TRANSITIONS.put(2, new HashSet(Arrays.asList(3, 5))); ALLOWED_TRANSITIONS.put(3, new HashSet(Arrays.asList(4))); ALLOWED_TRANSITIONS.put(4, Collections.emptySet()); ALLOWED_TRANSITIONS.put(5, Collections.emptySet()); }每次修改状态前先检查当前状态是否允许目标状态不允许就抛异常。这样哪怕以后加了新状态也只需要改这一张表。超时未支付的取消逻辑在单体项目里我用的是Spring自带的定时任务。每隔几分钟扫描一次当前时间减去订单创建时间超过30分钟、状态还是0的订单统一改成已取消并把库存加回去。这个方案简单、能跑、也够用比引入MQ做延迟消息要轻量得多。4.3 房态与库存可订、被锁定、已售出的判断逻辑民宿系统里“库存”和普通商品不一样。普通商品库存是一个恒定的总数民宿房间的库存却和日期强相关7月1日到7月3日被订了7月5日到7月7日还可以订。所以库存判断的逻辑不能只看房的stock字段还要考虑订单表的日期占用情况。每次用户查询某天能不能订系统要做的判断是目标入住日期和离店日期之间和现有订单的入住、离店日期有没有重叠。SQL可以这样写SELECT COUNT(*) FROM booking WHERE room_id #{roomId} AND status IN (0, 1, 2, 3) AND check_in_date #{newCheckOutDate} AND check_out_date #{newCheckInDate}这段SQL的逻辑就是“别人还没走的日期和你准备来的日期重叠了就不能再订”。只要查询结果的数量大于等于该房间的可用数量就说明已经满了。这个日期重叠判断是预订类系统最常见的业务点面试时也值得多提一句。下单成功时把stock扣掉订单取消或完成后stock加回配合每天定时清理过期订单这套逻辑就能保证房态数据大致正确。如果要做更严格的时段级房态可以引入房态日历表但那是后话。5. 数据库表设计几张核心表如何撑起整套业务5.1 六张核心表的结构与设计意图民宿租赁系统的数据库表数量不需要多但每一张表都得撑起一段业务流程。我梳理了最核心的六张表表名用途关键字段sys_user系统用户id、username、password、phone、role、statushomestay民宿表id、owner_id、name、city、address、cover_img、descriptionroom房间表id、homestay_id、room_type、bed_info、price、stock、statusbooking订单表id、order_no、user_id、homestay_id、room_id、check_in_date、check_out_date、total_price、statuscomment评论表id、order_id、user_id、homestay_id、content、rating、reply_contentfavorite收藏表id、user_id、homestay_id、create_time这张表组合基本覆盖了所有业务页面。用户表管登录注册和角色权限民宿表和房间表管房源展示订单表管交易流转评论表管用户反馈收藏表管用户个人行为。没有多余的冗余表功能边界清楚。以订单表为例实际建表SQL大概长这样CREATE TABLE booking ( id bigint(20) NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 订单号, user_id bigint(20) NOT NULL COMMENT 下单用户ID, homestay_id bigint(20) NOT NULL COMMENT 民宿ID, room_id bigint(20) NOT NULL COMMENT 房间ID, homestay_name varchar(100) DEFAULT NULL COMMENT 民宿名称冗余, room_type varchar(50) DEFAULT NULL COMMENT 房型名称冗余, price_snapshot decimal(10,2) NOT NULL COMMENT 下单时单价快照, total_price decimal(10,2) NOT NULL COMMENT 订单总价, check_in_date date NOT NULL COMMENT 入住日期, check_out_date date NOT NULL COMMENT 离店日期, guest_count tinyint(4) NOT NULL COMMENT 入住人数, contact_name varchar(50) NOT NULL COMMENT 联系人, contact_phone varchar(20) NOT NULL COMMENT 联系电话, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2已确认 3已入住 4已完成 5已取消, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user_id (user_id), KEY idx_room_time (room_id, check_in_date, check_out_date), KEY idx_create_time (create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单表;看到这里你可能注意到我对订单表做了冗余设计人为存了homestay_name和room_type。原因很直接订单列表页面需要显示民宿和房型信息如果全靠联表查询每次查询都要join民宿表和房间表数据量大的时候性能会越来越慢。订单表里冗余这几个展示字段列表页一次单表查询就出来了。代价是民宿改名时订单表里的冗余字段不同步但对订单这种历史记录来说保留下单时的名称其实更合理。5.2 金额、时间、状态这些字段为什么这样定义新手建表最容易踩的坑有三个金额用float、时间乱选类型、状态用字符串。金额字段必须用decimal(10,2)不要用float。float在计算机里是二进制浮点数0.1加0.2可能是0.30000000000000004而金额计算出现这种误差是绝对不允许的。decimal是定点数适合做货币、价格、百分比这类需要精确计算的场景。单价、总价、退款金额全部用decimal。时间字段我建议用datetime而不是timestamp。timestamp有一个2038年溢出的问题而且会受MySQL时区设置影响同一个时间在不同时区配置下读出来可能变了datetime存的就是字面值不管时区怎么切换它代表的都是你写入的那一刻。再加上配置里的serverTimezoneAsia/Shanghai基本能避开乱码和时间错乱的问题。状态字段我用tinyint加注释很少直接用varchar存“待支付”“已支付”这种中文。整型占用空间小后端用枚举或常量类来定义含义不容易出现“待支付”和“待付款”这种同一业务两种叫法的混乱。如果你希望数据库更可读可以用MySQL的ENUM类型但之后想加状态值就要改表结构所以我更推荐tinyint 代码枚举的方式。order_no字段值得一提。这张表给order_no加了唯一索引。订单号由后端生成常见做法是日期加时间戳加随机数例如2025070110304512345678。唯一索引的意义在于接口层面万一出现重复提交数据库兜底会拒绝第二条相同订单号避免产生重复订单。5.3 索引与外键实操中我倾向于怎么做索引这块实践比理论更重要。民宿系统的查询场景很有规律用户查自己的订单user_id、房东查自己民宿的订单homestay_id或owner_id关联、按城市搜民宿city、日期段查房间占用room_id check_in_date check_out_date。这些字段建索引之后慢查询基本消失。组合索引的原则是“最左前缀”比如(room_id, check_in_date, check_out_date)这个索引可以覆盖“先定位房间、再过滤日期段”的查询。你用EXPLAIN看一下SQL执行计划有没有走索引一目了然。外键这里我想多说一句。很多教材都强调外键能保证数据一致性但实际项目里我见过的大多数业务系统在代码层面控制关联关系不建物理外键。原因有几个物理外键在删除父表数据时会因为子表约束报错导致你无法灵活地做逻辑删除分库分表时物理外键基本没用高并发写入时外键校验会增加额外的锁开销。更推荐的做法是用逻辑外键也就是存peer_id、user_id这样的字段但约束由Service层代码保证。比如删除一个民宿前先检查它有没有未完成的订单有就拒绝删除不需要数据库来拦。如果你正在学的课程强行要求外键那按课程来就行工作里可以务实一点。6. 实操中绕不开的坑端口、版本、精度与时区问题6.1 端口冲突与后端启动失败排查后端启动后控制台直接飘红最常见的现象是“Port 8080 was already in use”。这时候很多人会反复重启项目其实问题根本不在代码层面。在Windows上打开命令提示符输入netstat -ano | findstr 8080能看到占用8080端口的进程PID然后去任务管理器找到对应进程结束掉。如果这台机器上有多个Java项目要一起跑更省事的办法是直接改Server端口application.yml里把8080改成8081。注意改了后端端口之后前端的代理配置也要同步改否则前端转发还是指向8080照样连不上。排查启动失败还有一条原则看控制台第一行错误不要只盯着最后一行异常。Spring Boot启动失败往往是一连串日志真正的原因通常在“Error creating bean with name...”“Unable to connect to database”这类早期信息里。数据库连不上就去看账号密码和URLRedis连不上就去看Redis有没有启动先把外部依赖解决再去抠代码逻辑。6.2 Node版本过高导致的构建失败这套系统是前后端分离的前端启动阶段有一类错误非常典型跑npm run serve的时候构建到一半报编译错误日志里带着“Error: error:0308010C:digital envelope routines::unsupported”。这个错误的根源是Node 17及以上版本默认启用了OpenSSL 3.0而项目里的Webpack版本用的是旧的OpenSSL 1.1 API两边不兼容。解决方案有两个一是降Node版本直接用16 LTS最省心二是不想换版本的话在package.json的scripts里给启动命令加上serve: NODE_OPTIONS--openssl-legacy-provider vue-cli-service serve这行配置的意思是让Node使用旧版OpenSSL提供者绕开兼容性报错。不过我建议能降Node就降Node因为后面你还会遇到其他ESLint、依赖版本的问题一个16 LTS能帮你躲开一大片雷。顺带一提很多人安装依赖失败是因为npm registry源太慢运行npm config set registry https://registry.npmmirror.com之后重新安装基本立竿见影。6.3 数据库连接报错与中文乱码数据库这块有两个高频问题。第一个是MySQL 8.0连接时报“Public Key Retrieval is not allowed”这是因为MySQL 8.0的caching_sha2_password加密方式需要先获取服务器的公钥。解决方式是在JDBC URL后面加上allowPublicKeyRetrievaltrue同时配合useSSLfalse把SSL校验关掉jdbc:mysql://localhost:3306/homestay?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue第二个是页面和数据库都出现中文乱码。排查链路是先看数据库表字符集是不是utf8mb4执行SHOW CREATE TABLE看看字符集再看JDBC连接URL有没有characterEncodingutf8最后看后端返回JSON时有没有统一设置UTF-8。三步都检查一遍乱码基本消失。建库时养成习惯直接指定utf8mb4不要把决定权交给数据库默认配置。6.4 Long类型精度丢失与跨域问题后端订单表主键是Long前端拿到后却经常出现ID后几位变成0的情况。原因在于JavaScript的Number类型最大安全整数是2的53次方而数据库自增主键生成的bigint一旦超过这个范围精度就丢了。常见的解决办法是把ID序列化为字符串返回给前端。Spring Boot项目里配置一个Jackson自定义序列化器或者直接在ID字段上加注解JsonSerialize(using ToStringSerializer.class) private Long id;如果项目统一使用Jackson也可以全局注册Long转String的序列化配置这样所有Long类型字段都对前端友好。前端拿到的ID变成字符串后传给后端的请求参数通常也要相应调整后端Controller的入参类型用String接收再转Long或者直接按字符串处理。跨域问题在前后端分离项目里迟早会遇到。浏览器直接访问前端页面前端向http://localhost:8080发请求会报CORS错误。开发环境最优雅的解决方式就是前面说的代理转发配置好之后前端请求看起来是同源的浏览器不再拦截。生产环境通常用Nginx做反向代理把 /api 转发到后端服务同样一劳永逸。最后一个我自己实操中的习惯建议整套系统跑通之后先打开git把当前状态打一个初始提交。之后不管你怎么改代码、折腾新功能随时能回退到“能跑”的状态。这个习惯让我少掉了很多头发写代码时心里特别有底。拿到这套源码后你顺着订单状态机和数据库表结构这两条主线读一遍再去动任何功能会觉得整个系统变得越来越顺手。