每年到毕业设计选题季,都会有一批同学来问我:“管理系统还能不能选?会不会太老套?”我的回答一直是:管理系统不是问题,问题是你做的是不是“有场景、有逻辑、有闭环”的管理系统。比如今天要说的这个题目——基于SpringBoot的农家乐管理系统,表面上是一套标准的Java Web毕设,但往深了看,它是把民宿预订、餐饮运营、活动预约、会员评价这些真实业务捏合在一起的信息化平台,背后的核心逻辑是“农旅融合场景下的多业态一体化运营”。无论是拿来做毕业设计,还是给小型农庄做信息化参考,这个项目都值得认真拆一遍。
这篇文章适合正在选题的计算机专业学生、准备开始动手写SpringBoot项目的初学者,也适合想了解“一个完整Web系统是怎么从零搭出来”的读者。我会从这个题目最容易被低估的地方讲起:业务建模、数据表设计、权限边界、订单状态机、并发防超卖,再到自测、部署和答辩准备,尽量把这些平时没人细讲的关键点一次说明白。
1. 选题拆解:这个毕设到底在做什么
1.1 一个系统,三块核心业务
“农家乐管理系统”这个名字听起来很普通,但看题目里那几个限定词就明白了——乡村休闲农庄、农旅融合、民宿餐饮一体化。它不是一个简单的信息展示网站,而是把农庄的日常经营拆成了三条业务线:
第一条是民宿线。游客要在线上看房、选日期、订房间、办理入住和退房,商家要能管理房型、房价、房间状态,还要处理不同日期的空闲与占用冲突。
第二条是餐饮线。游客到了农庄要吃饭,系统里得有菜品展示、分类筛选、加购下单、订单结算,商家要能查看订单、更新出餐状态,最好还有简单的菜品销量统计。
第三条是体验线。农家乐和普通酒店的区别在于“玩”,比如采摘、钓鱼、篝火晚会、季节限定的农事体验,这些活动需要发布、报名名额控制、预约记录。
三条业务线之间不是孤立的。同一个游客,上午订了民宿,中午在餐厅点餐,下午报名摘草莓,晚上在房间里看到订单明细。所以系统必须有一个统一的用户体系,有一套贯穿所有业务的订单数据模型,还要让管理员和商家在后台能看清每一笔流水。
这就是这个题目真正的价值:它很小,但五脏俱全。它逼着你把“用户、商品、订单、支付状态、评价”这一整套电商系统里的核心概念都过一遍,又比纯电商系统多了“房间日历库存”和“活动名额”这种非标准库存的建模问题,非常适合用来展示一个Java开发者对业务的理解能力。
1.2 从毕业设计角度看的加分点与难点
如果只做了一个CRUD,这个题目就浪费了。我见过太多人把农家乐管理系统做成了“四个表的增删改查”,答辩时被老师一问“房间日期冲突你怎么判断的”就直接卡住。其实这个题目天然埋着几个能让答辩加分的点:
第一是民宿的“日期区间库存”问题。房间不是无限库存,也不能像普通商品一样简单减库存。一个房间在某个日期段内要么可订、要么不可订,还要考虑已预订、已入住、已退房这些状态过渡,难点在“判断两个日期区间是否有重叠”。
第二是餐饮下单的事务处理。一个订单包含订单主表和多个菜品明细表,必须保证“头表插入成功、明细表插入失败”时全部回滚,这就是事务管理的天然示例。
第三是活动预报名的防超卖问题。一个活动只有30个名额,30个人同时报名,最后一个人不能把名额变成负数。用普通的“先查再减”写法一定会出问题,必须借助数据库的行锁或乐观锁机制。
第四是权限模型。系统至少要有管理员、农庄商家、普通用户三种角色,不同角色看到的功能菜单和能访问的接口完全不同,这扯到了登录认证、拦截器配置、越权访问防护。
这些点每一个都不难,但组合起来就是一个很有“业务完整性”的毕设。答辩老师不会要求你做分布式、做高并发,但你得能把这些问题用恰当的技术方案解释清楚,这就能和满屏“XX管理系统”的普通作品拉开差距。
2. 技术选型与工程骨架搭建
2.1 技术栈选型与版本避坑
先交代一下我推荐的组合,这是在带过多届毕设之后验证过最稳的一套:
- 基础框架:SpringBoot 2.7.x(不要追新用3.x,除非你只想用原生API,MyBatis等生态适配起来会多出很多麻烦)
- JDK版本:1.8 或 11,企业项目和教学环境兼容性最好
- 持久层:MyBatis-Plus 3.5.x(自带分页插件、逻辑删除、字段自动填充,能大幅减少重复代码)
- 数据库:MySQL 5.7 或 8.0
- 前端方案:第一种是Thymeleaf模板引擎加Semantic UI或Bootstrap,适合工期紧、想省事;第二种是前后端分离,前端用Vue3 + Element Plus,后端只写JSON接口,适合想多展示一些技术栈的同学
- 身份认证:JWT(无状态、前后端分离友好)或者基于Session的拦截器校验,二选一。如果前端是模板引擎,Session方式写起来更简单;如果用了Vue分离,JWT更顺手
- 项目管理:Maven 3.6+,本地仓库换阿里云镜像,否则第一次下载依赖能把人急哭
这里有个特别想提醒的点:SpringBoot版本别看教程用哪个就跟风用哪个。我遇到过有人下载了最新版3.2,结果很多老版本的代码片段、依赖坐标全部不兼容,光是修环境就花了三天。选2.7.x是现阶段学习成本最低的方案,网上能搜到的资料也最多。
2.2 项目包结构与分层设计
一个能撑起“二期开发”的工程,代码绝不能全堆在controller里。建议包结构如下:
com.farm.system ├── common // 通用返回结果、状态码、异常处理 │ ├── Result.java │ ├── ResultCode.java │ └── GlobalExceptionHandler.java ├── config // 配置类:WebMvc拦截器、跨域配置、MyBatisPlus配置 ├── controller // 控制层:只做参数接收与结果返回 ├── service // 业务接口 + impl ├── mapper // MyBatis-Plus的Mapper接口 ├── entity // 数据库实体 ├── dto // 请求参数对象 ├── vo // 返回给前端的视图对象 └── utils // JWT、日期处理等工具分层的好处不止是“看起来专业”,更重要的是每个类的职责边界清晰:controller不写SQL,service里面不直接拼HTML,mapper只负责数据库操作。后期排查问题时,只要看一眼报错在某一层,就能快速定位。尤其是做毕设展示的时候,老师问你“业务逻辑写在哪里”,你能指着service包说出调用链,这种结构化表达本身就很加分。
2.3 核心依赖与配置文件参考
极简版pom.xml里的关键依赖长这样:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.5</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency>application.yml里面,下面这几个配置是我每次都要检查的:
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/farm_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver servlet: multipart: max-file-size: 10MB max-request-size: 10MB mybatis-plus: global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0这里有三处容易踩坑:serverTimezone=Asia/Shanghai不加的话,数据库连接会有8小时时差;logic-delete-field开起来以后,所有删除都变成逻辑删除,避免误删物理记录,但查询时要留意MyBatis-Plus会自动追加deleted=0条件,如果自己写关联SQL时没注意,很容易查不到数据;上传大小限制不设的话,前端传图片时会报“FileSizeLimitExceededException”,这个问题我在后面会详细说。
3. 数据库设计:表结构才是这项目的灵魂
3.1 核心数据表清单
很多同学一上来就建表,想到哪写到哪,结果后面改表改到崩溃。我的习惯是先在纸上把业务对象列出来,再画关联关系。农家乐系统最少要有这九张表:
| 表名 | 用途 | 关键字段 |
|---|---|---|
user | 用户表,三种角色共用 | username, password, phone, role |
farm | 农家乐主体信息表 | name, address, description, cover_image |
room | 民宿房间表 | farm_id, name, type, price, bed_count, status |
room_order | 民宿订单表 | user_id, room_id, start_date, end_date, total_price, status |
dish | 菜品表 | category_id, name, price, image, description, sales |
dish_category | 菜品分类表 | name, sort |
dish_order | 餐饮订单主表 | user_id, order_no, total_amount, status |
dish_order_item | 餐饮订单明细表 | order_id, dish_id, dish_name, price, quantity |
activity | 活动表 | name, start_time, end_time, quota, remaining, price |
activity_order | 活动预约表 | user_id, activity_id, quantity, status |
comment | 评价表 | user_id, target_type, target_id, content, rating |
notice | 公告表 | title, content, publish_time |
再强调一下命名规范:表和字段都用小写加下划线,Java实体里用驼峰,配合MyBatis-Plus的mapUnderscoreToCamelCase(默认开启)就能自动映射。字段类型上,金额一律用decimal(10,2),日期用date,时间戳用datetime,描述类字段用text或varchar(500)。这些细节看起来无关紧要,但到了答辩演示时,老师一般都会看一下表设计,字段类型选得规范不人在行,一眼就能看出来。
3.2 民宿预订的日期冲突与状态流转
民宿预订是整个系统里最值得深讲的一张表。
一张room_order表里,一个关键的约束是:同一个房间在同一时间段内不能有第二条有效订单。判断冲突的SQL核心是“两个区间是否有重叠”,重叠条件可以表达为:
start_date <= 查询结束日期 AND end_date >= 查询开始日期也就是说,在插入新订单之前,要先执行这样一条校验:
SELECT COUNT(*) FROM room_order WHERE room_id = #{roomId} AND status IN ('已支付', '已入住', '待支付') AND deleted = 0 AND start_date <= #{newEndDate} AND end_date >= #{newStartDate}如果COUNT(*)大于0,说明该房间在这个日期范围内已经被占用,前端就要提示“该日期段房间已被预订”。这句话写起来简单,但它就是民宿类系统里所谓的“库存校验”逻辑,和普通商品库存扣减完全是两回事。
订单状态我建议用一个整数状态机,不要用字符串,规范且节省空间。比如room_order.status:
| 状态值 | 含义 | 可流转方向 |
|---|---|---|
| 0 | 待支付 | 1、4 |
| 1 | 已确认/待入住 | 2、4 |
| 2 | 已入住 | 3 |
| 3 | 已退房 | 终态 |
| 4 | 已取消 | 终态 |
状态流转不要散落在各处,统一放在service层的一个方法里做校验,比如updateRoomOrderStatus(orderId, targetStatus, operator),里面先查当前状态,再判断是否允许流转到目标状态,不允许就直接抛出业务异常。这种做法在答辩讲解时特别占优势,因为你能把“状态一致性”的问题讲得清清楚楚。
3.3 订单表设计要点
餐饮订单用主表加明细表的结构,原因很简单:一张订单要包含多个菜品,如果只在订单表里放一个“菜品集合”字符串字段,后续做销量统计、价格汇总、数据导出都会变得非常痛苦。正确做法是订单主表存订单号、总额、状态这些订单级信息,明细表每行一个菜品,存快照字段。
这里有个细节容易被忽略:明细菌表里除了dish_id外,一定要冗余存一份dish_name和price。为什么?因为菜品是随时可能改价、改名的,订单一旦生成,它记录的是“成交当时”的商品信息,不能跟着当前菜品表变化。这就是数据建模里的“快照”思想。类似的,用户下单时也把登录用户的id和用户名冗余进订单表,后续客服查订单时不需要再去关联查询用户表,效率高,逻辑也干净。
还有一个逻辑是dish_order_item里不设外键约束,只留order_id作为普通索引字段。毕设项目里我一般不推荐物理外键,因为外键会让插入、更新变得笨重,而且在项目后期改数据时会处处受限。表结构里的关联关系在业务层里维护,外键约束带来的“保证一致性”由程序来负责,这种设计风格也更贴近企业里实际项目的做法。
4. 权限模型与登录认证实现
4.1 三种角色的权限边界
农家乐系统的用户最少要分三种:管理员、农庄商家/店长、普通游客。
管理员负责系统运营,比如管理公告、查看所有订单统计、管理用户状态、审核活动;商家负责自己农庄的日常运营,比如维护房间信息、上架菜品、处理订单、发布活动;普通游客则是核心业务对象,可以浏览、下单、评价、管理个人资料。
在数据库层面,user表里用一个role整型字段就够了:0表示管理员,1表示商家,2表示普通用户。接口层面做权限控制时,最朴素也最有效的方法是实现一个拦截器,从请求头(或者Session)里取出登录用户信息,判断其角色,再决定是否放行。比如实现HandlerInterceptor时这样写:
@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); if (!StringUtils.hasText(token)) { // 未登录,返回 401 } Long userId = jwtUtil.getUserId(token); request.setAttribute("userId", userId); return true; }再配一个@RequireRole注解,标在Controller方法上,拦截器读取注解值进行二次校验。这种实现方式代码量不大,但能让你在答辩时说出“接口级别的权限控制”这个完整链路。
4.2 登录认证与越权防护
登录这块我自己踩过一个大坑。早期图省事,用户密码直接明文存数据库,答辩老师一眼看到就不太满意。后来统一改成加密存储,常用的做法是BCryptPasswordEncoder。Spring Security里自带这个工具,哪怕你不引入Security全家桶,也可以单独引入spring-security-crypto依赖,然后直接调用:
String hash = new BCryptPasswordEncoder().encode("123456"); boolean matches = new BCryptPasswordEncoder().matches("123456", hash);BCrypt有个特点——同一个密码每次加密生成的hash串都不同,但都能通过校验,这是因为盐值被内嵌到了hash串里。相比MD5加盐还要自己维护一套盐表,BCrypt的实现要干净太多。
越权防护是最容易被忽视的。很多毕设只做了“登录才能进后台”,却没做“普通用户不能操作商家数据”,结果A用户登录后把请求里的userId改成B用户,就能看到别人的订单。解决办法是在service层做一步数据归属校验:操作订单前,先查订单的user_id是不是当前登录用户;商家端操作房间、菜品前,先判断当前用户是否有对应权限。这些校验看着繁琐,但它才是“权限管理”真正的含金量所在。
5. 核心业务模块的实现细节
5.1 民宿预订模块:并发与状态
民宿预订的完整链路是:前端选日期 -> 后端查空闲房间 -> 展示可用房型 -> 填写预订信息 -> 生成订单 -> 支付(模拟)-> 商家在后台确认。
这里推荐一个“先查询,后下单”的步骤设计,既能保证数据不脏,又能保证演示效果:
第一步,前端把入住日期和离店日期传过来,后端用“区间重叠”SQL查出这段时间内所有已占用的房间ID,再用NOT IN或左连接反查出可订的房间列表。第二步,用户选中某个房间点击预订,后端再执行一次“区间重叠”校验。第三步,校验通过后创建订单,状态置为“待支付”。
为什么第二步还要再查一次?因为在用户浏览到点击下单这几十秒里,房间可能已经被别人订走。第二次校验就是为了防止这种“看得到但订不到”的脏数据。对于毕设场景来说,这一层重复校验已经足够应对大多数情况,如果你想让代码更严谨,还可以在room_order表里加一个唯一约束,让(room_id, start_date, end_date, status)的组合不能重复,但那会增加一些管理成本,学生项目里不强制。
这里再补充一个重要规则:民宿订单要有“取消截止时间”。比如入住前24小时可免费取消,出这个是业务常识,也顺带让你的状态机更完整。否则用户乱下单又不取消,房间就直接烂在库里了。
5.2 餐饮点餐模块:购物车与订单事务
餐饮模块的实现我用的是“前端购物车 + 后端一次性提交”方案,和电商网站的结算逻辑很像,演示时也很直观。
前端购物车维护在浏览器里,用户点菜品就加入购物车,选好后一次性把购物车里的菜品列表提交到后端。后端接口接收的参数长这样:
{ "userId": 1, "items": [ { "dishId": 1, "quantity": 2 }, { "dishId": 3, "quantity": 1 } ] }后端拿到这个数据后,在一个@Transactional事务方法里做三件事:第一,根据dishId批量查菜品当前价格;第二,动态计算totalAmount(注意:前端传来的总价绝对不能信,必须以数据库里的菜品价格为准);第三,插入dish_order主表,再循环插入dish_order_item明细表。
事务的意义在这时特别直观——如果第5个明细插入失败,前4个明细和主表已经写进去了,没有事务就会出现“只有订单头、没有订单行”的脏数据。加了@Transactional之后,任何一步抛异常,整笔订单都回滚,用户在页面上看到的就是一个“下单失败,请重试”的提示。
有个细节比较容易被忽略:购物车在下单接口里要校验菜品是否“上架”状态。如果某个菜品已经下架却被用户留在购物车里,后端直接拒绝下单,而不是悄悄忽略或者报个晦涩的错。这些边界情况的处理,恰恰是项目亮点所在。
5.3 活动预约模块:防超卖的写法
活动预约的防超卖是整项目最提气的部分。活动表里有个remaining剩余名额字段,用户报名时,最直观的写法是先查:
SELECT remaining FROM activity WHERE id = 1如果remaining > 0,再执行:
UPDATE activity SET remaining = remaining - 1 WHERE id = 1这个写法在单用户、低并发下没有问题,但如果两个用户同时报名,两个查询都读到remaining = 1,两个更新都执行了,最终remaining变成0,但实际报名了两个人,超额了。
正确的做法是把扣减和判断放在同一条SQL里,利用数据库更新时的行锁:
UPDATE activity SET remaining = remaining - 1 WHERE id = #{activityId} AND remaining > 0再用int rows = this.update(...)接收影响行数,rows == 1说明扣减成功,rows == 0说明名额不足或活动不存在。这个写法在数据库层面天然保证不会把remaining扣成负数,也不需要引入Redis锁,够用且好讲。
活动预约的记录要单独放在activity_order表里,而不是只扣个数字。因为用户预约后要展示“我报名的活动列表”,商家也要统计每个活动的报名名单,没有明细表这些需求全都会卡壳。
5.4 评价、公告、轮播图等辅助模块
评价模块的定位是“闭合用户体验闭环”。游客入住或就餐完成后,可以在订单详情页对该农庄、菜品或民宿进行评分与评论,前台详情页展示总的平均分和每一条评价。这里有个常见分页问题:评价列表如果要支持分页,用MyBatis-Plus的Page对象非常方便:
Page<CommentVO> page = new Page<>(pageNum, pageSize); IPage<CommentVO> result = commentMapper.selectCommentPage(page, targetType, targetId);公告模块比较简单,管理端发布公告,游客端首页展示最新几条。我的建议是公告内容用富文本编辑器提交,或者只做纯文本提示“暂不支持富文本”,避免引入XSS隐患。如果你用了富文本,务必做HtmlUtils.htmlEscape过滤,或者配合全局XSS过滤器。
轮播图表面上只是一个图片列表,但它牵扯到上传和文件路径存储,我在下一part专门说。
6. 前后端交互与文件上传
6.1 统一返回结构与API约定
前后端一旦分离,接口规范就是天。我强烈建议所有Controller都返回同一个结构体:
public class Result<T> { private Integer code; private String message; private T data; }成功时code = 200,业务失败时code可以是自定义的400、403、500等,配合一个GlobalExceptionHandler做统一异常捕获,这样前端只需要封装一个请求方法,根据code统一提示即可。后端抛业务异常时,自定义一个BizException,在GlobalExceptionHandler里返回:
@ExceptionHandler(BizException.class) public Result<?> handle(BizException e) { return Result.error(e.getCode(), e.getMessage()); }这个统一返回结构的好处是:每个接口的返回值格式一致,前端处理逻辑不会散乱;全局异常拦截能保证就算代码出错,前端拿到的也不是一堆乱七八糟的堆栈文本,而是一条干净的错误信息。这个设计在答辩时是一个很好的结构化加分点。
接口路径命名也要统一,容易区分角色。比如:
/api/user/**:游客自助接口,如注册、登录、个人中心/api/farm/**:农庄信息接口,如浏览农家乐、查看房间、菜品、活动/api/merchant/**:商家管理接口,如维护房间、上架菜品、处理订单/api/admin/**:管理员接口,如用户管理、全站统计、公告管理
这样前缀分类有一个天然好处:拦截器可以直接按前缀精确控制权限,比如拦截/api/admin/**时校验管理员角色,拦截/api/merchant/**时校验商家角色,代码逻辑非常清晰。
6.2 图片上传与静态资源处理
农家乐系统里最大的“实体文件”就是图片:房间照片、菜品图片、活动海报、轮播图。图片上传的常规做法是把文件保存到本地磁盘的一个目录里,然后把磁盘路径或访问URL存到数据库。
Controller里的典型写法:
@PostMapping("/upload") public Result<String> upload(@RequestParam("file") MultipartFile file) { if (file.isEmpty()) { return Result.error("文件不能为空"); } String originalFilename = file.getOriginalFilename(); String ext = originalFilename.substring(originalFilename.lastIndexOf(".")); String fileName = UUID.randomUUID().toString() + ext; File dir = new File("D:/farm/upload"); if (!dir.exists()) { dir.mkdirs(); } file.transferTo(new File(dir, fileName)); return Result.success("/upload/" + fileName); }这里有两个特别容易出错的地方:
第一,文件名一定要用UUID重命名,不要用用户上传的原始文件名。最初的版本图省事直接用原名,结果出现了两张都是“1.jpg”的图片互相覆盖,前端图片直接裂开;更严重的是,文件名里如果有特殊字符或路径符号,可能在拼接访问路径时导致安全风险。
第二,要配置静态资源映射,否则/upload/xxx.png这个路径访问不到。SpringBoot里需要在WebMvc配置类中加:
@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourceMapping("/**") .addResourceLocations("file:D:/farm/upload/"); }这个映射如果你忘了配,前端所有图片都是一片空白,但后端文件确实存上去了,排查起来最容易卡住。一定要在部署文档里把“图片目录”和“静态资源配置”写清楚。
7. 自测、常见问题与部署上线
7.1 功能自测清单
系统写完不能直接交,我强烈建议按下面的清单过一遍,这些都是我实际带项目时会测的“必踩点”:
| 测试模块 | 测试用例 | 预期结果 |
|---|---|---|
| 登录注册 | 注册普通用户、注册商家账号、错误密码登录 | 密码加密入库,错误密码提示统一 |
| 民宿预订 | 选同一房间的两个重叠日期段下单 | 第二次提交被拒绝,提示日期冲突 |
| 民宿退款 | 支付后取消订单 | 状态变为已取消,房间释放可再次预订 |
| 餐饮下单 | 购物车提交两个菜品 | 主表和明细同时生成,金额正确 |
| 餐饮超售 | 下架菜品出现在购物车并提交 | 接口拒绝下单,提示菜品已下架 |
| 活动报名 | 模拟剩余名额只有1人,重复提交 | 第二次提交提示名额不足 |
| 权限控制 | 普通用户直接访问管理员接口 | 拦截器返回401或403 |
| 越权访问 | 用户A尝试查询用户B的订单 | 接口拒绝,提示无权访问 |
| 图片上传 | 上传1KB小图和5MB大图 | 小图正常返回URL,大图有大小限制提示 |
| 分页 | 评价列表超过10条 | 分页参数生效,无数据时返回空数组 |
这套清单做完一遍,系统里绝大多数隐藏bug都会暴露出来。更重要的是,你把这套测试过程和结果整理成文档,答辩时直接展示“我是按功能模块做测试的”,比空口说“我测过了”要有说服力得多。
7.2 部署与常见问题实录
部署方案我推荐“打jar包 + 服务器运行”,Linux环境下用宝塔面板管理也行,手写部署也行,核心命令就这几条:
# 本地打包(跳过测试) mvn clean package -DskipTests # 上传到服务器后运行 java -jar farm-system-0.0.1-SNAPSHOT.jar --spring.profiles.active=prodapplication-prod.yml里连接的是云服务器上的MySQL,数据库初始化就用你本地导出的SQL文件,注意字符集要设为utf8mb4,否则中文字符会出现乱码。
部署环节我自己踩过的坑里,最值得说的是下面几个:
数据库时区问题。生产环境MySQL时区不是Asia/Shanghai时,连上后会出现“今天23点插入,查出来却是明天凌晨”这种诡异现象。解决办法是在url里显式加serverTimezone=Asia/Shanghai,千万别只靠服务器系统时间。
端口被占用。java -jar启动时报Port 8080 was already in use,排查时先用netstat -ntlp | grep 8080看是哪个进程占用的,别急着重启机器。
静态图片404。前面提过的addResourceHandlers路径写错,部署后又排查了一个晚上。后来我的习惯是先用浏览器直接访问http://ip:8080/upload/test.png,如果能访问就说明静态资源没问题,不行就先查映射配置。
接口跨域问题。如果前端和后端分别部署在不同端口,必须配置跨域。最简单的方案是加一个CorsFilter,或者用@CrossOrigin注解,但要搞清楚只对Controller生效;前后端部署在同一台服务器的同域环境下,就不需要跨域处理。
7.3 答辩准备与扩展方向
项目做完,别急着交,答辩前要准备几个“深度问题”的答案:
- “你如何处理民宿房间的日期冲突?”对应区间重叠SQL加二次提交校验。
- “活动名额怎么防止超卖?”对应
UPDATE ... SET remaining=remaining-1 WHERE remaining>0的行级原子操作。 - “你的事务在哪里使用?”对应餐饮下单的
@Transactional和回滚场景。 - “你的密码为什么存的是哈希串?”引出BCrypt和MD5的比较,说明盐值原理。
如果学有余力,这个项目的扩展方向也很多:小程序端对接同一套后端接口,旅行足迹或周边景点推荐,订单导出Excel统计,商家端数据看板。我见过有同学把前端换成Vue3+Element Plus后,把餐饮下单做成了扫码点餐,从用户进店扫码到后厨接单的完整演示,答辩效果非常好。只要核心数据结构和接口足够规范,这些扩展都是自然的增量,而不是重构。
8. 最后说几句实在话
这个项目我在实际带毕设时反复推荐,不是因为它的技术栈新,而是因为它“足够老实”——一个学生能通过它把Java Web开发的完整链路走通:需求分析、表建模、接口设计、权限控制、事务处理、文件存储、部署上线,每一步都是基本功,每一步又都能拿出来讲出深度。
我见过有同学照着网上的开源商城项目大改特改,最后连自己项目的几个核心表都讲不明白;反而选这个农家乐题目的同学,认认真真把民宿订单状态机画了一遍,把活动防超卖那个SQL背得滚瓜烂熟,答辩时反而拿到了很好的评价。说到底,毕业设计看重的不是选题多“高大上”,而是你对一个真实业务场景能不能做到逻辑自洽、闭环完整。
如果你正在写这个题目,我的建议是:先把民宿的日期冲突和活动的防超卖这两个点吃透,再把权限拦截做好,最后留一周时间专门做测试和部署演练。这三件事做到了,你的项目就比大多数“管理系统”扎实得多。做完之后你会发现,SpringBoot玩得熟不熟,其实不在注解背了多少,而在你能不能把一个“开农家乐”的小事,用工程化的方式讲得明明白白。