每年到了毕设季,我都能看到一大波同学在群里问“JavaWeb毕业设计做什么题目好”。说实话,高校志愿者管理系统这个题,属于JavaWeb方向里既稳妥又有话可说的经典选题。它不像电商系统那么烂大街,又比图书管理、学生管理这类纯CRUD多了不少业务深度,报名审核、时长统计、活动流程、多角色权限这些点都能展开讲,答辩时也不愁没东西聊。
这个项目到底怎么从零搭起来,技术栈怎么选,数据库怎么设计,哪些地方容易踩坑,我按自己做过的思路完整拆一遍。无论你是打算直接照着做一个,还是只在论文里写设计思路,这篇都能拿来当参考。
1. 项目整体设计与拆分思路
1.1 志愿管理系统到底在解决什么问题
先别急着写代码,把业务想清楚比什么都重要。高校志愿者管理,表面上就是“发活动、报名活动、记时长”,但在实际场景里,有大量细碎的问题需要系统去处理:活动发布方可能是校青协、院系分队、社团,活动状态要有人跟进;志愿者报名之后还需要审核、签到、签退、补录时长;到了期末或者评优的时候,又需要按院系、按时间维度去统计时长和参与次数。另外,现在很多学校对第二课堂学分有要求,志愿服务时长是其中一个硬性指标,这就意味着数据必须可追溯、可导出、可审核,不能让一个普通学生随意改自己的时长记录。
所以一个好的志愿者管理平台,至少要覆盖三个角色:管理员(校青协负责人)、活动发布者(可以是管理员兼任或单独角色)、普通学生(志愿者)。管理员管全局,活动发布者管自己发起的活动,学生负责报名和查看自己的服务记录。把这条主线理清楚,系统的功能边界就出来了。
1.2 功能模块的前置划分
我在设计功能模块时喜欢先列一个“角色-功能”对照表,画在纸上比记在脑子里清晰得多:
| 角色 | 核心功能 |
|---|---|
| 学生 | 注册登录、浏览活动、报名/取消报名、查看审核状态、查看个人时长与积分 |
| 活动发布者 | 创建活动、管理报名名单、签到签退登记、录入时长、发布活动公告 |
| 系统管理员 | 用户管理、活动审核、时长记录审计、数据统计、系统公告、基础数据配置 |
从这张表再往下拆,就是数据库的表结构和接口设计。比如学生注册时是普通账号,管理员可以设定一个“学生证号唯一”的校验逻辑,同时存入院系、年级这些基础信息,方便后续按学院统计时长分布。
1.3 核心流程的设计要点
系统中有一条最核心的业务闭环:管理员发布活动 → 学生报名 → 活动方审核 → 活动开始 → 签到签退 → 时长录入 → 学生查看记录。这里的每一步都需要一个状态字段来记录,活动表里要有类似status 0草稿 1报名中 2进行中 3已结束 4已取消这样的状态流转;报名表里要有status 0待审核 1已通过 2已拒绝 3已取消。千万不能省掉状态字段,否则后期统计和权限控制都会变得很别扭。
从毕设的角度看,这个业务闭环非常完整,从前端页面到后端接口到数据库设计都能扣上,论文里写“系统设计”和“系统实现”部分时会非常顺畅,这是我建议选这个题目的核心原因。
2. 技术选型与项目初始化
2.1 为什么我坚持用SpringBoot 2.7.x而不是3.x
搜索“springboot版本太高”的同学这两年明显变多了,原因就是3.x坑了一大批人。SpringBoot 3.0之后强制要求JDK 17及以上,很多同学本地环境还是JDK 8,换版本又容易引发Maven依赖冲突。而且网上能找到的教程、笔记、开源代码,大部分还是基于SpringBoot 2.x写的,2.7.x正好是2.x的最后一个稳定版本,生态最成熟,遇到问题随便一搜就有答案。
我在项目里选的是SpringBoot 2.7.18,配合JDK 1.8,这是目前最稳的毕设组合。
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <relativePath/> </parent> <properties> <java.version>1.8</java.version> </properties>顺便提一句,如果你是用IDEA创建SpringBoot项目,初始界面默认给的SpringBoot版本可能会比较高(比如3.x或4.x),记得手动把版本改成2.7.18再勾选依赖,否则后续会出现一堆莫名其妙的编译错误。
2.2 次要依赖的选择:MyBatis-Plus、JWT、Knife4j
- MyBatis-Plus:毕设阶段最友好的持久层框架。相比原生MyBatis,它能少写大量XML,分页查询、条件构造器都是现成的,尤其适合一个人短时间开发。
- JWT:前后端分离场景下最省事的认证方案。相比Spring Security自带的Session登录,JWT是无状态的,后端不需要存session,前端拿到Token之后放在请求头里即可。毕设答辩时还能讲清楚“无状态认证”的概念,加分。
- Knife4j:Swagger的增强版,接口文档自动生成,写完接口之后直接打开浏览器看API文档,联调和答辩演示都方便。
- Lombok:减少getter/setter的样板代码,这个不用多解释,属于Java开发标配。
- Redis:如果只做毕设,Redis不是必须的。但我建议至少接一个验证码存储或活动热点数据的缓存,哪怕用最简单的场景,也能在论文里体现出你的技术广度。如果机器内存紧张,可以先跳过。
前端方面,我建议用Vue 3 + Element Plus + Axios + ECharts,这是目前主流方案。如果你前端基础薄弱,也可以直接用模板渲染方案,比如Thymeleaf,但我个人不太推荐,因为现在高校毕设答辩越来越看重前后端分离的架构意识,Vue写出来的效果也好看得多。
2.3 从IDEA新建项目到跑通第一个接口
这里我简单复盘一下标准步骤,给还没建过项目的同学参考:
- 打开IDEA,选择
Spring Initializr,JDK选1.8,Java版本对应8。 - 在依赖界面勾选
Spring Web、MySQL Driver、Lombok。暂时不用勾MyBatis-Plus,因为它的starter不在这份列表里,需要手动加依赖。 - 生成项目后在
pom.xml里手动加入MyBatis-Plus和JWT相关依赖。 - 在
application.yml里配置数据源和MyBatis-Plus的日志。 - 写一个
HelloController测试,启动项目访问http://localhost:8080/hello,看到返回JSON即算成功。
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/volunteer_system?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver 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这个配置里有两个容易被忽略的点。第一是serverTimezone=Asia/Shanghai,MySQL 8驱动要求必须指定时区,否则连接会报错。第二是逻辑删除,我建议所有业务表都加一个deleted字段,用MyBatis-Plus的逻辑删除能力,删数据时实际上走的是UPDATE,这样能保留操作痕迹。
补充一个小众但实用的技巧:很多人嫌弃SpringBoot启动时那几行Banner太枯燥,可以去在线Banner生成器上弄一个自定义的ASCII艺术字,放在src/main/resources/banner.txt里面。这个细节虽然不影响功能,但在答辩演示时会让老师觉得你项目做得很用心。
3. 数据库设计:把每个字段都抠明白
3.1 核心表的数量和关系
数据库是整个系统的地基,我见过太多毕设因为表设计潦草,后面越写越乱。志愿者管理系统我的建议是至少设计6张核心表:
sys_user:用户主表,包含学生端和管理员端账号。user_profile:用户扩展信息表,比如学号、院系、专业、年级。也可以直接合并进主表,但分开写更规范。activity:活动表,保存活动基本信息和状态。activity_registration:报名表,记录学生报名行为和审核结果。service_hour_record:服务时长记录表,每一条记录都必须关联活动和用户。notice:公告表,用于管理员发布系统公告。
如果想让系统更丰满,还可以加activity_category(活动分类)、role(角色表)等,但毕设阶段这6张表足够支撑起整个业务闭环。
下面我贴几个关键表的建表SQL,大家可以直接改改字段名使用:
CREATE TABLE `activity` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `title` varchar(100) NOT NULL COMMENT '活动标题', `description` text COMMENT '活动详情', `category_id` bigint(20) DEFAULT NULL COMMENT '活动分类ID', `max_participants` int(11) DEFAULT 50 COMMENT '报名人数上限', `current_participants` int(11) DEFAULT 0 COMMENT '已报名人数', `service_hours_per_session` decimal(5,2) DEFAULT 0.00 COMMENT '单次可获得服务时长', `location` varchar(200) DEFAULT NULL COMMENT '活动地点', `activity_start_time` datetime DEFAULT NULL, `activity_end_time` datetime DEFAULT NULL, `registration_deadline` datetime DEFAULT NULL, `status` tinyint(4) DEFAULT 0 COMMENT '0草稿 1报名中 2进行中 3已结束 4已取消', `publisher_id` bigint(20) DEFAULT NULL COMMENT '发布者ID', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `deleted` tinyint(1) DEFAULT 0, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;CREATE TABLE `activity_registration` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `activity_id` bigint(20) NOT NULL, `user_id` bigint(20) NOT NULL, `status` tinyint(4) DEFAULT 0 COMMENT '0待审核 1已通过 2已拒绝 3已取消', `audit_time` datetime DEFAULT NULL COMMENT '审核时间', `audit_remark` varchar(255) DEFAULT NULL COMMENT '审核备注', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `deleted` tinyint(1) DEFAULT 0, PRIMARY KEY (`id`), UNIQUE KEY `uk_activity_user` (`activity_id`, `user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;3.2 报名表唯一索引背后的深意
activity_registration里的唯一索引uk_activity_user是我特别想强调的一个点。表面上它是为了避免同一个用户重复报名同一个活动,但更深层的作用是保证并发场景下的幂等性。假设页面卡了一下,用户连续点了几次报名按钮,如果没有这个唯一索引,就非常有可能插入多条报名记录,后面的签到、时长统计全部跟着乱掉。
所以哪怕你在代码里做了“先查后插”的判断,也一定要在数据库层面加这层保险。真正可靠的系统,应该是多道防线同时起作用。
3.3 字段类型与冗余字段的设计心得
数据库字段设计上没有绝对的对错,但有些经验是通用的。比如时间字段建议用datetime而不是timestamp,因为timestamp有2038年的上限,毕设虽然不会跑到那时候,但这个细节被老师问到时能答出来就是加分项。再比如描述类型用text,既能存长文本,也不会拖慢主表查询。
在冗余设计上,我建议给左右两个场景留余地:一是统计类的冗余,比如user_profile表里可以加一个total_service_hours字段,每次时长记录审核通过时同步更新,这样个人主页查询时长时就不需要每次都做SUM聚合查询,数据量大了以后这种优化非常关键。二是展示类的冗余,比如报名列表接口里会经常用到活动标题、用户姓名,如果只存了ID,每次都要JOIN两张表,比较繁琐,可以在冗余字段里直接存一份,代码会简单很多。当然冗余也要有限度,不能什么字段都往里塞,一般只冗余最常用的展示字段。
4. 后端核心模块与实现技巧
4.1 统一返回结果与全局异常处理
写后端接口最忌讳的就是每个接口返回结构都不一样。你想想,前端拿到数据时一会儿是{code:200, data: {...}},一会儿是{success: true, result: ...},联调阶段光是类型判断就能把人逼疯。所以项目一开始就要定义好统一返回体,后面所有Controller都基于它来返回。
@Data public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("success"); result.setData(data); return result; } public static <T> Result<T> error(Integer code, String message) { Result<T> result = new Result<>(); result.setCode(code); result.setMessage(message); return result; } }全局异常处理则用@RestControllerAdvice来实现,把业务异常、参数校验异常、系统异常分别捕获,转换成统一的Result格式返回。这样做的好处是,即使某段代码忘记做异常捕获,前端也不会收到一大堆英文堆栈信息,体验会好很多。
4.2 基于JWT的登录认证与权限控制
登录模块我推荐用JWT + 拦截器的方案,比引入Spring Security全家桶更轻量,也很容易讲清楚。具体流程是:用户提交用户名密码,后端校验通过后生成一个Token,Token里可以装userId、username、role这些信息,然后返回给前端。之后前端每次请求都在Authorization请求头里带上这个Token,后端通过拦截器解析Token,拿到当前登录用户的信息。
Token生成我使用io.jsonwebtoken库:
@Component public class JwtUtil { @Value("${jwt.secret}") private String secret; @Value("${jwt.expire}") private Long expire; public String generateToken(Long userId, String username, String role) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim("username", username) .claim("role", role) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + expire)) .signWith(SignatureAlgorithm.HS256, secret) .compact(); } public Claims parseToken(String token) { return Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); } }权限控制上,我建议先做一个自定义注解@RequireRole,然后在拦截器中读取注解里的角色值,和JWT里解析出来的角色做比对。比如活动发布接口标了@RequireRole("ADMIN"),那普通学生就没法调用。这样比在每个Controller方法里手动if判断要优雅得多,答辩时也更容易展示你对AOP思想的理解。
注意:JWT的密钥不要硬编码在代码里,要放到application.yml中,答辩时老师问到配置分离也能有话讲。另外Token过期时间不要设置太长,一般24小时就够,配合前端在401时跳转登录页即可。
4.3 活动报名与人数控制:并发和事务的取舍
活动报名是系统里最容易出bug的地方。假设一个活动报名上限是100人,第100和第101个人同时点击报名,如果你的代码是先查出当前人数,再加1写回,两个请求都查到99,都认为没满,结果就有101个人报名成功了。这就是经典的并发问题。
解决方式有几种:一种是给活动表加一个版本号用乐观锁,一种是直接使用数据库的原子更新语句。SpringBoot + MyBatis-Plus环境下,我推荐用UpdateWrapper做原子更新:
@Override @Transactional(rollbackFor = Exception.class) public Result<Void> registerActivity(RegisterRequest request) { Long activityId = request.getActivityId(); Long userId = UserContext.getUserId(); // 1. 查询活动信息 Activity activity = activityMapper.selectById(activityId); if (activity == null) { throw new BizException("活动不存在"); } if (activity.getStatus() != 1) { throw new BizException("当前活动不在报名时间内"); } // 2. 原子更新已报名人数,防止并发超卖 UpdateWrapper<Activity> updateWrapper = new UpdateWrapper<>(); updateWrapper.eq("id", activityId) .eq("current_participants", activity.getCurrentParticipants()) .lt("current_participants", activity.getMaxParticipants()) .setSql("current_participants = current_participants + 1"); int update = activityMapper.update(null, updateWrapper); if (update == 0) { throw new BizException("报名人数已满,请报名其他活动"); } // 3. 写入报名记录(利用唯一索引兜底) ActivityRegistration registration = new ActivityRegistration(); registration.setActivityId(activityId); registration.setUserId(userId); registration.setStatus(0); try { registrationMapper.insert(registration); } catch (DuplicateKeyException e) { throw new BizException("您已报名过该活动,请勿重复报名"); } return Result.success(null); }这段代码里有几个关键点。第一是整个方法加了@Transactional,确保报名人数+1和插入报名记录要么都成功要么都失败。第二是更新语句里带上了current_participants的相等条件,这是乐观锁的思路,如果两个请求同时来,只会有一个更新成功,另一个affected rows为0,就会抛“人数已满”异常。第三是插入报名记录时依靠数据库唯一索引捕获重复报名,避免代码层面先查后插的竞态窗口。这三点结合,基本可以把并发下的常见问题全部堵死。
时长录入也是同样的思路。活动结束后负责人给志愿者录时长,每一条service_hour_record记录都需要包含活动ID、用户ID、时长数值、录入人ID。并且要做一个限制:同一个活动同一个人只能有一条有效时长记录,靠唯一索引保证。
4.4 定时任务让活动状态自动流转
活动状态不能一直靠人工改。比如报名截止时间到了,状态应该自动从“报名中”变成“进行中”;活动结束时间过了,应该自动变成“已结束”。这种逻辑可以用SpringBoot自带的@Scheduled定时任务实现,非常简单:
@Component @Slf4j public class ActivityStatusTask { @Resource private ActivityMapper activityMapper; @Scheduled(cron = "0 */5 * * * ?") public void autoUpdateActivityStatus() { // 自动开始:到了开始时间且状态为“报名中”的活动 LocalDateTime now = LocalDateTime.now(); UpdateWrapper<Activity> startWrapper = new UpdateWrapper<>(); startWrapper.eq("status", 1) .le("activity_start_time", now) .set("status", 2); activityMapper.update(null, startWrapper); // 自动结束:过了结束时间且状态为“进行中”的活动 UpdateWrapper<Activity> endWrapper = new UpdateWrapper<>(); endWrapper.eq("status", 2) .le("activity_end_time", now) .set("status", 3); activityMapper.update(null, endWrapper); } }注意启动类上要加@EnableScheduling注解,否则定时任务不会生效。这段逻辑可以放到“系统工具类”或者“任务调度包”下,代码量不多,但对系统的完整性提升是肉眼可见的。
4.5 文件上传:活动图片与头像存储
如果系统允许发布活动时上传封面图、用户上传头像,就需要一个文件上传接口。我的建议是不要把文件保存进数据库字段,而是存在服务器的某个目录里,数据库只保存访问路径。实现方式也很直接:配置一个虚拟路径映射,把http://localhost:8080/files/**映射到本机的/usr/local/volunteer/files/目录,上传接口用MultipartFile接收文件,然后用UUID生成新文件名,避免重名覆盖。
存储路径的配置注意放在application.yml里,不要硬编码:
file: upload-path: /usr/local/volunteer/files/ access-path: /files/**跨域问题也要提前处理。前后端分离开发时,前端在http://localhost:5173,后端在http://localhost:8080,两者端口不同,必然会有跨域请求。最简单的方案是写一个WebMvcConfigurer配置类,添加跨域映射规则,允许所有来源、所有请求头、所有方法。如果后期用Nginx部署了,还可以在Nginx层面做反向代理,把/api转发给后端服务,这样前端请求的都是同域的/api路径,不跨域。
5. 前端页面设计与数据可视化
5.1 页面结构和路由划分
前端用Vue3的话,建议页面结构分成三块:登录注册页、学生端主界面、管理端主界面。路由上可以用vue-router,通过动态路由或登录后的角色判断来区分前后台。
管理端的页面可以这样划分:
- 仪表盘:展示活动总数、志愿者人数、服务总时长、待审核报名等核心指标,配合ECharts图表展示近一月活动数量趋势。
- 活动管理:活动列表、创建活动、编辑活动、下架活动。
- 报名审核:待审核列表、通过/拒绝操作。
- 时长管理:时长记录列表、录入、修正。
- 用户管理:学生列表、启用/禁用账号、重置密码。
- 公告管理:发布公告、公告列表。
学生端则简单很多:首页看活动列表,点进详情看活动内容、剩余名额、截止时间;个人中心看已报名的活动、审核状态、自己的服务时长记录。
5.2 Axios封装与Token自动携带
前端交互的核心是Axios,建议在项目里封装一个统一的请求工具。比如在请求拦截器里统一加上JWT Token:
import axios from 'axios' const request = axios.create({ baseURL: '/api', timeout: 10000 }) request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) request.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { ElMessage.error(res.message || '请求失败') return Promise.reject(new Error(res.message)) } return res }, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token') router.push('/login') } ElMessage.error(error.response?.data?.message || '网络异常') return Promise.reject(error) } ) export default request这段代码有一个设计细节值得注意:返回给组件的是res而不是res.data。这样业务组件里写const { data } = await request.get('/activity/list')就够了,不需要每次都手动剥一层code和message,实际写页面时会清爽很多。
5.3 数据可视化:用ECharts汇报成果
仪表盘页面是答辩的亮点环节,建议用ECharts展示3到4个图表:饼图展示志愿者院系分布,柱状图展示每月新增活动数,折线图展示每月累计服务时长。ECharts的Vue封装可以用vue-echarts,也可以直接用原生ECharts,在onMounted里初始化图表实例。数据接口方面,后端写一个统计Controller,用SQL聚合查询返回结果。
比如按院系统计志愿者人数的SQL可以这样写:
@Override public List<Map<String, Object>> countByCollege() { return userMapper.selectMaps(new QueryWrapper<UserProfile>() .select("college, COUNT(*) as count") .groupBy("college")); }MyBatis-Plus的selectMaps返回的结果是List<Map<String, Object>>,前端拿到直接渲染。这里有个小坑提醒:selectMaps在MySQL里别名会变成map的key,所以SQL里最好写清楚COUNT(*) as count,否则返回的key可能跟你预期不一致。
5.4 前后端联调时的注意事项
联调阶段最容易出问题的不是业务逻辑,而是一些基础配置。比如IDEA中后端启动在8080端口,Vue3默认是5173端口,这时需要在vite.config.js里配置代理,否则Ajax请求会404:
export default defineConfig({ server: { port: 5173, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, rewrite: path => path.replace(/^\/api/, '') } } } })这样前端请求/api/activity/list会被代理到http://localhost:8080/activity/list,前端代码里不需要写死后端地址。如果你没有配置代理,前端代码里直接写死http://localhost:8080也可以跑通,但配置了代理后,上线部署时只需要改Nginx,不用改前端代码,灵活性会高很多。
6. 常见问题排查与答辩重点准备
6.1 运行期最常见的几个报错
从我的经验来看,毕设项目跑不起来,90%是环境问题而不是代码问题。这里整理一份高频踩坑清单,基本上涵盖了大部分队伍遇到的问题:
| 报错现象 | 根因 | 解决方案 |
|---|---|---|
Access denied for user 'root'@'localhost' | MySQL密码错误或账号无权限 | 检查application.yml中的数据库密码,确认端口3306 |
Unknown database 'volunteer_system' | 数据库不存在 | 先在MySQL里执行create database volunteer_system |
Failed to configure a DataSource | application.yml中数据源配置缺失 | 检查配置文件是否被IDEA识别为资源文件 |
java.sql.SQLException: The server time zone value | 未指定时区 | 在JDBC URL最后加serverTimezone=Asia/Shanghai |
Invalid bound statement (not found) | Mapper接口与XML不匹配 | 检查Mapper接口包路径和@MapperScan是否一致 |
Port 8080 was already in use | 端口被占用 | 用netstat -ano查PID杀掉进程,或改server.port |
JWT解析报JWT signature does not match | 密钥不一致 | 确认所有服务实例使用相同的jwt.secret |
6.2 数据库字段和SQL引发的隐藏问题
还有一种报错排查起来更头疼,就是接口能通但数据不对。比如前端显示活动列表正常,但报错申请时提示“已满”,查了下数据库明明只有10人报名,活动上限写的是50。这种问题多半是current_participants字段没有正确维护,比如手动往报名表插了数据,但没有同步更新活动表的字段。解决思路是不要直接改数据库,所有报名和取消报名都走后台Service,保证字段一致性。
另一个常见问题是时间差八小时。MySQL连接中如果不设置serverTimezone=Asia/Shanghai,默认会使用服务器时区,如果服务器是UTC,存进去的时间就会差8小时。这个在前面配置文件里已经加了,但如果你有用到Jackson序列化时间,还需要在application.yml里配置统一的日期格式:
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+86.3 答辩前必须吃透的几个问题
很多同学项目做完了,但答辩时被老师一问就卡壳。我根据经验整理几个高频问题,大家提前把答案组织好:
为什么选SpringBoot?答:SpringBoot简化了Spring的配置流程,内置Tomcat,自动装配机制大大提升开发效率。同时它和Spring生态天然兼容,适合快速开发和迭代。
MyBatis-Plus和MyBatis的区别?答:MyBatis-Plus是MyBatis的增强工具,内置通用Mapper和通用Service,单表CRUD不需要写SQL,复杂查询还能用条件构造器。它只做增强不做侵入,可以无缝兼容原生MyBatis。
项目里的事务是怎么控制的?答:主要是在Service层加@Transactional注解,比如报名、时长录入这些写操作都要求原子性。同时利用乐观锁控制并发,避免超卖。
JWT和Session的区别是什么?答:Session是服务端存储,有状态;JWT是客户端存储,无状态。JWT把用户信息加密编码在Token里,服务端只需要验签,不需要保存用户状态,更适合前后端分离和分布式场景。
数据量大了怎么办?答:从几个维度考虑:数据库建索引、分页查询;热点数据缓存到Redis;活动列表可以做读写分离;静态资源用CDN。毕设阶段讲清楚前两点就足够了。
6.4 让演示效果翻倍的三个小细节
最后说几个能让答辩演示眼前一亮的小细节。第一,提前准备一份真实的演示数据,最好有200个用户、50条活动记录、几百条时长记录,这样图表页面渲染出来的效果特别饱满。第二,演示时先把系统的核心业务闭环跑一遍,从登录到报名到审核到录入时长到查看统计,这个流程走完,老师对系统的整体性就有了直观感受。第三,准备一段不超过3分钟的录屏备份,万一现场网络或电脑出问题,还能用录屏完成演示,避免尴尬。
我个人在实际操作中的体会是,这个项目最花时间的往往不是写代码,而是理清楚业务边界和状态流转。很多同学一上来就撸代码,写着写着发现报名状态要么多要么少,页面和接口对不上,返工成本很高。如果动手前老老实实把角色表、状态图、接口清单列出来,后面开发会顺利很多。另外,做毕设不要求功能堆得多天花乱坠,把核心业务闭环做扎实,比做一堆半成品功能要强得多。系统完成后建议自己把所有角色各注册一个账号完整跑几遍,站在使用者角度走一遍流程,很多体验问题一眼就能看出来。