半年前立项的时候导师给的题目是《基于Spring Boot的微信小程序大学生竞赛管理系统》我一听就觉得得玩点不一样的。恰逢学院里各种“创新创业大赛”“技能竞赛”通知满天飞报名表靠Excel传来传去评审靠手动汇总成绩公告发在年级群里经常刷屏手动统计漏人漏项几乎是家常便饭。于是我就把“搞笑大学生”这几个字也融进了项目定位做了一套正经中带点活泼气质的竞赛管理系统。这篇文章不聊虚的直接拆解我这套系统的设计思路、技术选型、核心代码片段和线上踩坑记录希望能给正在做同类毕设或者实际项目的朋友一点可以直接抄作业的参考。系统名字看着长其实核心就是两件事用Spring Boot把竞赛管理的业务逻辑和数据处理全部收拢到后端用微信小程序把学生报名、作品提交、成绩查询这些高频操作搬到手机端。面向人群很明确——在校大学生、学院竞赛负责人、评审老师。它能解决的痛点也很具体竞赛信息散落、报名数据难以汇总、多人评审结果算不对、成绩公示不及时。如果你正好在纠结Spring Boot如何跟微信小程序做完整联调或者被各种文件上传、权限拦截、列表分页折磨得头疼这篇内容基本覆盖了。1. 项目溯源为什么做一套“搞笑风格”的竞赛管理系统1.1 赛事管理的真实痛点大学里的竞赛活动看着热闹管理起来是真的琐碎。我调研了学院好几个赛事的组织过程发现大家普遍靠微信群Excel表格跑完全流程。负责人发一个竞赛通知到群里学生下载附件填写报名表再回传邮箱光是收集整理就得好几天。报名截止后负责人要把几十份Excel合并按专业、年级、参赛类型分类过程里还得反复跟学生确认信息是否填错。比赛结束之后评审老师拿到的作品如果没按规范命名文档和视频又散落在不同的聊天记录里容易搞混。最后成绩出来还要人工算平均分、排名再做成公示表格工作量非常大。所以这个系统立项之初我就把需求收敛成一条主线让一个竞赛从发布、报名、提交作品、评审到成绩公示都能在系统里闭环流转。学生不用再满世界找报名表老师在后台点几下就能导出汇总评委只需要在小程序端打分整个流程既省时间又少出错。1.2 “搞笑”定位的落地思路题目里带“搞笑”两个字一开始有点难绷后来我把它当成一个产品调性来思考。所谓搞笑风格不是说系统里全是沙雕动图而是在交互设计上尽量轻松让用户用起来没那么多心理负担。我做了几个具体动作用户初次登录后会随机分配一个“竞赛江湖称号”比如“又菜又爱玩选手”“熬夜赶工冠军”“学术锦鲤”每次报名成功也会弹一条趣味提示缓解学生面对严肃比赛流程的紧张感。排行榜模块里除了真实成绩排名还加了一个“活跃之星”维度按照报名次数、提交作品数量排序大家会为了一个搞笑的电子徽章更愿意去使用系统。这种设计上的小心思在技术层面对后台的影响不大却让前端界面有了温度。真正把“搞笑”落地的是前端文案和样式而后端依然要保持严谨的逻辑毕竟竞赛管理涉及成绩计算绝对不能为了有趣就放松数据准确性。1.3 用户角色与核心需求梳理我按照真实的管理场景把系统用户分成三类学生、赛事管理员、评审老师。学生端的主要需求是浏览竞赛列表、查看详情、在线报名、上传作品、查询自己的成绩和个人荣誉。赛事管理员需要维护竞赛信息、审核报名资格、分配评审任务、管理公告、导出各类汇总表。评审老师则要看到待评审列表、在线打分、填写评语。这三类角色权限完全不同在数据层面天然需要做角色字段和接口权限控制。除了角色还有几个关键业务规则需要提前定好。一是一个学生可以报名多个竞赛但同一个竞赛只能报名一次避免重复数据。二是竞赛状态有“报名中”“评审中”“已结束”几种状态变化会直接影响学生能不能继续提交作品。三是作品提交有截止时间后端必须在接口层校验时间不能只靠前端禁用按钮。这些规则我在设计数据库表结构的时候都直接落到了字段设计里后面实现起来就顺畅很多。2. 技术选型与整体架构详解2.1 Spring Boot后端主框架的版本选择项目后端我选的是Spring Boot 2.7.18没有直接上Spring Boot 3.x。原因很实际很多教学资料、网上博客、以及兼容性最稳定的MyBatis-Plus版本都对Spring Boot 2.x支持更成熟。Spring Boot 3.x已经全面拥抱Jakarta EE并且强制要求JDK 17以上如果你们实验室或服务器上的JDK还是8直接用3.x会遇到很多基础环境问题。用Spring Boot 2.7配合JDK 8启动快、依赖容易找、出现问题网上一搜一大片做毕设或者企业小项目都非常稳。项目构建工具我选了Maven这个也是主流。不管是Spring Initializr生成的项目结构还是后续用IDEA打开Maven的pom.xml都直观单独针对Spring Boot打包成可执行jar用mvn package就能搞定。Gradle虽然也优秀但团队协作时大家更熟悉Maven所以我个人建议后端新手项目直接用Maven。核心起步依赖大概包括这几个spring-boot-starter-web提供Web支持和内嵌Tomcatmybatis-plus-boot-starter数据持久层增强插件配合代码生成器非常舒服mysql-connector-javaMySQL驱动lombok减少实体类样板代码jjwt或hutool的JWT工具做登录令牌oss-sdk或直接文件上传处理看服务器情况选择。这里要注意Spring Boot版本不能乱配。有的朋友直接新建项目时选了Spring Boot 3.2、3.3甚至更高结果导入一个老版本的MyBatis-Plus启动直接报错。我的建议是先用2.7.x跑通整体流程等项目核心功能稳定了再考虑升级的事。2.2 微信小程序原生开发还是uni-app小程序端有两种主流方案一是微信开发者工具里的原生小程序二是跨端框架uni-app或Taro。我这次选的是原生小程序开发原因很简单原生具备微信平台最完整的能力没有中间层的兼容损耗登录、上传、支付这些官方API可以直接用调试定位问题时也更容易缩小范围。如果你本身已经会Vue那uni-app上手更快一套代码还能多端复用但要注意有些微信个性化能力在真机上会有奇怪差异。就这个项目而言功能主要集中在表单填写、列表展示、文件上传原生开发完全够用而且微信开发者工具自带云开发、真机调试、性能分析学习成本并不高。对了后面我还遇到一个uni-app打包体积超限的问题原生小程序本身也会遇到2MB包体限制这个我在第4部分专门讲。原生小程序的工程结构里pages目录放页面components放自定义组件utils放公共方法api目录专门放请求封装。我建议从一开始就把目录规划清楚不要所有js都堆在pages里否则功能一多改起来想死的心都有。2.3 数据库与缓存选型数据存储主库用的是MySQL 8.0。竞赛管理系统的核心数据都可以用关系型模型来表达用户表、竞赛表、报名表、作品表、评审表、积分表、公告表。为了后面统计方便我还在报名表里冗余了竞赛名称和学生姓名虽然这样有点反范式但在导出Excel和列表展示时不用连表查太多次性能更好。为什么不用Redis做缓存这个项目其实可以用但绝大多数操作并不复杂查询热点主要是竞赛列表和公告。我后期加了一个简单的手动缓存工具类把频繁查询的竞赛列表缓存到本地Map里5分钟过期。这个方案在低并发场景下已经够用根本没必要额外折腾Redis服务。不过如果在真实高并发环境里比如全校几千人同时抢报某个热门竞赛那还是建议上Redis做分布式缓存毕竟它的过期策略和并发控制更完善。2.4 系统整体分层与模块划分我的后端分层遵照最常见的Controller-Service-Mapper三层结构在这之上再拆出一个config包存放配置一个common包存放统一返回结果和异常处理一个utils包放JWT工具、文件工具。模块划分上我按业务领域拆成了下面几块用户模块登录注册、角色管理、个人资料、称号管理竞赛模块竞赛创建、竞赛审核、竞赛列表、详细展示、报名截止时间校验报名模块提交报名信息、取消报名、报名审核作品模块作品上传、重新提交、文件校验、提交截止时间校验评审模块评委分配、评分录入、平均分计算、排名生成公告模块后台发布、首页展示。模块之间尽量做到低耦合报名模块只关心报名单不直接操作作品表和评审表。评审打分时如果需要作品信息就通过服务层去查询避免循环依赖。这个设计习惯让我在后期加功能时省了很多时间强烈建议你也养成这样的习惯。3. 核心功能实现与代码级拆解3.1 微信登录鉴权链路从wx.login到JWT微信小程序跟后端交互第一步就是把微信的临时登录凭证换成本系统可识别的身份令牌。小程序的wx.login会返回一个code这个code有效期只有5分钟而且只能使用一次。后端接收到code后调用微信接口的code2Session拿到openid和session_key。我在这套系统里没有直接用openid作为用户主键而是创建了本地user表将openid作为唯一索引。首次登录时如果发现openid不存在就自动注册一个新用户昵称和头像先取微信默认值之后在个人中心可以修改。登录成功后后端为用户生成一个JWT令牌有效期为7天小程序每次请求都在请求头里带上Authorization字段。核心伪代码大致是// 微信登录接口 String code request.getCode(); String url https://api.weixin.qq.com/sns/jscode2session?appid appId secret appSecret js_code code grant_typeauthorization_code; // 使用RestTemplate或HttpClient调用微信接口 MapString, Object sessionInfo restTemplate.getForObject(url, Map.class); String openid (String) sessionInfo.get(openid); // 查用户表不存在则注册 User user userService.findByOpenid(openid); if (user null) { user new User(); user.setOpenid(openid); user.setNickname(微信用户 randomSixCode()); userService.save(user); } // 生成JWT String token jwtUtil.generateToken(user.getId(), user.getRole());有一个很坑的点微信接口的域名是https本地联调时后端如果不配置HTTPS证书会直接被微信服务器挡掉。所以我在本地开发时用了一个代理工具把微信请求转发到本地的Spring Boot服务等线上部署时再申请域名和SSL证书。这个我在后面第5部分会专门讲。3.2 竞赛发布与报名状态机设计竞赛不是一创建就能立刻报名的我把竞赛状态做了一个简单状态机草稿管理员刚创建的竞赛还没对外发布报名中学生可以浏览详情并报名评审中报名截止评委进入打分环节已结束成绩公布所有操作只读。状态转换非常清晰。管理员创建竞赛后点“发布”状态变为“报名中”系统会自动校验报名开始时间不能晚于结束时间。到了报名结束时间前端列表会显示“报名已截止”后端接口也有一层时间校验就算有人手动篡改请求参数也不能提交。报名表的核心字段包括竞赛id、学生id、组员姓名、学号、手机号、指导老师、报名时间、资格状态。这里有个细节我允许学生作为团队负责人报名把团队成员信息作为一个存储成JSON数组的字段。这样不需要额外建一张团队表查询报名详情时直接解析JSON即可对于中小型竞赛系统完全够用。3.3 文件上传本地存储与云存储的取舍作品提交是竞赛管理系统的重头戏。学生需要上传设计文档、演示视频或图片压缩包。微信小程序端使用wx.chooseMedia选择文件通过wx.uploadFile将文件以multipart/form-data格式提交到后端。在后端Spring Boot里我用的MultipartFile接收存到了服务器本地目录并把访问路径映射出来。如果你用的是本地存储要注意几点第一文件目录不要放在项目resources目录里否则每次重新打包jar都会清空我一般是放在服务器的/var/webapp/upload目录通过配置项指定一个绝对路径。第二要给文件做重命名避免学生传了同名文件互相覆盖我在文件名前面拼上用户id和时间戳。第三如果以后要接入分布式环境本地存储会导致图片无法共享这时候可以考虑把文件丢到对象存储或七牛云、阿里云OSS上。选型逻辑是项目初期用户量小、追求低成本本地存储完全没问题等到并发量上来再切换对象存储也不迟。文件校验方面我设置了单文件最大50MB压缩包和PDF文档体积一般不会太大。上传完成后我会把文件访问URL保存到作品表里同时记录文件原始名和大小方便评审老师下载时看到清晰的文件信息。3.4 评审打分与成绩计算逻辑评审环节是整个系统里我对数据准确性要求最高的一块。我不能让评委看到其他评委的打分避免互相干扰。所以在设计评审表时我把评委id和竞赛id作为联合唯一索引保证每个评委对一个作品只能打分一次。打分维度包括创新性、完整性、实用性、现场表现总分100分每个维度20-30分不等。分计算逻辑关键点在于“去掉最高分和最低分再取平均”这是很多竞赛的真实规则。如果只有两位评委就不去最高最低直接取平均如果三位及以上才执行去掉极端值的逻辑。我的实现如下public Double calcFinalScore(ListDouble scores) { if (scores null || scores.isEmpty()) return 0.0; if (scores.size() 2) { return scores.stream().mapToDouble(Double::doubleValue).average().orElse(0.0); } double sum 0.0; double max Collections.max(scores); double min Collections.min(scores); for (Double score : scores) { sum score; } return (sum - max - min) / (scores.size() - 2); }所有评分明细都会留着管理员可以随时查看每个评委对某个作品的打分记录系统里还生成了一个简单的成绩汇总Excel导出接口。这里我踩过一个坑就是浮点运算精度问题平均值如果不做四舍五入会出现88.9999999的情况。我最终用BigDecimal保留两位小数并采用四舍五入策略。3.5 趣味化交互称号、排行榜与彩蛋前面提到“搞笑大学生”定位我把趣味化做进了用户体系。用户第一次登录时系统会根据当天日期和一个简单的随机算法分配一个称号比如“咸鱼翻身选手”“卷王本王”“灵感永不枯竭”。这些称号存储在user表的title字段后台不参与竞赛评审纯粹是展示用。另一个趣味交互是“活跃之星”排行榜小程序首页会有一个单独入口展示本周报名次数最多、活跃度最高的前十名学生。这个排行版通过SQL对报名表按userId分组统计得出原本是考核系统里常用的groupBy操作放到竞赛系统里就变成一个有意思的社交激励。除了真实排名我还在成绩公布时送了“最佳人气奖”之类的趣味徽章数据逻辑是根据作品浏览量排序。效果意外地好不少同学愿意为了一个虚拟徽章去分享自己的作品链接。4. 小程序端关键流程与性能优化4.1 列表分页与“加载更多”的正确姿势小程序端最常见的列表形态就是首页竞赛列表。如果一次性把所有竞赛全部返回数据量一大页面就卡成狗微信官方也明确不建议这么干。我用的方案是常见的分页加载后端接口接收pageNum和pageSize这两个参数使用MyBatis-Plus的Page对象做分页查询返回总条数和当前页记录。小程序端用scroll-view作为滚动容器监听触底事件。每次滚动到底部如果当前页已经是最后一页就提示“没有更多啦”否则pageNum1继续请求下一页将新的列表数据追加到原有数组后面。这里面有一个很容易忽略的坑如果用户切换筛选条件列表数据要重置到第一页并且清空原有数组否则新数据会叠加在旧数据后面。我在写的时候用一个refreshType参数区分是下拉刷新还是触底加载核心逻辑如下loadList(reset false) { if (!reset this.data.listLoading) return; let pageNum reset ? 1 : this.data.pageNum 1; this.setData({ listLoading: true }); fetch(/contest/list, { pageNum, pageSize: 10 }) .then(res { let list reset ? res.rows : this.data.list.concat(res.rows); this.setData({ list, pageNum, hasMore: this.data.pageNum * 10 res.total }); }) .finally(() this.setData({ listLoading: false })); }4.2 上传前压缩与2MB包体积限制微信小程序上传到线上正式版时包体积不能超过2MB超过了就得走分包。这个限制影响很大尤其是当你本地开发久了没注意随手塞了几张测试图片进去很可能就爆了。我这次项目主包维持在1.8MB左右没有走分包因为业务相对简单。具体的做法是所有静态图片尽量压缩成WebP格式体积能缩小一半以上不要使用本地大图做页面背景改用图片CDN链接全局公共的样式和工具方法抽到common文件里避免每个页面重复引入如果有非核心页面比如排行榜详情、个人历史荣誉等放到subPackages分包里主包只留首页、竞赛和报名相关的核心页面。如果你用的是uni-app打包source size超限同样会被提示为“exceed max limit 2mb”解决办法基本一致把不常用的页面抽成分包或者对一些依赖库做按需引入。4.3 顶部导航栏与设备适配小程序顶部导航栏高度不是固定的因为不同机型有各自的胶囊按钮高度和刘海设计。一开始我直接给页面写死了一个固定高度结果在iPhone 12上顶部按钮被刘海挡住在安卓老机型上又被系统状态栏顶下来。后期我改成了动态获取系统信息用wx.getWindowInfo或wx.getMenuButtonBoundingClientRect()计算胶囊位置再得出自定义导航栏的高度。如果你不想折腾最稳妥的方式是使用微信官方提供的navigationStyle: custom配合胶囊高度计算。我目前的封装方法是在app.js里做一次全局全局参数const menuRect wx.getMenuButtonBoundingClientRect(); const systemInfo wx.getWindowInfo(); globalData.statusBarHeight systemInfo.statusBarHeight; globalData.menuButtonRect menuRect; globalData.navBarHeight (menuRect.top - statusBarHeight) * 2 menuRect.height;这个方法几乎能适配所有机型我自己在多个模拟器和真机上验证过表现稳定。4.4 小程序与H5/二维码的场景联动竞赛活动经常需要把链接分享到微信群或朋友圈我的做法是通过小程序码进入。管理员在后台为每个竞赛生成一个专属的小程序码图片学生扫码后可以直接打开微信小程序并进入对应竞赛详情页。生成方式有wxacode.getUnlimited接口后端调用它会拿到文件流再转成图片返回给前端展示。这里有个常见问题就是H5页面里如果想通过微信打开小程序微信官方限制必须使用“微信开放标签”wx-open-launch-weapp而且该标签只能在微信内置浏览器中使用。很多朋友问为什么H5唤起小程序链接无法访问多半是因为使用了非微信内置浏览器或者没有正确配置JS-SDK。如果是普通浏览器是无法直接唤起小程序的只能先引导用户复制链接到微信打开。这个限制我在开发过程中反复踩过最后是给管理员做了一个“复制小程序链接”的提示并把小程序码放在公告里帮助传播。5. 开发实测中的坑与排查记录5.1 后端跨域与Cookie/Session失效问题小程序端发起请求时域名为https后端本地是http://localhost:8080天然跨域。虽然小程序端不像浏览器那样有严格的CORS限制但后端如果不配置跨域响应头开发调试时常常会出现请求发送成功但拿不到数据的情况。我用了Spring Boot的CorsFilter做全局配置允许所有来源、所有请求头并允许携带凭证。不过要注意一旦使用了自定义Header比如Authorization就得在allowedHeaders里显式加上Authorization否则会被浏览器拦截。另外一个坑是Cookie/Session失效。因为小程序请求是异步的默认不会主动维护Session所以千万不要在Session里存重要状态。我在做登录时直接不用Session而是用JWT携带用户信息后端用一个拦截器解析JWT并设置到当前请求的上下文里这样状态就完全无状态化不会出现“登录一会儿就掉线”的问题。5.2 小程序request域名校验与本地联调小程序开发工具里如果你勾选了“不校验合法域名”本地请求可以随便发但真机预览时必须使用HTTPS的合法域名。很多同学第一次真机调试发现请求全部失败就是因为没把域名加到小程序后台的request合法域名列表里或者域名没有配置SSL证书。我的解决路径是本地开发时前端项目里把baseURL指向一个测试域名该域名已经通过Nginx反向代理到本地的Spring Boot服务并配置了有效证书。这样微信后台能校验通过本地的后端接口也能正常联调。当然最省事的方式是直接在微信开发者工具里关闭域名校验然后手机扫码预览时也勾上“不校验合法域名”但这只能在开发阶段用上线前一定要改回来。5.3 Spring Boot版本引发的容器兼容问题之前有一个同事的项目用的是Spring Boot 3.3结果部署到内网一台老服务器上启动就报“Unsupported class file major version”原因就是服务器JDK版本太低。Spring Boot 3.x要求JDK17而老服务器只有JDK8跑都没法跑。所以如果你不是特别需要最新特性直接上Spring Boot 2.7安全又省事。另外有的朋友会在IDEA里遇到“springboot版本太高”导致某些依赖导入失败这时候不要盲目把版本降得很低而是检查跟Spring Boot配套的依赖版本比如MyBatis-Plus、Hutool、Sa-Token等。建议用Spring Initializr生成的稳定版本而不是手动乱填一个最新版本否则很容易踩兼容性雷区。5.4 常见报错速查表我把开发期间遇到最多的几个报错整理成一张速查表方便你把错误关键字贴进搜索引擎或者直接对号入座。报错提示可能原因我的解决方式request:fail url not in domain list微信小程序合法域名未配置登录微信公众平台把服务器域名加入request合法域名列表Invalid status code: 405后端接口方法不支持该请求方式检查Controller里GetMapping/PostMapping是否匹配uni-app source size exceed max limit 2mb主包体积超限使用分包机制或压缩静态资源微信开发者工具无法登录端口被占用或网络代理异常打开工具设置检查本地代理关闭系统代理再试后端返回401 UnauthorizedJWT过期或未携带token前端统一在拦截器里带Authorization头token过期时跳转登录MultipartFile上传为空微信uploadFile的name和后端参数名不一致检查filePath、name字段是否与后端RequestParam一致平均分出现8.9999999浮点计算精度问题使用BigDecimal保留两位小数证书任然报错服务器证书链不完整用证书检测工具重新补全中间证书这些坑看起来多其实很多都是基础的配置问题和版本兼容问题。真正开发的时候尽量用统一封装好的请求方法和异常捕获错误信息会更清晰排查效率也高得多。最后再分享一个我自己的开发习惯不要一开始就追求把所有前端页面全部做完再联调我就是先用一个最丑但能跑通的页面把登录、列表、报名、文件上传这四条主线走通然后再慢慢优化页面样式和交互。这个思路让我避免了很多“前面前端改到一半后端接口又要重新设计”的尴尬局面。这套系统整体维护到现在稳定性和体验都还过得去尤其是那个趣味称号设计经常有同学特意问我那个称号是怎么随机出来的。如果你也在做类似的管理系统我建议你在技术之外多想一想用户真的需要什么一个看起来有点“搞笑”的小细节也许就是整个项目最有记忆点的地方。
阅读完成 · 觉得有帮助?