多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

SSM框架实战:高校志愿者管理系统全流程解析

SSM框架实战:高校志愿者管理系统全流程解析 开头每年毕业季总有一批计算机专业的同学被高校志愿者管理系统SSM框架毕设这类题目包围。说句实话这类题目确实是毕设里的万金油——业务场景清晰、功能边界明确、技术栈经典非常适合用来展示你对Java后端开发的基本功。但正因为做的人多答辩时老师一眼就能看出你是真写了还是糊弄的。这套系统虽然听起来就是个志愿者报名活动管理但真正把它做扎实背后涉及用户角色权限设计、数据库表结构规划、多条件分页查询、文件上传、数据统计导出等一系列Java Web开发的核心技能点。我这次开发的高校志愿者管理系统选择了经典的SSM框架组合也就是Spring Spring MVC MyBatis前端配合JSP和Bootstrap数据库用MySQL。整套系统覆盖了志愿者注册、活动发布、在线报名、后台审核、扫码签到、服务时长统计、公告通知、数据导出等全流程管理功能。如果你是正在准备毕设的Java方向学生或者想通过一个完整项目来巩固SSM框架知识的自学者这篇文章会把你从只会写增删改查带到能独立落地一个多角色、多模块的完整项目的水平。接下来我会把整个项目的骨架、核心模块、数据库设计、权限控制、报错排查和答辩加分项全部拆开来讲很多细节是我实际开发中一点点调出来的希望能让你少走弯路。1. 项目整体设计与技术选型思路1.1 为什么选SSM而不是Spring Boot先说一个很多人纠结的问题为什么不用Spring Boot其实不是不能用但从毕设角度出发SSM框架有它独特的价值。高校的课程体系里Spring框架、Spring MVC、MyBatis通常是被拆开来讲的SSM正好是对课堂知识的一次整合实践。答辩时老师大概率会追着问Spring的IoC和AOP在项目里哪里用到了MyBatis怎么解决SQL注入Spring MVC的请求流程是怎样的这些在SSM项目里都是有明确落点的而Spring Boot把这些细节都自动配置掉了一旦深问反而不好答。从开发难度来说SSM的配置确实繁琐——applicationContext.xml、spring-mvc.xml、mybatis-config.xml、web.xml四个配置文件来回调还要处理jar包冲突。但换个角度看这是好事你把这一套配置流程跑通了对Java Web底层原理的理解会深一个层次以后再来用Spring Boot会觉得豁然开朗。很多同学配置文件一报错就心态崩其实SSM的报错90%集中在jar包冲突和Bean注入失败后文我会专门讲排错方法。1.2 项目角色体系与核心业务流程这套系统的用户划分为三种角色普通学生志愿者、二级管理员、超级管理员。为什么这么分因为真实高校场景里通常是校青协管总、院系管理员分管本院活动的组织架构这个设计在答辩时能体现出你对业务场景有思考。学生端注册登录、查看活动列表、在线报名、查看报名审核状态、签到打卡、查看个人服务时长和志愿证明。二级管理员发布活动、审核本学院学生的报名申请、管理活动签到、录入/修正服务时长。超级管理员管理所有用户、给二级管理员分配权限、全院活动数据总览、公告管理、导出统计报表。核心业务流程是这样的管理员发布活动并设置活动时间段和人数上限 → 学生浏览活动并报名 → 报名进入待审核状态 → 管理员审核通过/驳回 → 活动当天管理员开启签到 → 学生凭报名编号签到 → 系统自动按活动时长累加个人服务时长 → 学生可在个人中心查看累计时长并申请开具证明。这个流程里有很多容易被忽视的边界情况比如活动被取消后已报名学生怎么办、签到时间窗口怎么控制、时长重复累加怎么避免。这些细节我在后文会逐个展开。1.3 工程项目结构约定我用标准的Maven多模块思想来组织代码虽然是单模块但包结构严格按照分层来约束自己com.example.volunteer ├── controller # 控制层负责接收请求和参数校验 ├── service # 业务层接口实现类分离 ├── mapper # MyBatis数据访问层接口 ├── entity # 数据库实体类 ├── dto # 前端传参对象VO避免实体直接暴露 ├── interceptor # 登录拦截器、权限拦截器 ├── common # 公共类返回结果封装、分页封装、常量类、工具类 └── config # 配置类使用Configuration替换部分XML配置这里有个值得养成的习惯controller不写业务逻辑service里不写SQLSQL只存在于mapper的XML文件中。这三层分离看起来很基础但我见过太多毕设代码把SQL写在service里、把JSON返回结构写在controller里答辩被老师一问就露馅。分层清晰还有一个实际好处——调试定位bug时你不用在整个项目里翻来翻去。2. 核心模块与关键细节拆解2.1 用户注册登录与角色权限落地用户的注册登录不能只做一个简单的密码比对我重点设计了三个细节第一个是密码加密。实际项目中密码绝不允许以明文存数据库我用的是Spring Security自带的BCryptPasswordEncoder来做哈希加密。注意BCrypt不是简单的MD5加盐它是自适应哈希算法每次加密同一个密码产生的盐值不同也就是同一个密码两次加密存储的结果不一样这样可以从机制上防止彩虹表攻击。有些同学图省事用MD5答辩时老师只要问一句MD5撞库怎么防就很容易被动。第二个是Token式的登录态管理。我没有用传统的Session而是选择JWTJSON Web Token。学生端登录成功后后端返回一个包含用户ID、角色、过期时间的签名Token前端存在localStorage里每次请求都带上这个Token后端写一个拦截器统一解析鉴权。这样做的好处是适合前后端分离部署——虽然项目主体是JSP渲染但我在一些需要异步请求的模块前端用了AjaxJWT让接口的鉴权更加无状态化服务端重启用户也不用重新登录。第三个是权限拦截器。Spring MVC里通过拦截器做访问控制我配置了两层拦截mvc:interceptors !-- 登录拦截所有页面都要校验是否登录 -- mvc:interceptor mvc:mapping path/**/ mvc:exclude-mapping path/login/ mvc:exclude-mapping path/register/ mvc:exclude-mapping path/captcha/ mvc:exclude-mapping path/static/**/ bean classcom.example.volunteer.interceptor.LoginInterceptor/ /mvc:interceptor !-- 权限拦截只拦截管理员相关路径校验角色 -- mvc:interceptor mvc:mapping path/admin/**/ bean classcom.example.volunteer.interceptor.AdminInterceptor/ /mvc:interceptor /mvc:interceptorsLoginInterceptor里写一个preHandle方法判断request的header里有没有合法Token没有就直接重定向到登录页有就放行并刷新过期时间。AdminInterceptor继承同一个基类多一步校验当前用户角色是否属于管理员。这个设计的核心思路是把权限校验集中到切面层避免在每个controller里重复写if判断。2.2 活动管理的完整生命周期设计活动管理模块是这套系统的业务重心我把它拆成了发布前、进行中、结束后三个阶段来设计。活动发布需要管理员填写的信息包括活动名称、活动描述、活动类型环境美化/敬老助残/社区服务等、活动地点、开始时间、结束时间、报名截止时间、招募人数上限、服务时长系数比如一次活动按4小时计。时间设置这里我踩过一个坑活动报名截止时间必须晚于当前时间、早于活动开始时间否则会出现活动还没开始报名就截止了的逻辑冲突所以前端表单提交时必须对这几个时间做联动校验。活动进行中的关键是签到。我设计的是管理员开启签到 → 志愿者在签到窗口内通过活动编号注册手机号后四位进行确认 → 系统记录签到时间。为什么不直接点名因为现场人多口杂手工点名效率太低。为什么不用复杂的扫码签到因为毕设项目做扫码涉及到生成二维码、小程序或公众号对接工作量会膨胀好几倍性价比不高。用活动编号手机尾号确认的方式既演示了业务逻辑又不会被答辩老师质疑为什么不做得简单一点。活动结束后要处理服务时长的自动核算。我的做法是在签到表里记录签到时间和活动预设时长定时任务在每天凌晨对状态为已完成的活动批量计算时长并写入志愿者的累计时长字段。这里要注意避免重复累加——我在时长记录表里加了一个record_status字段初始为0表示未计入总时长累加完成后置为1定时任务只处理状态为0的记录这样即使任务被重复触发也不会重复加时长。2.3 报名审核与状态流转的实现细节报名审核模块涉及多状态切换也是最容易写出bug的地方。我定义了一个状态机创建报名记录PENDING待审核→ 管理员通过APPROVED→ 志愿者签到CHECKED_IN→ 活动结束FINISHED其中任何一步被否定则进入CANCELED取消状态。报名模块有一个必须处理的并发问题活动有招募人数上限多个学生同时报名时如果校验逻辑是先查出当前报名人数再判断是否小于上限然后执行插入在高并发下会出现超卖现象——实际报名人数超过限制。我解决这个问题用的是数据库层面加锁在活动表的recruit_count字段上做乐观锁控制-- 更新活动表的当前报名人数MyBatis的Mapper方法 UPDATE activity SET current_count current_count 1 WHERE id #{activityId} AND current_count max_count这条SQL的执行结果是影响行数如果影响行数为0说明活动已经满员报名请求直接返回活动名额已满。这比先查询再判断要可靠得多因为数据库的行锁会在更新时自动串行化并发请求。这个点在答辩时是个很好的加分项说明你考虑了并发安全问题。2.4 个人中心与志愿时长统计学生个人中心展示的信息并不复杂但为了不让查询变得低效我设计了一张冗余字段的视图表volunteer_summary该表直接存储了每个志愿者的累计时长、服务次数、最近服务时间。这样个人中心的首页查询只需一次简单的SELECT就能搞定不必临时做多表聚合运算。时长统计页还需要按月份做柱状图展示我用的前端图表是ECharts后端提供一个按月统计服务时长的接口逻辑不复杂根据签到表的sign_in_time按月份分组SUM(service_hours)聚合。需要注意的是前端传月份参数时格式统一用YYYY-MM后端按字符串前缀匹配即可不要用日期范围比较避免时区导致的边界问题。3. 数据库设计与权限控制的实操要点3.1 核心数据表结构解析数据库设计是整个项目的根基表结构设计得合理后面写SQL会非常顺畅设计得不好后面每个功能都在跟连错表多表关联冗长较劲。我最终设计了8张核心表表名用途关键字段user用户表三种角色共用id, username, password(BCrypt), role, college, phoneactivity活动表id, title, type, location, start_time, end_time, signup_deadline, max_count, current_count, status, hoursregistration报名表id, user_id, activity_id, status(PENDING/APPROVED/CHECKED_IN/FINISHED/CANCELED), create_timesign_in_record签到记录表id, registration_id, user_id, activity_id, sign_in_time, record_statusvolunteer_summary时长汇总冗余表id, user_id, total_hours, total_count, last_service_timeannouncement公告表id, title, content, create_time, publisher_idcollege学院字典表id, nameoperation_log操作日志表id, user_id, action, target, create_time设计时我特别强调了两点不要把所有类型用户塞一张表后靠角色字段区分就完事——虽然我确实用的是单表共享角色的方案但我在user表里额外加了college字段配合二级管理员的本学院管理权限使用。这样既保证了登录校验简单又支持了按学院维度的数据隔离。签到记录和报名记录要分开。报名表示学生有意向参加签到表记录实际到场。两者分开后统计报名未到人数鸽了的人非常简单这种数据在管理员的活动复盘页面上很被看重。MyBatis的XML中我推荐把通用的查询字段抽成sql片段例如sql idactivityBaseColumns id, title, type, location, start_time, end_time, signup_deadline, max_count, current_count, status, hours /sql然后各条select直接include refidactivityBaseColumns/。虽然只是工程层面的小优化但在维护多条件分页查询时会明显减少重复代码。3.2 多条件分页查询的三种实现对比分页查询是毕设里的必考动作因为几乎每个列表页面都需要。实现上有三种常见方案MyBatis的PageHelper插件、自己用LIMITCOUNT手写、以及用MyBatis-Plus的IPage。我这次用的是PageHelper因为它和原生MyBatis配合最自然不需要引入额外的ORM框架。用法其实很简单业务层查询前先调用PageHelper.startPage(pageNum, pageSize); ListActivityDO list activityMapper.selectActivityList(query); PageInfoActivityDO pageInfo new PageInfo(list);这样返回的PageInfo里直接封装了总条数、当前页数据、页码列表等前端拿起来非常方便。但要注意一个坑PageHelper.startPage只对紧接着的一条SQL查询生效一定要保证分页参数写在查询语句之前中间不能插入其他数据库操作否则会导致分页失效整个列表返回全量数据——这是初学时最容易踩的坑。对于活动列表这种带多种筛选条件的场景我用一个ActivityQuery对象接收前端传来的参数属性包括keyword模糊搜索标题、type活动类型、status状态、startTime/endTime时间区间和college所属学院。对应的mapper XML里用where加if标签动态拼接SQLselect idselectActivityList resultTypecom.example.volunteer.entity.Activity SELECT include refidactivityBaseColumns/ FROM activity where if testkeyword ! null and keyword ! AND title LIKE CONCAT(%, #{keyword}, %) /if if testtype ! null and type ! AND type #{type} /if if teststatus ! null AND status #{status} /if if teststartTime ! null AND start_time gt; #{startTime} /if if testendTime ! null AND end_time lt; #{endTime} /if /where ORDER BY create_time DESC /select注意XML里比较运算符和需要转义成gt;和lt;否则XML解析会直接报错。这个细节看起来蠢但真的很多人栽在这里。3.3 基于拦截器的三级权限管控细节前文提到了登录拦截和角色拦截具体到数据权限控制我采用的是拦截器校验角色 Service层按学院隔离数据的组合方案。超级管理员登录后能查看所有学院的活动数据二级管理员登录后仅能查看和操作本学院的数据。这个逻辑在SQL层面就要限制select idselectActivityByCollege resultTypeActivity SELECT include refidactivityBaseColumns/ FROM activity WHERE college_id #{collegeId} if teststatus ! null AND status #{status} /if /select这里有一个经验性的建议不要把权限判断散落在各个controller里。我在AdminInterceptor中取出当前登录管理员对象并存到ThreadLocal里业务层需要当前用户信息时统一从UserContext工具类获取。这样做的最大好处是避免在不同方法里重复写从session取用户、判断为空、再获取学院ID这一整套样板代码。关于权限这块我还要提醒一点后端写好的权限逻辑前端菜单也要配合隐藏。很多学生项目只做了后端接口权限页面上的菜单按钮却谁都能看到。虽然真正的安全性在后端但一个普通学生登录后看到活动管理-删除的按钮点了之后跳404这种体验在答辩演示时非常尴尬也会让老师觉得你工程意识不足。所以前端的菜单根据角色动态渲染学生角色只显示与自己相关的功能入口。4. 实操过程与核心环节实现4.1 从零搭建SSM框架的全流程记录具体实现环节我从工程搭建开始记录方便你照着一步步操作。第一步创建Maven工程用IDEA新建一个Maven项目选择webapp骨架然后写pom.xml。依赖的选择有讲究直接列一个我用下来稳定的版本组合依赖版本说明spring-webmvc5.3.xSpring MVC核心自带Spring基础依赖mybatis-spring2.0.xMyBatis与Spring整合包mybatis3.5.xMyBatis核心mysql-connector-java8.0.xMySQL驱动druid1.2.x阿里连接池jackson-databind2.13.xJSON序列化前后端异步交互用jstl1.2JSP标签库javax.servlet-api4.0.1编译期使用provided作用域bcrypt0.9.x密码加密这里有个容易踩的坑javax.servlet-api必须设置为provided作用域否则打包成war后会和Tomcat自带的servlet相关类冲突导致启动报错java.lang.LinkageError。第二步配置web.xmlweb.xml是SSM项目启动的总入口需要注册Spring容器、Spring MVC的前端控制器DispatcherServlet以及字符编码过滤器。编码过滤器必须放在最前面否则POST请求提交中文时会出现乱码。我用的配置是filter filter-nameencodingFilter/filter-name filter-classorg.springframework.web.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param init-param param-nameforceEncoding/param-name param-valuetrue/param-value /init-param /filter注意forceEncoding必须为true因为它标注了是否对请求和响应都强制指定编码。不设置这个参数的话即使配置了编码为UTF-8也只是给response设置了编码请求体的解码还是会用默认编码照样乱码。第三步配置spring-mvc.xml开启注解驱动、配置包扫描只扫controller包、配置视图解析器mvc:annotation-driven/ context:component-scan base-packagecom.example.volunteer.controller/ bean classorg.springframework.web.servlet.view.InternalResourceViewResolver property nameprefix value/WEB-INF/views// property namesuffix value.jsp/ /bean静态资源放行也要在这里配置否则CSS/JS会被DispatcherServlet拦截。第四步配置applicationContext.xmlSpring核心配置包扫描范围从controller包扩大到除controller外的所有包重点配置数据源和SqlSessionFactoryBeancontext:component-scan base-packagecom.example.volunteer context:exclude-filter typeannotation expressionorg.springframework.stereotype.Controller/ /context:component-scan bean iddataSource classcom.alibaba.druid.pool.DruidDataSource property namedriverClassName valuecom.mysql.cj.jdbc.Driver/ property nameurl valuejdbc:mysql://localhost:3306/volunteer_db?useUnicodetrueamp;characterEncodingutf8amp;useSSLfalseamp;serverTimezoneAsia/Shanghai/ property nameusername valueroot/ property namepassword valueyourpassword/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property namemapperLocations valueclasspath:mapper/*.xml/ property nametypeAliasesPackage valuecom.example.volunteer.entity/ /bean bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.example.volunteer.mapper/ /bean数据库连接串里的serverTimezoneAsia/Shanghai必须加否则MySQL 8的驱动默认时区和本地不一致查询时间字段会差8小时。这个问题排查起来很痛苦一次就够让你记住。4.2 活动发布到签到全流程实操演示流程演示我以一个具体的例子来讲假设我要发布一个社区敬老助老活动。第一步管理员登录后进入活动发布页面前端页面用一个表单接收各项信息。这里我遇到的问题是把多行文本描述安全地提交给后端——前端用textarea后端接收时mybatis会自动做参数绑定不做数据处理直接存库存在XSS跨站脚本攻击风险。最简单的防御策略是在后端加一个Jsoup.clean()的过滤将HTML标签剥除String safeContent Jsoup.clean(content, , Safelist.basic());第二步活动创建成功后状态置为招募中。学生端页面展示活动的卡片列表已登录学生在活动详情页点击报名。报名时后端要做三个校验活动状态必须是招募中、当前时间必须在报名截止时间前、当前报名人数未满。三个条件都通过才插入registration记录状态为PENDING。第三步管理员在报名管理列表页审核学生报名。这里有个实用设计审核页按活动维度聚合管理员选择某场活动就能看到该活动的全部报名列表每个学生条目上有通过/驳回两个按钮。审核通过后系统给该学生发送一条站内通知存到announcement表里并标记接收人ID通知内容是活动开始时间、地点和签到方式。第四步活动当天管理员在操作台点击开启签到系统把签到窗口期设置为活动开始前30分钟到活动开始后30分钟。学生端我的活动列表里出现签到按钮点击后输入活动编号和注册手机号后四位后端校验成功后写入sign_in_record表并返回签到成功页面。第五步活动结束后定时任务自动处理时长。我习惯用一个Spring的Scheduled方法配置cron表达式为0 0 2 * * ?每天凌晨两点执行。任务的逻辑是找出所有活动状态为已完成且当前时间在活动结束时间之后、且时长记录尚未生成的签到记录批量生成时长记录并更新volunteer_summary。4.3 数据导出与统计报表的落地管理后台还有一个受欢迎的功能是数据导出——把某个时间段的全院志愿者服务记录导出为Excel。这里用的是Apache POI的XSSFWorkbook逐行写入后通过HttpServletResponse输出流返回给客户端下载。写Excel时的几个细节必须注意单元格样式如果统一使用默认样式导出1000行数据性能没问题但超过5000行时建议使用SXSSFWorkbook流式写入否则内存会被撑爆。导出前要设置Content-Disposition响应头文件名用URLEncoder编码避免下载时中文文件名乱码。不要把查询全部数据一次性灌入内存——用MyBatis流式查询Cursor类型或者分页循环查询保证内存压力可控。统计报表端我选了ECharts柱状图展示近6个月服务时长趋势、饼图展示活动类型的参与人数占比。接口返回的数据结构直接就是ECharts需要的{name, value}[]格式前端直接渲染不需要二次转换。4.4 文件上传功能的实现与注意事项活动封面图上传是标配功能。我用的是Spring MVC的CommonsMultipartResolver配置上传限制单个文件大小5MB、总大小20MB上传路径外置到项目的/uploads目录。上传这块有个很容易被忽视的安全问题文件名不能信任前端传上来的。注册一个用户把恶意脚本文件改成.jpg名字通过图片上传接口传上去后如果服务器直接拼接原始文件名保存并返回访问路径可能造成存储型XSS或文件上传漏洞。我的处理方式是用UUID重新生成文件名并通过文件扩展名白名单校验真实类型String originalFilename file.getOriginalFilename(); String ext originalFilename.substring(originalFilename.lastIndexOf(.)); if (!ext.matches((?i)\\.(jpg|jpeg|png|gif))) { throw new BusinessException(仅支持图片文件上传); } String newFilename UUID.randomUUID().toString().replace(-, ) ext;另外上传目录绝对不能放在项目部署目录的WEB-INF下虽然更安全但读取时要走servlet的getResourceAsStream不方便做成静态URL直接访问也不能放在系统盘根目录。我通常配置成项目同级目录/opt/volunteer/uploads再在Spring MVC里增加一个资源映射mvc:resources mapping/upload/** locationfile:/opt/volunteer/uploads//这样图片路径/upload/xxx.jpg就能被直接访问。5. 开发踩坑记录与问题排查实录5.1 四大高频报错的排查思路项目开发过程中一定会碰到几个报错我总结经验如下建议直接收藏报错一Invalid bound statement (not found): xxxMapper.xxx意思是MyBatis找不到对应的SQL语句。九成原因是mapper XML文件的namespace和Java接口全限定名不一致或者XML文件没有被mapperLocations扫描到。注意如果XML放在src/main/java目录下而又没配置Maven的resources指向IDEA编译时会直接忽略xml文件。解决方案是把mapper目录添加到pom.xml的resources配置中resources resource directorysrc/main/java/directory includes include**/*.xml/include /includes /resource /resources报错二org.springframework.beans.factory.NoSuchBeanDefinitionExceptionSpring容器里没有找到对应的Bean。大概率是忘记写Service/Repository注解或者包扫描范围没覆盖到该包。还有一种情况是接口和实现类分离后标注Service写在实现类上但接口没有标注这在Spring 5之前是正常的因为扫描的是实现类。如果用了Resource(name xxxService)按名称注入且service实现类的默认beanName开头小写不一致也容易报这个错。报错三JSP页面加载后CSS样式全丢检查请求路径下的静态资源是否被DispatcherServlet拦截。如果项目访问路径是/admin/activity/list而页面里引用的CSS路径是相对路径css/style.css浏览器实际请求的是/admin/activity/css/style.css自然404。解决办法是使用绝对路径c:url value/static/css/style.css/或者设置base href${pageContext.request.contextPath}/。报错四MySQL中文乱码检查三个环节数据库连接串带没带characterEncodingutf8、数据库表的字符集是不是utf8mb4、jsp页面是不是声明了pageEncodingUTF-8。三者缺一个都会乱码。这里建议用utf8mb4而不是utf8因为utf8在MySQL里是假utf8存不了emoji和生僻字比如有些学生名字里带用utf8mb4才是真正的4字节UTF-8。5.2 排查经验事务失效的经典场景事务管理是SSM项目里必考的知识点。我踩过一次典型的坑在Service实现类中methodA没有标注Transactional它调用了同一个类内部标注了Transactional的methodB结果methodB的数据操作没有事务保护操作中途异常时前面已执行的SQL没有被回滚。原因很简单Spring的Transactional是基于动态代理实现的外部调用代理对象时事务才生效同类内部调用走的是this.methodB()绕过了代理事务自然失效。解决方式是自注入代理对象或者把methodB的调用放到另一个Service里AOP切面才能生效。我在写签到时长更新联动逻辑时是把签到记录插入和志愿汇总表更新写入同一个事务方法为了保证两者原子性特意把两个方法放到了不同的Service中由上层Service统一调用事务标注在上层方法上。这个场景老师非常喜欢问说说你项目里事务怎么控制的你把这个真实案例讲出来比背书上的事务四大特性效果好十倍。5.3 前端页面开发中的三个易错点项目前端主要用的是JSP Bootstrap jQuery ECharts没上特别花哨的框架但在开发中依然有几个点值得提醒JSP中EL表达式要注意空值问题。比如${activity.title}如果activity对象为空页面会直接报500严格模式下。建议在页面渲染前统一做空值判断或者用${empty activity ? 待定 : activity.title}。我在活动卡片列表里就用了一个默认值兜底防止活动数据未完全填充时报错。Ajax请求的Response ContentType要统一。后端用ResponseBody返回JSON时Spring MVC会自动设置text/html或application/json。但如果你在Controller里手动设置过response.setContentType(text/html)ajax的dataType: json解析就会报错。解决方法是后端统一用一个RestControllerAdvice做返回内容包装保证所有接口的ContentType一致。列表页的搜索条件要保留。翻页或排序时如果前端没有把当前搜索条件拼接到分页链接上用户点第2页后搜索词就丢了体验非常糟糕。我把搜索条件统一序列化到隐藏域分页时自动拼接这样刷新页面时条件还在。5.4 常见问题排查速查表汇总一下开发过程中最常遇到的几个问题供你开发时对照问题现象可能原因解决方案登录后跳转404拦截器放行路径配置错误或过滤器顺序不对检查web.xml中过滤器的注册顺序编码过滤器必须最靠前分页查询返回全量数据PageHelper.startPage后被其他SQL抢先执行确保startPage后紧跟着目标查询SQL中间不穿插其他mapper调用上传图片显示404静态资源映射路径配置错误检查mvc:resources的location是否为绝对路径、是否带结尾斜杠日期时间差8小时数据库连接串缺少serverTimezone参数统一使用Asia/Shanghai时区Service实现类无法注入Service接口和实现类的包路径不在扫描范围内检查context:component-scan的base-package是否覆盖到前端传数组参数收不到Ajax没有设置traditional: truejQuery默认不会序列化数组参数需显式设置或后端用List接收操作数据库时报DuplicateKey并发重复提交在关键业务表中加唯一约束如registration表中u_user_id_activity_id唯一索引6. 答辩亮点与项目扩展方向思考6.1 如何让毕设项目在答辩中脱颖而出做毕设和做真实项目最大的区别是答辩时你要在有限时间内让评委老师快速看到你的技术亮点。我建议把你的汇报主线定位在一个真实业务场景下的全栈工程实践能力而不是罗列功能。我认为这个项目里有四个值得重点讲的技术亮点第一个是权限控制的三层设计即登录态校验JWT加角色校验拦截器加数据隔离Service层学院维度从用户能不能进到用户能做什么再到用户能看到哪些数据逐层收紧。这个阶梯式的安全设计在真实企业项目中也是通用方案。第二个是并发场景下的名额控制就是前面提到的UPDATE activity SET current_count current_count 1 WHERE current_count max_count方案把问题从应用层转移到了数据库行锁层面解决展示了事务与并发的理解。第三个是数据冗余表的设计用volunteer_summary表换取查询性能这个以空间换时间的思路符合企业开发里常见的反范式设计原则。第四个是定时任务的可靠处理用record_status状态标志避免重复结算本质上就是幂等设计思想。6.2 基于这个项目的扩展升级路径如果你的时间还充裕想让项目再往前走一步推荐两个不大不小、但能显著提升含金量的方向第一个方向是引入Redis缓存。活动列表因为是高访问量页面可以把首页热门活动列表缓存到Redis设置过期时间5分钟。这属于三级缓存中最简单的一级却能展示你对缓存穿透、缓存雪崩等知识点的理解。第二个方向是前后端分离改造。把JSP页面逐步替换为Vue或React前端项目后端改为纯JSON接口。毕设如果要给前后端分离架构定调SSM正好可以提供一套标准的RESTful API。这个改造对业务逻辑影响不大主要是前端渲染层的替换。考虑到毕设的时间成本不建议动后端结构做数据接口适配即可。第三个方向是引入工作流引擎。志愿者活动审批如果涉及多级审批校、院两级可以尝试集成Activiti或Flowable。这个扩展同样有价值但工作量大除非你有流程图绘制与流程引擎相关的加分需求否则不推荐在毕设阶段硬上。6.3 我在实际开发中最后想说的几句这个系统我断断续续开发了两个多星期最深的体会是SSM框架真正难的不是某个单独的技术点而是把所有技术点串起来后的整体工程掌控力。你可能会在配置Spring容器时卡住会在联调前端接口时烦躁会在排查一个诡异的中文乱码时怀疑人生——但这些恰恰是真实的开发日常。如果你现在正在做这个毕设我的建议只有一条先搭骨架再补血肉。把用户登录、管理员发布活动、学生报名签到这条主流程打通再去完善报表、导出、权限之类的外围功能。主流程能跑通项目就成功了一半外围功能做得再多主流程有bug演示时也会功亏一篑。最后再分享一个小技巧开发时给项目加上统一的日志输出在关键业务方法入口打印参数、出口打印结果。别小看这个习惯毕设项目不大但当你调试那个为什么审核通过后学生端状态没变的诡异bug时打日志是你唯一的救命稻草。
返回列表