☰
Spring Boot校园台球厅人员与设备管理系统设计实战
2026/10/13 13:54:58 网站建设 项目流程

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, "已禁用"); }

设备表字段可以这样设计:

字段名类型说明
idbigint主键
device_namevarchar设备名称,如"标准球桌1号"
device_typetinyint1-球桌, 2-球杆, 3-配件
statustinyint对应枚举状态
locationvarchar存放位置,如"A区3号桌"
purchase_datedate采购日期,用于统计折旧
pricedecimal采购价格
last_maintain_timedatetime上次维护时间
remarkvarchar备注

设备借还设计成一张明细表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:

字段名类型说明
idbigint主键
user_id / customer_namebigint / varchar下单顾客
table_idbigint球桌ID
start_time / end_timedatetime起止时间
total_minutesint总分钟数
fee_rule_idbigint关联计费规则
original_amountdecimal原始金额
discount_amountdecimal折扣金额
actual_amountdecimal实收金额
pay_methodtinyint1-现金,2-余额,3-其他
create_time / operator_iddatetime / 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: 0

map-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分上下,靠什么拉开差距?三个加分项:

  1. 日志系统:用Spring Boot自带的spring-boot-starter-actuator暴露一些监控端点,或者在代码里用Slf4j写清楚关键操作日志。光这个举动,答辩时你就能说"系统具备可审计性"。
  2. 统一异常处理:定义一个GlobalExceptionHandler,用@RestControllerAdvice捕获业务异常、参数校验异常和兜底异常,返回统一格式的JSON错误信息。页面错误时至少是个友好的中文提示,而不是Java堆栈。
  3. 定时任务:例如每天晚上清理超过取消状态的过期预约、给设备"连续7天未使用"生成盘点提醒清单。用@Scheduled(cron = "0 0 2 * * ?")做定时清理,这个实际编码也不过十几行,但论文里就能多写一节"系统健壮性设计"。

8. 写在最后的项目收尾建议

这个系统做到当前程度,已经能覆盖校园台球厅最常见的真实管理诉求了:管理员打开后台能看到设备状态一目了然,前台登记开台和结账不用再翻纸质表格,顾客用校园网就能预约黄金时段的球桌。整套代码跑起来大概在5000到8000行之间,数据库8到10张表,完全符合课设和毕设的工作量预期。

我在实际带课设的过程中发现,Code写得好不如演示讲得好。建议你在答辩前,按照这个顺序走一遍系统:

  • 用管理员账号登录,首页展示统计卡片—"今天我管理着8张桌子、收了2000多块";
  • 切换到设备管理,把1号球桌状态从"空闲"改成"维护中",再说一句"设备故障时,前台能实时停用这张桌子避免顾客白跑";
  • 演示预约功能时,故意挑一个已经被占用的时间段提交,把冲突提示弹给评委看;
  • 然后到后台把预约核销掉,开台计费,最后在报表页看今天的营收和预约量数据确实是增加的;
  • 整个过程不超过五分钟,但把"人员、设备、预约、计费、统计"五大模块全打通了。

最后分享一个小技巧:数据库脚本里预留两个测试账号,一个管理员,一个普通顾客,密码都写在文档里。因为评委很可能现场要求"你登录给我看看这个功能",提前准备演示账号能省很多尴尬时间。预祝项目顺利结束,答辩一次通过。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询