ARTICLE DETAIL

资讯详情

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

SpringBoot实战:校园活动报名系统从需求拆解到部署上线

SpringBoot实战:校园活动报名系统从需求拆解到部署上线 前阵子帮学生会做校园活动报名系统才真正意识到学校里大多数活动报名还停留在Excel表格加问卷星的混合阶段。一场百人讲座报名信息能变成三个版本重复提交、漏登记、到签时对不上号负责的同学整理到凌晨也很难捋清楚。后来我基于SpringBoot做了一套校园活动报名系统把线上发布、在线报名、后台审批、数据导出整个链路打通总算是把实际问题解决掉了。下面这套实现思路我会从需求拆解、表结构设计、核心代码、并发处理到部署上线完整讲一遍。适合正在做SpringBoot课程设计或毕业设计的同学也适合想在学院、社团里快速落地一个报名平台而不是买了源码包之后一脸懵的人参考。1. 项目缘起一张Excel报名表是怎么逼疯整个学生会的1.1 学生会的日常三份名单对不上的混乱现场每年纳新、讲座、比赛报名基本流程都是这样先在问卷星发一个链接再在年级群里安排接龙到了现场再用纸质表签到。三项数据放在一起重复的人名、漏掉的信息、报错的活动场次负责干事每次都要手动筛选比对。更有意思的是报名截止之后还会有人私聊负责人“我还能补报吗”负责人看着手里的Excel内心是崩溃的。这其实不是“多写一个网页”就能解决的问题而是报名数据从产生、审核、统计到归档整个链路缺少一套统一的状态管理。纸质表、接龙、问卷星各说各话本质上是数据没有以结构化方式落库。所以做这个校园活动报名系统第一件事不是写代码而是先把业务链捋清楚。1.2 真实需求拆解三方角色的关注点完全不同做系统之前我和学生会、辅导员、普通学生各聊了一圈发现同一个报名场景下不同角色的诉求差异非常大学生关心报名是否成功、活动时间地点是否清楚、临时去不了能不能取消。管理员关心名额还有多少、报名数据能不能一键导出、是否需要对报名名单逐人审核。负责老师/辅导员关心活动是否经过审批、最终参与名单是否完整、活动数据后续能不能追溯。如果只做“学生端报名”而忽略后台审批和数据导出系统做完基本就是废的。真正能落地的是一个覆盖“活动发布、在线报名、后台审核、数据导出、状态回收”的闭环。功能清单我定了这么个范围模块功能点说明用户认证学生登录 / 管理员登录学生用学号登录管理员走独立账号活动管理发布活动、修改活动、下架活动管理员操作带状态流转报名管理在线报名、取消报名、名额控制核心业务后续单独讲审核管理报名审核、活动审批部分活动需要先审后通过数据导出报名名单导出Excel给现场签到和归档用通知提醒报名成功、审核结果通知轻量实现不引入MQ1.3 立项时的功能边界先求闭环再谈优化很多同学一上来就想着要做支付、做抢票、做积分排行结果项目卡在前期。我这次明确把MVP边界画在“报名全链路”之内不做抢票算法、不做在线支付、不做复杂权限矩阵。先保证一场活动从发布到导出名单之间每一步都有据可查。边界设置清楚之后开发周期能压缩到两周以内。这也是这套源码能被反复复用的原因——它不是一堆花哨功能的堆砌而是一个结构完整的业务闭环。等基础跑通之后再往后加海报模块、积分模块、统计大屏都是在稳固的地基上添砖。2. 技术选型与架构落位为什么SpringBoot成了主流2.1 从SSH到SpringBoot选型背后的真实原因十年前做JavaWeb流行的是SSHStrutsSpringHibernate稍晚一点是SSMSpringSpringMVCMyBatis。这两套东西的问题都出在配置上要写web.xml、spring.xml、mybatis-config.xml项目一开始光把XML之间的引用关系理顺就已经很劝退了。SpringBoot把“约定优于配置”做到极致自动装配机制通过spring.factories里的自动配置类把常用的Web、数据源、事务、JSON序列化等组件全部默认初始化好。说白了以前是用一大堆XML告诉框架“你该怎么办”现在是框架默认给你配好你只需要改不满足需求的部分。这套校园活动报名系统采用SpringBoot之后核心配置只有一份application.yml项目结构清爽很多。如果后面去面试或者答辩SpringBoot自动装配原理几乎是必问的点理解了上面的机制你就知道为什么这个项目能“开箱即用”。2.2 依赖清单与版本锁定版本锁定这个问题标题里写了“附源码”但源码能不能跑起来七成取决于版本对不对。这里我锁的是SpringBoot 2.7.18配套Java 8或11都能跑。选2.7.x而不是3.x是因为2.7.x的资料最全、兼容性问题最少对课程设计和毕设场景最友好。核心依赖如下parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.1/version /dependency dependency groupIdcom.alibaba/groupId artifactIddruid-spring-boot-starter/artifactId version1.2.20/version /dependency dependency groupIdcom.alibaba/groupId artifactIdeasyexcel/artifactId version3.3.4/version /dependency /dependencies用Maven构建时我习惯按这个顺序走mvn clean清掉旧产物mvn compile做快速编译检查代码错误mvn package -Dmaven.test.skiptrue打包。不要一上来就跳到package否则报错之后很难定位是编译错还是打包配置错。2.3 工程分层与模块划分虽然是单人项目我还是按照正规的分层结构来组织com.campus.activity ├── controller // 接收请求、参数校验 ├── service // 业务逻辑、事务控制 ├── mapper // MyBatis数据访问 ├── entity // 数据库实体对象 ├── dto // 前端交互数据结构 ├── common // 统一返回结果、异常处理、枚举 └── config // 拦截器、跨域、序列化配置为什么不把所有代码堆在Controller里报名链路涉及名额扣减、状态流转、审核记录这些逻辑一旦写进接口类并发和事务问题一多根本没法维护。Controller保持轻薄只做参数接收和结果包装业务判断都放到Service层这也是源码后期扩展时最省力的一点。3. 表结构设计活动、报名、审批之间的核心数据关系3.1 活动表冗余字段背后的考虑活动的核心字段并不只有标题和地点。一个报名系统要支撑“活动生命周期”必须把时间、状态、名额、审批开关都落在表里。字段类型说明idbigint主键titlevarchar(100)活动标题categoryvarchar(20)活动分类locationvarchar(100)活动地点start_timedatetime活动开始时间end_timedatetime活动结束时间signup_startdatetime报名开始时间signup_enddatetime报名截止时间total_slotsint总名额remain_slotsint剩余名额audit_requiredtinyint是否需要逐人审核statustinyint0草稿 1报名中 2已截止 3已结束organizervarchar(50)主办方/负责人cover_imgvarchar(255)海报地址create_timedatetime创建时间update_timedatetime更新时间remain_slots看起来是冗余字段因为理论上报名人数可以用select count(*)算出来。但真实场景下报名接口会非常频繁地查询剩余名额和扣减名额如果每次都去count整张报名表数据库压力会成倍增加。冗余一个剩余名额字段配合后面的原子更新SQL是这个系统设计里比较核心的一笔。3.2 报名表唯一约束与状态机报名记录表是另一张关键表字段类型说明idbigint主键activity_idbigint活动IDstudent_novarchar(20)学号namevarchar(50)姓名phonevarchar(11)手机号collegevarchar(50)学院majorvarchar(50)专业statustinyint0待审核 1已通过 2已拒绝 3已取消signup_timedatetime报名时间remarkvarchar(255)学生备注这里必须建唯一索引UNIQUE KEY uk_activity_student (activity_id, student_no)。它的意义不只是优化查询更是数据库层面的“最后一道防重复报名防线”。业务代码可以先把逻辑写得漂亮但高并发下重复提交、用户连续双击按钮最终能兜住底的只有数据库约束。状态机也很重要报名记录不是只有“报名成功”一种状态。需要审核的活动报名后进入待审核状态管理员通过后才算真正占用名额用户主动取消则释放名额。状态流转如果不用枚举管理代码里写死数字两三个月后自己再看源码都会懵。3.3 审批日志表与索引策略学校场景对“可追溯”有天然要求。谁在什么时间审核了哪条报名记录、拒绝原因是什么都必须能查。所以我加了一张审批日志表字段类型说明idbigint主键activity_idbigint活动IDrecord_idbigint报名记录IDoperatorvarchar(50)操作人actionvarchar(20)通过/拒绝/批量导入commentvarchar(255)操作备注create_timedatetime操作时间索引方面报名表上建议组合索引(activity_id, status)活动表上建议(status, signup_start)。数据量小的时候感觉不到索引的威力但到了学期末把整学期活动数据汇总统计时有没有索引就是秒出和卡半天的区别。4. 核心流程实现发布、报名、审核、导出的完整链路4.1 活动发布与状态流转活动的状态从创建开始就由后端控制。管理员创建一个活动时先落草稿状态活动时间和报名时间经过校验之后再手动发布。校验逻辑里最容易踩坑的是时间参数要同时判断“报名截止时间不能晚于活动开始时间”和“报名开始时间不能晚于报名截止时间”两组关系。状态流转核心是Service层的一个方法比如publishActivity会把status从0改成1closeActivity在报名截止时间到达后把status改成2。这里我建议用定时任务兜底而不是只依赖管理员手动操作。用Scheduled每5分钟扫描一次把已过报名截止时间的活动批量置为“已截止”能避免活动明明过了时间还能报名的尴尬。4.2 学生报名防重复提交与名额扣减的完整链路报名接口是整套系统中并发压力最大、最容易出问题的接口。我把它拆成四步校验活动当前是否处于“报名中”状态校验剩余名额是否大于0校验当前学生是否已经报名过插入报名记录同时原子扣减剩余名额。前3步是查询校验第4步是写入操作。只做查询不做并发控制在高并发下一定会出现超卖。解决办法不是加synchronized而是让扣减剩余名额的SQL带上条件Transactional(rollbackFor Exception.class) public SignupResult signUp(SignupRequest request) { Activity activity activityMapper.selectById(request.getActivityId()); if (activity null || activity.getStatus() ! 1) { return SignupResult.fail(活动不存在或未在报名中); } if (activity.getRemainSlots() 0) { return SignupResult.fail(名额已满); } if (signupMapper.countByStudentNo(request.getActivityId(), request.getStudentNo()) 0) { return SignupResult.fail(请勿重复报名); } int updated activityMapper.decreaseRemainSlots(activity.getId()); if (updated 0) { throw new ServiceException(名额已满报名失败); } signupMapper.insert(buildRecord(request)); return SignupResult.success(); }对应的核心SQLUPDATE activity SET remain_slots remain_slots - 1 WHERE id #{id} AND remain_slots 0这条更新的关键在于remain_slots 0。数据库的行锁语义保证同一时刻只有一个事务能成功执行更新影响行数为0时说明其他线程已经把最后一个名额抢走了。signupMapper.insert和decreaseRemainSlots放在同一个事务里插入失败则整体回滚名额也不会被白白扣掉。选择SpringBoot加Spring声明式事务来处理这个问题不只是图方便更是因为Transactional能把这条逻辑包在同一个数据库事务中保证数据一致性。这是整套报名系统里最值得反复看的一段代码。4.3 后台审批与Excel导出审核逻辑相对简单管理员对“待审核”状态的记录执行通过或拒绝。通过时把状态改成1拒绝时写明原因并改成2。这里有一条规则需要注意如果拒绝时剩余名额已经被其他学生占掉不会再归还名额。我在代码注释里把这条写得很清楚避免后续自己改出名额不一致的bug。导出Excel用的EasyExcel。选型时也考虑过直接用POI手写但POI要自己处理样式、行列转换代码量大维护成本高。EasyExcel提供注解式字段映射几行代码就能把查询结果写进文件ListSignupRecord list signupMapper.listByActivity(activityId, status); String fileName 报名名单_ System.currentTimeMillis() .xlsx; EasyExcel.write(response.getOutputStream(), SignupRecord.class) .sheet(报名名单) .doWrite(list);生成的Excel直接给到现场签到负责人报名数据再也不用从三个表格里手工合并。4.4 轻量通知不引入消息队列的做法报名成功、审核结果这类通知很多教程会建议上MQ但放在这个场景里完全没有必要。我直接用了Spring的事件机制加线程池异步发送邮件或站内消息。系统里维护一张简单的通知记录表由定时任务统一扫描发送逻辑直观出了问题也好排查。只有当后续要做全校级消息广播、短信推送、并发量非常大的时候才需要考虑接入消息队列。做过几个项目之后我最大的体会是做系统最怕的不是功能少而是为了“显得技术高级”引入根本用不上的组件徒增部署成本和排查成本。5. 实战踩坑复盘并发扣名额、日期时区与其他边角料5.1 并发超卖问题从synchronized到数据库原子更新第一次写完报名接口的时候我用的是“先查剩余名额再执行更新”的普通写法。用JMeter模拟20个学生同时报名只剩10个名额的活动结果数据库里出现了13条报名记录。后来翻日志才发现多个线程同时查到了remain_slots 10然后在更新时互不影响导致最后一条记录插入成功但剩余名额已经变负数。尝试过在Service方法上加synchronized单机环境下确实解决了问题但一旦部署到两台服务器后面synchronized就完全失效。最终方案就是前面提到的原子更新SQL。这里提醒一句UPDATE ... WHERE remain_slots 0解决的是并发扣减问题唯一索引解决的是重复报名问题两者缺一不可。别想着用一个方案通吃所有问题。5.2 日期差8小时时区配置引发的血案系统第一次部署到测试服务器时所有活动时间都少了8小时。19点开始的活动界面上显示11点。排查之后发现是服务器和MySQL的时区配置不一致导致的。最直接的处理方式是在JDBC连接串里明确指定时区url: jdbc:mysql://localhost:3306/campus_activity?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai同时应用层的日期类型统一使用LocalDateTime避免使用带隐式时区转换的java.util.Date。JSON序列化时再统一格式yyyy-MM-dd HH:mm:ss。三处保持一致之后时间问题就彻底消失了。这种问题在本地开发时通常不出现一上服务器就冒出来非常典型。5.3 版本兼容问题MyBatis、Druid、Swagger组合拳源码里用的MyBatis Starter是2.3.1和SpringBoot 2.7.x配合没问题。Druid Starter版本和SpringBoot版本之间的兼容比较微妙锁1.2.20之后没有出现配置属性不识别的问题。Swagger方面老教程常写springfox但springfox的3.0.0在SpringBoot 2.6上经常报Failed to start bean documentationPluginsBootstrapper解决办法是加配置或者换用springdoc。这里给出一张常用版本搭配表直接照着选不会踩坑组件推荐版本SpringBoot2.7.18MyBatis Starter2.3.1Druid Starter1.2.20EasyExcel3.3.4Lombok1.18.30springdoc-openapi-ui1.7.05.4 权限与安全别把管理员接口裸奔很多课程设计项目能把页面做出来但权限管理基本等于零管理员接口完全裸奔。我这个系统里用了一个比较直接的方案自定义HandlerInterceptor拦截器加上简单的登录状态判断管理员接口额外校验当前用户角色。密码存储用BCrypt加密数据库里不出现明文密码。如果不想从零写拦截器也可以直接用SpringSecurity或Sa-Token。但推荐先搞懂拦截器原理再上框架否则项目一旦登录逻辑报错很难分清是框架配置问题还是业务问题。对于这种规模的项目一个拦截器加两个注解就能把权限体系撑起来没必要被框架绑架。6. 从源码到部署本地跑通与上线运行的完整步骤6.1 环境准备JDK、Maven、MySQL版本对照源码复现的第一步是把开发环境统一起来。推荐组合JDK 8或11、Maven 3.6以上、MySQL 5.7或8.0、IDEA 2023以上。这个组合对SpringBoot 2.7.x的兼容性最好不需要额外折腾补丁。数据库创建时务必设置utf8mb4字符集否则中文很容易变成问号。6.2 从源码到本地启动的完整流程具体步骤如下在MySQL中创建数据库campus_activity执行项目里的schema.sql脚本完成建表和初始化数据修改application.yml中的数据库地址、用户名和密码在项目根目录执行mvn spring-boot:run启动等待控制台出现SpringBoot启动日志后访问http://localhost:8080。如果希望打成可执行包执行mvn clean package -Dmaven.test.skiptrue然后用java -jar target/campus-activity-1.0.0.jar运行。这套流程走通之后源码才算真正属于你而不是下载一个压缩包放在硬盘里吃灰。提示不要一上来就复制整段代码。先看表结构再看Service层最后看Controller理解数据在系统里是怎么流动的。这样排查问题时心里会有一张完整的地图。6.3 部署上线jar包与Docker两种方式小型项目最直接的方式是服务器安装JDK然后把jar包扔上去用nohup启动nohup java -jar campus-activity-1.0.0.jar app.log 21 要上容器的话Dockerfile可以很简洁FROM openjdk:8-jre COPY campus-activity-1.0.0.jar /app/app.jar WORKDIR /app ENTRYPOINT [java, -jar, app.jar]但我的建议是先手动部署本地跑通理解JVM进程、端口、日志文件之间的关系再考虑容器化。一上来就用Docker遇到端口映射和数据库容器连接的问题反而更难排查。容器化是给运维提效的不是帮新手绕过问题的。6.4 上线前必须处理的配置遗漏项上线前我一般会强制自己过一遍清单修改默认管理员密码关闭Swagger文档的公共访问确认上传文件的目录存在并且有权限关闭或限制调试日志的输出等级数据库提前做好定时备份策略。还有一个有意思的小细节源码里放了一个自定义的SpringBoot启动Banner把项目名称和端口号直接打在启动日志里方便一眼确认当前跑的是哪个环境。这种细节虽然不影响功能但在本地、测试、生产三个环境切换的时候能省下不少确认的时间。我个人做完这套系统之后最大的体会是报名系统的价值不是界面多好看而是以前散落在Excel、问卷星和群接龙里的数据终于有了一个统一的状态流转管道。活动发布、名额变化、学生报名、审核意见、最终名单全部可以追溯。下一次办活动直接复用这套表结构和核心逻辑一天就能拉出一套可用的系统。源码我已经整理好需要的同学拿去按第六章步骤跑一遍遇到问题再回头翻每一章的原理会比直接复制粘贴理解深得多。
返回列表