简介:这份资源是面向高校计算机相关专业毕业设计场景的完整项目包,主题为基于微信小程序的校篮球联赛系统,适合正在准备毕设、需要可运行案例与配套代码参考的本科或高职学生。项目采用Java后端与微信小程序前端组合,覆盖球队、球员、赛事信息、赛事详情、公告及用户等模块的增删改查,并包含留言与收藏两类交互管理,功能划分贴近真实联赛运营流程。压缩包共1901个文件,约38.02MB,其中vue与js文件构成前端页面与逻辑,java文件承载后端接口,wxss与wxml对应小程序视图层,另有json配置、png与svg图片素材、sql建库脚本及md说明文档,整体结构完整、便于按模块检索。资源附有部署教程,可帮助读者快速理解前后端联调方式与目录组织思路。目前已有53人学习,适合作为毕设选题落地、功能扩展或答辩演示的参考方案。
1. 校篮球联赛系统:从赛程混乱到小程序端实时同步,这套 Java 后端怎么搭
院系篮球联赛最头疼的不是打球,是赛程。二十支队伍、小组循环加淘汰赛、场地只有两块、裁判要排班、比分靠微信群吼——去年我们院体育部用 Excel 排了三天,开赛后还是出现「同一队两场撞时间」「比分记错导致出线算错」的翻车现场。这套基于微信小程序的校篮球联赛系统,核心就是把这堆事搬到线上:管理员在后台建赛事、排赛程、录比分,球员和观众在小程序里看赛程、查积分榜、收比赛通知。技术栈是 Java(Spring Boot + MyBatis-Plus)做后端,微信小程序做前端,MySQL 存数据。它适合正在做计算机毕业设计、想找一个业务闭环完整又不太冷门的题目的同学,也适合真想把院系联赛管起来的体育部。下面按「后端怎么设计 → 小程序怎么对接 → 部署怎么落地 → 坑在哪」的顺序讲透,代码和参数都能直接抄。
2. 后端骨架:Spring Boot + MyBatis-Plus 怎么撑起赛事、球队、赛程三张核心表
先把数据模型想清楚,后面写接口就是顺水推舟。校篮球联赛的业务对象其实就四类:赛事(一个赛季)、球队、球员、比赛(对阵记录)。赛程和积分榜都是「比赛」这张表算出来的派生视图,不要单独建表存积分,否则改一次比分要同步三处,必出脏数据。
2.1 四张核心表的设计与建表 SQL
我一般用 MyBatis-Plus 的实体类反向生成建表语句,省得手写字段对不上。下面是最小可用的一组表结构,字段名和 Java 实体一一对应。
-- 赛事表:一个赛季一条记录 CREATE TABLE `league` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `name` VARCHAR(64) NOT NULL COMMENT '赛事名称,如2024院系篮球联赛', `season` VARCHAR(16) NOT NULL COMMENT '赛季,如2024春', `start_date` DATE NOT NULL, `end_date` DATE NOT NULL, `status` TINYINT DEFAULT 0 COMMENT '0未开始 1进行中 2已结束', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 球队表 CREATE TABLE `team` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `league_id` BIGINT NOT NULL COMMENT '所属赛事', `name` VARCHAR(32) NOT NULL COMMENT '队名', `college` VARCHAR(32) COMMENT '所属院系', `group_no` TINYINT COMMENT '分组号,小组赛用', PRIMARY KEY (`id`), KEY `idx_league` (`league_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 比赛表:赛程和比分的唯一来源 CREATE TABLE `match_game` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `league_id` BIGINT NOT NULL, `home_team_id` BIGINT NOT NULL COMMENT '主队', `away_team_id` BIGINT NOT NULL COMMENT '客队', `match_time` DATETIME NOT NULL COMMENT '开赛时间', `court` VARCHAR(32) COMMENT '场地', `home_score` INT DEFAULT NULL COMMENT '主队得分,未开赛为NULL', `away_score` INT DEFAULT NULL, `stage` TINYINT DEFAULT 0 COMMENT '0小组赛 1淘汰赛', `status` TINYINT DEFAULT 0 COMMENT '0未开始 1进行中 2已结束', PRIMARY KEY (`id`), KEY `idx_league_time` (`league_id`, `match_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;match_game是整张系统的枢纽。积分榜不落库,每次查询时用 SQL 聚合算出来,这样比分一改,榜单立刻跟着变,没有同步延迟。status字段控制比赛生命周期,只有status=2的比赛才计入积分,避免未打完的比赛污染排名。
2.2 积分榜聚合查询:一条 SQL 算出胜场、净胜分和排名
积分榜是观众看得最多的页面,也是最容易写错的地方。常见做法是遍历所有比赛在 Java 里累加,但队伍一多就是 N 次查询。我一般直接用一条 SQL 把主客场的得分合并后聚合。
SELECT t.id AS teamId, t.name AS teamName, COUNT(*) AS played, SUM(CASE WHEN g.win_team = t.id THEN 1 ELSE 0 END) AS win, SUM(CASE WHEN g.win_team != t.id AND g.win_team IS NOT NULL THEN 1 ELSE 0 END) AS lose, SUM(CASE WHEN g.home_team_id = t.id THEN g.home_score - g.away_score ELSE g.away_score - g.home_score END) AS diff FROM team t JOIN ( SELECT home_team_id, away_team_id, home_score, away_score, CASE WHEN home_score > away_score THEN home_team_id WHEN home_score < away_score THEN away_team_id ELSE NULL END AS win_team FROM match_game WHERE league_id = #{leagueId} AND status = 2 ) g ON g.home_team_id = t.id OR g.away_team_id = t.id WHERE t.league_id = #{leagueId} GROUP BY t.id, t.name ORDER BY win DESC, diff DESC;逻辑说明:内层子查询先把每场比赛的胜者算出来(平局win_team为 NULL,篮球一般不打平,但小组赛可能有特殊情况),外层再按球队聚合。diff是净胜分,排名规则是「先看胜场,再看净胜分」,这是院系联赛最常用的规则。参数leagueId从接口传入,status = 2保证只统计已结束的比赛。如果你们联赛规则是「胜场相同看相互战绩」,那这条 SQL 要再改,但 90% 的院系比赛用净胜分就够了。
2.3 赛程自动编排:避免同队撞时间的排程接口
手动排赛程是翻车重灾区。我写了一个简单的轮转排程接口,输入球队列表和可用时间段,输出不冲突的对阵表。核心约束只有两条:同一时间段一支队只能打一场;同一块场地同一时间段只能有一场比赛。
/** * 单循环赛程生成(轮转法) * @param teamIds 参赛队ID列表 * @param slots 可用时间段,每个元素是[开始时间, 场地] */ public List<MatchGame> generateSchedule(List<Long> teamIds, List<TimeSlot> slots) { List<MatchGame> result = new ArrayList<>(); int n = teamIds.size(); // 奇数队补一个虚拟队,保证轮转能整除 if (n % 2 != 0) { teamIds.add(-1L); n++; } int rounds = n - 1; int slotIndex = 0; for (int r = 0; r < rounds; r++) { for (int i = 0; i < n / 2; i++) { Long home = teamIds.get(i); Long away = teamIds.get(n - 1 - i); if (home == -1L || away == -1L) continue; // 跳过虚拟队 TimeSlot slot = slots.get(slotIndex % slots.size()); MatchGame m = new MatchGame(); m.setHomeTeamId(home); m.setAwayTeamId(away); m.setMatchTime(slot.getStartTime()); m.setCourt(slot.getCourt()); result.add(m); slotIndex++; } // 固定第一支队,其余轮转 Long last = teamIds.remove(n - 1); teamIds.add(1, last); } return result; }逻辑说明:轮转法的思路是固定第一支队,其余队顺时针轮转,每轮生成 n/2 场比赛。slots是管理员在后台配置的可用时间段列表,按顺序分配,天然保证同一时间段不会重复。参数teamIds是参赛队 ID,slots里每个TimeSlot包含开始时间和场地。注意虚拟队-1L的处理,奇数队时必须补,否则轮转会数组越界。这个算法生成的是单循环,如果要小组赛加淘汰赛,小组赛用这个,淘汰赛按小组排名手动或再写一个对阵树生成。
3. 小程序端对接:登录、赛程列表和比分实时刷新怎么做
后端接口写好了,小程序端要解决三件事:用户身份怎么认、赛程怎么展示、比分怎么及时更新。微信小程序的登录体系和普通 Web 不一样,不能直接发账号密码,得走wx.login换openid那套。
3.1 微信登录换 openid 与手机号获取的完整链路
小程序端调用wx.login拿到临时code,传给后端,后端拿code加上小程序的appid和secret去微信服务器换openid和session_key。这是标准流程,但有两个坑:code只能用一次,五分钟过期;session_key不能下发给前端。
// 小程序端:登录并获取手机号 wx.login({ success(res) { if (res.code) { wx.request({ url: 'https://your-domain.com/api/auth/login', method: 'POST', data: { code: res.code }, success(loginRes) { // 后端返回自定义 token,存本地 wx.setStorageSync('token', loginRes.data.token); } }); } } }); // 获取手机号:必须由 button 的 open-type 触发 // <button open-type="getPhoneNumber" bindgetphonenumber="onGetPhone"> onGetPhone(e) { if (e.detail.code) { wx.request({ url: 'https://your-domain.com/api/auth/phone', method: 'POST', header: { 'Authorization': wx.getStorageSync('token') }, data: { phoneCode: e.detail.code } }); } }逻辑说明:wx.login的code换openid在后端做,前端只拿后端签发的 token。手机号获取必须用button的open-type="getPhoneNumber",用户点击授权后拿到phoneCode,再传给后端换真实手机号。参数上,appid和secret配在后端配置文件里,绝对不能写在小程序代码里,否则被人扒出来就能冒充你的小程序。token建议用 JWT,有效期设 7 天,过期让前端重新登录。
3.2 赛程列表与积分榜的接口约定和分页参数
小程序端展示赛程,最怕一次拉全部数据。院系联赛虽然数据量不大,但养成好习惯:列表接口必须分页,按时间倒序或正序由前端传参决定。
@GetMapping("/matches") public Result<Page<MatchVO>> listMatches( @RequestParam Long leagueId, @RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer size, @RequestParam(required = false) Integer status) { Page<MatchGame> p = new Page<>(page, size); LambdaQueryWrapper<MatchGame> qw = new LambdaQueryWrapper<>(); qw.eq(MatchGame::getLeagueId, leagueId); if (status != null) { qw.eq(MatchGame::getStatus, status); } qw.orderByAsc(MatchGame::getMatchTime); return Result.ok(matchService.page(p, qw)); }逻辑说明:page和size是分页参数,默认第一页十条。status可选,前端传 0 只看未开始,传 2 只看已结束。返回的Page对象里带total和records,小程序端用records渲染列表,用total判断是否还有下一页。注意orderByAsc按开赛时间正序,观众最关心「下一场什么时候打」,所以未开始的比赛应该排前面。如果要做「最近比赛」模块,再加一个按时间倒序的接口。
3.3 比分更新的轮询方案与 WebSocket 取舍
比分实时刷新是个经典问题。小程序原生支持 WebSocket,但维护长连接对毕业设计来说偏重,而且部署时还要处理反向代理的升级配置。我一般先用轮询,简单可靠。
// 比赛详情页:每 15 秒拉一次比分 Page({ data: { match: {} }, onLoad(options) { this.matchId = options.id; this.loadMatch(); this.timer = setInterval(() => this.loadMatch(), 15000); }, onUnload() { clearInterval(this.timer); // 页面卸载必须清定时器 }, loadMatch() { wx.request({ url: `https://your-domain.com/api/match/${this.matchId}`, success: (res) => this.setData({ match: res.data.data }) }); } });逻辑说明:setInterval每 15 秒请求一次比赛详情,onUnload里必须clearInterval,否则页面关了定时器还在跑,内存泄漏。15 秒是折中值,太短费流量,太长观众觉得卡。如果比赛进行中需要更实时,可以在status=1时把间隔调到 5 秒,比赛结束后停止轮询。WebSocket 方案适合同时在线人数多的场景,但院系联赛通常几十人同时看,轮询完全够用,部署也少一层麻烦。
4. 部署落地:从本地跑通到服务器上线的完整步骤
毕业设计答辩前最怕「本地能跑,服务器上白屏」。部署这件事,把每一步拆开做,比一次性打包上传靠谱得多。下面按「本地验证 → 数据库准备 → 后端打包 → 小程序配置」的顺序走。
4.1 本地环境验证:JDK、Maven 和 MySQL 的版本对齐
先确认本地能跑通,再谈服务器。版本不对齐是新手最常见的翻车点:Spring Boot 3.x 要求 JDK 17,MyBatis-Plus 3.5.x 对 Spring Boot 3 的支持要选对 starter。
| 组件 | 推荐版本 | 说明 |
|---|---|---|
| JDK | 17 | Spring Boot 3.x 最低要求,8 也能跑但建议 17 |
| Maven | 3.8+ | 用于依赖管理和打包 |
| MySQL | 8.0 | utf8mb4 字符集,支持 emoji 存队名 |
| Spring Boot | 3.2.x | 与 MyBatis-Plus 3.5.5+ 搭配 |
| 微信开发者工具 | 最新稳定版 | 调试小程序端 |
本地验证命令:mvn clean package能打出 jar,java -jar target/xxx.jar能启动,浏览器访问http://localhost:8080/api/league/list有返回,就算本地通了。数据库连接串写在application.yml里,本地用localhost:3306,密码别用 root 空密码,答辩时被看到不专业。
4.2 服务器部署:jar 包上传、systemd 守护与 Nginx 转发
服务器上不需要装 Tomcat,Spring Boot 内置了。把 jar 传上去,用 systemd 守护,进程挂了自动重启。
# 上传 jar 到服务器 scp target/league-system.jar user@your-server:/opt/league/ # 创建 systemd 服务文件 sudo vim /etc/systemd/system/league.service[Unit] Description=League System After=network.target mysql.service [Service] User=www WorkingDirectory=/opt/league ExecStart=/usr/bin/java -jar /opt/league/league-system.jar --spring.profiles.active=prod Restart=always RestartSec=10 [Install] WantedBy=multi-user.targetsudo systemctl daemon-reload sudo systemctl enable league sudo systemctl start league sudo systemctl status league # 看是否 active (running)逻辑说明:ExecStart里--spring.profiles.active=prod指定生产配置,数据库连接串写在application-prod.yml里。Restart=always保证进程异常退出后自动拉起,RestartSec=10是重启间隔。Nginx 负责把 443 端口的请求转发到后端 8080,同时处理小程序要求的 HTTPS。
server { listen 443 ssl; server_name your-domain.com; ssl_certificate /etc/nginx/cert/your.pem; ssl_certificate_key /etc/nginx/cert/your.key; location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }小程序正式版要求所有请求走 HTTPS,且域名要在微信后台配置白名单。开发阶段可以在开发者工具里勾选「不校验合法域名」,但上线前必须配好。
4.3 小程序端配置:合法域名、appid 与体验版发布
小程序端要改三个地方:appid换成自己的,请求域名换成服务器域名,然后在微信公众平台配置request合法域名。开发者工具里点「上传」,在公众平台「版本管理」里设为体验版,扫码就能在真机上测。正式发布要提交审核,审核时注意:不能有测试数据、不能有未完成的页面入口、隐私政策要写清楚收集了哪些信息(手机号、openid)。毕业设计一般用体验版演示就够,不用走正式审核。
5. 避坑与排查:这套系统最容易翻车的 5 个地方
5.1 现象:小程序请求全部失败,报「不在以下 request 合法域名列表中」
原因:微信开发者工具默认校验域名,服务器域名没加到白名单,或者用了 HTTP 而非 HTTPS。解决:开发阶段在「详情 → 本地设置」勾选「不校验合法域名」;上线前在公众平台「开发管理 → 开发设置 → 服务器域名」里把https://your-domain.com加进request合法域名。注意域名不能带端口,必须是 443。
5.2 现象:积分榜排名和实际不符,胜场相同的队顺序乱了
原因:ORDER BY win DESC, diff DESC里diff是净胜分,但如果两队净胜分也相同,MySQL 返回顺序不确定。解决:再加一个稳定的排序字段,比如ORDER BY win DESC, diff DESC, t.id ASC。另外确认status = 2的条件没漏,未结束的比赛被算进去会导致胜场虚高。
5.3 现象:比分更新后小程序页面不刷新,要手动退出重进
原因:轮询定时器没生效,或者onUnload里没清定时器导致多个定时器叠加,反而请求混乱。解决:检查setInterval是否在onLoad里调用,onUnload里是否clearInterval(this.timer)。另外小程序切到后台时定时器会被暂停,切回来要重新触发一次请求,可以在onShow里补一次loadMatch()。
5.4 现象:服务器上 jar 启动报「Access denied for user」,本地却正常
原因:生产数据库密码和本地不一致,或者 MySQL 用户没有远程访问权限。解决:检查application-prod.yml里的数据库连接串,确认用户名密码正确。如果数据库和 backend 不在同一台机器,要授权GRANT ALL ON league.* TO 'user'@'%' IDENTIFIED BY 'password';并FLUSH PRIVILEGES;。生产环境建议数据库只监听内网,不要暴露公网端口。
5.5 现象:赛程生成后出现同一队同一时间打两场
原因:轮转法里虚拟队-1L没跳过,或者slots数量少于同时段比赛数导致时间复用。解决:确认if (home == -1L || away == -1L) continue;这行在,且slots的长度至少能覆盖一轮的比赛数。如果场地只有一块,slots里每个时间段只能出现一次,不能循环取模,否则就会撞时间。我一般会在生成后加一个校验:遍历结果,检查同一matchTime下是否有重复teamId,有就抛异常提示管理员增加时间段。
6. 进阶技巧:用 MyBatis-Plus 自动生成建表 SQL 和接口文档
最后一个实用技巧,能省掉大量手写 SQL 和接口文档的时间。MyBatis-Plus 有个Db工具类,可以根据 Java 实体类反向生成建表语句,改字段时不用手动同步 SQL。配合 Swagger(或 SpringDoc)自动生成接口文档,答辩时直接打开文档页面演示,比截图专业得多。
// 根据实体类生成建表 SQL import com.baomidou.mybatisplus.generator.config.DataSourceConfig; import com.baomidou.mybatisplus.generator.config.GlobalConfig; import com.baomidou.mybatisplus.generator.FastAutoGenerator; public class SqlGenerator { public static void main(String[] args) { FastAutoGenerator.create("jdbc:mysql://localhost:3306/league", "root", "password") .globalConfig(builder -> builder.author("you").outputDir("/tmp/gen")) .strategyConfig(builder -> builder .addInclude("league", "team", "match_game") // 要生成的表 .entityBuilder().enableLombok()) .execute(); } }逻辑说明:FastAutoGenerator是 MyBatis-Plus 的代码生成器,addInclude指定要处理的表,outputDir是输出目录。它会生成 Entity、Mapper、Service、Controller 一整套,建表 SQL 在resources下。参数上,数据库连接串换成你自己的,enableLombok省去写 getter/setter。注意生成前先备份现有代码,生成器会覆盖同名文件。
接口文档用 SpringDoc,加一个依赖就行:
<dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> <version>2.3.0</version> </dependency>启动后访问http://localhost:8080/swagger-ui/index.html,所有接口和参数一目了然。答辩时老师问「你这个接口怎么调的」,直接打开页面点「Try it out」,比口头解释强十倍。
我自己的习惯是:每次改完实体类,先跑一遍生成器更新 SQL,再启动服务看 Swagger 有没有报错,最后才去改小程序端。这个顺序能保证后端永远是「活的」,不会出现文档和代码两张皮。做毕业设计最怕的就是最后一周发现数据库字段和代码对不上,那时候再改就是血泪教训。希望帮到你。
本文还有配套的精品资源,点击获取