☰
基于SSM的健身房管理系统毕设:从业务闭环到并发控制实战
2026/10/14 3:51:56 网站建设 项目流程

每年毕设选题一开,总有一批同学盯着“健身房管理系统”这个题目。听名字它不复杂,用Java、SSM框架做一套会员管理加预约流程的Web系统,确实够得上一次完整的项目实训。但这类项目最容易做砸的地方,不是写不出代码,而是把一套明明需要流程闭环的系统,硬生生做成了几个互不相干的增删改查页面。这篇文章我把需求、技术选型、数据库设计、核心模块实现、SSM项目搭建、常见坑位和答辩思路完整拆一遍,不写那种只能应付查重的“课设说明书”,而是按一个正常从业者的标准,把细节尽量补全。

这个题目适合什么人?适合已经学过Java Web、Spring、SpringMVC、MyBatis基础,又想靠一个中等体量项目拿毕业设计高分的同学。也适合刚工作不久、想用SSM练手的前端转岗者。你可以照这篇文章的思路,自己从零搭一套健身房管理系统,也可以拿它当作代码审查清单,发现自己的毕设项目里还有哪些业务边界没处理。

1. 这个题目到底在考什么

1.1 别把它当成“增删改查”

很多同学拿到这个题,第一反应是:会员表增删改查,课程表增删改查,订单表增删改查,完了。做完之后自己再跑一遍流程,发现一个致命问题——前台说会员办了一张年卡,会员卡怎么生效?到期之后怎么不能预约?停卡之后有效期怎么顺延?次数卡余量在哪里扣?如果你没把这些规则写清楚,系统就是一张Excel换了皮。

健身房管理系统真正考察的,是你对业务状态流转的理解。所谓“全流程管理”,就是会员从注册、办卡、预约、签到、消课,到最后退卡退款的完整链路。每一步都要有数据记录,每一步都要有状态约束。比如会员卡状态要经历“待生效、生效中、已冻结、已过期、已注销”;预约记录要经历“已预约、已取消、已完成、已爽约”;订单要经历“待支付、已支付、已退款”。这些状态不是随便取一列字符串,而是要在代码里控制合法迁移,不允许从“已取消”直接跳到“已完成”。

所以拿到题目的第一周别盯着建表,先把业务流程图和数据流图画明白。画图不是给答辩老师看的,是给你自己梳理用的。等你把“办卡-预约-扣次-退款”这条主链路走通了,剩下的功能都是锦上添花。

1.2 “全流程管理”到底包括哪些流程

我按真实健身房前台的操作习惯,把核心流程拆成几条主线:

第一,会员管理。新增会员、修改资料、会员卡续费、会员卡挂失、会员卡冻结/解冻、会员等级成长。第二,课程管理。课程分类、课程定价、教练排课、课程成团人数、开课提醒。第三,预约与入场。会员在线约课,前台扫码或报手机号确认签到,课程开始后爽约处理。第四,财务收银。办卡收款、私教课收款、商品售卖、退卡退款、日/月/年营业收入统计。第五,运营管理。器材维护记录、操房使用情况、教练工作量统计、会员出勤率分析。

这些流程在毕设里不一定全部实现,但你需要有主次。我的建议是一期只做三条主链路:办卡续费、预约消课、退卡统计。再围绕它们补充权限、报表、日志。如果能做通这三条链路,答辩的时候就没有几个同学能问倒你。

2. 技术选型与项目环境

2.1 为什么选SSM而不是Spring Boot

现在再去新起项目,大多数公司直接用Spring Boot,脚手架一拉,依赖一配,Embedded Tomcat一打包,很快就能跑起来。但你做的是毕业设计,核心目标不是“最快上线”,而是“把学过的原理讲清楚”。SSM正好覆盖了Java Web开发的三层核心:Spring负责依赖注入和事务,SpringMVC负责请求路由和参数绑定,MyBatis负责数据库映射。

用SSM还有一个实际好处:答辩老师大概率就是教这门课的老师。你如果用了Spring Boot,他反而会追问:那你Spring Boot的自动配置原理是什么?MyBatis的SqlSessionFactory是你哪一步创建的?很多同学到这里就卡壳了。反过来,如果你用的是SSM,所有bean、数据源、事务通知都在XML或配置类里明明白白写着,怎么注入、怎么代理、怎么扫描,每一个环节都能讲出依据,这比“我打了注解就跑起来了”要扎实很多。

另外,SSM项目通常要打成war包放到Tomcat下运行。这逼着你理解Servlet容器、ContextLoaderListener、DispatcherServlet、web.xml加载顺序。这些知识点在后来的面试里也会反复出现,早踩比晚踩好。

2.2 依赖版本怎么搭配才不闹心

版本是SSM项目最大的坑。很多同学直接把网上不知哪年的pom.xml复制过来,结果Spring 4的jar包和Spring 5的javax.servlet-api不兼容,或者MyBatis和mybatis-spring版本对不上,启动就报BeanCreationException。

我实测下来比较稳妥的一套版本组合是这样的:

组件推荐版本说明
JDK1.8不要用JDK 11以上,部分老Tomcat和Spring版本会有兼容问题
Maven3.6.x配阿里云镜像或中央仓库直达
Tomcat8.5.x配合JDK8最稳,Servlet版本兼容Spring5
Spring5.2.x用5.2的最后一个release版本即可
SpringMVC与Spring同版本永远保持框架版本一致
MyBatis3.5.x支持乐观锁、多参数、注解SQL
mybatis-spring2.0.x注意2.0版本对应Spring5
MySQL5.7或8.08.0需要驱动8.0.x和URL加serverTimezone
Druid1.2.x连接池和监控,比DBCP做出来更有话说
Jackson2.9.x负责Controller返回JSON的序列化
PageHelper5.3.xMyBatis分页插件
Lombok需要才用答辩若问原理,说清楚编译期注解处理即可
Shiro1.7.x可选,做权限控制

别把所有依赖都拉最新版。SSM项目里“最新版”常常意味着“新的API名字变了、配置写法变了”。你用的是教材上那套XML语法,版本太新可能直接不识别。建议锁死版本,不要用Maven的LATEST惯用语。

2.3 开始前要准备的材料清单

动手之前,把环境一次性配好:IDEA或Eclipse、Maven、Tomcat8.5、MySQL5.7、Navicat或DataGrip、Git。如果你之前只写过基础练习,建议先创建一个最简单的SSM空项目,保证能输出“Hello World”再往里面加业务。不要一上来就建十个Controller,出问题都不知道去哪找。

数据库方面,字符集统一用utf8mb4,排序规则utf8mb4_general_ci。MySQL连接串一定要写清楚:

jdbc:mysql://localhost:3306/gym_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai

少了characterEncoding,中文存进去就是乱码;少了serverTimezone,MySQL8会给你甩一个时区错误。这两行都是老生常谈,但每年都有人卡在这上面。

3. 数据库设计:怎么设计才能少返工

3.1 核心表与关联关系

数据库设计是健身房项目里打分最重的地方。表建得合理,后面代码写起来顺畅;表建得随意,业务规则加一处改一处,最后加班到崩溃。

我建议至少设计这些核心表:

表名作用关键字段
sys_user系统登录用户id, username, password, real_name, role, status
member会员资料id, member_no, name, phone, gender, height, weight, created_at
card_type卡类型id, name, category, price, valid_days, total_times, remark
member_card会员拥有的卡id, member_id, card_type_id, start_date, end_date, remain_times, status
course课程id, name, type, coach_id, price, consume_type, description
course_schedule排课计划id, course_id, coach_id, room_id, start_time, end_time, max_students, booked_count
appointment会员预约id, member_id, schedule_id, status, appointment_time
order订单id, order_no, member_id, total_amount, pay_type, status, create_time
payment支付流水id, order_id, pay_no, amount, pay_time, pay_channel
refund退款记录id, order_id, member_card_id, amount, reason, operator_id, refund_time
equipment器材设备id, name, status, purchase_date, last_maintenance_date
equipment_record器材维修/使用记录id, equipment_id, type, content, operator_id, create_time
notice公告id, title, content, status, publish_time

这里有一个容易被忽略的关联:会员和会员卡是“一对多”还是“一对一”?我建议分开。真实场景里会员可能同时有年卡和次卡,也有可能带一张同事赠送的体验卡。所以member_card要独立成表,card_type维护卡类型。会员查卡的时候,查“状态为生效中或待生效的最新一张卡”,而不是直接在member表上开个card字段。

3.2 卡类型要能同时兼容时长卡、次数卡和储值卡

健身房最常见三种卡:时长卡(年卡、季卡、月卡)、次数卡(20次、50次卡)、储值卡(余额消费)。在设计上不要把三种卡拆成三张表,否则办卡、续卡、退卡逻辑会出现大量重复代码。正确做法是在card_type表里加一个category字段区分三类,再用可空字段分别承载不同数据:

  • 时长卡:total_times为空,valid_days填365。
  • 次数卡:total_times填20,valid_days可填180。
  • 储值卡:可以用balance字段,但这张表建议再设计字段balance、maximum_discount等,保持灵活性。

member_card表同理,除了关联卡类型,还要冗余存当前的remain_times和end_date。这个“冗余”不是多余,而是为了避免每次查询都去计算剩余次数。例如次数卡每消费一次就要update一次remain_times,如果你不去冗余存储,还得先查member_card再join card_type,计算量虽小,但代码乱。

会员卡状态我建议用固定字典:0待生效、1生效中、2已冻结、3已过期、4已注销。冻结期间要用frozen_start和frozen_end记录冻结区间,退卡和冻结都依赖这个时间区间去算剩余价值。

3.3 预约与扣次的并发控制怎么做

“并发”这个词听着高大上,但在健身房业务里很具体:两个会员同时抢最后一个团课名额,或者同一个会员在手机上连续点了两次预约,怎么办?

解决方案分两层。第一层,数据库唯一约束。在appointment表设计时,把member_id和schedule_id建联合唯一索引。就算同一个用户请求被重复发送,数据库也只会保留一天中的一条预约记录。建表SQL可以写成:

ALTER TABLE appointment ADD UNIQUE KEY uk_member_schedule (member_id, course_schedule_id, appointment_date);

第二层,扣减剩余次数时不要先查再改,而是直接用乐观锁更新:

UPDATE member_card SET remain_times = remain_times - 1 WHERE id = #{cardId} AND remain_times > 0;

这个SQL返回的受影响行数是1,扣减成功;是0,说明次数不够,业务层直接抛异常。不要用“查询剩余次数,程序判断,再update”的写法,因为中间隔了两次数据库往返,两个请求都可能读到同一份旧数据,导致超扣。我在实际项目里见过不少这种BUG,每次都要靠浪费时间来验证并发问题。

4. 核心功能实现:把“能跑”做到“像样”

4.1 登录与权限控制,用拦截器还是Shiro

健身房系统前台工作人员、教练、前台主管至少三类角色。如果你不想在权限上花太多时间,可以在sys_user表里放一个role字段,然后用一个HandlerInterceptor拦截所有需要登录的请求:

  • 未登录用户,拦截器返回JSON提示“请先登录”。
  • 管理员访问erp/admin路径时,再判断role是否为管理员。
  • 教练访问coach路径时,判断role是否为教练。

这种做法代码量小,也很容易讲清楚。如果你绑定Shiro做登录认证和授权,当然更有技术含量,但配置复杂,毕设周期紧的时候不建议冒险。如果有余力,可以在项目里保留一个自定义注解@RequiresRole("ADMIN"),用AOP实现接口级鉴权,这比Shiro轻量,又能体现你对Spring AOP的理解。

我的建议:开发阶段先做拦截器,项目收尾前三周,如果基础功能稳定,再考虑升级到Shiro。别在第一周就钻进权限框架的配置,业务链没跑通之前一切框架都是负担。

4.2 会员办卡与续卡的规则怎么落代码

办卡流程在Controller层很简单,就是接收会员信息、选择卡类型、生成订单、支付成功后给会员绑卡。真正难的是续卡。

续卡不是简单地把结束日期往上加。假设某会员买了一张年卡,从2024年1月1日到2024年12月31日,他在2024年6月1日又把卡续了一年。如果你的代码直接允许续卡365天,结束日期会变成2026年6月1日,这明显不对,因为旧卡还有214天没消费。严谨的续卡逻辑是:当前卡还未到期,则新到期时间等于原到期时间加365天;当前卡已到期,则新到期时间等于当前时间加365天。次数卡同理,剩余次数要做累加而不是覆盖。

我在代码里会把这类逻辑放到Service层独立方法,而不写在Controller:

@Transactional(rollbackFor = Exception.class) public void renewMemberCard(Long cardId, Long cardTypeId) { MemberCard card = memberCardMapper.selectById(cardId); CardType type = cardTypeMapper.selectById(cardTypeId); Date now = new Date(); if (card.getStatus() == MemberCardStatus.FROZEN) { throw new BusinessException("该卡处于冻结状态,不能续费"); } // 续费后开始日期取当前与原始结束日期的较大值 Date newEndDate = card.getEndDate().after(now) ? card.getEndDate() : now; // 增加N天 ... card.setEndDate(newEndDate); memberCardMapper.updateById(card); // 生成订单和流水 orderService.createRenewOrder(...); }

这里@Transactional必须加,因为一旦继续执行,会员卡更新了,订单也要同步生成,任何一个失败整个操作回滚。否则前台点了一次续费,结果卡没更新但订单扣了款,系统就是数据不一致的残次品。

4.3 预约课程、取消预约与爽约处理

预约课程是整个系统里最容易写出“看起来能跑,实际漏洞百出”的模块。核心流程分四步:

  • 查询哪些排课还能约:课程表join排课表,过滤未开始、booked_count小于max_students的记录。
  • 用户点击预约时,先校验会员卡是否有效,再校验会员是否已经预约过同一天的同一课程。
  • 创建预约记录,同时把course_schedule的booked_count加一。
  • 用户取消时,又要把booked_count减一。

这里要注意,不是提交预约请求就结束。你还需要一个“迟到未点课”的判定机制。最简单的做法是:当课程开始时间超过15分钟后,用一个定时任务把状态仍为“已预约”且课程日期小于当前的记录置为“已爽约”,并扣相应的信用分。如果不想用定时任务,也可以在查询“我的课程”时,实时判断当前时间是否大于开始时间且状态是“已预约”,自动置为已爽约。前者适合开发完成度高、有余力的同学,后者则能保证数据一致性且实现简单。

预约冲突的判断不能只靠appointment表唯一索引,还要考虑时间段重叠。比如同一个教练10:00-11:00带了私教课,10:30-11:00又安排了一节私教课,实际是冲突的。排课时就要检查教练时间段重叠:

SELECT COUNT(*) FROM course_schedule WHERE coach_id = #{coachId} AND start_time < #{newEndTime} AND end_time > #{newStartTime}

这就是区间重叠的经典写法。直接用start_time在“已有开始到已有结束”之间判断是会漏掉包含关系的。

4.4 退卡退款怎么算才合理

退卡退款是答辩时老师最喜欢问的部分。因为它没有标准答案,考察的是你对业务规则的理解。常见规则有两种:按天数比例退,按次数比例退。

年卡退卡:假设卡价值3650元,有效期365天,已经过去100天,剩余265天。如果合同约定“已使用不超过三分之一,可退剩余价值的90%”,那你需要把剩余天数和剩余价值相乘。次数卡退卡:例如30次卡,可退金额=购买金额×(剩余次数/总次数),再扣除未使用优惠。储值卡退卡:余额-消费赠送金额,或者直接按余额退款。

每一个退款单都必须关联原订单、会员卡、操作人、退款原因,并且状态是“待审核/已退款/已拒绝”。我还建议在退款单上生成一个唯一的退款单号,类似订单号,方便财务对账。你可以不用真的对接微信支付,但要留expand接口,让答辩老师觉得你考虑到了支付扩展性。

4.5 首页看板和财务报表

一个“像样”的系统一定不会只给你一张空表格。首页至少要展示:今日营业额、今日新增会员数、本周活跃会员、剩余会员卡数、预约人数。财务模块用ECharts画两条曲线——近30天营收趋势、各课程类型预约人数占比。统计SQL要会用MySQL的GROUP BY和DATE_FORMAT:

SELECT DATE_FORMAT(pay_time, '%Y-%m-%d') AS day, SUM(amount) AS total FROM payment WHERE pay_time >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) GROUP BY DATE_FORMAT(pay_time, '%Y-%m-%d')

需要注意的是,如果你在分页列表和折线图用了同一条数据源,最好拆两个方法。分页查询要加PageHelper,统计查询千万不要加分页插件,否则返回的total会被自动改写,影响图表展示。这个问题我见过不止一次。

5. 从0到1实操:SSM项目搭建手册

5.1 创建Maven工程并规划包结构

我建议你新建一个普通的Maven Web项目,不要用IDEA自带的Spring Initializr模板,否则生成出来的目录结构和SSM课设不符。groupId按个人习惯,artifactId建议叫gym-management-system。包结构这样规划:

com.gym ├── controller │ ├── MemberController.java │ ├── CardController.java │ ├── CourseController.java │ ├── AppointmentController.java │ ├── OrderController.java │ └── ReportController.java ├── service │ ├── MemberService.java │ ├── ... │ └── impl ├── dao │ ├── MemberMapper.java │ ├── CardTypeMapper.java │ └── ... ├── entity │ ├── Member.java │ ├── MemberCard.java │ ├── CourseSchedule.java │ └── ... ├── dto │ ├── LoginUser.java │ ├── RenewCardRequest.java │ └── ... ├── interceptor ├── common │ ├── Result.java │ ├── BusinessException.java │ └── GlobalExceptionHandler.java └── utils

包结构本身也是答辩评分点。有些同学把所有实体类和Mapper写在同一个包,答辩一翻代码就露怯。分层清晰,至少说明你有工程意识。

5.2 pom.xml核心依赖

基础依赖至少需要这些:

<dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.2.25.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.2.25.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>5.2.25.RELEASE</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.13</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.0.7</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.28</version> <scope>runtime</scope> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.20</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.12.7</version> </dependency> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>5.3.2</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> </dependencies>

同时记得设置maven-compiler-plugin为1.8,再把Tomcat插件或外置Tomcat跑起来。我建议直接用IDEA配置本地Tomcat,不要用Maven Tomcat插件,因为外置Tomcat更贴近后来部署到服务器的场景。

5.3 Spring、SpringMVC和MyBatis的配置要点

SSM的配置文件一般有四份:jdbc.properties、spring-dao.xml、spring-service.xml、spring-mvc.xml,再加上web.xml。它们在web.xml中的加载顺序是:ContextLoaderListener读取spring-dao.xml和spring-service.xml,DispatcherServlet读取spring-mvc.xml。

需要注意两个经典问题。第一个:spring-mvc.xml的组件扫描要只扫Controller,不能把Service、Mapper也扫进去,否则Spring容器里出现两份相同类,Service事务代理会失效。我常用写法:

<context:component-scan base-package="com.gym"> <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Service"/> </context:component-scan>

第二个:MapperScannerConfigurer必须写在spring-dao.xml,让所有Mapper接口被扫描为Spring bean。

<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.gym.dao"/> </bean>

SqlSessionFactoryBean里要设置mapperLocations指向classpath:mapper/*.xml,否则你的Mapper接口方法找不到SQL语句,运行期会报“Invalid bound statement (not found)”。

5.4 关键Mapper写法和乐观锁示例

Mapper接口里写了一个deductRemainTimes方法:

int deductRemainTimes(@Param("cardId") Long cardId);

对应XML:

<update id="deductRemainTimes"> UPDATE member_card SET remain_times = remain_times - 1 WHERE id = #{cardId} AND remain_times > 0 </update>

这个更新的返回值可以直接作为判断依据。如果你的系统使用时长卡,不涉及次数扣减,那也得保证状态判断:

<select id="selectValidCardByMemberId" resultType="MemberCard"> SELECT * FROM member_card WHERE member_id = #{memberId} AND status = 1 AND end_date &gt; NOW() </select>

注意XML里大于号要用&gt;,否则文档解析报错。这是新手最容易忽略的细节。

5.5 前端页面与Ajax交互

SSM的前端不要写太复杂。后端Controller返回JSON,前端用Bootstrap+jQuery发起Ajax请求即可。页面结构可以做成左侧菜单,右侧iframe嵌内容页。前后端交互规范统一为:

@RequestMapping("/member/list") @ResponseBody public Result list(int page, int limit, String keyword) { PageHelper.startPage(page, limit); List<Member> list = memberService.list(keyword); return Result.success(new PageInfo<>(list)); }

返回体统一用Result包装:

public class Result { private int code; private String msg; private Map<String, Object> data; }

前端拿着code判断成功失败,统一弹toast提示。这样维护起来特别舒服,答辩的时候也能说“我做了统一返回体和全局异常处理”,这比在每个Controller里返回各种List/Map干净得多。

6. 实测踩过的坑,每个都能让人卡一下午

6.1 项目启动404,Tomcat里依赖包没打进去

用IDEA部署SSM项目时,如果Tomcat启动后页面404,先去WEB-INF/lib检查有没有引入Maven依赖。很多新手的IDEA Artifact没有把Maven依赖打包进去,只打包了src目录,导致Spring框架类都找不到,常见的表现是启动不报错,一访问ClassNotFound。解决方式是在Project Structure里把Artifact的类型选为Web Application Exploded,并在Available Elements里右键将library加入WEB-INF/lib。验证方法很简单,编译后点开ARTIFACT输出目录,看有没有lib文件夹。

6.2 Bean重复导致事务不生效

我见过几十次这种场景:Service里明明加了@Transactional,但是数据库没有回滚。大部分原因是spring-mvc.xml和spring-service.xml都扫描了com.gym.service,导致同一个Service类被Spring容器创建两次。第二次创建时不经过事务代理,于是你在Controller里拿到的Bean是不带事务的裸类。

排查方法很简单,在Service实现类里加一个构造器或者初始化方法,打印一行日志。如果日志打印了两次,就说明组件扫描配错了。

6.3 日期字段返回变成了时间戳

SSM项目如果没配Jackson的ObjectMapper,LocalDateTime或Date字段很可能会被序列化成毫秒时间戳,前端拿到是一串数字,没法直接显示。解决方法是配置一个自定义的MappingJackson2HttpMessageConverter,并在其中设置DateFormat:

<mvc:annotation-driven> <mvc:message-converters> <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"> <property name="objectMapper"> <bean class="com.fasterxml.jackson.databind.ObjectMapper"> <property name="dateFormat"> <bean class="java.text.SimpleDateFormat"> <constructor-arg value="yyyy-MM-dd HH:mm:ss"/> </bean> </property> </bean> </property> </bean> </mvc:message-converters> </mvc:annotation-driven>

如果你用的是Java8日期时间类型LocalDateTime,还需要引入jackson-jsr310模块,并在ObjectMapper中注册JavaTimeModule。

6.4 MySQL中文乱码

乱码这个老问题的排查顺序:数据库表的字符集是否utf8mb4,JDBC连接串是否带characterEncoding,Tomcat的server.xml是否设置了URIEncoding。三层必须同时配好。建议在Tomcat的Connector加上:

URIEncoding="UTF-8"

同时在web.xml里加一个CharacterEncodingFilter,强制请求和响应用UTF-8:

<filter> <filter-name>encodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>encodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>

不要小看这个filter,没有它,POST请求的中文参数基本必乱,浏览器设置的编码在这里根本不可靠。

6.5 PageHelper分页数据错乱

PageHelper使用要求必须和第一条SQL紧邻。如果你在Service里先执行了别的SQL,或者PageHelper在循环里调用,分页数据就会错乱。正确姿势是:

PageHelper.startPage(pageNum, pageSize); List<Member> list = memberMapper.selectList(keyword); PageInfo<Member> pageInfo = new PageInfo<>(list);

如果在selectList之前又有一次insert或select,分页会把那个SQL也拦截掉,最终返回的错误会让你怀疑人生。

6.6 文件上传过大报错

如果做会员头像上传,SpringMVC必须配置MultipartResolver:

<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"> <property name="maxUploadSize" value="5242880"/> </bean>

同时要引入commons-fileupload与commons-io依赖。注意文件名不能直接用用户上传的原始文件名,否则目录遍历漏洞和路径混乱问题都来了。建议用UUID重命名,存储到指定的upload目录,数据库里只保存相对路径。

7. 答辩与演示怎么讲才能加分

7.1 演示流程要设计成“有剧情”

答辩当天不要从登录页开始讲配置。用一个准备好的测试账号,按一条完整业务链演示:

新增会员“张伟”,手机号188xxxx。给张伟购买一张“年卡”,然后用该卡预约明天的“动感单车团课”。预约成功后模拟到店,前台确认签到并扣减预约状态。此时再打开课时列表,能看到可预约人数减少。接着演示退卡退款,选择张伟的会员卡,提交退款,查看退款记录。最后打开报表页面,看今天的营收和预约统计是否跟着变了。

这套流程走下来,技术点一个一个自然砸出来:一表多业务、事务、乐观锁、聚合统计SQL,每一环都有数据在页面变化。答辩老师最怕听到“这里我调好了,你回去自己测”,所以你要主动讲清楚“如果我重复点击预约会发生什么”“如果会员卡过期了还能不能预约”。

7.2 讲深度而不是讲框架

答辩时不要花10分钟讲Maven怎么配仓库,那没有信息量。要讲的是几个“为什么”:

  • 为什么会员卡表要冗余end_date和remain_times?因为要避免频繁计算,也是为了乐观锁扣次。
  • 为什么扣次要用UPDATE...WHERE remain_times > 0?因为要防止并发超卖。
  • 为什么预约表要建唯一索引?因为要防止同一个用户重复预约。
  • 为什么退款单要单独放一张表?因为退款和支付是两种不同的业务流,退款要留痕,不能只改订单状态。

老师追问业务场景时,你要能说出具体异常:比如两个人同时登录同一账号,同时点了最后一节私教课,最后谁能预约成功?用这个乐观锁机制,数据库只让一条UPDATE成功,另一条返回0,服务层直接提示“名额不足”。这句话一说出口,你的项目深度立刻和普通增删改查拉开差距。

7.3 论文结构建议

论文目录可以按这个顺序:摘要、绪论(背景、意义、国内外现状)、需求分析(功能性需求、非功能性需求、用例图)、系统设计(总体架构、模块设计、数据库设计)、系统实现(每个核心模块配上代码和截图)、系统测试(测试用例表、测试结果、性能测试)、总结与展望。测试部分不要只写“点击通过”,至少要列出正常流程、异常流程、边界流程三条:

  • 正常:会员办卡预约课程。
  • 异常:提交预约时会员卡已过期。
  • 边界:会员卡剩余次数为0,再点击签到扣次。

每一条测试要写出预期结果和实际结果。论文里循环贴出的页面截图不要重复到十个,重点页面四五个就够。

最后再分享一点个人体会

做了这么多年项目,越来越觉得毕业设计最值钱的不是“用到了多少酷炫框架”,而是“有没有想清楚业务闭环”。健身房管理系统这个题目,放到真实场景里就是一个中小型SaaS的简化版。如果你能把“办卡—预约—签到—扣次—退卡—报表”这条链路处理干净,那你对事务边界、状态流转、用户权限、数据权限的理解就已经超过了大多数只会写接口的同学。

我自己的经验是,遇到拿不准的业务规则,不要拍脑袋,先把它画成状态图,再写状态迁移表。比如会员卡一共几种状态,哪些状态能办预约,哪些状态能续费,哪些状态能退款,这些规则列成表,写代码就是照着翻译。哪怕这个表只是写在项目笔记里,不放进论文,它都能帮你省掉大量返工时间。

如果你想在这个项目上加亮点,不需要一上来就设计分布式锁,先把单机下的乐观锁和唯一索引用明白。再加一个二维码签到、一个Excel导出、一个小程序预约前端,哪怕只是做一个最简Demo,都能给答辩老师留下“可扩展性强”的印象。但千万别为了亮点把项目搞成“全家桶”,SSM能老老实实承载完这一套流程,已经很不容易了。

最后,给所有正在赶工的同学一个忠告:如果你只有一个星期,先保证主链路能跑通,再把各种异常提示写完整。一个能演示“过期卡不能预约”“重复点击不会超卖”的系统,比一百个空页面的系统值钱得多。

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

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

立即咨询