十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SSM体育器材管理系统开发实战:从业务需求到答辩演示全解析

SSM体育器材管理系统开发实战:从业务需求到答辩演示全解析 接手“基于SSM的体育器材管理系统”这类课题时别被“管理系统”四个字误导以为就是简单地在网页上做增删改查。这个题目看起来不起眼但它是很典型的JavaWeb业务系统麻雀虽小五脏俱全非常适合用来验证你对SSM框架的掌握程度。我自己在读书期间做过学院体育部器材借用登记的琐碎工作后来又把把套流程整理成毕业设计所以对这个题目的痛点和开发中的坑位都比较熟悉。这篇文章就按我当时的完整开发思路来讲从需求梳理、技术选型、数据库设计到SSM前后端代码实现再到答辩演示的准备工作一次性说清楚。1. 器材管理系统的需求其实藏在线下登记的细节里1.1 用Excel做器材登记到底哪里不行很多同学做毕设时容易犯一个毛病拿到题目不看业务直接建表写代码。器材管理系统如果只做成“器材信息录入后能查询”那价值就很低答辩时也经不起老师追问。先回到真实场景。我当时在体育部帮忙管理器材情况是篮球、足球、羽毛球拍、排球、瑜伽垫这些器材放在储物间登记用一本纸质台账后来好一点用Excel。借用流程是这样的——学生在微信上或者直接跑过来说“借一个篮球”我在纸上写“张三借篮球若干”归还时画勾。听起来很顺利但实际使用中问题非常多借出去的球没人追着要经常一周后才送回来中间别人想借却没库存。器材损坏、丢失无法追踪账面上的数量和实物永远对不上。期末盘点统计“哪个器材被借最多”“某学院学生最常借什么”纯靠人肉翻记录费时费力。有人想续借但记录乱很难确认原始借出时间。所以这个系统的核心价值不是“管器材”而是管器材的借用状态和流转过程。懂得这一点功能需求才算真正想清楚。1.2 功能清单先分角色再定流程我按角色来划分功能这样页面设计、接口设计都会很清晰。系统里就两类角色不要贪多普通用户学生/教师注册登录、查看器材列表和库存、发起借用申请、归还器材、查看自己的借用记录。管理员登录、器材信息管理增加、修改、删除、上下架、审核借用申请、处理归还、维护用户列表、查看所有借用记录和数据统计。核心流程其实就三条借用流程用户选择器材 → 提交借用申请 → 管理员审批或者用户直接借出根据规模→ 库存扣减。归还流程用户提交归还 → 管理员确认 → 库存回补 → 生成历史记录。查询统计流程分角色查看记录、统计借出次数和排行。业务不复杂但每个流程的状态变化一定要清晰器材是“可借/已借出/维修中”借用记录是“申请中/已借出/已归还/已逾期”。这些状态字段在后面的模块开发和答辩演示中都会被反复问到。1.3 从需求到表结构之间还差一个原型图需求想清楚之后我建议你先用几分钟画个功能模块图不需要用Axure那么重直接在白纸上画矩形框分层就行。当时我画了这样一张模块划分后面开发基本就是照这个图走的用户管理模块注册、登录、个人信息、修改密码。器材管理模块器材分类、器材信息、库存数量、器材状态。借用管理模块提交借用申请、审核、归还、续借、逾期标记。统计报表模块器材借出次数、热门器材排行榜、用户借用排行。系统管理模块管理员维护基础数据。画出这个图之后你会发现数据库表结构几乎是顺理成章出来的。需求分析阶段多花二十分钟数据库设计阶段就能少改三遍表。2. 为什么选SSM这个组合以及项目初始化该怎么做2.1 在三层架构里SSM各自负责什么有人问我为什么都202x年了还选SSM其实这个问题的答案在毕业设计场景里很简单课题给定的是SSM同时SSM也是最多公司面试题里绕不开的经典组合它体现的“分层的职责边界”比Spring Boot这种全家桶更明显。SSM就是由三个框架组合而成Spring核心IoC容器负责对象的创建和依赖关系的管理。Service、Controller、Mapper等Bean都由Spring统一管理要用别的类里的方法注入即可不需要new对象。SpringMVC负责Web层的请求分发。从前端页面发来的请求到Controller由HandlerMapping决定交给谁的哪个方法处理然后返回视图给前端。它本质上就是MVC模式在Web中的实现。MyBatis负责数据持久层。把数据库查询结果自动映射为Java对象同时把Java对象参数映射到SQL语句中实现ORM。我习惯用一个生活例子解释它的架构Spring像学校后勤处统一管理所有部门和人员SpringMVC像学校门口的传达室所有来访的人先到这里由接待人员分发给相应办公室MyBatis像档案室的档案员你要什么档案告诉他条件他去数据库系统里给你查出来打包好。2.2 项目目录结构和依赖配置我用Maven管理项目依赖目录结构是很标准的SSM项目划分src/main/java ├── com.example.sports.controller # Controller层 ├── com.example.sports.service # Service接口 ├── com.example.sports.service.impl # Service实现类 ├── com.example.sports.mapper # MyBatis Mapper接口 ├── com.example.sports.pojo # 实体类 ├── com.example.sports.common # 公共配置类、统一结果封装 └── com.example.sports.util # 工具类JWT、日期处理等使用Java 8依赖版本需要匹配好不然特别容易踩坑常见的坑后续会提到。properties project.build.sourceEncodingUTF-8/project.build.sourceEncoding maven.compiler.source1.8/maven.compiler.source maven.compiler.target1.8/maven.compiler.target spring.version5.2.9.RELEASE/spring.version mybatis.version3.5.6/mybatis.version mybatis-spring.version2.0.6/mybatis-spring.version /properties dependencies !-- Spring核心 -- dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version${spring.version}/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version${spring.version}/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-jdbc/artifactId version${spring.version}/version /dependency !-- MyBatis核心 -- dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version${mybatis.version}/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version${mybatis-spring.version}/version /dependency !-- 数据库驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version5.1.49/version /dependency !-- 数据库连接池 -- dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.1.22/version /dependency !-- JSON处理和分页插件 -- dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId version2.11.3/version /dependency dependency groupIdcom.github.pagehelper/groupId artifactIdpagehelper-spring-boot-3-mybatis${mybatis.version}/artifactId version1.4.1/version /dependency /dependencies2.3 Spring和MyBatis整合的三个配置文件SSM项目最容易被大家搞混的就是配置文件我的做法是分成三个xml文件各自职责单一spring-mvc.xml扫描Controller层配置视图解析器和注解驱动。spring-mybatis.xml扫描Service和Mapper接口配置数据源、SqlSessionFactory和事务管理器。web.xml配置SpringMVC的DispatcherServlet和Spring的ContextLoaderListener。一个容易忽略的细节Mapper接口的包扫描不要连Controller层一起扫否则Spring容器初始化时会因为找不到Mapper实现类而报错。我当时就在这上面花了一晚上才排查出来。下面给一个最精简的spring-mybatis.xml配置context:component-scan base-packagecom.example.sports.service / bean iddataSource classcom.alibaba.druid.pool.DruidDataSource property namedriverClassName valuecom.mysql.jdbc.Driver/ property nameurl valuejdbc:mysql://localhost:3306/sports_equipment?useSSLfalseamp;characterEncodingutf-8/ property nameusername valueroot/ property namepassword value123456/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property namemapperLocations valueclasspath:mapper/*.xml/ !-- 开启驼峰自动映射例如 user_name 映射到 userName -- property nameconfiguration bean classorg.apache.ibatis.session.Configuration property namemapUnderscoreToCamelCase valuetrue/ /bean /property /bean bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.example.sports.mapper/ /bean bean idtransactionManager classorg.springframework.jdbc.datasource.DataSourceTransactionManager property namedataSource refdataSource/ /bean3. 数据库设计器材借还是典型的“状态流转”业务3.1 五张核心表的设计思路做完需求分析后数据库设计就是整个项目的地基。我当时设计了五张核心表第一版还加了器材分类表但发现分类数据极少就先用一个字符串字段存分类名了避免表关联过多。表名核心字段说明userid, username, password, real_name, role, dept, phone, status用户表role区分管理员和普通用户dept存学院/部门名equipmentid, name, category, total_quantity, available_quantity, status, location, description器材表总量、可借量、状态是核心字段borrow_recordid, user_id, equipment_id, borrow_time, expected_return_time, actual_return_time, status, remark借用记录表记录每次借还流程adminid, real_name, username, password, role管理员表也可并入user表这里单独拆出来方便权限逻辑repair_recordid, equipment_id, description, status, create_time, finish_time器材维修记录表后续扩展很实用注意我在建表时没有弄太多外键约束。原因是MyBatis本身就强调SQL可控数据库层的外键约束会让插入删除顺序变得很死板。借用记录的 user_id 和 equipment_id 只需要建普通索引即可逻辑关联放在业务代码里维护。这样查询性能更好删除器材时也不用考虑外键限制。3.2 用一段SQL把表结构说清楚下面给出核心的三张表建表SQL你可以直接照着改CREATE TABLE user ( id int(11) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 登录名, password varchar(100) NOT NULL COMMENT 密码建议MD5或BCrypt加密, real_name varchar(50) DEFAULT NULL COMMENT 姓名, role tinyint(1) NOT NULL DEFAULT 0 COMMENT 0普通用户 1管理员, dept varchar(50) DEFAULT COMMENT 所属学院或部门, phone varchar(20) DEFAULT NULL, status tinyint(1) NOT NULL DEFAULT 1 COMMENT 1启用 0禁用, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB AUTO_INCREMENT1001 DEFAULT CHARSETutf8mb4; CREATE TABLE equipment ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(100) NOT NULL COMMENT 器材名称, category varchar(50) DEFAULT 其他 COMMENT 分类, total_quantity int(11) NOT NULL DEFAULT 0 COMMENT 总数量, available_quantity int(11) NOT NULL DEFAULT 0 COMMENT 当前可借数量, status tinyint(1) NOT NULL DEFAULT 1 COMMENT 1可借 0不可借, location varchar(100) DEFAULT NULL COMMENT 存放位置, description varchar(255) DEFAULT NULL, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_category (category) ) ENGINEInnoDB AUTO_INCREMENT2001 DEFAULT CHARSETutf8mb4; CREATE TABLE borrow_record ( id int(11) NOT NULL AUTO_INCREMENT, user_id int(11) NOT NULL, equipment_id int(11) NOT NULL, borrow_time datetime NOT NULL COMMENT 借出时间, expected_return_time datetime DEFAULT NULL COMMENT 应还时间, actual_return_time datetime DEFAULT NULL COMMENT 实际归还时间, status tinyint(1) NOT NULL DEFAULT 0 COMMENT 0申请中 1已借出 2已归还 3逾期 4拒绝, remark varchar(255) DEFAULT NULL COMMENT 备注比如损坏情况, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user_id (user_id), KEY idx_equipment_id (equipment_id), KEY idx_status (status) ) ENGINEInnoDB AUTO_INCREMENT3001 DEFAULT CHARSETutf8mb4;表建好之后有一个细节非常关键available_quantity可借数量是冗余字段。它可以通过“总数量 - 已借出总数”算出来但为了减少联表查询我直接把它存到了器材表中。这个冗余设计在中小型系统里是实用的但也要通过事务来保证一致性。3.3 器材“可借性”的判断规则器材能不能借要考虑两个条件一是设备状态是“可借”二是可借数量大于0。条件不足时前端按钮置灰后端也要做校验不能只靠前端拦截。这是答辩时容易考察的点。4. 核心模块从登录到统计报表的实现思路4.1 登录与会话权限控制登录模块是SSM项目里最基础但也最能体现细节的部分。我对密码进行了加盐MD5加密存储数据库中不要存明文密码。登录成功后将用户信息放入Session同时通过SpringMVC的拦截器统一判断登录状态。接口层面我用角色字段区分管理员和普通用户拦截器按角色判断是否能访问某些路径。public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(); User loginUser (User) session.getAttribute(loginUser); if (loginUser null) { // 未登录跳转登录页 response.sendRedirect(request.getContextPath() /login); return false; } // 可以进一步判断角色 return true; } }在spring-mvc.xml中注册拦截器mvc:interceptors mvc:interceptor mvc:mapping path/**/ mvc:exclude-mapping path/login/ mvc:exclude-mapping path/register/ bean classcom.example.sports.interceptor.LoginInterceptor/ /mvc:interceptor /mvc:interceptors4.2 器材管理模块增删改查和条件分页器材管理本质上是一个典型的CRUD模块。要注意的一点是删除器材不要用物理删除而是用一个status字段做逻辑删除。因为器材一旦被删除历史借用记录里的外键就会变成悬挂引用统计时也无法追溯。Controller层的写法以分页查询为例Controller RequestMapping(/equipment) public class EquipmentController { Autowired private EquipmentService equipmentService; RequestMapping(/list) public String list(RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize, RequestParam(required false) String name, RequestParam(required false) String category, Model model) { PageHelper.startPage(pageNum, pageSize); ListEquipment list equipmentService.queryEquipmentList(name, category); PageInfoEquipment pageInfo new PageInfo(list); model.addAttribute(pageInfo, pageInfo); model.addAttribute(name, name); model.addAttribute(category, category); return equipment/list; } }Service层里要校验参数、做空值处理。MyBatis的Mapper中要写动态SQL让名称、分类这些条件都是可选参数select idqueryEquipmentList resultTypecom.example.sports.pojo.Equipment SELECT * FROM equipment where if testname ! null and name ! AND name LIKE CONCAT(%, #{name}, %) /if if testcategory ! null and category ! AND category #{category} /if /where ORDER BY create_time DESC /select4.3 借出、归还、续借的Service层逻辑这是整个系统的重头戏。先说借出。用户提交借用申请后管理员审核审核通过后才真正扣减库存。这里要注意的是库存扣减不能用“先查询后更新”的方式而应该用一条带条件判断的UPDATE语句保证并发安全。Override Transactional public boolean borrowEquipment(Integer userId, Integer equipmentId, Integer borrowDays) { if (borrowDays 0 || borrowDays 30) { throw new ServiceException(借用天数必须在1到30天之间); } // 原子扣减只有可借数量大于0时才更新成功 int rows equipmentMapper.decreaseAvailableQuantity(equipmentId); if (rows 0) { throw new ServiceException(库存不足或器材不可借); } // 插入借用记录 BorrowRecord record new BorrowRecord(); record.setUserId(userId); record.setEquipmentId(equipmentId); record.setBorrowTime(new Date()); record.setExpectedReturnTime(DateUtils.addDays(new Date(), borrowDays)); record.setStatus(1); borrowRecordMapper.insert(record); return true; }对应的Mapper SQL是这样update iddecreaseAvailableQuantity UPDATE equipment SET available_quantity available_quantity - 1 WHERE id #{equipmentId} AND available_quantity 0 AND status 1 /update归还的Service层逻辑正好相反但还多了一个判断判定是否逾期。逾期了就更新状态为“逾期”而不是“已归还”。Override Transactional public boolean returnEquipment(Integer recordId) { BorrowRecord record borrowRecordMapper.selectById(recordId); if (record null || record.getStatus() ! 1) { throw new ServiceException(借用记录不存在或已归还); } // 回补库存 equipmentMapper.increaseAvailableQuantity(record.getEquipmentId()); // 更新记录 record.setActualReturnTime(new Date()); if (record.getActualReturnTime().after(record.getExpectedReturnTime())) { record.setStatus(3); // 逾期归还 } else { record.setStatus(2); // 正常归还 } borrowRecordMapper.updateStatus(record); // 检查逾期是否还需要额外罚款处理可后续扩展 return true; }这一块特别值得在答辩时展开。因为借还逻辑直接涉及事务、并发、状态流转面的老师也最喜欢从这个模块展开追问。4.4 统计报表的聚合查询统计报表模块看起来很“高大上”但实际操作起来就是几个聚合SQL。热门器材排行可以这样写SELECT e.name, COUNT(br.id) AS borrow_count FROM borrow_record br LEFT JOIN equipment e ON br.equipment_id e.id WHERE br.status IN (1, 2, 3) GROUP BY e.id, e.name ORDER BY borrow_count DESC LIMIT 10;用户借用排行、分类统计等都是类似的思路。把这些报表做到Controller后返回给前端页面用一个ECharts柱状图展示效果会很好。5. 开发中真正让人头疼的四个问题5.1 并发借出导致器材超借这是开发中非常实际的问题。最初我写的借出逻辑是“先查库存再判断再UPDATE”但全校体育课集中选课那段时间多个用户同时借同一批器材就出现过把库存执行成负数的情况。问题根源很简单查询和更新之间不是原子操作。先后有两个请求同时查到available_quantity1都认为可以借出结果一个人借成功后另一个人又把库存减成了0甚至负数。解决办法就是在SQL层面做“乐观锁”式的更新UPDATE equipment SET available_quantity available_quantity - 1 WHERE id #{equipmentId} AND available_quantity 0如果受影响行数为0说明库存不足直接抛异常。这就是我在4.3节里写的那种方式。它比“查了再判断”多不到一行代码但彻底解决了超借问题。5.2 日期时间处理上的低级错误借用时间和归还时间都是核心数据但日期问题的坑非常多。我当时踩过的一个典型坑是在Controller中直接用了new Date()传给前端返回到页面上显示的是“Wed Jun 15 14:22:10 CST 2022”这种格式前端需要格式化才能友好显示。还有一个很隐蔽的问题MySQL时区。连接串里如果没有加serverTimezoneAsia/Shanghai而数据库服务器的时区是UTC拿出来的时间会和本地时间差8个小时。特别是借出时间、逾期判断都会受影响。我的方法是数据库连接串统一设置时区审核和借入借出时间在Service层统一用new Date()前端展示时通过JSP标签或Vue过滤器格式化。5.3 MyBatis里#{}和${}混用引发的SQL问题MyBatis中#{}会预编译生成占位符?安全的${}是直接拼接字符串有SQL注入风险。我见过不少人写排序字段或表名时想用${}实现动态排序结果被注入了非法条件。器材查询模块中如果允许用户自定义排序字段我的做法是白名单校验private static final SetString SORT_WHITELIST new HashSet(Arrays.asList(name, create_time, available_quantity)); public String checkSortField(String sortField) { if (!SORT_WHITELIST.contains(sortField)) { return create_time; } return sortField; }5.4 分页插件与SQL性能SSM项目里我建议直接用PageHelper它能省掉大量手写分页的逻辑。但要记住一个使用规则PageHelper.startPage() 后面必须紧跟第一条查询语句中间不能夹带其他查询。如果你在startPage之后先执行了一次安全检查或日志Mapper查询分页就会作用在错误的SQL上。还有一点是PageHelper生成的count语句在表数据量大的时候会占用一些时间但母校体校正门课也就几千条借用记录完全不用担心这个。6. 毕业设计和答辩时的演示安排6.1 演示流程设计不要让评委看你敲代码项目做完了答辩演示同样重要。先准备演示数据把器材库里的记录补到30条以上借用记录补到50条以上有正常归还、有逾期、有维修状态数据越丰富统计图表效果越好。千万不要用空数据库演示也不要在评委面前敲代码。我的建议是准备两条演示主线用户流程线注册/登录 → 浏览器材 → 发起借用 → 管理员审核 → 查看我的借用记录 → 提交归还。管理流程线登录管理员账号 → 器材管理增删改查→ 借用审核 → 归还处理 → 查看统计报表。演示时要突出借还闭环和库存变化借用之后可借数量减少了归还之后可借数量恢复了。这是最能体现你对业务理解的地方。6.2 答辩常见问题的准备方向根据我自己答辩和帮同学模拟答辩的经验把这些问题提前准备好为什么用SSM而不用Spring Boot回答要点SSM框架边界清晰适合理解原理Spring Boot是一种封装和简化并不是另一个框架数据库表和表之间关系是什么为什么不用外键用户提交借用后库存是怎么扣减的并发时怎么防止超借如果器材损坏了系统怎么处理回答要点维修记录表状态改为维修中角色权限是怎么控制的回答要点拦截器Session角色判断6.3 如果想在答辩中让项目更出彩可以扩展的功能点如果做完上面这些还有余力我建议做两个小而精的扩展一是给借用记录增加“逾期未还自动标记”的定时任务用Spring的Scheduled就能实现二是增加简单的ECharts可视化统计报表把器材分类、热门排行、每月借出趋势用图表展示出来视觉上会非常加分。这两个扩展不会增加太多代码量但会让项目从“一个作业”变成“一个完整的有业务深度的系统”而且答辩时非常容易引导老师往你擅长的方向提问。如果你正准备做这个题目我不建议去网上随便下载一套看不懂的源码交差。按上面这个思路自己把表建出来、把借还流程跑通再认真把并发扣库存和统计这两个点做好这个项目就已经超过周围大多数人。我自己当时做这套系统最大的体会是毕设的重点不是代码量而是对业务闭环的理解和对常见问题的把握把这些想明白答辩的时候你会非常从容。
返回列表