☰
基于Java+Spring Boot的社团活动网站源码设计与部署实战
2026/10/7 20:51:00 网站建设 项目流程

简介:基于Java的社团活动网站设计与实现完整源码,主要面向Java全栈开发者、高校毕业设计或课程设计人群,用于解决高校/机构内社团信息发布、活动报名、成员管理等活动组织效率低下的问题,采用前后端分离架构。压缩包共108个文件,大小约902KB,其中包含47个Java源文件处理后端业务与数据逻辑,22个Vue组件负责前端页面搭建,12个JavaScript脚本增强交互与动态渲染,另有XML、CSS、SQL、JSON等文件覆盖配置、样式与数据层面。项目目录组织清晰,文件命名规范,可读性与可维护性较好,目前已有262人学习浏览。开发者既能借此深入掌握Java后端开发细节,也能学习Vue等前端框架的动态网站构建思路,是一份适合作为社团管理平台改造或全栈技术进阶的实战参考资料。

1. 基于 Java 的社团活动网站设计与实现源码:别把它当成黑匣子

基于 Java 的社团活动网站设计与实现源码,听上去像是又一个毕业设计模板题,但它真正解决的是“活动发布、在线报名、后台管理”这一条完整业务链。很少有一个 Java 项目能像它这样,既能当课程设计交差,又能被改造成二手交易、竞赛报名甚至会议室预约系统。这套源码的核心不是花哨页面,而是几张表、几个接口和一组管理页面。我见过不少学生拿到源码跑不起来,问题往往不在代码本身,而是不知道 Web 容器怎么配、数据库初始化脚本放哪、JSP 和静态资源为什么 404。这篇笔记会把一套典型的 Java 社团活动网站从技术选型拆到部署排查,讲清楚表怎么建、登录和报名怎么写、上线前怎么验证,以及那些你迟早会踩的坑。适合刚学 Java Web 的初学者、准备二次开发的毕业生,以及想快速搭活动管理后台的社团技术负责人。

2. 社团活动网站的技术选型:为什么 Spring Boot + MyBatis 比 SSM 更省心

如果你手里拿到了多个版本的“社团活动网站源码”,会发现 Java 系的无外乎三种:纯 JSP+Servlet、SSM(Spring+Spring MVC+MyBatis)、Spring Boot+MyBatis。对社团活动这种业务边界清晰、并发不高的系统,我一般会直接选 Spring Boot + MyBatis。不是因为 SSM 不能跑,而是 SSM 的配置文件太多,启动排错成本高;Spring Boot 用自动配置和起步依赖把大部分样板代码收走了。这里的关键不是“哪个更高级”,而是“哪个更快跑通,出问题后更快定位”。我曾帮人调过一份 SSM 老源码,一本 spring-mvc.xml 里把<context:component-scan>写错扫了两遍路径,启动时不报错,注入时全是 null,这种黑匣子问题在 Spring Boot 里几乎不会出现。

2.1 从 Servlet 到 Spring Boot:Java Web 框架的三代变化

Java Web 的演进主线,是从 JSP+Servlet 的脚本式开发,到 Spring MVC 的注解驱动,再到 Spring Boot 的自动装配。对社团网站这个场景,JSP+Servlet 不是不能写,而是代码量翻倍:一个活动列表页要写一堆 out.println,登录校验每个 Servlet 重复 copy。SSM 解决了分层问题,但配置繁琐。Spring Boot 则把 Tomcat 内嵌进来,启动一个 main 方法就能看到网站,部署时又可以打成 war 包放到外部 Tomcat。这套组合对“拿源码自己改”的人来说是最友好的:依赖由 Maven 统一管理,配置集中在 application.yml,出问题时错误信息也能直接指向 bean 初始化还是数据库连接。

很多 Java 开发工程师面试题里会问“SSM 和 Spring Boot 的区别”,核心答案就是自动配置与起步依赖。自动配置是 Spring Boot 根据 classpath 下的依赖推断出你要干什么:加了 spring-boot-starter-web,它就帮你配好 DispatcherServlet 和 Jackson;加了 mybatis-spring-boot-starter,它就帮你扫描 Mapper 接口。你只需要关心业务代码,而不是那些 XML。社团网站源码如果给你的是 SSM 版本,也不是不能改造成 Spring Boot,但建议直接拿 Spring Boot 版做基础,省下至少一个下午的配置时间。

当然,这不是说不用理解底层原理。恰恰相反,正因为 Spring Boot 封装得太好,很多人连 DispatcherServlet 都不认识。但作为落地项目,我们追求的是先在本地跑起来,再往深处看源码。所以选型结论是:项目用 Spring Boot + MyBatis + Thymeleaf(如果要服务端渲染),接口层可以用 JSP 或模板引擎二选一。我见过一半以上的社团网站源码用的是 JSP,所以后面部署部分我同时兼容外部 Tomcat 的 war 部署。

2.2 可复现的项目结构:一个 Spring Boot 项目如何组织

拿到源码第一件事,不是双击启动类,而是看目录结构。一个标准的 Maven 工程,必须能区分 controller、service、mapper、entity、config 五层。下面这个结构是社团网站最常见的组织方式:

src/main/java/com/example/club/ ├── controller/ # 接收 HTTP 请求,返回 JSON 或页面 │ ├── ActivityController.java │ ├── UserController.java │ └── AdminController.java ├── service/ # 业务逻辑,事务边界写在这里 │ ├── ActivityService.java │ └── UserService.java ├── mapper/ # MyBatis 的 Mapper 接口,一个接口对应一个 XML │ ├── ActivityMapper.java │ └── UserMapper.java ├── entity/ # 数据库表对应的实体类 │ ├── Activity.java │ ├── User.java │ └── Club.java └── config/ # 拦截器、WebMvc 配置 └── LoginInterceptor.java src/main/resources/ ├── mapper/ # MyBatis 的 XML 文件,和 mapper 接口同包路径 │ ├── ActivityMapper.xml │ └── UserMapper.xml └── application.yml # 数据源、端口、文件上传等配置 src/main/webapp/ # 如果使用 JSP 作为视图层 ├── static/ # CSS、JS、图片 └── WEB-INF/views/ # JSP 页面

这个结构就是三层架构的一个具体实例。controller 层不做 SQL 拼接,service 层是业务逻辑的边界,mapper 层只管数据库读写。很多学生喜欢把业务逻辑写在 controller 里,比如在 getActivityList 方法里直接调 mapper,结果后面加一个“报名人数统计”功能就要把 controller 重写一遍。按这个结构,mapper 的 SQL 写在 XML 里,service 组合各种 mapper 方法,controller 只做参数收集和结果封装。源码阅读建议也按这个顺序:先 application.yml 看数据源和端口,再 entity 看字段,然后打开 Mapper XML 看 SQL,最后看 service 和 controller。这样两小时就能把整个项目摸清。

application.yml 里有几个必配项:spring.datasource.url、driver-class-name、username、password,以及 mybatis.mapper-locations。mapper-locations 写成 classpath:mapper/*.xml 是最常见配置,如果你的 XML 没放在 resources/mapper 下,启动后调用 Mapper 会报 Invalid bound statement,这个问题在第 4 章会专门说。另一个值得提前留意的参数是 server.servlet.context-path,如果配置了 /club,那么所有接口访问路径都要加 /club 前缀,部署到 Tomcat 后还要和 war 包名区分开,容易混乱。

2.3 数据模型设计:活动、用户、社团、报名四张核心表

社团活动网站的业务闭环是:用户加入社团,社团发布活动,用户在活动页报名,管理员导出名单。最少需要四张表:club、user、activity、activity_signup。下面是一份可以直接执行的初始化 SQL,字段命名用下划线,Java 实体里对应驼峰,MyBatis 开启 map-underscore-to-camel-case 后就能自动映射。

-- 社团表 CREATE TABLE `club` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `name` VARCHAR(64) NOT NULL, `description` TEXT, `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 用户表 CREATE TABLE `user` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `username` VARCHAR(32) NOT NULL UNIQUE, `password` VARCHAR(128) NOT NULL, -- 建议存 BCrypt 哈希,不要存明文 `real_name` VARCHAR(32), `role` TINYINT NOT NULL DEFAULT 1, -- 1 普通用户 2 管理员 `club_id` INT, FOREIGN KEY (`club_id`) REFERENCES `club`(`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 活动表 CREATE TABLE `activity` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `club_id` INT NOT NULL, `title` VARCHAR(128) NOT NULL, `content` TEXT, `location` VARCHAR(128), `start_time` DATETIME NOT NULL, `end_time` DATETIME NOT NULL, `max_people` INT NOT NULL DEFAULT 100, `remain_count` INT NOT NULL DEFAULT 100, `status` TINYINT NOT NULL DEFAULT 1, -- 1 报名中 2 进行中 3 已结束 4 已取消 FOREIGN KEY (`club_id`) REFERENCES `club`(`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 报名表 CREATE TABLE `activity_signup` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `activity_id` INT NOT NULL, `user_id` INT NOT NULL, `signup_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `status` TINYINT DEFAULT 1, -- 1 有效 2 取消 UNIQUE KEY `uk_activity_user` (`activity_id`, `user_id`), FOREIGN KEY (`activity_id`) REFERENCES `activity`(`id`), FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这里的核心设计点是 activity 表加了 remain_count 字段,默认等于 max_people,每次报名成功就减一。这是后面解决并发超卖的基础,比“先 select count(*) 再比较”要安全得多。activity_signup 表加了联合唯一索引 uk_activity_user,从数据库层面保证同一个用户不能重复报名。这两处是很多零散源码容易忽略的,逻辑上看着没问题,并发一压或者手滑点了两次提交就翻车。

数据模型还有个常见误区:用户表直接存明文密码,或者用 md5 不带盐。社团网站虽然不一定被攻击,但既然要做完整源码,就用 BCrypt。另外,字段类型上尽量用 utf8mb4,不要用 utf8,因为活动内容里很容易出现 emoji 表情,utf8 会报 1366 错误。如果你拿到的源码表里把报名用户存在 activity 的 signup_ids 字段里,用逗号分隔,建议尽早拆出单独的报名表,否则后面统计人数、取消报名都写不出简洁的 SQL。

3. 把登录、发布活动、在线报名写进源码:Controller + Service + Mapper 三层实现

3.1 登录接口与登录态拦截:Session 超时怎么处理

社团网站的前后端一般是同部署,登录用 Session 比 Token 简单得多。登录成功后把用户对象放进 HttpSession,后续接口通过拦截器判断 Session 里有没有 user,没有就重定向到登录页。下面是一个登录接口的典型写法:

@RestController @RequestMapping("/api/user") public class UserController { @Autowired private UserService userService; @PostMapping("/login") public Result login(@RequestBody LoginRequest req, HttpSession session) { User user = userService.login(req.getUsername(), req.getPassword()); if (user == null) { return Result.error("用户名或密码错误"); } // 登录成功,把用户ID、用户名、角色放进 Session // 注意不要存整个 password 字段,后续展示时也建议用 VO 过滤 session.setAttribute("loginUser", user); return Result.success(user); } @PostMapping("/logout") public Result logout(HttpSession session) { session.removeAttribute("loginUser"); return Result.success(); } }

这里的 userService.login 内部会先查用户,再用 BCrypt 校验密码。注意代码里直接返回了 User 对象,如果 User 类里有 password 字段,Jackson 序列化时会把它带出去。常见做法是在 User 的 password 字段上加 @JsonIgnore,或者返回一个只带 id、username、role 的 LoginResult。Session 默认超时时间由 server.servlet.session.timeout 控制,我一般给 30 分钟,太短了用户填完报名表单发现被踢出去,太长了后台管理有安全风险。

光有登录接口还不够,要拦截未登录的访问。实现一个 HandlerInterceptor:

public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(); if (session.getAttribute("loginUser") == null) { // 如果是 AJAX 请求,返回 401 让前端跳转;普通请求直接重定向登录页 if (request.getHeader("X-Requested-With") != null) { response.setStatus(401); } else { response.sendRedirect("/login"); } return false; } return true; } }

然后注册进 WebMvc 配置:

@Configuration public class WebConfig implements WebMvcConfigurer { @Autowired private LoginInterceptor loginInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor) .addPathPatterns("/**") .excludePathPatterns("/login", "/api/user/login", "/static/**", "/activity/list"); } }

这个配置里最容易翻车的是路径匹配。addPathPatterns("/") 匹配所有路径,excludePathPatterns 里的 /static/放行静态资源。很多源码忘了放行 /static,结果登录页 CSS 全挂。另外,如果项目是前后端分离,前端请求都带 X-Requested-With: XMLHttpRequest 头,那么 401 状态的处理要由前端统一拦截,不要在拦截器里 sendRedirect,否则 AJAX 会拿到一段 HTML。

3.2 活动发布接口:文件上传与数据入库的边界

管理员发布活动,通常不止文本字段,还要传一张封面图。上传文件的关键是“存哪里”和“怎么访问”。不要把图片存数据库 BLOB,而是存到本地磁盘目录,数据库只存相对路径。

@PostMapping("/activity/publish") public Result publish(@RequestParam("title") String title, @RequestParam("content") String content, @RequestParam("location") String location, @RequestParam("startTime") String startTime, @RequestParam("endTime") String endTime, @RequestParam("maxPeople") Integer maxPeople, @RequestParam(value = "cover", required = false) MultipartFile cover, HttpSession session) { User loginUser = (User) session.getAttribute("loginUser"); if (loginUser.getRole() != 2) { return Result.error("无权限"); } Activity activity = new Activity(); activity.setClubId(loginUser.getClubId()); activity.setTitle(title); activity.setContent(content); activity.setLocation(location); activity.setStartTime(LocalDateTime.parse(startTime, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); activity.setEndTime(LocalDateTime.parse(endTime, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); activity.setMaxPeople(maxPeople); activity.setRemainCount(maxPeople); activity.setStatus(1); if (cover != null && !cover.isEmpty()) { activity.setCoverUrl(fileStorageService.save(cover)); } activityService.publish(activity); return Result.success(); }

这段逻辑有几个参数细节:startTime 和 endTime 从前端传来的是字符串,用 DateTimeFormatter 解析成 LocalDateTime;如果前端传的是时间戳数字,就要用 Instant.ofEpochMilli 转换。maxPeople 同时赋给 max_people 和 remain_count,初始名额就是最大名额。权限判断用的是 Session 里的 role,这个字段在前端也可以展示,但真正的校验必须放在后端,否则有人直接 POST 接口就能发活动。

文件存储服务的参考实现如下:

@Service public class FileStorageService { @Value("${file.upload-dir:./uploads}") private String uploadDir; public String save(MultipartFile file) { // 用 UUID 重命名,避免中文文件名和路径问题 String original = file.getOriginalFilename(); String ext = original == null ? "" : original.substring(original.lastIndexOf(".")); String fileName = UUID.randomUUID() + ext; File dir = new File(uploadDir); if (!dir.exists()) { dir.mkdirs(); } try { file.transferTo(new File(dir, fileName)); return "/uploads/" + fileName; } catch (IOException e) { throw new RuntimeException("文件保存失败", e); } } }

如果使用 Spring Boot 内置 Tomcat 启动,/uploads 并不是默认静态资源路径,直接把 /uploads/xxx.jpg 写在 img 标签里会 404。需要在 WebConfig 加一行:

@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/uploads/**") .addResourceLocations("file:" + uploadDir + "/"); }

参数说明:uploadDir 默认是工程目录下的 ./uploads,生产环境部署后我一般会改成 Linux 绝对路径,比如 /data/club/uploads,并且保证该目录有写权限。否则用 Tomcat 部署 war 包时,uploads 目录会在临时解压目录里,重启 Tomcat 文件就没了,这是经典翻车点。

3.3 在线报名接口与“名额超卖”的修正

报名是社团网站要求最严格的接口,因为它涉及数据一致性。最直白的写法是“查剩余名额,大于 0 就插入”,但并发下两个请求同时查出来都是 1,就会都通过,最终超卖。正确做法是把扣减名额做成一个带条件的原子更新,再用受影响行数判断是否成功。

@Transactional public void signUp(Integer activityId, Integer userId) { Activity activity = activityMapper.selectById(activityId); if (activity == null || activity.getStatus() != 1) { throw new BusinessException("活动不存在或不在报名期"); } // 原子扣减:只有当 remain_count > 0 时才会更新成功 int rows = activityMapper.reduceRemainCount(activityId); if (rows == 0) { throw new BusinessException("名额已满"); } try { signupMapper.insert(activityId, userId); } catch (DuplicateKeyException e) { // 联合唯一索引兜底,防止重复报名时把名额也扣了 throw new BusinessException("你已经报名过该活动"); } }

对应 Mapper XML:

<update id="reduceRemainCount"> UPDATE activity SET remain_count = remain_count - 1 WHERE id = #{activityId} AND remain_count > 0 </update>

逻辑说明:reduceRemainCount 的 update 语句利用 MySQL 行锁,多个并发事务同时执行时,只有第一个能拿到行锁并把 remain_count 改掉,后续事务要么等锁,要么条件不满足返回 0。@Transactional 保证扣名额和插报名记录要么都成功,要么都回滚。如果 insert 因为唯一索引冲突抛出 DuplicateKeyException,事务回滚会把刚才扣掉的名额加回来,所以不需要在 catch 里手动 update 回滚,回滚会自动恢复。

这里有个必要前提:activity 表里得有 remain_count 字段。如果源码里没有,可以执行 ALTER TABLE activity ADD COLUMN remain_count INT NOT NULL DEFAULT 0,然后执行 UPDATE activity SET remain_count = max_people 把存量数据补上。另外注意,报名接口一定要校验活动状态 status,否则已取消的活动还能报名。还有,不要把状态判断放在前端,管理员下架活动后,老页面仍可能带着报名表单,后端校验是最后防线。

4. 社团活动网站源码里的常见问题与排查:5 个高频翻车点

下面这五个问题,是我在帮人调试社团网站源码时遇到概率最高的。每一条都按“现象、原因、解决”来说,你可以直接对照排查。

4.1 数据库连接池连接耗尽,网站突然卡死

现象:网站运行半天后,所有涉及数据库的接口全部超时,日志里刷 Connection is not available, request timed out after 30000ms。

原因:连接池最大连接数配得太小,或者代码里有数据库连接泄漏。很多旧源码用 C3P0 或 DBCP,配置里 maxPoolSize 只有 5,稍微有几个慢查询就把连接占满。还有人喜欢在业务代码里手动写 JDBC 工具类,开了 connection 不关,时间一长连接池被借空。

解决:如果项目用的是 Spring Boot + HikariCP,确保配置为:

spring: datasource: hikari: maximum-pool-size: 30 minimum-idle: 5 connection-timeout: 30000

如果源码是 SSM 里的 C3P0,我建议直接换掉,C3P0 在并发稍微上来后容易出现连接清理不及时。排查泄漏可以先看数据库 side 的 show processlist,看有没有大量 Sleep 状态的连接。代码层面重点查所有自定义 JDBC 工具,把 Connection、PreparedStatement、ResultSet 都用 try-with-resources 关闭。

4.2 静态资源 404,页面有内容但样式全乱

现象:启动后能访问登录页,HTML 骨架正常,但所有 CSS、JS、图片都 404,控制台一片红色。

原因:这套组件里最常见的,就是拦截器把 /static/** 也拦截了,或者 Spring Boot 没找到静态资源目录。另一个可能:项目是 JSP 方式打成 war 包部署到外部 Tomcat,静态资源放在 src/main/webapp/static 下,但 application.yml 里的 spring.mvc.static-path-pattern 被改成自定义值,导致默认映射失效。

解决:先看控制台有没有“No mapping for GET /static/css/common.css”之类日志。然后在 WebConfig 里确认 excludePathPatterns 包含了所有静态路径。如果用了自定义静态路径,要显式加资源映射:

@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/static/**") .addResourceLocations("classpath:/static/"); } }

注意 Tomcat 部署 war 时,资源映射路径要确认 classpath 正确。另外,如果 JSP 页面里引用的是相对路径 ../static/css,还要注意页面请求 URL 的层级,Chrome Network 里点一下请求路径就能判断是映射问题还是路径问题。

4.3 MyBatis 报 Invalid bound statement,接口刚调用就炸

现象:启动不报错,一旦调用某个 Mapper 方法就抛 BindingException: Invalid bound statement (not found): com.example.club.mapper.ActivityMapper.selectById。

原因:Mapper 接口和 XML 没有绑定成功。常见情况有三种:XML 文件的 namespace 不是接口全限定名;XML 没有放在 mapper-locations 扫描路径下;或者接口方法名在 XML 里的 id 不一致。有时还会遇到多模块项目里 XML 没被 Maven 打进 package。

解决:打开 Mapper 接口,按住 Ctrl 点进去,看 XML 是否存在。然后用文本编辑器核对第一行:

<mapper namespace="com.example.club.mapper.ActivityMapper">

再检查 application.yml:

mybatis: mapper-locations: classpath:mapper/*.xml

这里 classpath:mapper/*.xml 是相对 resources 根目录的写法,如果 XML 实际放在 src/main/java 下而不是 resources 下,编译后不会出现在 classes 里,必须把 XML 移到 resources/mapper。另外,如果项目里用了 MyBatis-Plus,还要注意 Mapper 接口上是否加了 @Mapper 注解,或者在启动类加了 @MapperScan,扫描路径写错也会报相同错误。

4.4 中文乱码:数据库、请求、响应三处不一致

现象:表单提交“迎新晚会”后,库里存的是“???”,或者页面上展示出来一半乱码。

原因:三个环节至少有一个用了非 UTF-8。最常见的是 JDBC 连接 URL 没指定 characterEncoding,其次是 MySQL 表字符集不是 utf8mb4,然后是 JSP 页面编码或者 Tomcat 的 URI 编码不对。如果是 POST 请求,Tomcat 8 以后默认 UTF-8,但老项目里可能有 CharacterEncodingFilter 被重复配置覆盖。

解决:在 application.yml 里把连接 URL 写完整:

url: jdbc:mysql://127.0.0.1:3306/club_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai

MySQL 8 还要注意 driver-class-name 要用 com.mysql.cj.jdbc.Driver。表字符集用 SHOW CREATE TABLE 检查,需要改就执行 ALTER TABLExxxCONVERT TO CHARACTER SET utf8mb4。如果页面是 JSP,检查 pageEncoding 是否 UTF-8;如果部署在外部 Tomcat,在 server.xml 的 Connector 里加上 URIEncoding="UTF-8"。排查时依次检查请求日志、数据库存储、响应头 Content-Type,就能定位乱码发生在哪一段。

4.5 报名名额超卖,一个活动报了 120 人

现象:max_people 设 100,最终报名记录 120 条,甚至同一用户有多条报名记录。

原因:代码用了“先 select count 再判断”的逻辑,在高并发下多个请求同时读到 99,然后一起 insert。另一个原因是报名表没有联合唯一索引,重复提交也不会报错。

解决:先给 activity_signup 表加唯一索引:

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

再把报名方法改成第 3 章里的原子扣减方式,using update activity set remain_count = remain_count - 1 where remain_count > 0。这两个改动一起做,基本能杜绝超卖。做完后模拟并发:用 IDEA 的 HTTP 脚本或 JMeter 发 50 个并发请求,观察最终报名记录数与 remain_count 之和是否等于 max_people。如果不一致,几乎可以确定是代码里还有别的地方直接改了 remain_count。

5. 把源码部署到服务器:Maven 打包、Tomcat 上线和验证命令

5.1 环境准备:先统一 JDK、Maven、MySQL 的版本搭配

源码能本地跑,不代表能部署到服务器。最常见的问题是版本不一致导致的玄学错误。我一般用下面这套组合:JDK 8 或 11,Maven 3.6 以上,MySQL 5.7 或 8.0,Tomcat 9。如果你的源码是 Spring Boot 2.x,JDK 8 足够;如果是 Spring Boot 3.x,那必须 JDK 17。看到这里你可能想骂“版本匹配怎么又是玄学”,其实规则很简单:看 pom.xml 里 spring-boot-starter-parent 的版本号,2.x 用 JDK8,3.x 用 JDK17。不要盲目装最新版 JDK,不然启动时直接报 UnsupportedClassVersionError。

部署前先检查这三条命令的输出:

java -version mvn -version mysql --version

我整理了一个版本要求表,按这个配能省掉一半启动问题:

组件建议版本说明
JDK1.8 或 11对应 Spring Boot 2.x;3.x 用 17
Maven3.6+3.8+ 对仓库镜像配置更严格
MySQL5.7 / 8.08.0 驱动名变化,见 5.2
Tomcat9.0.x如果打 war 包部署才需要

5.2 修改 application.yml 和初始化数据库

拿到源码后,先把数据源改成你本机的库名、账号和密码。不要用 root 空密码跑生产,最少也要设一个专用账号。下面是 Spring Boot 2.x 常见的配置,注意 MySQL 8 的驱动名和时区:

server: port: 8080 servlet: context-path: / session: timeout: 30m spring: datasource: url: jdbc:mysql://127.0.0.1:3306/club_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: club_admin password: 换成你的密码 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.club.entity

逻辑说明:context-path 会影响所有接口前缀;如果留空就是根路径。driver-class-name 在 MySQL 5.7 用 com.mysql.jdbc.Driver,8.0 用 com.mysql.cj.jdbc.Driver。用错会报 ClassNotFound。然后初始化数据库,在 MySQL 里执行源码带的 .sql 文件:

mysql -uclub_admin -p club_db < club.sql

执行完用 SHOW tables; 确认四张核心表都存在。不要跳过这步,很多用户直接改完配置就跑,结果启动时 Hibernate 或 MyBatis 建表失败,再回来查才发现少执行了初始化脚本。

5.3 Maven 打包、war 部署和 curl 验证

配置改好后开始打包。在项目根目录执行:

mvn clean package -DskipTests

参数说明:clean 清掉旧文件,package 打包,-DskipTests 跳过测试,因为社团网站源码里的测试往往依赖本地数据库,跑不过会打断打包。打包成功后 target/ 目录下会生成 war 或 jar。如果是 war,把它复制到 Tomcat 的 webapps 目录:

cp target/club-website.war /opt/tomcat/webapps/ /opt/tomcat/bin/startup.sh

Tomcat 启动时会自动解压 war。注意:war 包的文件名就是上下文路径,比如 club-website.war 对应的访问前缀是 http://ip:8080/club-website/。如果不想要前缀,把 war 命名为 ROOT.war。这两者的区别会让前端 JS 请求接口路径差出 /club-website,很多人部署完页面 404 就是这里没对应上。

启动后不要急着点页面,先用 curl 做接口冒烟测试:

# 1. 检查登录页是否返回 200 curl -I http://127.0.0.1:8080/club-website/login # 2. 登录并保存 Cookie curl -c cookies.txt -X POST http://127.0.0.1:8080/club-website/api/user/login \ -H "Content-Type: application/json" \ -d '{"username":"admin","password":"123456"}' # 3. 携带 Cookie 发布活动 curl -b cookies.txt -X POST http://127.0.0.1:8080/club-website/activity/publish \ -d "title=迎新晚会&content=第一次迎新&location=礼堂&startTime=2025-09-01 19:00:00&endTime=2025-09-01 21:00:00&maxPeople=100"

这里用 -c 保存 Cookie,-b 发送 Cookie。如果登录接口要求 JSON,注意 -d 的 Content-Type;如果要求 form-urlencoded,不用加 -H 也行。curl 返回的 JSON 里如果包含 code=200 且 data 里有 JSESSIONID,说明登录通过。发布活动返回成功后再去数据库查一条记录,确认写入字段都正确。这一步能挡住 80% 的接口路径问题。

提示:curl 的 -c 和 -b 只是最简单的 Cookie 处理方式,如果换到 HTTPS 环境,还要注意证书参数,或者直接用浏览器开发者工具验证。

如果你用的是 Spring Boot 内置 Tomcat 打成 jar,部署命令更简单:nohup java -jar club-website.jar --server.port=8080 即可。但要注意 jar 包内静态资源和上传文件路径的处理方式不同,第 3 章的 addResourceHandlers 配置要对应实际路径。

6. 给社团活动网站加一层慢查询监控:用 MySQL 慢日志和 Spring AOP 找到最耗时的接口

网站上线之后,最折磨人的不是功能报错,而是“感觉变慢了”却不知道慢在哪。我习惯在上线第二周做一次性能体检,第一件事就是开 MySQL 慢查询日志。登录服务器执行:

SET GLOBAL slow_query_log = ON; SET GLOBAL long_query_time = 2; SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';

这个方案不用重启数据库,也不用改动源码,跑一天后看 slow.log,凡是超过 2 秒的 SQL 都会在里面。注意 SET GLOBAL 在 MySQL 重启后会失效,生产环境记得把这三行写进 my.cnf 的 [mysqld] 配置段。

慢日志只能告诉我们 SQL 有问题,如果要定位到具体接口,我还会在 Controller 层加一个 AOP 切面,统计每个接口的耗时:

@Aspect @Component public class TimeLogAspect { @Around("execution(* com.example.club.controller.*.*(..))") public Object logTime(ProceedingJoinPoint pjp) throws Throwable { long start = System.currentTimeMillis(); Object result = pjp.proceed(); long cost = System.currentTimeMillis() - start; if (cost > 500) { System.out.println(pjp.getSignature().toShortString() + " 耗时 " + cost + "ms"); } return result; } }

这个切面的关键是 @Around 和 ProceedingJoinPoint。pjp.proceed() 放行原方法,前后分别计时。超过 500ms 的打印到应用日志里。配合慢 SQL 日志,就能把“活动列表页慢”和“一条 select * from activity where ... limit 10 执行了 3 秒”对应起来。

我吃过一次亏:某次帮人调的社团网站,用户反馈活动列表越来越慢,我一开始怀疑数据库问题,结果慢日志里没有一条 SQL,后来加了 AOP 切面才发现接口每天定时生成 Excel 统计报表,一次性查出全表数据还做了 BigInteger 拼接。AOP 日志把这个 8 秒的耗时暴露出来后,改成异步生成文件才解决。这个教训让我现在上线任何 Java Web 项目,第一周就加上慢 SQL 和接口耗时监控,不要等用户来截图才想起来。希望帮到你。

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

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

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

立即咨询