
做民宿平台管理系统这个选题我看下来最值钱的部分不是那一堆CRUD代码而是“订单状态怎么流转”和“库存怎么扣减”这两个环节。Spring Boot本身是套成熟的轮子真正拉开差距的是业务建模和数据库设计。这篇就把我当时从拿到需求到部署上线的完整过程拆开讲重点放在那些文档里不会写、但实操一定会碰到的决策点上。1. 项目整体拆解这个民宿管理系统到底在管什么1.1 核心需求解析民宿平台管理系统和传统酒店管理系统有本质区别。酒店是标准客房房型固定、价格固定、退改规则统一民宿则是“一套房子一个样”有的按整间出租有的按床位出租房价随节假日浮动房东和平台之间的结算还涉及清洁费、押金、平台佣金。如果直接套用酒店管理系统的思路后续扩展一定会卡壳。这个系统的核心参与角色分三类对应不同的功能边界角色核心诉求典型操作游客/注册用户找房、比价、下单、退改按城市/日期/人数搜索浏览图片与评论在线支付平台管理员管房源、管订单、管交易房源审核上下架处理纠纷单查看经营报表房东如有管理名下房源与收益维护房态日历设置房价查看入住安排我拿到这个项目时页面原型里并没有“房东后台”只有管理员和C端用户两套界面。考虑到毕业设计和课程设计的体量这其实是合理的取舍。如果硬加房东端等于多出一套权限系统和一套MIS后台工作量直接翻倍。所以第一版只做管理员端和用户端房东管理能力合并到管理员端用house_owner_id字段做逻辑归属。后续要升级加一个角色和几套页面就行不用动表结构。1.2 功能模块划分系统拆成七个功能域每个域对应一组紧密耦合的用例用户域注册登录、个人信息维护、收藏民宿、浏览历史。注册用手机号加短信验证码但考虑到本地演示环境没有真实短信服务验证码用Redis存储并打印到控制台同时预留阿里云短信接口的适配层。民宿域民宿信息发布、多图上传、房型管理、房态日历。民宿信息至少包含城市、区域、详细地址、经纬度、配套设施标签、入住须知、退订政策。订单域创建订单、支付模拟、取消订单、申请退款、入住登记、离店结算。这是整个系统最复杂的域牵扯库存状态机。评价域订单完成后评价支持文字加图片管理员可屏蔽违规评论。营销域优惠券发放与核销、限时折扣价、新人立减。内容域平台公告、民宿推荐位、城市攻略文章。管理域管理员登录、权限分配、数据统计报表、操作日志。这七个域不用一次性全部实现但表结构设计时需要全部预留。很多人的项目死在第二步——表结构只覆盖了当前页面做到订单模块发现缺字段再回去改表一改就牵动三四张表的外键。我的建议是开工第一周先做完所有表的建模和CRUD骨架再回头填业务逻辑。2. 技术选型的取舍Spring Boot版本、MyBatis Plus还是JPA2.1 Spring Boot版本选择的坑这个项目标题里点名了Spring Boot所以我直接基于Spring Boot来做。但版本问题必须先说清楚因为无数人栽在这里。Spring Boot 3.x要求JDK 17以上并且javax.*命名空间换成了jakarta.*。这意味着网上大量基于Spring Boot 2.x的教程代码直接复制过来会编译报错。另外Spring Boot 3.x的很多第三方starter还没完全跟上比如一些老版本的MyBatis Plus、Shiro、JWT库都不兼容。我的建议是如果做毕业设计或课程设计优先选Spring Boot 2.7.x JDK 8/11。理由很务实网上可参考的资料最多遇到问题搜得到答案Tomcat、MyBatis Plus、Redis客户端这些配套组件的兼容版本都是现成的部署到学校机房或服务器时老版本JDK环境更容易满足不用强行安装JDK17。当前IDEA2024.3以上版本新建Spring Boot项目时默认会拉到3.x版本。想创建2.7.x项目有两种靠谱方式# 方式一直接用Spring Initializr指定版本 curl https://start.spring.io/starter.zip \ -d dependenciesweb,mybatis-plus,mysql,redis \ -d bootVersion2.7.18 \ -d javaVersion8 \ -o demo.zip方式二是在IDEA的Spring Initializr界面里把Server URL改成https://start.spring.io/然后在Infrastructure页签里点Versions下拉框手动选2.7.18。如果下拉框没有就到pom.xml里直接改parent版本号默认模板生成的3.x代码需要同步调整包名导入工作量不大。2.2 ORM选型为什么选MyBatis Plus这个项目没有选择Spring Data JPA而是选MyBatis Plus主要有三个考量培训班和社区主流代码都是MyBatis体系后续大家查资料、面聊时沟通成本低MyBatis Plus的BaseMapper提供了单表CRUD的现成实现对于民宿这种以单表操作为主、多表关联为辅的业务能省掉大量样板代码SQL可控性强统计类SQL比如月度订单量、房型入住率可以直接写Mapper XML性能优化空间明确MyBatis Plus有几个配置项是必须打开的不然会遇到一些很诡异的问题。首先是逻辑删除mybatis-plus: global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0这个配置打开后deleteById()物理删除会被自动改写成UPDATE ... SET deleted1。民宿行业的数据有审计需求用户删除订单、管理员下架房源这类操作不应该物理消失逻辑删除是底线。另一个必须配置的是字段自动填充。业务表里普遍有create_time和update_time字段用MyBatis Plus的MetaObjectHandler接口统一维护Component public class MyMetaObjectHandler implements MetaObjectHandler { Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, createTime, LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } }实体类上记得加TableField(fill FieldFill.INSERT)和TableField(fill FieldFill.INSERT_UPDATE)注解。不配置自动填充的话每写一次insert和update都要手动set时间漏掉一次就会出现创建时间为NULL的数据排查起来非常费劲。2.3 数据库选型MySQL 8.0还是5.7数据库直接选MySQL。版本上如果服务器是云主机且可以自由安装环境用MySQL 8.0字符集选utf8mb4。utf8mb4不是可选项是必选项因为民宿评价里用户可能输入emoji表情旧的utf8mb3也就是常说的utf8存emoji会直接报错或者存成乱码。MySQL 8.0的默认认证插件是caching_sha2_password如果用5.x版本的驱动连接会报Unable to load authentication plugin错误。解决的唯一正确姿势是使用mysql-connector-java8.x版本并且在连接串上显式写serverTimezoneAsia/Shanghaispring: datasource: url: jdbc:mysql://localhost:3306/homestay?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: root driver-class-name: com.mysql.cj.jdbc.DriverallowPublicKeyRetrievaltrue这个参数很多人不知道。MySQL 8.0使用caching_sha2_password认证时如果客户端没有配置SSL且需要从服务器获取公钥连接会失败。本地开发时加上这个参数避免莫名其妙报公钥检索失败。3. 数据库设计民宿系统最核心的几张表3.1 民宿与房型的分层设计民宿和酒店最大的数据差异在于“房型到底怎么建模”。酒店一个RoomType对应多个Room实体房间号每间房独立状态而民宿通常是“一个房源就是一个可售单元”房东上架的是房源本身最多内部划分几个卧室但并不代表每个卧室可以独立售卖。所以在建表时做了三层模型-- 民宿主体表 CREATE TABLE homestay ( id BIGINT PRIMARY KEY COMMENT 雪花ID, owner_user_id BIGINT NOT NULL COMMENT 房东用户ID, title VARCHAR(100) NOT NULL COMMENT 民宿标题, city_code VARCHAR(20) NOT NULL COMMENT 城市编码, address_detail VARCHAR(255) COMMENT 详细地址, latitude DECIMAL(10,7) COMMENT 纬度, longitude DECIMAL(10,7) COMMENT 经度, cover_url VARCHAR(255) COMMENT 封面图, description TEXT COMMENT 详情描述, house_rules TEXT COMMENT 入住须知, refund_policy VARCHAR(20) DEFAULT NON_REFUNDABLE COMMENT 退订政策, status TINYINT DEFAULT 0 COMMENT 0待审核 1上架 2下架 3审核驳回, deleted TINYINT DEFAULT 0, create_time DATETIME, update_time DATETIME ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci; -- 房型/价格表 CREATE TABLE homestay_room ( id BIGINT PRIMARY KEY, homestay_id BIGINT NOT NULL, room_name VARCHAR(50) COMMENT 如: 山景大床房, bed_type VARCHAR(30) COMMENT 大床/双床/榻榻米, guest_capacity INT COMMENT 可住人数, area INT COMMENT 面积(平方米), base_price DECIMAL(10,2) COMMENT 平日价格(每晚), weekend_price DECIMAL(10,2) COMMENT 周末价格(每晚), holiday_price DECIMAL(10,2) COMMENT 节假日价格(每晚), attributes VARCHAR(500) COMMENT 设施标签, 逗号分隔, status TINYINT DEFAULT 1, deleted TINYINT DEFAULT 0 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci;为什么把价格放在房型表而不是订单表因为民宿的价格体系是动态的早鸟价、连住折扣、节假日调价都会影响最终金额。订单里有一个total_amount的最终快照但基础价格始终以房型表为准后续做价格日历表homestay_price_calendar时直接挂到homestay_room_id下不需要重建表关系。3.2 订单表一次预订状态流转的完整记录订单表是这类系统的核心。我的设计里把支付流水和订单主体分开因为一笔订单可能经历多次退款操作每次退款都有对应的资金变动记录。把状态全塞进订单表会导致字段冗余而且不好对账。CREATE TABLE order_info ( id BIGINT PRIMARY KEY, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT 业务订单号, user_id BIGINT NOT NULL, homestay_id BIGINT NOT NULL, room_id BIGINT NOT NULL, check_in_date DATE NOT NULL COMMENT 入住日期, check_out_date DATE NOT NULL COMMENT 离店日期, nights INT NOT NULL COMMENT 入住晚数, guest_count INT NOT NULL COMMENT 入住人数, total_amount DECIMAL(10,2) NOT NULL COMMENT 订单总额, deposit_amount DECIMAL(10,2) DEFAULT 0 COMMENT 押金, clean_fee DECIMAL(10,2) DEFAULT 0 COMMENT 清洁费, status TINYINT NOT NULL COMMENT 0待支付 1已支付 2已入住 3已离店 4已完成 5已取消 6退款中 7已退款, cancel_reason VARCHAR(255), remark VARCHAR(500), create_time DATETIME, payment_time DATETIME, checkin_time DATETIME, checkout_time DATETIME, deleted TINYINT DEFAULT 0 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci;订单状态流转是这个系统最容易出隐藏bug的地方。我建议在项目里直接写一个状态机工具类而不是把状态校验散落在Service的各个if里public enum OrderStatus { WAITING_PAYMENT(0), PAID(1), CHECKED_IN(2), CHECKED_OUT(3), COMPLETED(4), CANCELLED(5), REFUNDING(6), REFUNDED(7); private final int value; // 合法状态迁移表 private static final MapInteger, SetInteger TRANSITIONS new HashMap(); static { TRANSITIONS.put(0, Set.of(1, 5)); // 待支付 - 已支付 / 取消 TRANSITIONS.put(1, Set.of(2, 5, 6)); // 已支付 - 入住 / 取消 / 退款 TRANSITIONS.put(2, Set.of(3)); // 已入住 - 已离店 TRANSITIONS.put(3, Set.of(4)); // 已离店 - 已完成 TRANSITIONS.put(6, Set.of(7)); // 退款中 - 已退款 } public static boolean canChange(int from, int to) { SetInteger allowed TRANSITIONS.get(from); return allowed ! null allowed.contains(to); } }这个查询当前状态并使用状态机的代码量不大但可以有效防止用户在未支付状态下直接申请退款、已取消订单又被标记入住之类的数据错乱。业务层在修改状态前先调canChange校验不合法直接抛异常。3.3 库存扣减基于日期维度的房态锁民宿预订的库存和标准酒店又有差异。酒店有固定房间数民宿按“房源维度”售卖同一时间段内一套房源只能被一笔有效订单占用。最简单的库存控制方案是homestay_inventory表CREATE TABLE room_stock ( id BIGINT PRIMARY KEY, room_id BIGINT NOT NULL, stock_date DATE NOT NULL COMMENT 日期, total_count INT DEFAULT 1, locked_count INT DEFAULT 0 COMMENT 已被锁定数量, UNIQUE KEY uk_room_date (room_id, stock_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci;创建订单时事务内做两次操作扣减订单覆盖日期范围内的locked_count事务提交后不可回滚则定期释放超时订单占用的库存。选这个方案的重点是库存锁定和订单创建必须在同一个事务里否则会出现订单没建成但库存已经扣了或者库存扣了但订单还挂着的脏数据。实际代码里可以用MyBatis Plus的update配合条件构造器做原子更新LambdaUpdateWrapperRoomStock wrapper new LambdaUpdateWrapper(); wrapper.eq(RoomStock::getRoomId, roomId) .eq(RoomStock::getStockDate, date) .ge(RoomStock::getTotalCount, 1) // 乐观锁防超卖 .setSql(locked_count locked_count 1); boolean success roomStockService.update(wrapper);如果success为false说明这一天的库存已被锁完事务直接回滚并给前端返回“该日期已被预订”的提示。这种基于纯SQL条件更新的做法比先select再update的经典两步操作安全得多不会出现并发超卖。4. 核心功能实现与流程从搜索房源到完成订单4.1 民宿检索多条件组合查询用户搜索民宿时通常会选城市、入住日期、离店日期、入住人数。房源列表页面需要根据这四个条件组合过滤。刚开始我直接表join然后where拼参数做成了一坨几百行的SQL。后来拆成两步才清爽。第一步先用城市、时间段约束找到候选房型。第二步对候选房型做库存校验。因为每晚的库存是独立的用户要住3晚必须3天的locked_count都未满这天然是一个子查询/分组判断select idselectAvailableRooms resultTypecom.demo.pojo.RoomVO SELECT r.id, r.room_name, r.guest_capacity, r.base_price, h.title AS homestay_title, h.cover_url FROM homestay_room r INNER JOIN homestay h ON r.homestay_id h.id AND h.status 1 WHERE r.guest_capacity gt; #{guestCount} AND r.id NOT IN ( SELECT rs.room_id FROM room_stock rs WHERE rs.stock_date BETWEEN #{checkInDate} AND #{checkOutDate} GROUP BY rs.room_id HAVING SUM(CASE WHEN rs.locked_count gt; rs.total_count THEN 1 ELSE 0 END) gt; 0 ) AND r.deleted 0 ORDER BY r.base_price ASC /select这个SQL的思路先找出所有在入住日期区间内有任意一晚满房的房型ID用NOT IN排除掉剩下的就是全程可订房型。HAVING子句里的SUM(CASE...END)可以稍微绕一下但性能没有问题几条记录加上索引基本毫秒级返回。搜索接口还需要支持按价格区间、评分、区域筛选。这些最好都放在SQL里做不要查出全量数据后在内存里过滤。数据量小的时候感觉不大等表数据上万条后差距就出来了。4.2 下单流程事务与幂等下单接口是整个系统的关键路径。我定义了如下接口约束PostMapping(/api/order/create) public ResultOrderVO createOrder(RequestBody Valid CreateOrderRequest req) { // req包含 userId, homestayId, roomId, checkInDate, checkOutDate, guestCount // 1. 参数校验: 日期合法性(入住晚数0) // 2. 计算总价: base_price * nights clean_fee deposit_amount // 3. 锁定库存 room_stock.locked_count 1 // 4. 生成订单记录 status 0(待支付) // 5. 返回订单号 }创建订单必须用Transactional(rollbackFor Exception.class)。最容易被忽略的是步骤2的价格计算这里不要前端传金额过来要以服务端数据库的房价为准。前端传金额等于给用户留了一个任意改价的后门哪怕是课程设计这种漏洞也会被老师挑出来。下单的幂等性处理也很重要。用户快速点击“立即预订”前端请求可能被发送两次或者三次如果不做幂等处理库存会被扣多次订单也会建多条。我选择了客户端传入requestId的方式在Redis里存requestId对应的订单号重复请求直接返回第一次的订单结果。订单创建完成后返回orderNo业务订单号支付时用orderNo查询订单。不要把数据库自增id直接暴露给前端订单号加密出来见光死影响面太广。4.3 支付模拟与超时关单真实对接微信支付或支付宝需要商户资质课程设计场景基本不可能。我做了两种支付模式演示模式前端点支付后端直接把订单状态从0改成1模拟支付成功接入模式预留PayService接口保存订单时记录支付请求号通过回调接口更新状态两种模式不影响核心表结构。唯一要考虑的是如果订单一直不支付大厅库存就被一直锁住所以需要定时关单。定时关单我用Spring自带Scheduled每30秒扫描一次Component public class OrderTimeoutTask { Scheduled(fixedDelay 30000) public void closeExpiredOrders() { LocalDateTime deadline LocalDateTime.now().minusMinutes(15); ListOrderInfo expiredOrders orderInfoService.list( new LambdaQueryWrapperOrderInfo() .eq(OrderInfo::getStatus, 0) .lt(OrderInfo::getCreateTime, deadline) ); for (OrderInfo order : expiredOrders) { orderInfoService.cancelOrder(order.getId(), 支付超时自动取消); // 原子释放库存 inventoryService.releaseLockedStock(order); } } }这里有一个业务细节取消订单后释放库存释放操作不能直接在循环里简单更新locked_count - 1。因为同一房型同一日期可能有多个订单的库存被锁释放必须严格对应当前订单占用的日期范围。我的做法是遍历订单的每个入住日期对每个日期的locked_count做减1更新。看似笨拙实际上正好和执行节点清晰匹配后续对账出问题也好查。4.4 登录与鉴权JWT还是Shiro民宿系统属于前后端交互频繁的Web项目鉴权选JWT方案理由是这个系统没有复杂的权限树只有“用户”和“管理员”两个角色JWT的一个role字段就够了。JWT最适合这种轻量级场景。JWT实现需要注意两个细节第一密钥不能写死在代码里放到application.yml的jwt.secret配置项中。第二token过期时间设为2小时因为用户下单、支付、评价这几个操作间隔可能比较长过期太短会反复弹登录框。拦截器校验token后把用户ID放到ThreadLocal或Request attribute里后续Service层就能直接取到当前用户public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String token request.getHeader(Authorization); if (token null || !token.startsWith(Bearer )) { response.setStatus(401); return false; } // 解析token后放入请求上下文 Claims claims JwtUtil.parseToken(token.replace(Bearer , )); request.setAttribute(userId, claims.get(userId)); request.setAttribute(role, claims.get(role)); return true; } }注意一个坑跨域时前端如果带Authorization头后端必须允许ajax自定义header否则请求被浏览器拦截。针对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); } }allowedOriginPatterns(*)配allowCredentials(true)是可以的老版本Spring Boot只有allowedOrigins(*)配allowCredentials(true)会冲突甚至报错。升级到5.3.x以上后就没有这个问题。4.5 评论与评分计算逻辑要独立民宿几乎没有不带评论管理的评分也直接挂钩搜索结果排序。评论模块设计时把“评分汇总”单独做成了一个缓存字段而不是每次都实时取AVG。订单完成后用户可评价评价内容包括五个维度卫生、环境、设施、服务、位置每项1-5分。插入评论时同时更新homestay表的avg_score和comment_count。这个更新动作和插入评论在同一个事务里。如果暂时不需要五维评分做成单一评分即可。表设计CREATE TABLE homestay_comment ( id BIGINT PRIMARY KEY, order_id BIGINT NOT NULL, user_id BIGINT NOT NULL, homestay_id BIGINT NOT NULL, room_id BIGINT NOT NULL, score DECIMAL(2,1), content VARCHAR(1000), images VARCHAR(1000) COMMENT 图片URL列表, JSON数组, reply_content VARCHAR(1000) COMMENT 管理员回复, status TINYINT DEFAULT 1 COMMENT 1显示 0隐藏, create_time DATETIME ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci;一个比较常见的业务约束一笔订单只能评价一次。为了防止重复评论我在order_id上建了唯一索引并在代码里插入前先查一遍。不做防重的话用户在评价弹窗里反复点提交评论表就会出现一模一样的记录。4.6 多图上传与文件存储民宿信息包含多张实拍图Spring Boot接收MultipartFile后要保存到服务器。本地演示我直接存在项目的upload目录下配置静态资源映射即可访问spring: servlet: multipart: max-file-size: 10MB max-request-size: 50MB web: resources: static-locations: classpath:/static/,file:${user.dir}/upload/图片保存路径建议按日期分目录单一目录放几千张图后操作系统和文件管理都会变慢而且后续迁移到OSS也方便按前缀迁移。文件上传接口要限制文件类型至少检查扩展名和Content-Type防止上传jsp、php等可执行文件。以课程设计标准来看扩展名白名单后缀限制就可以但如果项目后续要部署到公网建议切到OSS并配合内容校验。5. 部署与常见问题排查从IDEA到Linux服务器5.1 前后端分离的宿命Vue打包放进Spring Boot这个项目虽然是Spring Boot主导但前端实现通常用Vue。开发阶段前后端分离后端跑8080端口Vue跑5173或8080端口通过proxy代理解决跨域。部署阶段有两种选择方案A前端Nginx后端Spring Boot独立部署通过域名划分路径方案B把Vue打包产物放进Spring Boot的src/main/resources/static目录前后端合并成单一服务方案B在课程设计和演示场景里使用频率最高因为只需要一台服务器、一个Tomcat端口答辩时不用分别启动Nginx和Java应用。操作流程是npm run build # 构建产物默认在 dist/ 目录 cp -r dist/* src/main/resources/static/ mvn clean package -DskipTests打包后Spring Boot会优先读取classpath:/static/下的静态资源。前端页面用相对路径访问/api/**接口后端接口统一加/api前缀。这样打出来就是一个可独立运行的jar包。需要注意一个小坑Vue路由如果使用history模式直接访问某个子路径比如/home会404因为后端没有对应的controller映射。开发阶段Vue Router会用devServer自动回退但部署为静态资源后需要在后端添加一个转发规则把非接口路径都转发到index.htmlController public class RouterController { RequestMapping(value /{path:[^\\.]*}) public String forward(PathVariable String path) { return forward:/index.html; } }这个写法对带多级子路径的路由会失效比如/user/order最佳方案还是配一个自定义ErrorPage或转发规则。这里仍是课程设计中最常见的报错点务必注意。5.2 IDEA启动配置的常见问题最近好几个读者问我“IDEA高版本新建的Spring Boot项目为什么启动不起来”我大致总结了几类高频问题。第一类Lombok插件与新版本IDEA的兼容问题。IDEA 2024.3之后自带Lombok插件支持但如果你手动处理过Annotation Processors设置很有可能把处理器关掉了导致Data注解的getter/setter全部编译失败。解决方式Settings - Build, Execution, Deployment - Compiler - Annotation Processors勾选Enable annotation processing。第二类Spring Boot 3.x项目运行到一半报ClassNotFoundException: javax.servlet.Filter。这是因为依赖冲突项目里某些库还在用javax版本而Spring Boot 3.x已经切到jakarta。排查方式是mvn dependency:tree找冲突的依赖排除旧的servlet-api。第三类改端口不生效。IDEA的application.yml明明是server.port: 8081启动日志还显示8080。这种情况多半是IDEA没有重新编译resources目录target/classes里还是旧的配置文件。清理项目重新编译基本能解决先执行mvn clean再启动。5.3 数据库文件导入与编码问题拿到这个项目的SQL文件后导入数据库最常见的问题有两个中文乱码和Unknown collation报错。中文乱码的根源是SQL文件本身编码或客户端连接编码不一致。推荐用命令行导入不要用Navicat直接双击mysql -u root -p --default-character-setutf8mb4 homestay init.sql如果SQL文件开头有CREATE DATABASE语法导入前先删掉这行避免每次执行都把已有库清空。课程设计开发过程中反复导入是很正常的但每次都清库会丢失测试数据给自己找麻烦。Unknown collation utf8mb4_0900_ai_ci这个报错是MySQL 8.0环境的SQL拿到了MySQL 5.7环境执行导致的。解决办法是全局替换SQL文件里的排序规则把utf8mb4_0900_ai_ci批量替换成utf8mb4_general_ci再重新导入。5.4 生产部署jar包跑起来本地跑通之后部署到服务器上也有标准套路。我用的是最基础的systemd服务托管方式不用Docker因为课程设计节点的环境越简单越不容易背锅。在/etc/systemd/system/homestay.service创建服务配置[Unit] DescriptionHomestay Platform Service Afternetwork.target mysql.service [Service] Userroot WorkingDirectory/opt/homestay ExecStart/usr/bin/java -Xms256m -Xmx512m -jar /opt/homestay/homestay.jar --spring.profiles.activeprod Restarton-failure RestartSec10 [Install] WantedBymulti-user.target启动命令就是systemctl daemon-reload systemctl enable homestay systemctl start homestay生产环境连接MySQL时不要复用本地开发用的root账号。新建一个专用数据库账号只授予homestay库的增删改查权限这样SQL注入即使突破框架限制伤害也被控制在单个数据库内。6. 安全保障敏感信息不能直接裸奔最后专门聊一下安全因为很多课程设计项目拿到代码后第一个被老师问的就是“你这个系统安全吗”。即使业务功能没有大缺陷安全上留一堆口子也会被判定不够完整。需要落实的安全点有五项密码存储用BCryptPasswordEncoder加密不能用MD5。MD5防不住彩虹表BCrypt每次加密结果随机有效抵抗暴力破解防SQL注入MyBatis动态SQL用#{}不要用${}拼表名或模糊关键词身份校验除了JWT所有涉及删除、修改的API接口后端必须校验当前用户是否有权限操作该数据数据脱敏用户手机号只显示前3后4位接口返回前统一处理操作日志后台管理端的关键操作删除民宿、审核下架、修改用户状态记录操作人、操作时间、操作内容其中最容易忽略的是“越权操作”。很多人在前端把操作按钮隐藏了就觉得安全了。实际上一个简单的curl请求完全可以直接调用接口。比如用户删除评论的接口必须校验评论所属的user_id是否等于当前登录用户否则任何登录用户都能删别人的评论。这类漏洞在答辩时被发现很丢分。7. 项目管理与答辩文档和源码同样重要这个项目名带了“源码数据库文档”说明文档交付物和代码是同等重要的。我在写文档时发现很多项目文档写得像需求说明书没有从实现角度解释为什么这样做答辩时容易被追问到卡壳。建议至少包含四份文档需求分析文档包含用例图和核心业务流程描述解释民宿和酒店的差异说明为什么订单中心是主业务数据库设计文档包含ER图和每张表的字段说明重点标明唯一索引、外键关系、逻辑删除字段接口文档列出所有Controller暴露的API标注请求方式、请求参数、返回示例。推荐使用Apifox或Postman导出的格式部署文档部署步骤写清楚包括JDK版本、MySQL版本、Redis安装、初始化SQL导入、jar包启动命令答辩演示时一定要提前准备好演示数据。包括至少5个城市的20套民宿房源、多张真实的房态被预订状态、10条以上订单不同状态、支付时间跨度均匀、至少3条用户评论。展示时重点演示完整链路注册登录 → 搜索房源 → 查看房型 → 创建订单 → 支付 → 生成评价中间穿插取消订单和库存释放。也可以准备一个反向案例比如下单后15分钟不支付展示定时任务自动关单并释放库存的日志。我个人在实际操作中的体会是Spring Boot民宿项目能不能拿高分核心不在技术栈的新旧而在于对业务真实感的把握度。一个能把订单状态机讲清楚、能解释为什么用逻辑删除而不是物理删除、能展示并发超卖拦截效果的项目比套用一堆高级技术但逻辑漏洞百出的项目要扎实得多。这也是我把大部分篇幅放在订单、库存和表设计上的原因。毕竟你答辩时说的每一句“这里为什么这么设计”都有可能决定这个项目是仅仅能跑起来还是真正能被认可。