简介:这份资源面向计算机相关专业做毕业设计的学生,提供一套基于微信小程序的大学生科技竞赛管理系统完整实现方案,后台采用Spring Boot框架搭配Vue管理页面,数据库为MySQL,开发环境兼容Eclipse、MyEclipse、STS与IDEA,JDK版本为1.8。系统功能覆盖个人中心、主办方管理、竞赛分类与公告栏管理、竞赛信息与报名管理、竞赛成绩管理及系统管理等模块,学生端支持注册、在线报名、查询成绩与收藏管理,主办方端可进行报名与成绩维护,管理员则统筹竞赛与系统配置。压缩包共1338个文件,以png图片、js脚本、svg图标、vue组件、java源码、json配置、wxss与wxml小程序页面为主,另含sql脚本、doc文档与mp4演示视频,整体约77.3MB。资源内含源码、数据库脚本、论文、开题报告、环境工具包及同框架项目安装教程,已有132人学习,适合需要完整赛题方案与排错参考的毕业生。
1. 从一份“能跑起来”的毕设说起:微信小程序 + SpringBoot 竞赛管理系统到底在做什么
每年到了毕设季,计算机专业的学生最头疼的不是写不出代码,而是不知道写什么、怎么写才能既像样又能过答辩。我见过太多人从网上抄一份源码,改个名字就交上去,结果答辩时老师问一句“你这个登录鉴权怎么做的”就当场翻车。今天要聊的这个题目——基于微信小程序的大学生科技竞赛管理系统,配合 SpringBoot 后端——其实是一个非常适合本科毕业设计的选题,因为它同时覆盖了移动端开发、后端接口设计、数据库建模和权限管理这几个核心考点,而且业务逻辑足够清晰,不会让你陷进某个冷门算法里出不来。
这个系统解决的核心问题是:高校里科技竞赛的报名、审核、通知、成绩管理往往靠 Excel 和微信群完成,信息散落、统计困难。做成小程序之后,学生可以在手机上直接浏览竞赛列表、提交报名材料、查看审核进度;管理员在后台发布竞赛、审核报名、导出成绩。技术栈上,前端用微信小程序原生开发或 uni-app 打包,后端用 SpringBoot 提供 RESTful 接口,数据库用 MySQL,整体是一个典型的前后端分离架构。适合正在做毕设的本科生,也适合想拿一个完整项目练手 SpringBoot 和微信小程序的初级开发者。
2. 技术选型与架构拆解:为什么是 SpringBoot + 微信小程序而不是别的
2.1 后端为什么选 SpringBoot 而不是 SSM 或 Node.js
很多学校的课程还在教 SSM(Spring + SpringMVC + MyBatis),但如果你现在做毕设还用 SSM,光是 XML 配置就能耗掉你三分之一的时间。SpringBoot 的核心优势是自动装配和起步依赖,你只需要在pom.xml里引入spring-boot-starter-web和mybatis-plus-boot-starter,一个可运行的 Web 服务就搭好了。对于毕设这种周期短、功能明确的项目,SpringBoot 能让你把精力放在业务逻辑上而不是环境配置上。
另一个常见的选择是 Node.js + Express,优点是前后端都是 JavaScript,学习成本低。但问题在于,国内高校答辩老师对 Java 技术栈的认可度普遍更高,而且 SpringBoot 生态里的 Spring Security、MyBatis-Plus、Redis 缓存这些组件,能让你的系统在“技术含量”上看起来更扎实。如果你的毕设还想顺便练一下 Java 面试题里常问的 IOC、AOP、事务管理,那 SpringBoot 就是不二之选。
版本选择上,我一般会建议用 SpringBoot 2.7.x 而不是 3.x。原因很实际:3.x 要求 JDK 17,而很多学校机房的 JDK 还是 8 或 11;另外 3.x 对 Jakarta EE 的迁移会让一些老教程里的代码直接报错,你搜到的很多“SpringBoot 整合 XX”的文章可能还没更新。用 2.7.x 可以避开这些玄学问题,保证你的项目在答辩现场能稳定跑起来。
2.2 微信小程序端的技术决策:原生开发还是 uni-app
微信小程序开发有两条路:一是用微信官方的小程序原生框架,二是用 uni-app 这类跨端框架。原生开发的好处是文档齐全、API 调用直接、调试工具成熟,缺点是只能跑在微信里。uni-app 的好处是一套代码可以打包到微信小程序、H5 甚至 App,但代价是引入了额外的抽象层,遇到一些微信特有的 API 时反而更麻烦。
对于毕设来说,我建议直接用微信小程序原生开发。原因很简单:你的题目就是“基于微信小程序”,用原生框架更贴合题意,而且微信开发者工具自带模拟器和真机调试,你不需要额外配置 HBuilderX 的打包环境。小程序端的核心页面包括:竞赛列表页、竞赛详情页、报名表单页、个人中心页、消息通知页。其中竞赛列表页需要处理分页加载,这是微信小程序页面列表加载更多的典型场景,后面会专门讲。
2.3 数据库表设计:五张核心表撑起整个系统
数据库设计是毕设里最容易体现你功底的部分。这个系统最少需要五张表:用户表(user)、竞赛表(competition)、报名表(registration)、成绩表(score)、通知表(notice)。用户表里要区分角色,一般用role字段标记student和admin。竞赛表里要有报名开始时间、截止时间、最大人数这些字段,方便后端做校验。报名表是核心业务表,关联用户 ID 和竞赛 ID,还要有审核状态字段(pending、approved、rejected)。
这里有一个血泪经验:报名表一定要加唯一索引,约束同一个用户不能重复报名同一场竞赛。我见过有人没加这个约束,结果测试时狂点提交按钮,数据库里插入了十几条重复记录,答辩演示时直接翻车。唯一索引的 SQL 如下:
ALTER TABLE registration ADD UNIQUE KEY uk_user_competition (user_id, competition_id);这条语句的含义是在registration表的user_id和competition_id两列上建立联合唯一索引,数据库层面保证同一用户对同一竞赛只能有一条报名记录。参数说明:uk_user_competition是索引名,可以自定义,但建议见名知意。加上之后,即使前端没做防重复提交,后端插入时也会抛异常,你在 Service 层捕获这个异常并返回友好提示即可。
3. 后端接口实现:从登录鉴权到竞赛 CRUD 的完整链路
3.1 微信登录与 JWT 鉴权的落地步骤
微信小程序的登录流程和传统 Web 不一样。用户在小程序里点击登录,前端调用wx.login()拿到一个临时code,然后把code发给后端。后端拿着code加上你的小程序appid和secret,去请求微信的code2Session接口,换取用户的openid和session_key。拿到openid之后,你在自己的用户表里查一下有没有这个用户,没有就自动注册一个,有就直接登录。最后后端生成一个 JWT token 返回给前端,后续请求都带着这个 token。
这里有一个关键点:session_key不要返回给前端,它应该留在后端用来解密用户信息。很多教程会把session_key一起返回,这是不安全的。JWT 的生成可以用jjwt库,设置一个合理的过期时间,比如 7 天。下面是一个简化的登录接口代码:
@PostMapping("/login") public Result login(@RequestBody LoginDTO loginDTO) { // 1. 调用微信接口换取 openid String url = "https://api.weixin.qq.com/sns/jscode2session" + "?appid=" + appid + "&secret=" + secret + "&js_code=" + loginDTO.getCode() + "&grant_type=authorization_code"; String response = restTemplate.getForObject(url, String.class); JSONObject json = JSON.parseObject(response); String openid = json.getString("openid"); // 2. 查库或自动注册 User user = userService.findByOpenid(openid); if (user == null) { user = new User(); user.setOpenid(openid); user.setRole("student"); userService.save(user); } // 3. 生成 JWT String token = JwtUtil.generateToken(user.getId(), user.getRole()); return Result.success(token); }逻辑说明:这段代码先通过restTemplate请求微信的jscode2session接口,拿到openid。然后查库判断用户是否存在,不存在则自动注册。最后用JwtUtil生成 token 返回。参数说明:appid和secret建议放在application.yml里,不要硬编码在代码中;LoginDTO里只有一个code字段;Result是统一返回体,包含code、msg、data三个字段。
3.2 竞赛列表接口的分页与条件查询
竞赛列表是使用频率最高的接口,需要支持分页、按状态筛选、按关键字搜索。用 MyBatis-Plus 的话,分页只需要配置一个PaginationInnerInterceptor,然后调用page()方法即可。下面是一个典型的列表查询:
@GetMapping("/list") public Result list(@RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer size, @RequestParam(required = false) String keyword, @RequestParam(required = false) String status) { LambdaQueryWrapper<Competition> wrapper = new LambdaQueryWrapper<>(); if (StringUtils.hasText(keyword)) { wrapper.like(Competition::getTitle, keyword); } if (StringUtils.hasText(status)) { wrapper.eq(Competition::getStatus, status); } wrapper.orderByDesc(Competition::getCreateTime); Page<Competition> result = competitionService.page(new Page<>(page, size), wrapper); return Result.success(result); }逻辑说明:LambdaQueryWrapper是 MyBatis-Plus 提供的条件构造器,like方法对应 SQL 的LIKE '%keyword%',eq对应=。orderByDesc按创建时间倒序,保证最新的竞赛排在前面。参数说明:page和size有默认值,前端不传也不会报错;keyword和status是可选参数,用required = false标记。注意Page对象返回的records是当前页数据,total是总记录数,前端需要这两个字段来实现分页加载。
3.3 报名审核的状态流转与并发控制
报名审核涉及状态流转:学生提交报名后状态是pending,管理员审核通过变成approved,拒绝变成rejected。这里有一个容易被忽略的问题:如果两个管理员同时审核同一条报名记录,可能会产生覆盖。解决办法是用乐观锁,在registration表加一个version字段,每次更新时带上版本号。
@Transactional public boolean audit(Long registrationId, String status, Long adminId) { Registration reg = registrationService.getById(registrationId); if (reg == null || !"pending".equals(reg.getStatus())) { throw new BusinessException("报名记录不存在或已审核"); } reg.setStatus(status); reg.setAuditTime(new Date()); reg.setAuditBy(adminId); // updateById 会自动带上 version 条件 return registrationService.updateById(reg); }逻辑说明:先查记录,判断状态是否为pending,只有待审核的记录才能被审核。然后设置新状态、审核时间、审核人,调用updateById。如果配置了乐观锁插件,MyBatis-Plus 会自动在WHERE条件里加上version = 旧值,更新失败说明有人抢先改了,返回false。参数说明:status只允许传approved或rejected,建议在 Controller 层做校验;adminId从 JWT 里解析,不要从前端传。
4. 微信小程序端关键页面:列表加载更多与表单提交的避坑写法
4.1 竞赛列表页的下拉刷新与上拉加载
微信小程序的列表加载更多是一个经典场景,但很多人写出来的效果是:上拉之后重复加载同一页数据,或者加载完了还在触发请求。正确的做法是维护page、hasMore、loading三个状态变量。page记录当前页码,hasMore标记是否还有数据,loading防止重复请求。
Page({ data: { list: [], page: 1, hasMore: true, loading: false }, onReachBottom() { if (!this.data.hasMore || this.data.loading) return; this.loadData(); }, loadData() { this.setData({ loading: true }); wx.request({ url: `${baseUrl}/competition/list`, data: { page: this.data.page, size: 10 }, success: (res) => { const records = res.data.data.records; this.setData({ list: this.data.list.concat(records), page: this.data.page + 1, hasMore: records.length === 10, loading: false }); }, fail: () => { this.setData({ loading: false }); wx.showToast({ title: '加载失败', icon: 'none' }); } }); } });逻辑说明:onReachBottom是小程序页面的生命周期函数,用户上拉到底部时触发。先判断hasMore和loading,避免无效请求。请求成功后,把新数据拼接到list后面,页码加一。hasMore的判断逻辑是:如果本次返回的记录数等于size,说明可能还有下一页;小于size则说明已经到底了。参数说明:baseUrl建议放在单独的配置文件里,方便切换开发和生产环境;size设为 10 是经验值,太大首屏加载慢,太小请求次数多。
4.2 报名表单的校验与防重复提交
报名表单通常包含姓名、学号、联系方式、参赛项目描述等字段。前端校验只能防君子,后端校验才是底线。但前端校验能提升用户体验,所以两边都要做。前端在提交前检查必填项是否为空,后端在接口里再次校验,并且利用数据库唯一索引防止重复报名。
submitForm() { const { name, studentId, phone } = this.data.form; if (!name || !studentId || !phone) { wx.showToast({ title: '请填写完整信息', icon: 'none' }); return; } if (this.data.submitting) return; this.setData({ submitting: true }); wx.request({ url: `${baseUrl}/registration/submit`, method: 'POST', header: { 'Authorization': wx.getStorageSync('token') }, data: this.data.form, success: (res) => { if (res.data.code === 200) { wx.showToast({ title: '报名成功' }); wx.navigateBack(); } else { wx.showToast({ title: res.data.msg, icon: 'none' }); } }, complete: () => { this.setData({ submitting: false }); } }); }逻辑说明:先做前端必填校验,然后用submitting标记防止用户狂点按钮。请求头里带上 JWT token,后端从 token 里解析用户 ID。complete回调里重置submitting,无论成功失败都要重置,否则按钮会一直处于禁用状态。参数说明:wx.getStorageSync('token')是从本地缓存读取登录时存的 token,注意 token 过期时要引导用户重新登录。
4.3 消息通知页的未读标记与跳转逻辑
消息通知页需要展示管理员发布的通知,并且区分已读和未读。实现方式是在notice表里加一个read字段,或者单独建一张user_notice关联表记录每个用户的已读状态。简单起见,毕设里可以直接在notice表加read字段,但这样只能支持全体通知,不能针对特定用户。如果要做定向通知,就需要关联表。
小程序端的展示逻辑是:未读通知加一个红点或加粗字体,点击后调用标记已读接口,然后跳转到详情页。这里要注意的是,标记已读的接口应该在跳转前调用,而不是在详情页的onLoad里调用,否则用户快速返回时可能状态还没更新。
5. 避坑与排查:毕设答辩前最容易翻车的五个问题
5.1 跨域问题导致小程序请求全部失败
现象:微信开发者工具里请求后端接口,控制台报request:fail或者net::ERR_CONNECTION_REFUSED。原因:小程序的请求域名需要在微信公众平台配置合法域名,开发阶段可以在开发者工具里勾选“不校验合法域名”。但即使勾选了,如果后端没开跨域,浏览器环境下的 H5 调试也会失败。解决:在后端加一个全局跨域配置,用WebMvcConfigurer实现addCorsMappings,允许所有来源、所有方法。注意生产环境要收紧来源,但毕设演示阶段可以放开。
5.2 SpringBoot 版本太高导致依赖冲突
现象:项目启动时报ClassNotFoundException或NoSuchMethodError,通常出现在javax.servlet和jakarta.servlet之间。原因:SpringBoot 3.x 把javax包名改成了jakarta,而你引入的某些第三方库还在用旧包名。解决:把 SpringBoot 版本降到 2.7.x,或者把所有相关依赖升级到兼容 3.x 的版本。对于毕设,降版本是最省事的做法。
5.3 数据库时区不对导致时间差八小时
现象:前端显示的比赛开始时间比实际时间少了八小时。原因:MySQL 的时区配置和 JVM 的时区不一致,或者 JDBC 连接串没指定时区。解决:在application.yml的 JDBC URL 里加上serverTimezone=Asia/Shanghai,同时在 MySQL 配置文件里设置default-time-zone='+08:00'。另外,实体类里的时间字段建议用LocalDateTime而不是Date,避免一些历史遗留问题。
5.4 小程序真机调试时请求超时
现象:开发者工具里一切正常,真机预览时接口请求超时。原因:真机访问的是你电脑的局域网 IP,如果电脑防火墙没关或者手机和电脑不在同一个 WiFi 下,就会连不上。解决:确保手机和电脑在同一局域网,关闭电脑防火墙,后端启动时绑定0.0.0.0而不是127.0.0.1。如果还是不行,可以用内网穿透工具临时映射一个公网地址,但注意不要用于生产环境。
5.5 答辩现场数据库连不上
现象:答辩时换了一台电脑,项目启动报数据库连接失败。原因:数据库密码、端口、库名和开发环境不一致。解决:提前把数据库导出成 SQL 文件,答辩前在演示电脑上重新导入,并且把application.yml里的数据库配置改成演示电脑的配置。更稳妥的做法是准备一个 Docker Compose 文件,一键启动 MySQL 和 Redis,但这对毕设来说可能有点超纲,量力而行。
6. 进阶技巧:用 AOP 做操作日志和接口耗时统计
毕设如果只做 CRUD,答辩时很难拿到高分。加一个 AOP 切面做操作日志和接口耗时统计,既能体现你对 Spring 核心概念的理解,又能让系统看起来更完整。具体做法是自定义一个@Log注解,然后在切面里拦截所有标注了该注解的方法,记录方法名、参数、执行时间、操作人,写入日志表。
@Aspect @Component public class LogAspect { @Around("@annotation(logAnnotation)") public Object around(ProceedingJoinPoint joinPoint, Log logAnnotation) throws Throwable { long start = System.currentTimeMillis(); Object result = joinPoint.proceed(); long cost = System.currentTimeMillis() - start; // 从 JWT 里解析当前用户 String username = SecurityUtil.getCurrentUsername(); String methodName = joinPoint.getSignature().getName(); logService.save(new SysLog(username, methodName, cost, new Date())); return result; } }逻辑说明:@Around环绕通知可以在方法执行前后插入逻辑。joinPoint.proceed()执行原方法,返回结果。System.currentTimeMillis()计算耗时。SecurityUtil是一个工具类,从ThreadLocal里拿当前请求的用户信息。参数说明:@annotation(logAnnotation)表示只拦截标注了@Log的方法;SysLog是日志实体类,包含用户名、方法名、耗时、时间四个字段。
这个切面加上之后,你可以在管理后台加一个日志查询页面,管理员能看到谁在什么时候调用了什么接口、耗时多少。答辩时老师问“你这个系统有没有做性能监控”,你就可以把这个拿出来讲。另外,接口耗时统计还能帮你发现慢查询,比如某个列表接口耗时超过 500 毫秒,你就知道该加索引了。
还有一个实用技巧:用@Transactional注解时,注意默认只对RuntimeException回滚。如果你在 Service 里抛的是Exception,事务不会回滚。我一般会写成@Transactional(rollbackFor = Exception.class),避免这个坑。这个细节在答辩时如果被问到,能加分不少。
做毕设这件事,我的习惯是先把最小可运行版本跑通,再往上加功能。不要一上来就设计十几张表、几十个接口,那样很容易卡在某个细节里出不来。先让登录、列表、报名这三个核心流程跑通,再去加审核、通知、日志这些锦上添花的东西。希望帮到你。
本文还有配套的精品资源,点击获取