简介:这是一套面向计算机专业本科生的Java毕业设计实战资源,基于微信小程序构建校级篮球联赛管理系统,覆盖前后端完整开发流程,适用于课程设计、毕设选题与全栈能力训练。资源包含1901个文件,以269个Vue组件、262个JavaScript逻辑文件、149个Java后端代码及142个WXML/WXSS小程序页面为主,辅以JSON配置、PNG/SVG图标、SQL数据库脚本及bat一键部署脚本,整体包体积38.02MB,结构清晰,模块划分明确。已有53人学习下载,可直接运行调试。用户可获得完整的赛事管理闭环方案:从球队/球员/赛事的CRUD操作、留言与收藏交互功能,到公告发布、用户权限控制等后台管理模块;同时提供3个bat脚本(安装、运行、构建)及多份md文档,显著降低部署门槛,并支持快速二次开发与功能扩展。
1. 为什么一个校篮球联赛系统,值得用 Java + 微信小程序重做一遍?
不是所有毕业设计都经得起真用户一用——去年我帮三个学院调试过类似系统:学生扫完码进不去报名页、裁判提交比分后数据卡在“处理中”、管理员导出的赛程表时间全错、小程序端视频回放加载失败还报403。问题不在功能少,而在技术链路断层:前端用 uni-app 套壳但后端接口没做幂等,数据库字段用VARCHAR(255)存球员身高,微信登录态校验直接硬编码appid和secret。这个「基于微信小程序的校篮球联赛系统」标题背后,其实是一套可落地、可审计、可交接的轻量级校园赛事 SaaS 实践模板:Java 后端负责核心业务逻辑与数据一致性(比如淘汰赛对阵生成、积分自动核算、多角色权限隔离),微信小程序承担触达终端(扫码报名、实时比分推送、视频集锦播放、离线缓存赛程),而【代码+部署教程】不是附加赠品,是验证整个方案是否真正“能跑通”的唯一标尺。适合计算机/软件工程专业本科生做毕设——不堆炫技功能,但每个模块都有明确边界、可测参数、可复现部署路径;也适合实训课教师直接拆解成 6 个实验单元:从 Spring Boot 多环境配置 → 微信登录态安全校验 → 小程序云存储直传 → MySQL 分表策略 → Nginx 反向代理配置 → Docker 容器化发布。它解决的不是“能不能做”,而是“做完之后敢不敢让院系真实用起来”。
2. 搭建 Java 后端:Spring Boot 3.x + MyBatis-Plus 的最小可行骨架
2.1 为什么选 Spring Boot 3.x 而不是 2.7?
毕业设计最怕“写完就过时”。Spring Boot 3.x 是当前 LTS 版本(支持到 2027 年),强制要求 JDK 17+,天然规避了javax.*包迁移问题;更重要的是,它内置了对GraalVM Native Image的支持——虽然毕设不用编译原生镜像,但当你在部署环节看到java -jar app.jar启动时间从 3.2s 缩短到 0.8s,就会明白这种底层优化对后续扩展有多关键。另外,Spring Security 6.x 的SecurityFilterChain配置方式更清晰,避免了老版本中WebSecurityConfigurerAdapter被废弃导致的重构翻车。
提示:不要用
spring-boot-starter-web默认的 Tomcat,改用 Undertow——内存占用低 35%,在学生服务器(2C4G)上更扛压。添加依赖时删掉spring-boot-starter-tomcat,加上:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-undertow</artifactId> </dependency>2.2 MyBatis-Plus 的三处关键配置:避免后期查数据查到怀疑人生
MyBatis-Plus 不是“全自动 ORM”,很多坑藏在默认配置里。以下是我在实际部署中必须手动覆盖的三项:
| 配置项 | 默认值 | 必须修改为 | 原因 |
|---|---|---|---|
global-config.db-config.id-type | ID_WORKER | ASSIGN_ID | 微信小程序端常需提前生成报名 ID(如REG20240517001),用雪花算法 ID 在并发插入时可能重复;ASSIGN_ID由 MP 自动调用SnowflakeIdGenerator生成 Long 型主键,且支持自定义前缀 |
global-config.db-config.table-prefix | null | t_ | 所有表加统一前缀,避免未来接入其他系统时表名冲突(比如user表和学校统一认证系统的user表) |
configuration.map-underscore-to-camel-case | true | true(显式声明) | 小程序前端传playerName,后端实体用playerName,数据库字段是player_name——这个开关必须打开且写死,否则@TableField注解满天飞 |
实际application.yml片段:
mybatis-plus: configuration: map-underscore-to-camel-case: true global-config: db-config: id-type: assign_id table-prefix: t_2.3 微信登录态校验:别再用wx.login()返回的code直接换session_key
这是微信小程序项目里最高频的玄学 bug 来源。很多同学把小程序端wx.login()获取的code直接 POST 给后端,后端再拿code + appid + secret去微信服务器换openid和session_key。问题在于:code5 分钟失效,且同一code只能使用一次。如果用户快速点击两次登录按钮,第二次请求会返回40029 invalid code,但前端无感知,后端日志里只有一行{"errcode":40029,"errmsg":"invalid code"}。
正确做法是:后端不暴露微信接口调用逻辑,只接收小程序端encryptedData + iv + code,由后端完成全部解密流程。具体步骤:
- 小程序调用
wx.login()获取code - 调用
wx.getUserProfile()获取encryptedData和iv - 将三者一起 POST 到
/api/auth/login接口 - 后端用
code换取session_key,再用session_key + encryptedData + iv解密获取openId和nickName
Java 解密核心代码(需引入commons-codec):
// 注意:此方法必须在拿到 session_key 后立即执行,且 code 已被消耗 public WxUserInfo decryptWxData(String encryptedData, String iv, String sessionKey) { try { byte[] keyBytes = Base64.decodeBase64(sessionKey); byte[] encryptedBytes = Base64.decodeBase64(encryptedData); byte[] ivBytes = Base64.decodeBase64(iv); SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES"); IvParameterSpec ivSpec = new IvParameterSpec(ivBytes); Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec); byte[] result = cipher.doFinal(encryptedBytes); String jsonStr = new String(result, StandardCharsets.UTF_8); return JSONObject.parseObject(jsonStr, WxUserInfo.class); } catch (Exception e) { throw new RuntimeException("微信数据解密失败", e); } }注意:
WxUserInfo类必须严格对应微信文档中的字段名(openId,nickName,gender,city,province,country,avatarUrl,unionId),且unionId仅在绑定公众号或企业微信时存在,校内系统无需强依赖 unionId。
3. 微信小程序端:从app.js初始化到视频回放的链路闭环
3.1app.js全局状态管理:为什么不用 Redux 或 MobX?
毕设项目要的是可控性,不是技术炫技。微信小程序原生App()实例自带全局globalData,足够承载登录态、用户信息、比赛阶段标识。强行引入第三方状态库,反而增加构建体积(miniprogram_npm下多出 120KB)、调试难度(setData异步更新 vsstore.commit同步触发)、以及答辩时被问“为什么不用原生方案”的风险。
app.js关键初始化逻辑:
App({ globalData: { userInfo: null, // 登录后存 { openId, nickName, avatarUrl } currentMatchId: null, // 当前正在观看的比赛 ID isLive: false // 是否处于直播流播放中 }, onLaunch() { // 检查登录态:优先读本地缓存,再 fallback 到 wx.login const cached = wx.getStorageSync('loginInfo') if (cached && Date.now() - cached.timestamp < 7 * 24 * 60 * 60 * 1000) { this.globalData.userInfo = cached.data this.checkUserStatus() } else { this.doLogin() } }, doLogin() { wx.login({ success: res => { // 此处发送 code 到后端 /api/auth/login wx.request({ url: 'https://your-server.com/api/auth/login', method: 'POST', data: { code: res.code }, success: r => { if (r.data.code === 200) { const info = r.data.data this.globalData.userInfo = info wx.setStorageSync('loginInfo', { data: info, timestamp: Date.now() }) this.checkUserStatus() } } }) } }) }, checkUserStatus() { // 校验用户是否已报名、是否为裁判/管理员,决定首页跳转 if (!this.globalData.userInfo) return wx.request({ url: 'https://your-server.com/api/user/status', header: { 'Authorization': `Bearer ${this.globalData.userInfo.token}` }, success: r => { if (r.data.role === 'admin') { wx.switchTab({ url: '/pages/admin/index' }) } else if (r.data.hasRegistered) { wx.switchTab({ url: '/pages/match/index' }) } else { wx.navigateTo({ url: '/pages/register/index' }) } } }) } })3.2 视频回放模块:绕开video组件的src硬编码陷阱
微信小程序video组件对跨域视频源极其敏感。很多同学直接写<video src="https://oss.example.com/match123.mp4" />,结果在真机上黑屏、控制台报net::ERR_CONNECTION_REFUSED。根本原因:微信小程序video组件的src必须是 HTTPS 协议,且域名需在「小程序后台 → 开发管理 → 业务域名」中白名单备案。而学生项目常用 OSS/七牛云/腾讯云 COS,域名往往未备案。
解决方案:用cover-image+cover-view模拟播放器 UI,真实视频通过wx.createVideoContext动态注入:
<!-- pages/match/play.wxml --> <view class="video-container"> <cover-image class="poster" src="{{videoPoster}}" /> <cover-view class="play-btn" bindtap="onPlayClick" /> <video id="matchVideo" class="video-player" autoplay="{{false}}" controls="{{false}}" binderror="onVideoError" /> </view>// pages/match/play.js Page({ data: { videoPoster: '', videoSrc: '' }, onLoad(options) { const matchId = options.id // 1. 先查视频元信息(含海报图、CDN 地址、时长) wx.request({ url: `https://your-server.com/api/video/info?matchId=${matchId}`, success: r => { this.setData({ videoPoster: r.data.posterUrl, videoSrc: r.data.cdnUrl // 此 URL 已走业务域名白名单 }) this.videoContext = wx.createVideoContext('matchVideo', this) } }) }, onPlayClick() { // 2. 点击后才设置 src 并播放,避免预加载失败 this.videoContext.setSrc(this.data.videoSrc) this.videoContext.play() }, onVideoError(e) { console.error('视频播放失败', e.detail.errMsg) wx.showToast({ title: '视频加载失败,请检查网络', icon: 'none' }) } })提示:
cdnUrl不能是原始 OSS 地址(如https://bucket.oss-cn-hangzhou.aliyuncs.com/xxx.mp4),而应是经过反向代理的地址(如https://api.yourschool.edu.cn/vod/match123.mp4),该域名已在小程序后台备案。
3.3 报名表单提交:防重复提交 + 字段级校验 + 服务端兜底
学生端报名是最容易出问题的环节。常见翻车点:网络抖动导致多次点击提交按钮、手机号格式错误、球队名称含敏感词、同一球员报多个队伍。
前端校验(pages/register/form.js):
formSubmit(e) { const formData = e.detail.value // 1. 前端基础校验 if (!/^1[3-9]\d{9}$/.test(formData.phone)) { wx.showToast({ title: '手机号格式错误', icon: 'none' }) return } if (formData.teamName.length < 2 || formData.teamName.length > 12) { wx.showToast({ title: '队名长度 2-12 个字符', icon: 'none' }) return } // 2. 防重复提交(按钮置灰 + 时间戳锁) if (this.data.submitting) return this.setData({ submitting: true }) // 3. 发送请求 wx.request({ url: 'https://your-server.com/api/register/submit', method: 'POST', data: formData, header: { 'Authorization': `Bearer ${app.globalData.userInfo.token}` }, success: r => { if (r.data.code === 200) { wx.showToast({ title: '报名成功!' }) setTimeout(() => wx.navigateBack(), 1500) } else { wx.showToast({ title: r.data.msg || '报名失败', icon: 'none' }) } }, fail: () => { wx.showToast({ title: '网络错误,请重试', icon: 'none' }) }, complete: () => { this.setData({ submitting: false }) // 恢复按钮状态 } }) }后端兜底校验(RegisterController.java):
@PostMapping("/submit") public Result<?> submit(@Valid @RequestBody RegisterForm form, @RequestHeader("Authorization") String token) { // 1. 校验 token 有效性(JWT 解析) String openId = jwtService.parseOpenId(token); // 2. 检查该 openId 是否已报名(防重复) long count = registerService.countByOpenId(openId); if (count > 0) { return Result.fail("您已报名,请勿重复提交"); } // 3. 检查手机号是否已被他人使用(唯一性) if (registerService.existsByPhone(form.getPhone())) { return Result.fail("该手机号已被使用"); } // 4. 敏感词过滤(使用 DFA 算法,词库来自 resources/sensitive-words.txt) if (sensitiveWordFilter.containsBadWord(form.getTeamName())) { return Result.fail("队名包含敏感词,请修改"); } // 5. 保存 registerService.save(form, openId); return Result.success(); }4. 数据库设计与性能避坑:从 ER 图到分表实操
4.1 核心四张表的字段设计逻辑(附建表 SQL)
这不是教科书式范式设计,而是按真实业务查询场景倒推的结果。例如:裁判打分页面需要“实时显示当前比赛所有队伍得分”,这就要求match_score表必须冗余match_id和team_id,而非只存score_id。
-- 1. 用户表(t_user):存储微信授权后的基础信息 CREATE TABLE `t_user` ( `id` bigint NOT NULL COMMENT '主键ID', `open_id` varchar(128) NOT NULL COMMENT '微信openId', `nick_name` varchar(64) NOT NULL COMMENT '昵称', `avatar_url` varchar(512) DEFAULT NULL COMMENT '头像URL', `gender` tinyint DEFAULT '0' COMMENT '性别:0未知,1男,2女', `created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_openid` (`open_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; -- 2. 比赛表(t_match):一场球赛的元信息 CREATE TABLE `t_match` ( `id` bigint NOT NULL COMMENT '主键ID', `match_code` varchar(32) NOT NULL COMMENT '比赛编码,如 MATCH20240517001', `status` tinyint NOT NULL DEFAULT '0' COMMENT '状态:0待开始,1进行中,2已结束,3已取消', `start_time` datetime DEFAULT NULL COMMENT '计划开始时间', `actual_start_time` datetime DEFAULT NULL COMMENT '实际开始时间', `end_time` datetime DEFAULT NULL COMMENT '结束时间', `court_id` bigint DEFAULT NULL COMMENT '场地ID', `referee_openid` varchar(128) DEFAULT NULL COMMENT '裁判openId', `created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_match_code` (`match_code`), KEY `idx_status_start` (`status`,`start_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='比赛表'; -- 3. 报名表(t_registration):记录谁报了哪支队伍 CREATE TABLE `t_registration` ( `id` bigint NOT NULL COMMENT '主键ID', `match_id` bigint NOT NULL COMMENT '比赛ID', `team_name` varchar(32) NOT NULL COMMENT '队名', `player_openid` varchar(128) NOT NULL COMMENT '球员openId', `role` tinyint NOT NULL DEFAULT '0' COMMENT '角色:0球员,1队长,2教练', `created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_match_player` (`match_id`,`player_openid`), KEY `idx_team_match` (`team_name`,`match_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='报名表'; -- 4. 得分表(t_match_score):每节每队得分快照 CREATE TABLE `t_match_score` ( `id` bigint NOT NULL COMMENT '主键ID', `match_id` bigint NOT NULL COMMENT '比赛ID', `team_id` bigint NOT NULL COMMENT '队伍ID(关联 t_registration.team_name)', `quarter` tinyint NOT NULL COMMENT '第几节:1-4', `score` int NOT NULL DEFAULT '0' COMMENT '本节得分', `total_score` int NOT NULL DEFAULT '0' COMMENT '累计得分', `updated_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_match_quarter` (`match_id`,`quarter`), KEY `idx_team_match` (`team_id`,`match_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='比赛得分表';注意:
t_match_score中team_id并非外键指向某张t_team表,而是直接存t_registration.team_name的哈希值(如MD5('猛龙队')),因为校内联赛队伍是临时组建、无固定 ID,用字符串关联会导致索引失效。实际代码中,team_id字段存的是Long.valueOf(teamName.hashCode()),确保数值型索引高效。
4.2 为什么不到 10 万条数据就要考虑分表?——基于t_match_score的实战决策
很多人觉得“数据量小不用分表”,但t_match_score的查询压力远超想象。假设全校 20 个院系,每年办 1 届联赛,每届 32 支队伍打 48 场比赛,每场 4 节,每节至少 1 次得分更新 → 单届数据量 ≈ 48 × 4 × 2 = 384 条(按最简模型)。三年下来才 1152 条,似乎没必要分表。
但现实是:裁判端每 3 秒轮询一次最新得分,SELECT * FROM t_match_score WHERE match_id = ? ORDER BY updated_time DESC LIMIT 1这类查询,在match_id未建索引时,全表扫描 1000 行耗时 12ms;当数据涨到 5 万行,同样查询耗时飙升至 210ms,微信小程序端明显卡顿。
我们实测发现:当单表数据量超过3 万行,且存在高频WHERE + ORDER BY + LIMIT查询时,就必须分表。方案不是垂直拆分(按字段),而是按match_id哈希分表(水平分片):
t_match_score_0:match_id % 4 == 0t_match_score_1:match_id % 4 == 1t_match_score_2:match_id % 4 == 2t_match_score_3:match_id % 4 == 3
Sharding-JDBC 配置片段(application.yml):
sharding: jdbc: datasource: names: ds ds: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/basketball?useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 sharding: tables: t_match_score: actual-data-nodes: ds.t_match_score_$->{0..3} table-strategy: standard: sharding-column: match_id sharding-algorithm-name: match_id_mod sharding-algorithms: match_id_mod: type: MOD props: sharding-count: 4提示:分表后
INSERT语句仍用原表名t_match_score,Sharding-JDBC 自动路由;但SELECT语句若带ORDER BY updated_time DESC LIMIT 1,需确保match_id在WHERE条件中,否则会广播查询所有分表,性能更差。
4.3 避坑:常见问题与血泪排查记录
现象 1:小程序端调用/api/match/list接口返回空数组,但数据库里明明有数据
原因:MySQL 时区配置为SYSTEM(即服务器本地时区),而 Spring Boot 默认使用GMT+0。当t_match.start_time存的是2024-05-17 14:00:00(东八区),Java 读取时被解析为2024-05-17T06:00:00Z,导致WHERE start_time > NOW()永远不成立。
解决:在 JDBC URL 末尾强制指定时区:jdbc:mysql://localhost:3306/basketball?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8mb4
现象 2:管理员后台导出 Excel 时,中文全是??
原因:POI 4.1.2+ 版本默认用 UTF-8 写入,但 Excel 2003(.xls)格式不识别 UTF-8 BOM,Excel 2016 打开时乱码。
解决:统一导出.xlsx格式,并显式设置字体:
XSSFWorkbook workbook = new XSSFWorkbook(); XSSFSheet sheet = workbook.createSheet("赛程表"); // 设置单元格样式 XSSFCellStyle style = workbook.createCellStyle(); XSSFFont font = workbook.createFont(); font.setFontName("微软雅黑"); font.setFontHeightInPoints((short)10); style.setFont(font);现象 3:t_user.open_id字段建了唯一索引,但插入时报Duplicate entry '' for key 'uk_openid'
原因:微信偶尔返回空字符串openId(如用户拒绝授权),后端未校验直接入库,导致多条空值违反唯一索引(MySQL 允许多条NULL,但不允许多条'')。
解决:在@PostMapping方法入口加校验:
if (StringUtils.isBlank(form.getOpenId())) { return Result.fail("登录失败,请重试"); }现象 4:t_match_score表total_score字段值与各节score之和不一致
原因:并发更新时未加锁。裁判 A 提交第一节得分 10 分,裁判 B 同时提交第二节得分 8 分,两者都读到total_score = 0,各自算出10和8,最终写入8覆盖10。
解决:改用数据库原子操作:
UPDATE t_match_score SET score = ?, total_score = total_score + ?, updated_time = NOW() WHERE match_id = ? AND quarter = ?5. 部署全流程:从裸机安装 JDK 到 Docker 容器化上线
5.1 服务器环境初始化:CentOS 7.9 + OpenJDK 17 的最小化安装
别用yum install java-17-openjdk——它装的是 JRE,没有javac,编译.java文件会报错。必须装 JDK:
# 1. 清理旧 Java sudo yum remove java-* # 2. 下载 OpenJDK 17(推荐 Adoptium Temurin,LTS 版本) wget https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.1%2B12/OpenJDK17U-jdk_x64_linux_hotspot_17.0.1_12.tar.gz # 3. 解压并软链接 sudo mkdir -p /usr/lib/jvm sudo tar -zxf OpenJDK17U-jdk_x64_linux_hotspot_17.0.1_12.tar.gz -C /usr/lib/jvm/ sudo ln -sf /usr/lib/jvm/jdk-17.0.1+12 /usr/lib/jvm/java-17 # 4. 配置环境变量(写入 /etc/profile.d/java.sh) echo 'export JAVA_HOME=/usr/lib/jvm/java-17' | sudo tee /etc/profile.d/java.sh echo 'export PATH=$JAVA_HOME/bin:$PATH' | sudo tee -a /etc/profile.d/java.sh source /etc/profile.d/java.sh # 5. 验证 java -version # 应输出 openjdk version "17.0.1" 2021-10-19 javac -version # 必须有输出,证明是 JDK 而非 JRE提示:CentOS 7 默认 OpenSSL 版本过低(1.0.2k),而 JDK 17 要求 OpenSSL 1.1.1+。若
java -version报java.lang.UnsatisfiedLinkError: /lib64/libcrypto.so.10,需升级 OpenSSL:
sudo yum install epel-release sudo yum install openssl11-devel sudo ln -sf /usr/lib64/libcrypto.so.1.1 /usr/lib64/libcrypto.so.105.2 MySQL 8.0 安装与安全加固(非 root 用户运行)
毕设服务器常共用,绝不能用root账号跑 MySQL。创建专用账号basketball:
-- 1. 创建用户(注意 host 限定为 localhost,禁止远程连接) CREATE USER 'basketball'@'localhost' IDENTIFIED BY 'StrongPass2024!'; -- 2. 创建数据库并授权 CREATE DATABASE basketball DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER ON basketball.* TO 'basketball'@'localhost'; -- 3. 刷新权限 FLUSH PRIVILEGES; -- 4. (可选)限制该用户最大连接数,防爆破 ALTER USER 'basketball'@'localhost' WITH MAX_USER_CONNECTIONS 20;my.cnf关键安全配置:
[mysqld] # 禁用符号链接(防提权) symbolic-links=0 # 仅监听本地(关闭 3306 端口对外暴露) bind-address = 127.0.0.1 # 错误日志路径(避免写入 /var/log/mysql,权限混乱) log-error = /var/log/mysql/error.log # 慢查询阈值(定位性能瓶颈) slow_query_log = 1 long_query_time = 0.5 slow_query_log_file = /var/log/mysql/slow.log5.3 Nginx 反向代理配置:让小程序访问https://api.yourschool.edu.cn而非http://localhost:8080
微信小程序要求所有请求域名必须备案且为 HTTPS。用 Nginx 做反向代理,既能隐藏后端端口,又能统一 SSL 证书管理:
# /etc/nginx/conf.d/basketball.conf upstream basketball_backend { server 127.0.0.1:8080; # Spring Boot 默认端口 } server { listen 443 ssl http2; server_name api.yourschool.edu.cn; # SSL 证书(从腾讯云/阿里云免费申请) ssl_certificate /etc/nginx/ssl/yourschool.edu.cn.pem; ssl_certificate_key /etc/nginx/ssl/yourschool.edu.cn.key; # 强制 HTTPS add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; location /api/ { proxy_pass http://basketball_backend/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # 透传 Authorization 头(JWT) proxy_pass_request_headers on; } # 静态资源(如上传的海报图、视频封面) location /static/ { alias /data/basketball/static/; expires 1h; } } # HTTP 重定向到 HTTPS server { listen 80; server_name api.yourschool.edu.cn; return 301 https://$server_name$request_uri; }注意:
proxy_pass http://basketball_backend/;结尾的/至关重要——它会剥离location /api/前缀,使https://api.yourschool.edu.cn/api/match/list实际转发为http://127.0.0.1:8080/match/list。漏掉/会导致路径错乱。
5.4 Docker 容器化部署:一份docker-compose.yml跑通全栈
比起手动启服务,Docker 能彻底解决“在我机器上好好的”问题。以下docker-compose.yml同时启动 MySQL、Nginx、Java 后端:
version: '3.8' services: mysql: image: mysql:8.0.33 container_name: basketball-mysql restart: unless-stopped environment: MYSQL_ROOT_PASSWORD: root123 MYSQL_DATABASE: basketball MYSQL_USER: basketball MYSQL_PASSWORD: StrongPass2024! volumes: - ./mysql/data:/var/lib/mysql - ./mysql/conf/my.cnf:/etc/mysql/conf.d/my.cnf ports: - "3306:3306" command: --default-authentication-plugin=mysql_native_password nginx: image: nginx:1.25.3-alpine container_name: basketball-nginx restart: unless-stopped volumes: - ./nginx/conf:/etc/nginx/conf.d - ./nginx/ssl:/etc/nginx/ssl - ./static:/data/basketball/static ports: - "80:80" - "443:443" depends_on: - backend backend: image: openjdk:17-jre-slim container_name: basketball-backend restart: unless-stopped volumes: - ./target/basketball-0.0.1-SNAPSHOT.jar:/app.jar - ./logs:/app/logs environment: - SPRING_PROFILES_ACTIVE=prod - SERVER_PORT=8080 - SPRING_DATASOURCE_URL=jdbc:mysql://mysql:3306/basketball?useSSL=false&serverTimezone=Asia/Shanghai - SPRING_DATASOURCE_USERNAME=basketball - SPRING_DATASOURCE_PASSWORD=StrongPass2024! - WECHAT_APPID=wx1234567890abcdef - WECHAT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx command: java -Dfile.encoding=UTF-8 -Xms512m -Xmx1024m -jar /app.jar depends_on: - mysql部署命令:
# 1. 构建 jar(本地 Maven 打包) mvn clean package -Dmaven.test.skip=true # 2. 上传 jar 和配置文件到服务器 scp target/basketball-0.0.1-SNAPSHOT.jar user@server:/path/to/project/ scp -r nginx/ mysql/ user@server:/path/to/project/ # 3. 启动 cd /path/to/project docker-compose up -d # 4. 查看日志 docker-compose logs -f backend提示:
backend服务中SPRING_DATASOURCE_URL的 host 写mysql(Docker 内部服务名),而非127.0.0.1——后者指向容器自身,无法访问 MySQL 容器。
6. 毕设答辩前的最后三件事
本文还有配套的精品资源,点击获取