每年一到10月中下旬,我的私信里就会集中出现同一类问题,内容几乎一字不差:“有没有适合做毕设的Java项目推荐?最好带小程序,代码能跑、文档能写、答辩有的讲。”说实话,毕设选题这件事,难的不是写代码,而是找不到一个“工作量适中、技术栈合理、演示效果好、老师问得住”的题目。今天聊的这个——基于微信小程序的考研资源共享平台,后端用Spring Boot,就是我近几年反复推荐给学生的一个常规选择。
它解决什么问题?考研的人越来越多,公开资料却散乱在各处:QQ群文件过期、网盘链接失效、贴吧论坛真假难辨。一个专门用来整理、上传、审核、下载考研资料的小程序平台,正好切中刚需,又不像商城类项目那样烂大街。对毕设来说,这个题目从业务逻辑、数据表设计到前后端联调都有内容可写,从头做完能拿不错的分数,关键是一整套做下来,该踩的坑你基本都能遇到一遍。
这篇文章会按一个实际能落地的方案拆开来讲:为什么选这个题、技术选型怎么解释、数据库怎么设计、小程序端和后端各自的关键实现、本地调试到上线的坑,最后是几个可以在答辩里加分的扩展点。适合两类人看:一是正在选题的计算机相关专业学生,二是想快速跑通一个全栈小项目的初级开发。
1. 为什么考研资源共享平台适合当毕设题目
1.1 一个能“讲出业务价值”的题目,答辩一开口就赢了
我见过太多毕设项目,功能做了一大堆,老师问一句“你这个系统解决了什么问题”,学生答不上来。这个问题看似简单,其实很难,因为它要求你从题目本身的业务逻辑出发,说得清用户痛点。
考研资源共享平台的痛点非常具体:考研资料的数量庞大、渠道分散、质量参差不齐。学生在QQ群、网盘、论坛之间来回切换,链接动不动就失效;上传者没有统一的整理标准,资料更新了也没人通知;更麻烦的是,谁都能发资料,没有人对内容的完整性负责。这些痛点几乎是每个经历过考研复习的人都能脱口而出的。
这个题目把痛点拆解成了一套完整的业务逻辑:用户上传资料,管理员审核资料,普通用户浏览、搜索、预览、下载、收藏。整个流程里天然存在两种角色、三种状态,业务上不空洞,数据表之间有真实的关联关系。你可以在开题报告里写“本系统旨在解决考研资料分散、质量无保障、分享效率低下的问题”,这句话一出来,论文的开篇就有说服力了。相比之下,“图书管理系统”“学生选课系统”这类题目虽然经典,但太容易写成单纯的增删改查展示,答辩时很难讲出深度。
1.2 技术覆盖面刚好卡在全栈入门到进阶之间
这个题目对技术栈的覆盖也是恰到好处。前端是微信小程序,要写WXML、WXSS、JS、JSON四种文件;后端是Spring Boot,天然分Controller、Service、Mapper/DAO三层;数据库用MySQL,要设计表结构、写外键关联、做分页查询;中间还有文件上传下载、JSON交互、跨域处理这类联调问题。
从工作量来看,它比“单表增删改查”重,因为涉及文件存储和权限控制;但比微服务、高并发那些方向轻得多,不依赖分布式中间件,一个人完全能做完。对毕设来说这是最舒服的位置:工作量看得见摸得着,又不至于把自己陷进调不完的坑里。更重要的是,这个题目里几乎所有难点都是“跳一跳够得着”的水平——比如审核状态流转、登录状态保持、文件存储路径规划,这些内容既能写进论文,又是实际开发中一定会碰到的通用问题。
2. 技术选型的逻辑:微信小程序 + Spring Boot 为什么是黄金组合
2.1 前端为什么选微信小程序而不是App或H5
很多学生纠结前端到底用什么形态。我的建议很直接:本科生毕设、个人开发、还要兼顾演示效果,微信小程序是性价比最高的选择。
先说原生App的问题。如果你做安卓App,需要处理不同机型的适配、权限申请、应用签名;如果还要上架应用商店,流程更长。做iOS更是需要开发者账号和证书,成本直接翻倍。App虽然“看起来更工程化”,但对一个毕设来说,把大量时间耗在环境配置和打包上,往往是亏的。
H5网页版倒是简单,但有几个天然短板。第一,手机浏览器里打开体验一般,考试倒计时、资料卡片这类界面做出来不够精致;第二,H5不方便“传播”——考研资料平台的用户是同学之间互相推荐的,微信小程序扫个码就能打开,这一点H5比不了。还有更重要的一点:微信小程序自带了一整套登录体系,用户不需要注册账号密码,用微信身份一键登录,这个体验对非技术背景的用户非常友好。
也可以提一下uni-app。如果你想兼顾“多端复用”,uni-app可以写一套代码同时出小程序和App。但如果是学校课程考核明确的“微信小程序开发”方向,我更建议直接用原生小程序,因为代码结构更直观,答辩时老师从源码上能看到WXML和JS的分工,而不是被一层Vue封装隔开。
2.2 后端为什么用Spring Boot而不是直接用云开发
微信云开发是另一个常见选择。它最快,写云函数连后端服务器都不用自己搭,数据库直接用云数据库,上线也快。但作为毕设,这里有一个很大的隐患:云开发把后端细节屏蔽得太多了,老师问“你的Service层怎么设计的”“你的接口怎么鉴权”时,你拿不出对应的代码来讲。
Spring Boot是Java课程体系里的主流框架,IOC容器、AOP切面、Starter自动配置、REST接口这些概念在课程里基本都讲过。选择Spring Boot意味着你的项目架构是透明可见的,从Controller到Service再到Mapper,每一层都能对应到一个知识点。答辩时你能指着代码说“这是依赖注入”“这是事务控制”“这是拦截器”,这些是实打实的后端开发基本功。
再说生态。Spring Boot + MySQL + MyBatis/MyBatis-Plus是Java毕设里最常见的组合,遇到任何报错基本都能在搜索引擎里找到答案。你选一个冷门框架虽然显得有个性,但出了问题没人帮你排坑,那就得不偿失了。用主流的组合,本质上是用前人的经验帮你省时间。
2.3 一套能直接落地的工程目录结构
决定了技术栈,接下来是工程怎么组织。很多学生项目写到最后Controller里写了一堆业务逻辑,Service层是空的,这不是好习惯。一个能让答辩老师点头的标准分层结构大概是这样的:
exam-share-backend ├── pom.xml └── src/main/java/com/example/examshare ├── controller/ # 接收请求,参数校验,返回统一结果 ├── service/ # 业务逻辑,事务控制 ├── mapper/ # MyBatis接口,数据访问 ├── entity/ # 数据库实体类 ├── common/ # 统一返回Result、全局异常处理 ├── config/ # 拦截器、跨域配置、静态资源映射 └── util/ # JwtUtil、FileUtil等工具类这层的原则就一句话:Controller不写业务、Service不碰SQL、Mapper只做数据访问。你把Android课设里写SQL写在Activity里的习惯改掉,这个项目的代码质量就能超过一半的同组学生了。后面我会详细说每个层里面具体放什么代码。
3. 功能模块拆解:从游客到管理员的完整业务闭环
3.1 小程序端八大功能
考研资源共享平台的前端功能不需要特别花哨,但每一个功能都得落到数据库的某张表上。通常我会把小程序端拆成下面八个模块,这套功能列表直接就能当开题报告里的“系统功能模块图”文字版:
- 微信一键登录:通过
wx.login()获取临时code,交给后端去微信接口换取openid,自动完成注册和登录,用户无感。 - 首页分类导航:政治、英语、数学、专业课等分类,分类列表由后端接口动态加载,不写死在代码里。
- 资料列表展示:按分类加载考研资料,支持分页,按发布时间、下载量排序,展示封面图和文件大小。
- 搜索功能:按资料标题模糊搜索,后端用SQL的
LIKE查询实现。 - 资料详情页:展示资料简介、上传者、下载次数,提供“预览”和“下载”两个按钮。
- 文档在线预览与下载:先下载到本地临时文件,再用微信内置的
openDocument打开。 - 收藏功能:用户可以收藏资料,个人中心里可以查看历史收藏。
- 个人中心:展示头像昵称、我的上传、我的收藏、我的下载记录。
这八个功能看着不多,但已经覆盖了一个内容类平台最核心的闭环:浏览、检索、消费(下载)、沉淀(收藏)、贡献(上传)。做毕设不需要做成第二个百度网盘,把主流程跑通、跑稳,比堆功能重要得多。
3.2 管理端四项核心操作
管理端我建议做四个功能就够,不要贪多:
- 分类管理:对考研资料的分类做增删改查,调整排序。
- 资料审核:查看待审核状态的资料,通过或驳回,驳回时必须填写原因,方便用户知道哪里有问题。
- 用户管理:查看用户列表,可以对恶意用户禁用账号。
- 内容统计:统计资料总数、用户总数、今日下载次数,用几个简单的SQL聚合查询就能实现,但课件和论文里可以写成“数据可视化模块”。
管理端可以做成小程序里的一个角色页面,也可以单独做一个Vue管理后台。毕设为了控制工作量,直接在同一个小程序里通过role字段区分管理员角色是最常见的做法。管理员登录后看到多了一个“管理”Tab,里面就是上面四个功能。
3.3 一个用户操作流走通整个项目
我每次让学生画系统流程图,他们都觉得难。其实不用画得很复杂,你把一个典型用户从进入系统到完成操作的全过程捋一遍,流程图自然就出来了。以这个平台为例:
学生A打开小程序,进入首页看到一个分类列表,点进“英语”分类,发现一份《考研英语历年真题汇编》,点击详情页预览了几页内容,觉得质量不错,点击“收藏”并“下载”。此时服务器端记录了一条下载记录,资源的下载量加1,上传者的积分加1。如果学生A自己有一份整理好的专业课笔记,他可以点“上传资料”,填写标题、简介、选择分类、上传PDF文件,资料进入“待审核”状态。管理员在后台看到这份资料,点开预览,确认没有违规内容后点击通过,资料状态变成“已发布”,可以在前台被搜索到。
这条链路看起来简单,但它同时串联了登录、分页查询、文件上传下载、状态流转、数据统计五个核心功能。答辩的时候老师让你讲“系统整体流程”,你就把这个故事讲一遍,每个环节顺势提一句对应的数据表和接口,这一段基本就能拿到不错的印象分。
4. 数据库设计:五张表怎么撑起资源流转
4.1 用户表与登录态设计
数据库是整个项目的根基。很多学生一上来就建十几张表,结果一张表里就三五个字段,冗余得一塌糊涂。考研资源共享平台的核心数据表其实五张就够了,用户表是第一张。
用户表(user)的核心字段:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键,自增 |
| openid | varchar(64) | 微信openid,唯一索引 |
| nickname | varchar(50) | 昵称 |
| avatar_url | varchar(255) | 头像地址 |
| role | tinyint | 角色,0普通用户,1管理员 |
| status | tinyint | 状态,0正常,1禁用 |
| create_time | datetime | 注册时间 |
这里有两个细节值得注意。第一,openid必须加唯一索引,因为它是微信用户的唯一身份标识,后端靠它判断是登录还是注册。第二,不要直接用openid当主键,它是字符串,做关联查询时性能不如bigint自增主键,而且一旦微信侧策略调整,字符串主键会让你非常被动。多一层“内部id”,实际上多了一层隔离。
4.2 资源表与分类表的字段取舍
分类表特别简单,但作用很大。分类表(category)就四个字段:id、name、sort_order(排序号)、create_time。为了扩展方便,你可以在后台写好默认数据:政治、英语一、英语二、数学一、数学二、专业课等。
资源表(resource)是整个项目的核心中的核心:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| category_id | bigint | 所属分类,外键关联category表 |
| title | varchar(100) | 资料标题 |
| description | text | 资料简介 |
| file_url | varchar(255) | PDF文件相对路径 |
| cover_url | varchar(255) | 封面图相对路径 |
| file_size | varchar(20) | 文件大小,如“12.5MB” |
| uploader_id | bigint | 上传用户id |
| download_count | int | 下载次数 |
| favorite_count | int | 收藏次数 |
| status | tinyint | 审核状态,0待审核,1已发布,2已驳回,3已下架 |
| create_time | datetime | 上传时间 |
一个容易被忽略的设计是status状态字段。资料上传后不直接上架,而是进入待审核状态,管理员审核后状态变为已发布。这个“软状态”的设计让整个系统有了内容管控的能力,也是论文里可以重点写的“审核流设计”。
关于文件地址还有一个经验:file_url和cover_url都存相对路径(比如/files/20251012/xxxxx.pdf),不存完整URL。这样开发环境和生产环境的域名可以随时切换,部署时不用改数据库。这个细节在答辩现场讲出来,老师会觉得你有实际部署经验。
4.3 收藏表和下载记录表的价值
收藏表(favorite)和下载记录表(download_record)分别记录用户行为和资源热度。
收藏表字段:id、user_id、resource_id、create_time。唯一约束加在(user_id, resource_id)上,防止用户重复收藏同一份资料。前端页面上点“收藏”时,前端先查询是否已收藏,再决定调收藏接口还是取消收藏接口。
下载记录表字段:id、user_id、resource_id、create_time。每次用户点击下载,后端就往这张表里插一条记录,同时把资源表的download_count加1。这张表的价值在于它是所有统计数据的来源:后台的“今日下载量”“热门资料Top10”都是从这张表聚合出来的。另外,如果后续要设计积分体系,用户上传得积分、每日签到得积分,这些流水记录表可以继续沿用同样的设计思路。
很多人会把收藏和下载放在同一张“用户资源关系表”里,加个type字段区分。这样看起来少建一张表,但两类行为的数据性质完全不同:收藏是低频的意愿表达,下载是高频的行为记录。拆开之后,后续统计、清理、查询都清爽得多。数据库设计虽然简单,职责分离的思路还是要有。
5. 小程序端三件事:登录、请求封装和PDF预览
5.1 微信登录换openid的完整链路
小程序端第一重要的功能是登录。微信小程序不需要用户名密码,整条链路是这样的:
前端调用wx.login()获取一个临时code。这个code有效期只有5分钟,而且只能用一次。前端把这个code通过后端接口POST /api/auth/login发给后端。后端用code、appid、secret去请求微信的jscode2session接口,拿到openid和session_key。openid是用户在当前小程序下的唯一标识,后端查数据库:如果这个openid已经存在,说明是老用户,直接生成一个自定义token返回;如果不存在,先自动注册再生成token。
关键代码在后端,前端只需要这么做:
wx.login({ success: (res) => { if (res.code) { wx.request({ url: `${BASE_URL}/api/auth/login`, method: 'POST', data: { code: res.code }, success: (loginRes) => { const { token } = loginRes.data.data; wx.setStorageSync('token', token); } }); } } });这里要注意一个坑。很多学生会想“我用openid做token不就行了吗”,千万不要。把openid长期暴露在请求里等同于把用户身份写在脸上。正确做法是后端用UUID或JWT生成一个随机token,然后把这个token和用户id的对应关系保存起来,后续请求都带这个token。对于毕设来说,用一个ConcurrentHashMap存token对userId也行,但更稳妥的做法是引入Redis。如果项目里加了Redis,论文里又能多一个“基于Redis的分布式登录态管理”的亮点,性价比很高。
还有一个和登录相关的细节:微信新版的小程序里,获取头像和昵称不再是无声调用的。用户在个人中心需要主动点击“头像昵称”授权按钮,前端才能拿得到。常见做法是先给用户一个默认昵称“微信用户”和默认头像,登录后用资料完善页引导用户设置头像昵称。千万别在页面onLoad里直接调用wx.getUserProfile,用户没点击的情况下是拿不到信息的。
5.2 request请求封装与登录失效处理
小程序如果每个页面都直接写wx.request,项目写到最后会非常痛苦:每个请求要重复写URL前缀、处理token、处理错误码。我习惯在项目一开始就封装一个全局的request.js,所有页面都走这一个入口。
const BASE_URL = 'http://你的后端地址:8080'; function request(path, method = 'GET', data = {}) { return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + path, method: method, data: data, header: { 'token': wx.getStorageSync('token') }, success: (res) => { const result = res.data; if (result.code === 0) { resolve(result.data); } else if (result.code === 401) { wx.removeStorageSync('token'); wx.navigateTo({ url: '/pages/login/login' }); reject('登录过期'); } else { wx.showToast({ title: result.msg, icon: 'none' }); reject(result.msg); } }, fail: (err) => { wx.showToast({ title: '网络异常', icon: 'none' }); reject(err); } }); }); } module.exports = { request, BASE_URL };快照几个关键点:token从本地缓存里统一读取,每个请求自动带上;后端返回code=401时统一做登录失效处理——清token、跳登录页;其他错误码统一弹toast。封装完之后,页面里调用就非常简单了:
const { request } = require('../../utils/request.js'); wx.showLoading({ title: '加载中' }); const res = await request('/api/resource/page', 'GET', { categoryId: 1, page: 1 }); wx.hideLoading(); setData({ resourceList: res.list });这个封装的价值在联调阶段体会最深。不用在每个页面里处理错误,改一个地方全局生效。答辩时老师如果问“您的项目对token失效是怎么处理的”,你直接讲这一段,就是标准答案。
5.3 顶部导航栏适配与本地缓存
小程序端的适配问题,最典型的就是自定义导航栏。如果不用微信默认的导航栏,而是自己做顶栏(很多考研类小程序会在顶部放搜索框和倒计时),就必须动态计算导航栏高度。一个常见方案是:
const menuButton = wx.getMenuButtonBoundingClientRect(); // 胶囊按钮的top减去状态栏高度,再乘以2,就是导航栏的额外高度借助胶囊按钮的位置来计算页面头部高度,可以保证不同机型上的显示效果一致。如果写死一个statusBarHeight,在刘海屏和正常屏上会错位,这个细节在真机预览时很容易被发现。
本地缓存也很重要。首页的分类列表往往变化不大,但用户每次打开小程序都请求一次接口会显得慢。可以给分类列表设置10分钟缓存,过期后才重新请求:
function getCache(key, expires) { const cached = wx.getStorageSync(key); if (cached && Date.now() - cached.time < expires) { return cached.data; } return null; } function setCache(key, data) { wx.setStorageSync(key, { time: Date.now(), data: data }); }第一次请求时存入缓存,10分钟内再次进入首页直接读缓存;超过时间后重新请求并刷新缓存。这个小优化会让小程序启动速度快很多,用户体验更好,写论文的时候还能加一句“客户端缓存策略”。
5.4 PDF预览与下载的实现细节
资料类平台最核心的操作就是打开PDF。微信小程序里标准做法是先用wx.downloadFile下载文件,再用wx.openDocument打开:
wx.downloadFile({ url: fileUrl, success: (res) => { if (res.statusCode === 200) { wx.openDocument({ filePath: res.tempFilePath, fileType: 'pdf', showMenu: true, success: () => console.log('打开文档成功'), fail: (err) => wx.showToast({ title: '打开失败', icon: 'none' }) }); } } });这里有两个细节。第一,wx.openDocument只是打开文档供浏览,如果要让用户能把文件转发给微信好友,必须设置showMenu: true,这样文档右上角才会有“转发”按钮。第二,下载下来的文件是存在临时文件目录下的,小程序重启后临时文件会被清理,所以不要试图把下载好的文件永久保存在本地。更合理的做法是给资源加一个“收藏”功能,用户想看的时候再重新下载预览。
PDF文件大小也要注意。超过10MB的文件在手机上打开速度会很慢,甚至可能内存不足。后台上传资料时最好限制文件大小,比如最大20MB,服务端配置spring.servlet.multipart.max-file-size=20MB。如果有真正的超大文件需求,那就要走“转存到网盘/发送到邮箱”的流程,这套逻辑就超出毕设范围了。
6. Spring Boot端:接口设计与文件存储
6.1 Controller层:统一返回结构与接口清单
后端好不好用,先看接口设计得规不规范。我强烈建议所有接口统一返回一个Result<T>结构,格式固定为:
{ "code": 0, "msg": "success", "data": {} }code=0表示成功,code=401表示未登录,其他code对应业务错误。这样前端封装请求时只要判断一次code,不需要每个接口单独处理。对应的Java类大致长这样:
@Data public class Result<T> { private Integer code; private String msg; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(0); result.setMsg("success"); result.setData(data); return result; } public static <T> Result<T> error(Integer code, String msg) { Result<T> result = new Result<>(); result.setCode(code); result.setMsg(msg); return result; } }核心接口清单如下,这张表基本就是我写接口文档时的模板:
| 接口路径 | 请求方式 | 说明 | 是否需登录 |
|---|---|---|---|
| /api/auth/login | POST | 微信登录,换取token | 否 |
| /api/category/list | GET | 获取分类列表 | 否 |
| /api/resource/page | GET | 分页查询资料 | 否 |
| /api/resource/detail | GET | 获取资料详情 | 否 |
| /api/resource/upload | POST | 上传资料 | 是 |
| /api/resource/favorite | POST | 收藏/取消收藏 | 是 |
| /api/resource/download | POST | 记录一次下载 | 是 |
| /api/user/center | GET | 个人中心信息 | 是 |
| /admin/resource/audit | POST | 审核资料 | 管理员 |
列表和详情接口不要求登录,因为游客也可以浏览;上传、收藏、下载必须登录,因为要用userId做关联记录。这个“哪些接口公开、哪些接口登录”的划分本身就是接口设计的核心思路。
6.2 JWT拦截器:哪些接口要登录
接口划分好了,怎么在Spring Boot里统一实现登录校验?答案是拦截器。写一个LoginInterceptor,在请求到达Controller之前检查token:
@Component public class LoginInterceptor implements HandlerInterceptor { @Autowired private TokenService tokenService; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("token"); Long userId = tokenService.getUserIdByToken(token); if (userId == null) { response.setStatus(401); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("{\"code\":401,\"msg\":\"未登录或登录过期\"}"); return false; } request.setAttribute("userId", userId); return true; } }然后在配置类里注册拦截器,并用excludePathPatterns排除掉公开接口:
@Configuration public class WebConfig implements WebMvcConfigurer { @Autowired private LoginInterceptor loginInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor) .addPathPatterns("/**") .excludePathPatterns( "/api/auth/login", "/api/category/list", "/api/resource/page", "/api/resource/detail" ); } }这样配置之后,所有非公开接口都会自动做登录校验,校验通过后userId放到request属性里,Controller层直接request.getAttribute("userId")就能拿到当前用户。这种设计的好处是业务代码里完全不用关心登录逻辑,关注点分离。答辩时可以讲一句话:“我通过拦截器统一处理登录态,Controller里只关注参数校验和业务返回。”
6.3 文件上传的本地存储方案与安全注意点
文件上传是这类平台最敏感的部分,也是最容易踩坑的部分。毕设阶段不需要上云,用本地磁盘存储就够了,关键是路径要规划好。
在application.yml里配置一个上传目录:
file: upload-dir: /usr/local/files/Controller里接收MultipartFile,用UUID重命名文件,避免文件名冲突和路径穿越问题:
@PostMapping("/api/resource/upload") public Result<String> upload(@RequestParam("file") MultipartFile file, @RequestParam("title") String title, @RequestParam("categoryId") Long categoryId, HttpServletRequest request) throws IOException { // 1. 校验文件是否存在 if (file.isEmpty()) { return Result.error(400, "文件不能为空"); } // 2. 校验文件类型,只允许PDF和图片 String originalFilename = file.getOriginalFilename(); String ext = originalFilename.substring(originalFilename.lastIndexOf(".") + 1).toLowerCase(); if (!Arrays.asList("pdf", "png", "jpg", "jpeg").contains(ext)) { return Result.error(400, "仅支持PDF、JPG、PNG格式"); } // 3. UUID重命名,避免中文文件名乱码和路径穿越 String newFilename = UUID.randomUUID().toString().replace("-", "") + "." + ext; // 4. 按月份分目录,避免一个目录下文件过多 String datePath = new SimpleDateFormat("yyyyMM").format(new Date()); File destDir = new File(uploadDir + datePath); if (!destDir.exists()) { destDir.mkdirs(); } file.transferTo(new File(destDir, newFilename)); // 5. 返回相对路径,数据库只存 /files/202510/xxxx.pdf return Result.success("/files/" + datePath + "/" + newFilename); }文件类型校验这里有个容易踩的坑:只看file.getContentType()是不够的,因为这个值由客户端指定,可以伪造。更可靠的做法是拿扩展名做白名单校验。当然,如果要求更高的安全强度,还可以用Apache Tika读取文件的真实MIME类型,但从毕设角度讲,扩展名白名单已经能应对99%的情况。
存好文件之后,还要配置静态资源映射,让/files/**开头的URL能找到本地目录:
@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/files/**") .addResourceLocations("file:" + uploadDir); }做到这一步,前端通过BASE_URL + /files/202510/xxxx.pdf就能直接访问到文件。还有一个持久化的小坑:本地存储的文件如果项目重启,路径配置不变就不受影响;但如果你在Windows上开发时用的D:/files,部署到Linux服务器上就要改成/usr/local/files。所以我才强调数据库里存相对路径,这样环境切换时只需要改配置文件,不用改数据库。
7. 调试与部署踩坑记录
7.1 真机预览连不上本地后端
这个坑几乎每个第一次做小程序毕设的学生都会踩到。在微信开发者工具里模拟器上跑得好好的,数据正常加载,一换到真机预览就白屏,请求全部失败。
原因很简单:模拟器跑在电脑上,localhost指向电脑本身,访问后端自然通。但真机是另一台设备,它访问localhost只会访问到手机自己,根本没有后端服务。所以调试时:第一,把request.js里的BASE_URL改成电脑在局域网里的IP,比如http://192.168.1.101:8080;第二,手机和电脑必须连同一个WiFi;第三,后端要配置跨域支持,否则微信开发者工具里能过,真机上会被浏览器的同源策略拦住。
在Spring Boot里配置跨域很简单:
@Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("*") .allowedHeaders("*") .allowCredentials(true); }另外一个开发阶段的问题:正式版的微信小程序要求所有请求域名必须是HTTPS且在小程序后台配置过“request合法域名”。如果你的后端跑在http://192.168.1.101:8080,开发阶段需要在微信开发者工具的“详情-本地设置”里勾选“不校验合法域名、web-view(业务域名)、TLS版本以及HTTPS证书”。真机预览时还要在手机微信里打开调试模式,否则真机上依然会拦截请求。这个开关每次重新编译后都要重新勾选一次,忘了的时候请求就会悄悄失败,排查时第一反应先看它。
7.2 “图片/PDF打不开”的排查顺序
资源打不开是另一个高频问题。我的排查顺序是固定的,从前往后走:
第一,后端返回的fileUrl是不是相对路径,前端有没有拼上BASE_URL。很多学生数据库里存了/files/xxx.pdf,前端直接拿这个路径去请求,自然404。第二,电脑浏览器直接访问BASE_URL + fileUrl能不能打开。浏览器能开,说明后端资源映射没问题,问题在小程序端;浏览器也打不开,问题在静态资源映射或文件路径。第三,看微信开发者工具的Network面板,请求状态码是多少:404说明路径错,403说明没有权限,500说明后端抛异常。第四,确认application.yml里的file.upload-dir对应的磁盘目录真实存在,并且文件确实落进去了。很多人配置文件写的是相对路径./files,IDEA启动项目时工作目录变了,文件就写到别处去了。
按这个顺序排查,90%的资源打不开问题都能在十分钟内定位。不要像无头苍蝇一样改前端代码,先定位是哪一层出的问题。
7.3 上线部署要改的四个地方
如果你的项目要部署到云服务器做演示,有四处地方必须改,少一处都别想顺利跑起来:
第一,数据库连接。application.yml里的spring.datasource.url要从localhost:3306改成云数据库的公网或内网地址,账号密码也要换。第二,文件上传目录。本地调试用的D:/files要改成服务器上的/usr/local/files,并且要保证目录有读写权限。第三,访问域名。右键小程序后台的“开发管理-服务器域名”里,把request合法域名配成你的HTTPS域名;如果服务器没有HTTPS证书,那就只能走前面说的开发调试模式。第四,静态资源映射方式。建议用Nginx接管/files/的访问,Spring Boot只管接口,这样文件访问性能更好,配置也更清晰:
location /files/ { alias /usr/local/files/; }部署这步如果觉得麻烦,最简单的方式是直接用宝塔面板管理服务器,数据库、Nginx、文件的配置都在Web界面里操作,对毕设学生来说比纯命令行友好太多。但论文里不要写“宝塔”,要写“通过Nginx配置静态资源映射,实现前后端分离部署”。
7.4 答辩演示脚本建议
演示环节是答辩的重头戏,我的建议是提前写好一个固定脚本,别临场发挥。按这个顺序演示最顺畅:
先以游客身份打开小程序,演示首页分类导航、资料列表浏览、搜索功能;再点登录,演示微信一键登录后自动注册;然后打开一个资料详情页,演示预览PDF、收藏、下载;接着切换到个人中心,展示“我的上传”“我的收藏”“我的下载记录”三个列表;最后切换到管理员账号,展示待审核资料的审核操作,把一份状态从待审核改为已发布。
每一步操作背后准备一句话,用来回答老师可能的追问。比如点击“下载”时准备好解释“后端记录了一条下载记录,下载量加一,这个操作落在download_record表里”;点击“审核通过”时准备好解释“资料的状态字段从0变成了1,前端列表只查询状态为1的数据”。数据一定要准备得真实一点:放几份PDF真题,管理员账号提前登录好,测试用户提前注册好。现场最尴尬的事情就是演示到一半,数据库里一条资料都没有,页面空空如也。
8. 五个差异化加分项设计
8.1 学习打卡与考研倒计时
考研平台天然适合做“学习激励”功能。加一张check_in表,字段是id、user_id、check_date、create_time,用户在首页点一下“今日打卡”,后端判断今天有没有打过卡,没有就插入一条记录。首页展示“已连续打卡X天”、“距离考研还有XX天”,这两个数字放在顶部,视觉冲击力很强。距离考研的天数可以用一个简单的日期差计算,打卡连续天数用SQL查出最近记录再循环统计。这个功能代码量不大,但在答辩现场特别能引起共鸣,老师也知道考研学生确实需要这种激励。
8.2 评论与评分
在资料详情页加一个评论区,用户可以对资料评分并发表文字评价。评论表(comment)字段包括id、user_id、resource_id、content、score、create_time。资源列表页可以显示每条资源的平均分,排序时多一个“按评分排序”的选项。评分的聚合查询很简单,用AVG(score)加GROUP BY resource_id就能实现。这个功能做完,论文里的“系统功能模块”就多了一块完整的UGC内容,同时资料列表也不再是干巴巴的文件堆,而是有用户反馈的社区内容。
8.3 敏感词过滤
资源共享平台有一个很现实的问题:用户上传的资料可能包含广告、联系方式甚至违规内容。敏感词过滤这个点写进论文里,可以提升一下项目的“安全意识”。最简单的方式是准备一个敏感词列表,上传资料时把title和description跟列表里的词做匹配,命中则直接进入人工审核状态。用String.contains()做遍历是O(n)的效率,虽然不高但够用。答辩时如果老师问“如何防止恶意内容传播”,这个功能就是答案。更进阶的做法是引入开源的HanLP分词库,对文本做分词后再匹配,但毕设用到这个级别有点超纲。
8.4 积分体系
积分体系是这个平台最自然的扩展点,也是老师们很感兴趣的一块。核心逻辑就两句话:上传资料得积分,下载资料消耗积分。用户上传一份资料审核通过后,user_points表里加10分;用户下载一份资料时,扣除2分。为了防止有人只下载不上传,可以加一条规则“注册赠送20积分,积分不足时提示上传资料获取积分”。实现上需要一个积分流水表,记录每条积分变化的来源(上传奖励、下载扣费)。论文里可以把这个写成“激励体系设计”一章,作为系统的特色功能之一。
8.5 文档里怎么把加分项写进“项目特色”
前面这些加分项,无论你最终实现了几个,在开题报告和论文里都要有条理地写出来。我的建议是论文里单开一节“系统特色”,从三个维度去写:业务上,系统解决了考研资料分散、质量不可控的痛点,提供上传、审核、下载的完整闭环;技术上,采用Spring Boot分层架构,通过拦截器统一处理登录鉴权,文件存储采用相对路径与UUID命名,兼顾安全性与可移植性;交互上,微信小程序免安装、扫码即用,借助微信原生登录能力降低用户使用门槛。
还有一招很实用:在“未来展望”里写清楚哪些功能已经做了、哪些还没做、哪些是后续规划。答辩时老师看到你有思考、有规划,印象分会明显提升。比如“下一步计划实现学习打卡和积分激励体系”,这句话本身不增加工作量,但体现了你对自己项目的规划能力。
最后分享一点实际带的感受。见过不少学生把精力花在堆功能上,结果核心的登录、上传、审核、下载主链路没跑通,这属于捡了芝麻丢西瓜。这个项目真正的主链路就一条:用户登录、上传资料、管理员审核、用户下载。把这个闭环做扎实、做稳定,再考虑评论、打卡、积分那些扩展功能。做学问也好,做系统也好,先把地基打牢,上面的装饰才有意义。
还有一个小建议:整个项目跑通之后,花一个晚上把数据库里的测试数据清理一下,把管理员账号、几个分类、几份文档资料重新整理得干净、真实。答辩演示时,老师看到的是数据整齐、逻辑清晰的系统,而不是塞满“test”“aaa”的调试残留。这个习惯不光对毕设有用,以后做任何项目都受用。