1. 一个大学台球厅的管理难题,为什么值得做成毕设
很多同学在选题时都会纠结:管理系统是不是太老套了?CRUD项目还能做出什么花来?说实话,管理系统类题目确实多,但"校园台球厅人员与设备管理系统"这个题目,反而被我带的学生做成了答辩时最有话可说的项目之一,原因很简单——台球厅这个场景比图书馆、超市、仓库管理更"活",它天然带有人员预约、设备状态流转、按时计费、会员体系这些真实业务逻辑,而不是单纯往数据库里塞几条记录。
先说说我在实际带课设时观察到的痛点:校园里的台球厅通常面积不大,但客流量高峰集中(晚上和周末),设备(球桌、球杆、球)损耗快,而且经常出现"桌子被人占着但没人核验身份""球杆丢了不知道谁拿的""计时靠前台手写表格,月底算账对不上"这些管理混乱。如果你只是做一个"增删改查台球桌信息"的CRUD,那确实没意思,但如果你把预约、计时计费、设备借用与归还、人员权限、统计报表串起来,这就是一个有完整业务闭环的系统,毕设答辩时完全说得清。
这套Spring Boot系统,我建议的定位是:面向校园内台球厅的轻量化管理后台,覆盖三类用户(管理员、前台员工、顾客)的核心操作。顾客端不需要做App,用浏览器访问即可;管理员端管人管设备管账单;前台端处理预约核销、开台计费、设备借还。毕业设计嘛,别一上来就想着微服务、分布式、Redis缓存全上——用最扎实的单体架构把业务跑通,才是拿高分的关键。
2. 选型逻辑:Spring Boot 3 + MyBatis Plus + Thymeleaf,为什么这套组合最稳
2.1 技术栈选择背后的思考
现在网上随便一搜就是Spring Boot + Vue前后端分离的模板,但我不太建议课设阶段直接上前后端分离,原因有三个:
第一,答辩时要讲清楚的东西太多。你会发现时间全花在Vue和Axios的交互上了,而评委真正关心的业务逻辑反而没时间展示;第二,纯后端项目不好演示,你总不能现场开两个终端跑前后端项目吧;第三,Thymeleaf服务端渲染虽然老,但它把页面、数据绑定、权限控制串在同一个请求链路里,理解起来非常直观,特别适合用来讲清楚"MVC三层架构到底是怎么回事"。
技术栈定下来:
- JDK 17 / Spring Boot 3.x:新项目直接用Boot 3,跨过2.x的老版本,避免答辩时被问"为什么不用新版本"
- MyBatis Plus:不是MyBatis,而是MP——它提供的
BaseMapper和LambdaQueryWrapper能省掉大量XML编写,适合课设这种需要快速产出且易读的代码风格 - MySQL 8.x:数据量不大,用默认的InnoDB引擎即可
- Thymeleaf:服务端模板,配合AdminLTE或H-ui这类开源管理模板,页面颜值比裸写HTML高很多
- Maven:统一管理依赖,别用Gradle,答辩老师不一定熟悉
这套组合最大的好处是:每个框架你都能说清楚它负责哪一层。Boot管请求分发和自动配置,MP管数据库访问,Thymeleaf管页面渲染,MySQL管持久化。没有冗余中间件,也没有"引入但根本没用"的依赖,这在答辩时是加分项。
2.2 项目目录结构设计
billiards-manager/ ├── src/main/java/com/campus/billiards/ │ ├── controller/ # 控制器层,接收请求 │ ├── service/ # 业务逻辑层,核心计费、预约逻辑 │ ├── mapper/ # MyBatis Plus接口 │ ├── entity/ # 数据库实体类 │ ├── dto/ # 数据传输对象,比如预约表单 │ ├── vo/ # 视图对象,比如统计报表 │ ├── config/ # 拦截器、全局异常配置 │ ├── common/ # 统一返回结果、枚举定义 │ └── BilliardsApplication.java # 启动类 ├── src/main/resources/ │ ├── templates/ # Thymeleaf模板页面 │ ├── static/ # CSS、JS、图片资源 │ ├── application.yml # 数据源、端口配置 │ └── sql/billiards.sql # 初始化脚本 └── pom.xml需要特别提醒:controller和service分离规范化。我在看过不少课设代码,最大的问题就是业务逻辑全堆在Controller里,一个方法写200行。答辩时老师翻开代码问"计费逻辑在哪",结果你从Controller找起,印象分直接掉。把计时计费、预约冲突检测这些逻辑放到Service层,Controller只保留参数接收和响应封装,这既是工程规范,也是答辩时能拿出来说的亮点。
3. 核心业务模块拆解:人员、设备、预约、计费,四个模块怎么设计最合理
3.1 人员管理模块:三种角色,三种权限
人员是系统的基础,我设计了user表和role字段,用整数区分角色(1-管理员,2-前台,3-顾客)。为什么不用RBAC三张表(用户表、角色表、权限表)?因为课设场景里权限粒度不需要精细到按钮级别,一个字段搞定三种角色,简单直接,而且够用——过度设计反而是课设毕业论文里一个常见扣分点。
但是,拦截器必须做。我写了一个LoginInterceptor,实现HandlerInterceptor接口,在preHandle里校验Session中是否存在用户对象:
@Component public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(); User user = (User) session.getAttribute("loginUser"); if (user == null) { // 未登录,重定向到登录页 response.sendRedirect("/login"); return false; } return true; } }对应地,在WebConfig中注册拦截器并配置放行路径(登录页、静态资源),VIP页面或管理操作再通过UserUtil.getCurrentUser().getRole()做权限判断。记住:拦截器放行静态资源时,/css/**、/js/**、/images/**、/fonts/**这些路径必须加进去,否则登录页样式会全部丢失,我见过很多次这个问题。
顾客的会员等级可以设计为普通会员和VIP会员,VIP折扣率在数据库中存浮点数(比如0.85),结账时统一套用。这个设计虽然简单,但在统计模块里就能产生"会员消费占比"这种报告,论文里有数据可写。
3.2 设备管理模块:球桌、球杆、配件,状态机设计是核心
设备这块关键不是CRUD,而是状态流转。一张球桌有四种状态:空闲、占用、维护中、已禁用。球杆更细,还可能存在"借出未还"的状态。我建议用一个status字段统一管理:
public enum DeviceStatus { AVAILABLE(0, "空闲"), OCCUPIED(1, "使用中"), MAINTENANCE(2, "维护中"), DISABLED(3, "已禁用"); }设备表字段可以这样设计:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| device_name | varchar | 设备名称,如"标准球桌1号" |
| device_type | tinyint | 1-球桌, 2-球杆, 3-配件 |
| status | tinyint | 对应枚举状态 |
| location | varchar | 存放位置,如"A区3号桌" |
| purchase_date | date | 采购日期,用于统计折旧 |
| price | decimal | 采购价格 |
| last_maintain_time | datetime | 上次维护时间 |
| remark | varchar | 备注 |
设备借还设计成一张明细表device_borrow_record,记录员工借出、归还的时间,归还时把设备状态从"使用中"改回"空闲"。关键细节:归还时必须校验该条记录是否已归还,避免重复归还操作导致数据错乱,在Service层用一个if判断就能挡掉。
好的设备管理模块,答辩时可以向评委说清楚:后台能直观看到"哪些球杆借出去了超过24小时没归还",那就是一张SQL的事。这比单纯的添加删除有说服力得多。
3.3 预约模块:同一张球桌同一时间段不能重复预约,怎么实现
预约是这个项目最核心的业务逻辑,也是面试官大概率会追问的点。设计表reservation:
CREATE TABLE reservation ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL COMMENT '预约顾客ID', table_id BIGINT NOT NULL COMMENT '球桌ID', reserve_date DATE NOT NULL COMMENT '预约日期', start_time TIME NOT NULL COMMENT '开始时间', end_time TIME NOT NULL COMMENT '结束时间', status TINYINT DEFAULT 0 COMMENT '0-待核销,1-已核销,2-已取消', create_time DATETIME DEFAULT CURRENT_TIMESTAMP );预约冲突检测的SQL很多人写错。正确的做法是查询"在预约时间段内存在时间重叠的记录":
long count = reservationMapper.selectCount( new LambdaQueryWrapper<Reservation>() .eq(Reservation::getTableId, tableId) .eq(Reservation::getReserveDate, reserveDate) .ne(Reservation::getId, currentId) // 排除自己(编辑场景) .in(Reservation::getStatus, Arrays.asList(0, 1)) .lt(Reservation::getStartTime, endTime) // 已有记录开始 < 新记录结束 .gt(Reservation::getEndTime, startTime) // 已有记录结束 > 新记录开始 );两个时间区间[start1, end1]和[start2, end2]存在交集的条件是start1 < end2 AND start2 < end1,这个逻辑不光在预约里,在会议室预订、酒店订房里都一样,答得出来就是加分项。
前端页面上再配合一个思路:球桌时间轴用表格展示,每小时一格,已预约的格子标灰。虽然很简单,但用户直观感受到"系统真的能防冲突"。
3.4 计时计费模块:开台、暂停、换桌、结账,状态机走一圈
计费逻辑是目前为止所有模块里最容易出bug的地方,设计时要把场景想全。我建议的设备状态机是:
空闲 -> 开台 -> 使用中 -> 暂停/继续 <-> 使用中 ↓ 结账(计算费用) -> 空闲开台时记录start_time,结账时用end_time减start_time得到总分钟数。这里注意精度:分钟按整数计算,不满15分钟按15分钟计,这个规则要在代码里写清楚注释。
费用规则:普通桌20元/小时,VIP桌35元/小时,不足一小时按分钟折算(每分钟=单价/60)。同时支持套餐/充值余额支付和会员折扣。订单表设计为billing_order:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| user_id / customer_name | bigint / varchar | 下单顾客 |
| table_id | bigint | 球桌ID |
| start_time / end_time | datetime | 起止时间 |
| total_minutes | int | 总分钟数 |
| fee_rule_id | bigint | 关联计费规则 |
| original_amount | decimal | 原始金额 |
| discount_amount | decimal | 折扣金额 |
| actual_amount | decimal | 实收金额 |
| pay_method | tinyint | 1-现金,2-余额,3-其他 |
| create_time / operator_id | datetime / bigint | 操作时间与操作人 |
计费小技巧:在Service层写一个专门的计算方法,比如calculateFee(Table table, LocalDateTime start, LocalDateTime end),里面统一处理时长向上取整、折扣、四舍五入到分等问题,这样Controller层不需要关心计费细节,未来要加VIP多级折扣也只需要改这个方法。
这里有个很实用的体验细节:结账时前端展示费用明细——原始金额、会员折扣、实收金额,以及计费规则上的一个"说明",比如"使用时长65分钟,按70分钟计算(不满15分钟按15分钟计)"。评委一看"这个系统计算逻辑真的有考虑业务细节",比你说一百句都管用。
4. 数据库设计的几个关键细节:外键用不用?时间字段怎么存?统计怎么优化
4.1 外键:课设选型中不要用
我从来不建议在课设里用数据库外键,因为MyBatis Plus是面向逻辑删除和Java对象关联的,外键约束在批量删除、更新操作时反而碍事。外键缺失带来的数据一致性由Service层保证——比如删除用户前,先查一下预约表中是否有未完成订单。答辩时有人问起来,你可以说:"为了提升系统在大数据量下的写入性能,且业务一致性通过应用层事务控制,所以没有使用外键。"这句话一出来,就比很多说"不知道"的强得多。
4.2 时间字段统一用datetime与LocalDateTime映射
MySQL的datetime类型和Java的LocalDateTime在MyBatis Plus里已经天然兼容,千万别自己转来转去。创建时间用create_time,更新时间用update_time,并在实体类上配置MyBatis Plus的自动填充:
@TableField(fill = FieldFill.INSERT) private LocalDateTime createTime; @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime;然后写一个MetaObjectHandler实现类,在insertFill和updateFill里统一赋值。养成用自动填充的习惯,代码里就少了很多new Date()的重复劳动。
4.3 统计报表:一条SQL解决,不要用Java内存计算
报表模块是论文里体现"数据分析"能力的好地方。比如"近7日营收":
SELECT DATE(start_time) AS day, SUM(actual_amount) AS total FROM billing_order WHERE start_time >= DATE_SUB(CURDATE(), INTERVAL 6 DAY) GROUP BY DATE(start_time) ORDER BY day;MyBatis Plus的selectMaps返回List<Map<String, Object>>,配合ECharts在页面上画折线图。在Service层算好汇总数据(预约总数、平均消费、复购率等),在Controller组装成VO返回给页面。
这里分享一个经验:统计报表的VO尽量与实体分离。不要直接返回BillingOrder列表让前端算,而是在Service层就处理好DailyRevenueVO(含日期、订单数、营收、客单价),前端直接渲染,让答辩时的数据链路显得清清爽爽。
5. 页面架构与交互设计:不用前后端分离,怎么把管理后台做得可看
5.1 用现成开源管理后台模板,别自己从头写CSS
我非常推荐用AdminLTE或H-ui这类开源模板,它们自带导航、卡片、表格、弹窗样式,自己再改改配色和Logo,视觉效果就相当不错了。网上有些现成的Spring Boot + AdminLTE整合仓库,但要注意license规范,直接在自己的项目里写清楚引用来源即可。
5.2 几处值得花时间的交互细节
首页统计卡片:放4张大数字卡片(今日订单数、今日营收、空闲球桌数、待归还设备数),刷新即见,演示时先打开首页,评委第一印象就上来了。
预约列表的颜色区分:用Bootstrap的badge或AdminLTE的label来区分,待核销黄色、已完成绿色、已取消灰色。你自己一眼能看出来哪条还没有核销,就是好的交互。
前端校验和后端校验都必须有:预约表单里日期不选、时间乱填、球桌没选等情况,前端用required属性和js做初步拦截;后端Service再Assert判断一次。不要信任任何前端输入,这是做管理系统的底线。
5.3 通用页面脚手架的模块化
三个典型页面:device-list.html(设备列表+状态更新)、reservation-manage.html(待核销预约+冲突处理)、statistics.html(ECharts报表)。
每个管理页面都包含:顶部导航栏、侧边栏菜单、内容区卡片、表格和分页按钮。我建议抽取layout.html作为Thymeleaf公共模板,其他页面th:replace引入头部和侧边栏,避免重复代码。
6. 从0到1跑通项目:环境准备、数据库初始化、踩坑记录
6.1 环境准备
本地开发必备三件套:JDK 17(或21)、MySQL 8.0、IntelliJ IDEA(社区版即可)。Spring Boot 3.2要求JDK最低是17,如果你还在用JDK 8,要么升级,要么老老实实退回Boot 2.7。这不算妥协,是版本兼容的基本知识,自己心里要有数。
application.yml里最重要的配置:
spring: datasource: url: jdbc:mysql://localhost:3306/billiards?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver thymeleaf: cache: false # 开发阶段关闭模板缓存,改了页面立即生效 mybatis-plus: configuration: map-underscore-to-camel-case: true global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0map-underscore-to-camel-case是MyBatis Plus驼峰映射的开关,开启了之后create_time才能自动映射到createTime,不开启这个配置,你写代码时会莫名查不到值,这是新人最常见的坑之一。
logic-delete-field: deleted是全局逻辑删除配置,注意实体类需要写一个@TableLogic注解字段deleted,然后用逻辑删除代替物理删除——用户误删还能恢复,统计库存时也不会漏。
6.2 数据库初始化脚本
项目自带billiards.sql,需要包含如下内容:建库建表语句、基础管理员账号(admin/123456)、部分演示数据(比如5张球桌,其中1张在维护,2条历史预约记录,5个测试用户)。演示数据很重要——答辩时页面一打开就有内容,比空表好看太多了。
6.3 实跑过程中的坑位汇总
坑一:Thymeleaf页面改了不生效。原因多半是spring.thymeleaf.cache没设为false。这个配置加上,IDEA里重启应用页面就变了。但注意生产环境部署时记得把它改成true。
坑二:日期格式化报错,页面显示Cannot parse "2024-05-20T10:30"。原因是在Thymeleaf模板里直接[[${reservation.startTime}]],Java 8的LocalDateTime默认不是2024-05-20 10:30格式,而是带T。要么在实体类字段上加@JsonFormat(pattern = "yyyy-MM-dd HH:mm"),要么在模板里用#temporals.format(reservation.startTime, 'yyyy-MM-dd HH:mm')。我推荐后者,因为不会污染实体类的序列化逻辑。
坑三:MyBatis Plus分页插件失效。很多人写了new Page<>(current, size)但没配分页插件,SQL会退化成LIMIT全表。记得在配置类里加:
@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }坑四:前端关闭了Session作用域。用Thymeleaf做登录态时,如果发现登录后刷新页面就失效,检查一下浏览器的SameSite Cookie策略。Spring Boot可以设置:
server: servlet: session: cookie: same-site: lax如果使用前后端分离部署,Cookie的SameSite=None和跨域CORS又是一个大坑,但我们已经不分离了,这个坑就自动规避了。
6.4 打包部署的注意事项
本地开发用IDEA直接跑,但毕设文档里往往要写"系统部署方案",所以我建议加上这段:
mvn clean package -DskipTests java -jar target/billiards-0.0.1-SNAPSHOT.jar打成Jar包,在服务器上跑,访问http://localhost:8080。如果你用的Spring Boot版本比较新,Jar包可能会出现"端口占用""MySQL版本驱动不匹配"等问题,逐一排查即可。
7. 课程设计/毕业设计阶段:论文该怎么写、答辩容易被问到哪些点
7.1 论文结构建议
毕设论文跟项目代码一样重要。我的习惯是引导学生这样排:
- 第一章 绪论:台球厅管理现状、选题意义、国内外研究现状,两页纸够
- 第二章 相关技术介绍:Spring Boot、MyBatis Plus、MySQL、Thymeleaf,每种技术写清楚为什么用、怎么用
- 第三章 需求分析:包含功能需求(用用例图/文字描述)和非功能需求(性能、安全性、可维护性)
- 第四章 总体设计:架构设计(三层结构)、数据库E-R图、表结构说明
- 第五章 详细设计:核心模块时序图、类图、关键代码片段
- 第六章 系统实现与测试:页面截图+测试用例表格(正常流程和异常流程都要覆盖)
- 第七章 总结与展望
7.2 答辩高频问题应对
以下问题几乎每个来我这里模拟答辩的人都被问过,提前准备好答案:
"你这个系统的设计模式是什么?"答案:三层架构。Controller负责接受HTTP请求并调用Service接口,Service实现类里写业务逻辑并调用Mapper接口,Mapper负责与数据库交互。数据校验用@Valid和统一异常处理@RestControllerAdvice。
"为什么用MyBatis Plus而不是MyBatis?"答案:MyBatis Plus在MyBatis的基础上封装了BaseMapper通用CRUD方法,单表操作不需要手写SQL,减少了重复的XML代码;需要联表查询或复杂SQL时依然可以用注解或XML实现,灵活性更强。
"计费时长不满一小时怎么算?"答案:以分钟为单位,计算Duration.between(start, end).toMinutes(),如果余数不为零,向上取整到15分钟。并按小时单价折算每5分钟的价格,最终金额保留两位小数。
"如何保证一台球桌在同一时间不被重复预约?"答案:每次插入预约前执行时间重叠检测的SQL查询,查不到重叠记录才能插入;并且在数据库表上给table_id + reserve_date + start_time + end_time添加UNIQUE约束作为兜底。
"如果并发下两个人同时抢订同一张桌怎么办?"答案:在Service层的检测方法上加@Transactional,并在关键SQL后加上FOR UPDATE行锁,或者先查后插的check步骤放到一个事务里,确保至少有一个提交失败后抛异常回滚。
"报表的数据来源和实时性如何?"答案:直接用订单表生成的汇总数据,牺牲一点统计精度换取实时响应。如果数据量大了,就需要定时任务在凌晨跑批把前一天的数据汇总到统计表中,生成日报。话说到这里,已经能体现出你对高阶架构的理解了。
7.3 如何把"普通系统"变成"有亮点系统"
评委评分一般在80分上下,靠什么拉开差距?三个加分项:
- 日志系统:用Spring Boot自带的
spring-boot-starter-actuator暴露一些监控端点,或者在代码里用Slf4j写清楚关键操作日志。光这个举动,答辩时你就能说"系统具备可审计性"。 - 统一异常处理:定义一个
GlobalExceptionHandler,用@RestControllerAdvice捕获业务异常、参数校验异常和兜底异常,返回统一格式的JSON错误信息。页面错误时至少是个友好的中文提示,而不是Java堆栈。 - 定时任务:例如每天晚上清理超过取消状态的过期预约、给设备"连续7天未使用"生成盘点提醒清单。用
@Scheduled(cron = "0 0 2 * * ?")做定时清理,这个实际编码也不过十几行,但论文里就能多写一节"系统健壮性设计"。
8. 写在最后的项目收尾建议
这个系统做到当前程度,已经能覆盖校园台球厅最常见的真实管理诉求了:管理员打开后台能看到设备状态一目了然,前台登记开台和结账不用再翻纸质表格,顾客用校园网就能预约黄金时段的球桌。整套代码跑起来大概在5000到8000行之间,数据库8到10张表,完全符合课设和毕设的工作量预期。
我在实际带课设的过程中发现,Code写得好不如演示讲得好。建议你在答辩前,按照这个顺序走一遍系统:
- 用管理员账号登录,首页展示统计卡片—"今天我管理着8张桌子、收了2000多块";
- 切换到设备管理,把1号球桌状态从"空闲"改成"维护中",再说一句"设备故障时,前台能实时停用这张桌子避免顾客白跑";
- 演示预约功能时,故意挑一个已经被占用的时间段提交,把冲突提示弹给评委看;
- 然后到后台把预约核销掉,开台计费,最后在报表页看今天的营收和预约量数据确实是增加的;
- 整个过程不超过五分钟,但把"人员、设备、预约、计费、统计"五大模块全打通了。
最后分享一个小技巧:数据库脚本里预留两个测试账号,一个管理员,一个普通顾客,密码都写在文档里。因为评委很可能现场要求"你登录给我看看这个功能",提前准备演示账号能省很多尴尬时间。预祝项目顺利结束,答辩一次通过。