☰
Spring Boot校园组团平台实战:从项目拆解到部署上线
2026/10/6 10:29:58 网站建设 项目流程

简介:面向高校学生与Spring Boot初学者的校园组团平台完整项目源码包,涵盖用户注册登录、小组创建与管理、活动发布报名、消息通知及团队讨论区等核心模块,适用于课程设计、毕业设计或课余自学场景。包内共789个文件,以Java后端、Vue前端、JavaScript与CSS样式代码为主,同时包含SQL数据库脚本、项目说明文档、启动部署脚本及SVG图标等资源,便于从环境搭建到功能实现全流程学习。压缩包大小为31.21MB,已有30人学习下载。源码目录结构完整,db.sql与说明文档.txt可辅助理解数据库设计和系统架构,欢迎使用.txt及批量安装脚本进一步降低了上手门槛,适合希望快速搭建同类校园服务平台的开发者参考。

1. 校园组团平台到底是什么:这个Spring Boot项目能解决什么

拿到一个名为 springboot项目校园组团平台.zip 的压缩包,第一反应通常是解压找README,但更该先想清楚的是业务:校园组团,本质就是把线下靠群聊接龙完成的组队和拼团搬上Web。社团招新要凑人、课程项目要组队、食堂团购要拼单,这些场景都需要一个支持活动发布、报名、成团判断和成员管理的闭环。用Spring Boot实现,意味着这套代码覆盖的不只是页面跳转,而是从用户体系到状态流转的完整后端工程。适合两类人:准备毕设的在校生,以及刚转Java后端想拿真实业务练手的工程师。这篇我按项目结构、核心流程、数据模型、排错和上线顺序把这个包拆到能复现、能改造成自己的版本。

2. 从zip到运行:先拆Spring Boot项目结构与启动配置

2.1 解压后第一时间:看懂Spring Boot框架的项目结构

看一个Spring Boot项目,第一步不是找代码而是在 src 目录下识别分层。Spring Boot框架对目录结构没有强制约定,但社区形成了成套的分层惯例:controller 层只管接收HTTP请求并转交参数,service 层处理业务规则,mapper 或 repository 层负责和数据库交互,entity 或 domain 层定义表对应的Java对象。校园组团平台这类业务项目,结构上不会超出这个框架。解压后你会看到类似下面的目录:

campus-group/ ├─ pom.xml ├─ src/main/java/com/campus/group/ │ ├─ GroupApplication.java │ ├─ controller/ │ │ ├─ AuthController.java │ │ ├─ ActivityController.java │ │ └─ JoinController.java │ ├─ service/ │ │ ├─ ActivityService.java │ │ └─ JoinService.java │ ├─ mapper/ │ │ ├─ ActivityMapper.java │ │ └─ JoinRecordMapper.java │ └─ entity/ │ ├─ User.java │ ├─ Activity.java │ └─ JoinRecord.java └─ src/main/resources/ ├─ application.yml └─ mapper/ └─ ActivityMapper.xml

这种分层的价值在于:当你在调试“报名不生效”或者“状态不更新”的时候,不用把整个项目读一遍,只要顺着“请求 → controller → service → mapper”这条链路定位。比如组团平台最常见的报错——报名接口返回成功但数据库里查不到记录,问题大概率出在service层的事务没生效或者mapper的SQL写错了;如果你不分层把这些逻辑塞在controller里,排查时就得在HTTP层里找SQL问题,那才是真正的翻车现场。

顺便说一句,启动类GroupApplication.java是项目的入口,它上面的@SpringBootApplication注解同时开启了自动配置和组件扫描。这个注解干活的核心是Spring Boot框架自动装配的原理落地:启动时扫描当前包以及子包下的所有 @Component、@Service、@Controller 类并注册到容器里。常见问题是扫描范围不对——比如你把启动类放在 com.campus 而业务类放在 com.campus.group.controller,能扫到;但如果你把启动类放在 com.campus,controller 却放在 org.campus.group 这种完全不同根的包结构里,Spring Boot默认扫描就扫不到,接口直接404。这是刚碰Spring Boot项目时特别容易踩的坑。

2.2 application.yml里的关键配置:端口、数据库连接与日志级别

Spring Boot项目80%的启动问题都能在 application.yml 里找到答案。校园组团平台涉及用户注册登录、活动发布和报名操作,至少需要配置三块:服务端口、MySQL数据源、日志输出级别。一个小型组团平台的典型配置长这样:

server: port: 8080 servlet: context-path: / spring: datasource: url: jdbc:mysql://localhost:3306/campus_group?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0

这里要重点说明每一项的作用。server.port 是端口,默认8080,如果本机8080被占用会启动失败,常见处理是改成8081或9090,后面排错章节会专门讲。spring.datasource 这段是数据源配置,url里的serverTimezone=Asia/Shanghai不是可加可不加的——MySQL 8.0以上连接时区不指定,会报时区错误或者让时间偏移8小时;username和password换成你自己本地的账号密码。jackson 的配置决定接口返回的日期格式,组团平台的活动截止时间如果没有这段配置,返回给前端的是时间戳数组或者带T的ISO格式,前端解析后在页面上显示成NaN,这是非常典型的配置缺失问题。

最后一段mybatis-plus的配置,是很多Spring Boot项目在用的MyBatis增强工具。log-impl 指定把SQL打印到控制台,调试阶段必开,等上线了再关掉避免日志刷屏;logic-delete 系列配置开启逻辑删除后,你的删除操作都会变成UPDATE把deleted置为1,而不是物理DELETE,这样组团记录和活动数据都能留底备查。

2.3 Maven构建与启动:本地跑起来的最小命令

配置文件改好之后,启动项目就很简单。前提是你本机装了JDK 8或11(具体看pom.xml里的java.version,后面排错章节会说明)和Maven。在项目根目录执行:

# 编译并打包,跳过测试 mvn clean package -DskipTests # 进入target目录,用java -jar启动 cd target java -jar campus-group-0.0.1-SNAPSHOT.jar

第一行命令的作用是清理target目录、重新编译、打包成可执行jar。-DskipTests的意思跳过单元测试,毕设阶段或者本地联调时绝大多数场景用不到测试用例,跳过能减少一半构建时间。第二行启动命令会把内嵌Tomcat拉起来。看到类似Tomcat started on port(s): 8080的日志就说明启动成功了,这时候访问http://localhost:8080/就能看到项目的接口文档或者前端页面。

如果你不想每次改代码都手动打包,也可以用开发模式启动:

mvn spring-boot:run

这个命令的特点是不需要先package,直接编译并启动,配合IDEA的自动编译功能,改完Java代码按Ctrl+F9重启应用就能看到效果。不过我个人的习惯是先用mvn clean package验证一次完整构建,再切到spring-boot:run做日常开发。原因是完整构建会暴露依赖缺失、插件版本不兼容这类问题,这些问题在开发模式下经常被IDE缓存掩盖,等最后打包部署时才爆发,那会儿再查就狼狈了。

3. 组团核心流程落地:活动发布、报名与成团判断的实现

3.1 活动发布:从Controller到Service的参数传递

组团平台的起点是活动发布。一个简单但完整的发布接口涉及三个层次:Controller接收HTTP参数、Service做业务校验、Mapper写库。核心代码在Service里,Controller这层要尽可能薄。以Spring Boot项目里最常见的写法为例:

@RestController @RequestMapping("/api/activity") public class ActivityController { @Resource private ActivityService activityService; @PostMapping("/create") public Result<Long> create(@RequestBody ActivityCreateDTO dto) { Long activityId = activityService.createActivity(dto); return Result.ok(activityId); } }

Controller层做的事情只有三件:声明接口路径/api/activity/create、接收JSON请求体、调用Service并返回结果。@RequestBody注解表示前端传的是JSON,Spring Boot会自动把JSON字段映射到ActivityCreateDTO对象的属性上。DTO是数据传输对象,只承载参数不参与业务逻辑,这里故意不用Activity实体类接参,原因是为了避免前端多传的字段直接污染数据库实体——比如前端传了一个id过来就可能覆盖已有记录的ID。

Service层的逻辑才是组团平台的核心:

@Service public class ActivityServiceImpl implements ActivityService { @Resource private ActivityMapper activityMapper; @Override @Transactional(rollbackFor = Exception.class) public Long createActivity(ActivityCreateDTO dto) { Activity activity = new Activity(); activity.setTitle(dto.getTitle()); activity.setDescription(dto.getDescription()); activity.setTargetCount(dto.getTargetCount()); activity.setCurrentCount(0); activity.setDeadline(dto.getDeadline()); activity.setStatus(0); // 0=招募中 1=已成团 2=已截止 3=已取消 activity.setCreateTime(new Date()); // 核心校验:成团人数至少2人 if (dto.getTargetCount() == null || dto.getTargetCount() < 2) { throw new BizException("成团人数必须大于等于2"); } activityMapper.insert(activity); return activity.getId(); } }

这段逻辑里有几个参数设置值得注意。@Transactional(rollbackFor = Exception.class)声明事务回滚的触发条件:默认Spring只对RuntimeException回滚,如果你抛出的自定义BizException是普通Exception就触发不了回滚,所以必须显式指定rollbackFor。targetCount小于2直接拒绝,这个规则看似简单,实际是防止拼单业务里的孤独团——一个人的团没有意义,也是在数据层面保证组团平台的最低价值。status用整数表示状态的写法是中小型项目的常态,从0到3四个数字的含义后面会反复用到。

3.2 报名参团:并发控制与防止重复提交

报名是组团平台里最容易出问题的一环。两个用户同时点报名,数据库层面如果控制不好,会出现超员问题——活动目标5人,结果6个人报名成功。Spring Boot项目里处理这个问题的常见方案有两种:数据库行锁和唯一索引防重。先看最稳妥的做法:

@Transactional(rollbackFor = Exception.class) public JoinResult join(Long activityId, Long userId) { // 1. 用悲观锁锁住活动行,防止并发超员 Activity activity = activityMapper.selectByIdForUpdate(activityId); if (activity == null) { return JoinResult.fail("活动不存在"); } if (activity.getStatus() != 0) { return JoinResult.fail("活动不在招募期"); } // 2. 单用户防重复 Long count = joinRecordMapper.selectCount( new LambdaQueryWrapper<JoinRecord>() .eq(JoinRecord::getActivityId, activityId) .eq(JoinRecord::getUserId, userId)); if (count > 0) { return JoinResult.fail("你已经报名过这个活动"); } // 3. 人数校验 if (activity.getCurrentCount() >= activity.getTargetCount()) { return JoinResult.fail("组团名额已满"); } // 4. 写入报名记录 JoinRecord record = new JoinRecord(); record.setActivityId(activityId); record.setUserId(userId); record.setJoinTime(new Date()); joinRecordMapper.insert(record); // 5. 活动人数+1 activity.setCurrentCount(activity.getCurrentCount() + 1); // 6. 达到目标立刻成团 if (activity.getCurrentCount() >= activity.getTargetCount()) { activity.setStatus(1); } activityMapper.updateById(activity); return JoinResult.ok(); }

这段代码的关键在第一步selectByIdForUpdate。这个方法的SQL本质是SELECT ... FOR UPDATE,它在事务内锁定活动这行记录,其他事务要更新这行就得等当前事务提交。这样两个用户同时报名时,后到达的事务会阻塞在行锁上,锁释放后再读取活动数据,看到的人数已经是更新后的值,就不会超员。

这里有个细节:selectByIdForUpdate不是MyBatis-Plus内置方法,需要自己在Mapper里写。常见做法是在 ActivityMapper 里加一段自定义查询:

<select id="selectByIdForUpdate" resultType="com.campus.group.entity.Activity"> SELECT * FROM activity WHERE id = #{id} FOR UPDATE </select>

FOR UPDATE的锁粒度是行锁,前提是 WHERE id=#{id} 命中了主键索引;如果锁的是整张表或者无索引字段,性能会急转直下。另外锁在事务提交或回滚后释放,所以这段代码必须在事务方法里执行,脱离了事务的FOR UPDATE不会生效,这是新手排查“明明加了锁还是超员”时最不可忽视的原因之一。

重复提交的校验也很有讲究。上面代码用的是先查询后写入,查不到才insert。这个方案在并发场景下有竞态窗口——两个请求同时执行selectCount都得到0,然后双双插入。所以需要数据库唯一索引兜底:

ALTER TABLE join_record ADD UNIQUE KEY uk_activity_user (activity_id, user_id);

这样即使业务层漏判,数据库也会用唯一索引把第二条插入挡回来,报DuplicateKeyException。在代码里捕获这个异常并转换成友好提示即可。数据库唯一索引是报名防重的最后一道防线,业务层的先查后插只是优化体验的手段——这个认知顺序要摆正。

3.3 成团判断:状态流转与自动截止

成团判断散落在两个时机:报名时人数达到目标立刻成团,以及到达截止时间系统自动截止。前者在报名事务内已经处理,后者需要Spring Boot的定时任务。先看状态字段的设计:

0 = 招募中(可报名) 1 = 已成团(不可报名) 2 = 已截止(时间到了没成团,不可报名) 3 = 已取消(发布者主动取消)

状态机不复杂,但有个容易遗漏的边界:截止时间到达时活动可能还处于招募中状态,人数没够但不能再报名了。Spring Boot里做定时任务,常用做法是在配置类上加 @EnableScheduling 开启调度,然后写一个定时清扫任务:

@Component public class ActivityExpireTask { @Resource private ActivityMapper activityMapper; @Scheduled(cron = "0 */5 * * * ?") public void expireActivities() { // 每5分钟扫描一次到期的活动 List<Activity> list = activityMapper.selectList( new LambdaQueryWrapper<Activity>() .eq(Activity::getStatus, 0) .lt(Activity::getDeadline, new Date())); for (Activity activity : list) { activity.setStatus(2); activityMapper.updateById(activity); } } }

cron表达式的含义是每5分钟执行一次:秒固定0,分钟是*/5,小时任意,日期和月份任意。如果你把活动粒度控制到秒级,可以改成0 0/1 * * * ?每分钟执行,但注意定时清扫的粒度不要小于业务容忍度,否则数据库压力会上来。另一个细节是活动截止的判断标准——lt(Activity::getDeadline, new Date())意思是截止时间小于当前时间就视为过期,查询时只扫status=0的活跃活动,避免每次跑全表。

这里还要补一个被动校验点:用户报名时也要判断时间。如果活动status还是0、deadline却已经过了(定时任务还没跑),用户提交过来代码还是会放行。所以报名处的校验条件要加上时间:

if (activity.getDeadline().before(new Date())) { activity.setStatus(2); activityMapper.updateById(activity); return JoinResult.fail("活动已截止"); }

这样被动校验和主动定时任务互补:定时任务兜底修正状态,接口在任何时刻都返回正确结果。这个“定时任务清扫 + 接口内监修”的组合在订票、抢购类系统里是通用模式,组团平台只是这个模式的小型化实践。

4. 数据模型与接口规范:组团平台的表结构设计

4.1 五张核心表的设计与字段参数

校园组团平台的表数量不多,但每一张都直接对应业务模块。常见的最小数据集如下:

表名用途核心字段备注
user用户表id, username, password, student_no, nicknamestudent_no加唯一索引
activity活动表id, publisher_id, title, description, target_count, current_count, deadline, status, deletedstatus见3.3
join_record报名记录表id, activity_id, user_id, join_time联合唯一索引防重复
notification通知表id, user_id, content, create_time, is_read成团/截止后发通知
category分类表id, name, sort可选,用于活动分类

拿activity表来说,publisher_id关联user表的id,在业务上体现了组团平台“谁发起、谁负责”的模式。target_count和current_count分开存是有意的:查询活动列表时不需要count报名表,直接读current_count字段,性能好;但代价是插入报名记录后要手动维护这个冗余字段。deleted列是为了配合逻辑删除配置,查询时MyBatis-Plus的全局配置会自动追加deleted=0条件,不需要手写。

join_record表的设计重点在于那个联合唯一索引。业务上一个人对一个活动只能报一次名,这个约束由数据库来保证,比靠业务代码可靠得多。student_no在user表加唯一索引,对应校园场景里“一个学号只能注册一次”的规则,这是组团平台防重复账号的基础。

4.2 MyBatis-Plus还是Spring Data JPA:选型逻辑与参数对比

Spring Boot项目里操作数据库的主流方案有两个:MyBatis-Plus和Spring Data JPA。校园组团平台这种CRUD占比高的业务,两个都能用,但写法差别明显。如果你打开这个zip发现用的是MyBatis-Plus,下面这段选型逻辑可以作为参考。

MyBatis-Plus的优势在于SQL可控。活动列表页想加分类筛选、按成团人数排序、按发布日期做范围过滤,用MP的条件构造器能拼出比较清晰的查询:

List<Activity> list = activityMapper.selectList( new LambdaQueryWrapper<Activity>() .eq(Activity::getStatus, 0) .eq(StringUtils.hasText(categoryId), Activity::getCategoryId, categoryId) .orderByDesc(Activity::getCreateTime) .last("LIMIT 10"));

这段代码里,.eq(Activity::getStatus, 0)表达“status等于0”,.orderByDesc(Activity::getCreateTime)按创建时间倒序,.last("LIMIT 10")原样拼接分页语句。中间那个.eq(StringUtils.hasText(categoryId), ...)是动态条件:当分类ID不为空才追加该条件,这个特性在带筛选的列表查询中特别实用,免去手写一堆if判断拼接SQL的麻烦。

Spring Data JPA的写法更面向对象,但复杂查询需要写 @Query 或依靠方法名推导,SQL的可控性相对弱。组团平台里像“统计每个活动当前报名人数”这类聚合查询,MyBatis-Plus可以直接写SQL,JPA则要绕几层。所以我的建议很明确:以毕设和中小型内部系统为目标的Spring Boot项目,选MyBatis-Plus更合适——学习曲线平缓、SQL直观、排错方便。如果你在团队里长期维护且已经统一用JPA,那就跟随团队规范。选型没有绝对对错,关键是你在排错时对工具有没有掌控感。

4.3 接口参数规范与Swagger调试的几个边界

开发阶段调试接口的方式,常见是Swagger或者Postman。如果你在依赖里引入了springfox或Knife4j,启动后访问/swagger-ui.html就能看到接口清单和参数模板。处理组团平台接口时,有几个边界是调试必查的。

第一个边界是参数为空的校验。活动创建接口里targetCount如果不传,后端必须自己兜住,不能得到NullPointerException。常见做法是用Jakarta Validation注解:

public class ActivityCreateDTO { @NotBlank(message = "活动标题不能为空") private String title; @NotNull(message = "目标人数不能为空") @Min(value = 2, message = "成团人数至少2人") private Integer targetCount; @NotNull(message = "截止时间不能为空") @Future(message = "截止时间必须晚于当前时间") private Date deadline; }

然后在Controller入口加 @Validated:

@PostMapping("/create") public Result<Long> create(@Validated @RequestBody ActivityCreateDTO dto) { ... }

这样Spring Boot会在进入Service之前完成参数校验,不合法直接返回400和message。注意@Future校验日期必须晚于当前时间,这防止了发布活动时把截止时间写成过去时间,直接把活动变成废团。

第二个边界是时间格式的入参。前端传JSON里的deadline如果传的是字符串2025-06-01 12:00:00,Spring Boot默认没法直接把字符串转成Date字段,必须在配置里设置Jackson格式化。第二章配置的spring.jackson.date-format解决的就是这个问题;如果不想全局改,也可以加@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")在DTO字段上。两处设置冲突时字段注解优先,这样避免改了全局把其他接口的时间格式也带偏。

接口调试完成后,一个容易被忽视的动作是关闭Swagger等接口文档在生产环境的暴露。把Knife4j的production环境开关关掉,不然活动列表、报名记录等接口被扫到后可能被恶意调用,这是安全上的血泪教训,尤其学生开发的项目经常挂在公网IP上被扫描。

5. Spring Boot组团平台排查手册:5个必踩的坑与修复路径

5.1 启动失败:端口占用与数据源连接被拒

现象:在IDE里启动项目,控制台刷出几行日志后报Port 8080 was already in use或者Failed to configure a DataSource。

原因:端口被其他进程占用,常见的有别的Java进程或前端devserver占着;数据源连接失败通常是 application.yml 里的URL、账号密码与本机MySQL不一致。

解决:端口问题看启动日志就能发现。换端口在 application.yml 里改server.port: 8081,或者命令行覆盖java -jar app.jar --server.port=8081。数据源问题先确认MySQL有没有启动,再看用户名密码是否匹配。排查占用进程的命令,Windows用netstat -ano | findstr 8080,macOS/Linux用lsof -i:8080,找到PID后按需结束进程或换端口。

5.2 接口返回500:Lombok和MyBatis映射的黑匣子

现象:接口没有任何业务异常日志,但返回500;或者报Invalid bound statement (not found)。

原因:第一类情况多是Lombok版本和JDK不匹配,生成不了getter/setter,Spring序列化时反射不到字段。第二类是Mapper接口和XML文件路径不一致——你写了 @Mapper 接口但对应的XML文件放在别的包,MyBatis按接口全限定名找不到映射语句。

解决:Lombok问题检查IDEA的Annotation Processing有没有开启,或者直接手写getter/setter验证是不是它在作怪。Invalid bound statement最稳的做法是确保Mapper接口和Mapper.xml同名同包,并在application.yml确认mapper-locations指向classpath:mapper/*.xml。这两类问题都属于配置层面的黑匣子,排查思路不是先看业务逻辑,而是回到构建工具和框架约定上。

5.3 报名接口不生效:事务注解失效的三个原因

现象:join接口返回成功,list接口也能看到新增报名记录,但刷新页面偶尔丢数据;或者明明代码里加了@Transactional却出现部分数据写入。

原因:Spring的声明式事务基于代理实现。三个典型失效场景是:方法被同类内部调用(this.join调用this.checkSomething,后面那个方法的事务注解不生效);异常被try-catch吃掉(方法里catch住Exception没抛出,事务不会回滚);@Transactional加在非public方法上,代理不拦截。

解决:同类内部调用要把事务方法拆到另一个Service类里注入调用,或者用TransactionTemplate手动控制。异常处理上始终坚持一个原则:外层方法只负责事务边界,业务代码抛出异常,由统一异常处理器转成HTTP响应,不要自己catch掉。非public方法上的事务注解直接移除。这三个原因占了事务不生效九成以上的场景,排查顺序就按这三条走。

5.4 日期时间差8小时:时区配置的玄学

现象:活动列表返回的deadline比数据库里存的小8小时或大8小时,前端显示的时间总是和预期对不上。

原因:JDBC连接串里的serverTimezone没设置或者设错了,Jackson序列化时又走了JVM默认时区,两边时区不一致导致读取后转换偏移。

解决:在数据源URL里加上serverTimezone=Asia/Shanghai,配合useLegacyDatetimeCode=false是MySQL 8.0下的常规组合。同时Spring Boot配置里spring.jackson.time-zone=GMT+8和spring.jackson.date-format=yyyy-MM-dd HH:mm:ss一起上。MySQL兼容模式下,也可以在URL里用serverTimezone=CTT,效果等同。配完重启,拿一条已知时间的记录验证无误后再继续后续工作,别凭感觉认为改完就好了。

5.5 打包后运行报错:JDK版本和Spring Boot版本不对齐

现象:本地mvn spring-boot:run跑得好好的,mvn clean package后java -jar启动,一启动就报UnsupportedClassVersionError或Spring容器上下文创建失败。

原因:本机安装多个JDK,IDE用的JDK11但命令行java用的JDK8,class文件版本超出JVM版本。或者spring-boot-maven-plugin的版本和Spring Boot parent版本不一致,打包出来的jar结构不完整,找不到主类。

解决:先看pom.xml里java.version的值,再运行java -version检查命令行实际用的JDK版本,把两个环境切到一致。打包后用unzip -l target/*.jar查看BOOT-INF/classes和主类是否存在,再用java -jar xxx.jar --debug看详细的自动配置报告。版本不齐导致的启动问题不是改业务代码能解决的,这类底层环境问题就要按环境问题去排查。

6. 上线前的最后技术细节:缓存与部署验证

6.1 给组团流程加一层缓存

热门活动开团瞬间的报名请求是最常见的瓶颈。行锁保证结果不超员,但每次都查库,数据库压力会快速累积。用Caffeine本地缓存扛住读流量是相对轻量的做法:

@Configuration public class CacheConfig { @Bean public CacheManager cacheManager() { CaffeineCacheManager manager = new CaffeineCacheManager("activity"); manager.setCaffeine( Caffeine.newBuilder() .expireAfterWrite(Duration.ofMinutes(5)) .maximumSize(1000)); return manager; } }

缓存过期时间5分钟、容量1000个,适合校级规模。关键注意点是报名成功后必须主动清除活动缓存,否则用户看到的人数还是旧的,要等5分钟过期才更新,这个体验很糟糕。用Spring Cache的 @CacheEvict 在报名事务里清掉当前活动键即可。缓存只做读优化,报名人数这种核心数字必须以数据库为准,这个原则不能含糊。

6.2 systemd托管jar,部署后跑一遍自检

本地跑通后部署到服务器,常用systemd管理Spring Boot进程,让它开机自启、崩溃自动拉起:

[Unit] Description=Campus Group Platform After=network.target [Service] ExecStart=/usr/bin/java -jar /opt/campus/campus-group.jar --server.port=8080 Restart=on-failure RestartSec=10 [Install] WantedBy=multi-user.target

Restart=on-failure让异常退出后10秒自动拉起,对可能因为内存或网络抖动挂掉的服务是保命符。ExecStart里写死JVM绝对路径,避免服务器上多版本JDK带来混乱。部署完我习惯跑一遍端到端自检:发布测试活动、用第二个账号报名、确认成团状态变化、再查数据库记录,全部通过才算真正交付。曾经有一次部署后报名接口一直超时,排查半天发现是防火墙没放行8080端口,请求全卡在握手阶段。那之后我把部署自检固定成四步:端口监听、接口连通、表存在、定时任务日志有输出,这四步能挡住九成低级问题,剩下的交给日志去查。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询