SpringBoot+Vue校园意见箱毕业设计:从表结构到JWT鉴权落地
简介这是一份面向校园意见箱场景的Java毕业设计项目资源采用SpringBootVue前后端分离架构结合ElementUI与MyBatis实现系统功能适合计算机专业学生用于毕业设计、课程设计或前后端整合实战学习。资源打包为RAR格式共包含849个文件压缩包整体约39.18MB覆盖Java后端源码、Vue前端组件、JavaScript逻辑、CSS样式、SVG图标、MySQL数据库SQL脚本以及毕业论文Word文档等并附带安装与运行的批处理脚本和演示视频环境要求JDK1.8和MySQL可使用IDEA或Eclipse导入。目前已有129人学习/下载。项目完成了用户登录管理等核心模块的设计与实现完成了系统数据库设计基于MySQL数据管理提供了从界面到后端再到数据库的完整闭环。借助该资源可以快速理解SpringBoot与Vue的整合方式、MyBatis的数据交互流程同时参考论文结构完善毕设文档节省大量从零搭建的时间。1. 用 VueSpringBoot 做校园意见箱这类 Java 毕业设计怎么落地vueSpringBoot 校园意见箱的 Java 毕业设计源码包每年都很多但能跑通的人一半时间其实花在环境配置、表结构返工和答辩被问倒上。这个题目的考察点非常集中多角色权限学生、辅导员、部门负责人、管理员、意见状态流转待处理到已办结、匿名与实名切换、图片上传、统计图表正好覆盖 SpringBoot 后端和 Vue 前端的大部分必修考点。这里按一套能在本地跑通的完整方案来拆表结构怎么建、JWT 鉴权怎么接、前端路由怎么守卫、论文和答辩怎么把源码讲成系统能力。适合正在选这个题或者已经写了一半的人照着做可以少踩一半的坑。2. 意见箱系统的四种角色与数据库表结构设计2.1 角色权限拆解学生、辅导员、部门负责人、管理员很多教程把意见箱简化成“学生 管理员”两个角色答辩时被追问“匿名意见后台能不能查”“意见处理不了谁来督办”就会卡住。常见的做法是拆成四类角色学生提交意见可选匿名辅导员只能查看和处理自己班级的意见部门负责人处理跨班事务比如后勤维修、教学安排系统管理员负责人账号、角色分配和全局统计。这样权限控制有的写状态机也有的画。四类角色对应的是校园里真实的意见流转链学生提交 → 辅导员初步处理 → 解决不了转到部门负责人 → 部门处理完回传结论 → 学生收到回复并在详情页评价“满意/不满意”。这个流程直接决定后面接口的数量比如“处理中”“已回复”“已办结”三个状态缺一不可。2.1.1 用户表与角色表分离权限字段不落字符串角色表 sys_role 固定四行数据用户表 sys_user 用 role_id 关联不要在用户表里直接存 role_name 字符串否则后面加个“年级主任”角色就要改表结构。用户表核心字段如下字段类型说明idbigint主键usernamevarchar(50)登录名passwordvarchar(100)BCrypt 密文real_namevarchar(50)真实姓名role_idbigint关联角色表collegevarchar(100)学院gradevarchar(20)年级/班级statustinyint0禁用 1正常password 字段长度留 varchar(100) 而不是 50是为了放 BCrypt 生成的 60 位密文。如果用 MD5长度确实可以短但答辩问“密码怎么存的”就会露怯。Java 八股文里 BCrypt 加盐是高频题毕业设计提前用它正好对上。2.2 意见表匿名开关、状态机与回复关系意见表 tb_opinion 是整个系统的核心字段设计决定了后端接口的复杂程度。直接看建表语句CREATE TABLE tb_opinion ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT NOT NULL COMMENT 提交人ID匿名时也必填, category VARCHAR(20) NOT NULL COMMENT 分类教学/后勤/宿舍/食堂/其他, title VARCHAR(100) NOT NULL, content TEXT NOT NULL, is_anonymous TINYINT DEFAULT 1 COMMENT 1匿名 0实名, status TINYINT DEFAULT 0 COMMENT 0待处理 1处理中 2已回复 3已办结 4已驳回, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, handle_time DATETIME NULL COMMENT 办结时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;几个字段的设计逻辑要能说清楚user_id 在匿名时也存不要为了“匿名”把它置空。版权归作者所有这句答辩可以直接用“匿名是对其他用户匿名后台可追溯防止恶意投诉。”就这一句把很多人讲不清楚的匿名边界问题解决了。status 从 0 走到 3 是正常路径4 是驳回。驳回之后要允许学生重新编辑提交这就产生一个“重新提交”接口实际是对 status4 的记录做 update再把状态重置为 0。分类 category 用 varchar 而不是数字枚举。毕设阶段数字枚举需要额外维护映射表展示端的代价更高varchar 存中文分类前端直接下拉取值省去一层翻译。2.2.1 回复表与附件表一对多关系的边界回复表 tb_opinion_reply 记录整个处理过程一个意见可以有多条回复包括辅导员回复、部门回复、学生追问。关键设计是每条回复带 replier_role页面渲染时才能区分“老师回复”和“学生追问”样式可以分开显示这也是论文里 ER 图的一个亮点。附件表用 business_id business_type 做成通用附件结构不要单独建一个“意见图片表”。这样以后回复带图、导出意见详情时都能复用同一套上传和查询逻辑比每个模块建一张附件表干净很多。2.3 状态流转固化到枚举启动建表环节直接避开大坑动手写接口之前先把状态流转固化成常量类或枚举public enum OpinionStatus { PENDING(0, 待处理), PROCESSING(1, 处理中), REPLIED(2, 已回复), FINISHED(3, 已办结), REJECTED(4, 已驳回); private final int code; private final String desc; OpinionStatus(int code, String desc) { this.code code; this.desc desc; } }状态更新接口不要做成“前端传什么就改成什么”否则会出现“已回复但下面没有任何回复内容”的脏数据。受控接口只有两个handle待处理 → 处理中和 finish处理中/已回复 → 已办结外加 reply 时自动把状态置为已回复。按钮和状态一一对应数据就不会乱。关于建表MyBatis-Plus 本身不提供“表不存在时自动建表”的能力这一点网上经常被问。如果想让 SprintBoot 启动时自动执行建表语句常见做法是在启动类上加一个 ApplicationRunner读取 classpath 下的 schema.sql 执行毕业设计阶段直接手动执行 SQL 脚本反而更可控。另一个低成本加分项是改掉默认的 SpringBoot banner用 springboot banner生成器 做成学校名字答辩开场就能让老师对细节有好感。3. SprintBoot 后端接口JWT 鉴权、分页查询与状态更新3.1 依赖选型与配置文件里的三个细节毕业设计不追求版本最新追求保守和能讲清楚。我一般选 SpringBoot 2.7.x对应 Java 8 或 11 都兼容网上排错资料最多。如果用了 3.xJava 版本被强制拉到 17有些老师本地的 JDK 跑不起来这是很现实的演示事故。核心依赖spring-boot-starter-web、mybatis-plus-boot-starter、spring-boot-starter-validation、jjwt、hutool 和 spring-boot-starter-test。接口清单先列出来后面实现不跑偏方法路径角色说明POST/api/login匿名登录获取 tokenPOST/api/opinion学生提交意见GET/api/opinion/list登录用户分页查询学生只见自己的POST/api/opinion/reply辅导员/部门回复意见PUT/api/opinion/finish辅导员/管理员办结意见GET/api/statistics管理员分类与趋势统计配置文件里三个细节值得提前处理。第一数据库连接串写上 useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai否则时间字段差 8 小时。第二application.yml 里的数据库密码用明文答辩容易被追问“生产环境怎么办”可以引入 jasypt-spring-boot-starter把密码写成 ENC() 密文启动参数里传密钥。第三如果 mvn -v 命令不识别先检查 Java 环境变量配置JAVA_HOME 要指向 JDK 安装目录而不是 jre这是零基础环节最常见的卡点。3.2 登录接口与 JWT 拦截器登录接口的逻辑是写死的三步查用户、验密码、发 token。PostMapping(/login) public Result login(RequestBody Valid LoginDTO dto) { User user userMapper.selectOne(new LambdaQueryWrapperUser() .eq(User::getUsername, dto.getUsername())); if (user null || !passwordEncoder.matches(dto.getPassword(), user.getPassword())) { return Result.error(用户名或密码错误); } if (user.getStatus() 0) { return Result.error(账号已被禁用); } String token JwtUtil.createToken(user.getId(), user.getRoleId(), user.getRealName()); return Result.success(new LoginVO(token, user)); }代码里三个关键点答辩必讲matches 方法用明文和密文比对整个校验过程后端日志里不会出现密码明文token 的 claims 里只放 id、roleId、realName不要塞整个 user 对象否则 token 体积膨胀还有信息泄露风险Valid 配合 LoginDTO 里的 NotBlank空用户名请求在进入 Service 前就被挡掉。token 有效时间一般设 2 小时后端在拦截器里校验过期时间前端收到 401 后跳登录页。拦截器实现用 WebMvcConfigurer 注册 HandlerInterceptor拦截 /api/**排除 /login 和 /register。解析成功的用户信息放到 ThreadLocal 工具类后续 Controller 通过 UserContext.getUserId() 取当前用户不需要每个接口都传 userId 参数。3.3 意见提交与条件分页查询提交接口的核心是把 UserContext 里的 userId 填充进去匿名标记由前端传入PostMapping(/opinion) public Result submit(RequestBody Valid OpinionDTO dto) { Opinion opinion new Opinion(); BeanUtils.copyProperties(dto, opinion); opinion.setUserId(UserContext.getUserId()); opinion.setStatus(OpinionStatus.PENDING.getCode()); opinionService.save(opinion); return Result.success(opinion.getId()); }意见内容如果是纯文本页面用插值表达式渲染没有太大问题如果支持了富文本就必须在入库前用 Jsoup.clean 过滤 script 标签否则就是 XSS 漏洞。答辩时主动说出“我对内容做了 HTML 标签过滤”比被老师问出来再解释要好得多。列表查询用 MyBatis-Plus 的 Page 对象PageOpinion page new Page(current, size); LambdaQueryWrapperOpinion wrapper new LambdaQueryWrapper(); wrapper.eq(StringUtils.hasText(category), Opinion::getCategory, category) .eq(status ! null, Opinion::getStatus, status) .eq(StringUtils.hasText(keyword), Opinion::getTitle, keyword) .orderByDesc(Opinion::getCreateTime); IPageOpinion result opinionMapper.selectPage(page, wrapper);参数说明current 从 1 开始size 是每页条数前端表格组件传过来是字符串Controller 里要转 int 并且做上限限制size 最大 50防止一次请求拉全表。orderByDesc 必须放在条件之后否则条件拼接顺序会影响最终 SQL。学生角色进来时必须追加 user_id 条件这个条件在 Service 层根据当前登录角色拼进去管理员才不加。3.4 状态流转接口的幂等写法“处理中 → 已办结”这类更新直接 updateById 之前要先查一次当前状态再判断是否允许流转。但两个人同时点击办结就会出现重复更新问题。常见做法是把状态判断写进 SQL 的 where 条件UPDATE tb_opinion SET status 3, handle_time NOW() WHERE id #{id} AND status 2受影响行数为 0 说明状态早已变化提示“该意见状态已变更请刷新页面”。即使用户连点两次也只有一个请求能改成功。MyBatis-Plus 里写起来也很直接int rows opinionMapper.update(null, new UpdateWrapperOpinion() .eq(id, id) .eq(status, OpinionStatus.REPLIED.getCode()) .set(status, OpinionStatus.FINISHED.getCode()) .set(handle_time, new Date())); if (rows 0) { throw new BizException(状态已变更请刷新); }这是把数据库的原子性当业务锁用不依赖 Redis 也能讲通面试时提到“乐观锁思路”就能把这个问题答到点子上。4. Vue 前端路由守卫、分页列表与统计图表4.1 模板选型Vue2 Element UI 的兼容性理由这类源码包里出现频率最高的是 Vue2 搭配 Element UI原因很现实老师的参考项目大多是这套组合依赖安装报错时能搜到答案前端面经里问的也是 Vue2 生命周期居多。如果用 Vue3 Ant Design Vue需要补充 setup 语法、Teleport 等新概念答辩风险更高。Vue 环境配置最容易卡在依赖安装环节Node 版本保持在 14.17 到 16.x 之间npm install 用--registryhttps://registry.npmmirror.com换镜像源装完后如果报 node-sass 错误基本都是 Node 版本问题换 LTS 版直接解决。4.2 路由守卫与 axios 拦截器前端路由控制登录态核心在全局前置守卫router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next(/login) } else if (to.path /login token) { next(/) } else { next() } })路由守卫的意义不是绝对安全而是让用户在刷新页面后不会看到空白页面。axios 封装里做两层处理请求拦截器统一加 Authorization 头响应拦截器判断 HTTP 状态码 401 时清空登录态并跳转登录页。细节是判断以 HTTP 状态码为准不要依赖业务 code因为后端处理 JWT 过期时可能在不同位置返回不同 code。详情页拿意见 id 传参要注意用$router.push({ name: OpinionDetail, params: { id } })刷新页面后 params 会丢要传 query 才能持久化这是 vue 路由参数最典型的坑。4.3 意见列表分页与状态标签渲染表格页用 el-table el-pagination状态列不要直接输出数字写一个映射对象转成带颜色的标签statusMap { 0: { text: 待处理, type: warning }, 1: { text: 处理中, type: primary }, 2: { text: 已回复, type: success }, 3: { text: 已办结, type: info }, 4: { text: 已驳回, type: danger } }模板里用el-tag :typestatusMap[row.status].type{{ statusMap[row.status].text }}/el-tag渲染状态色一眼就分清。匿名列表项显示“匿名”详情页管理员能看到真实姓名这是后端返回详情时单独关联 user 表拼出来的字段前端只负责展示不负责判断权限。4.3.1 查询条件的防抖与列表刷新分类下拉、状态下拉、关键词输入三个条件共同驱动列表刷新最常见的问题是关键词每次输入都请求一次把数据库压出慢查询。用 lodash 的 debounce 包一层 300ms 防抖条件变化时把 current 重置为 1 再重新请求。自己写 setTimeout 防抖容易在组件销毁后触发 setData 报错直接用 lodash 就不需要考虑 this 指向问题。4.4 意见提交表单与图片上传提交页用 el-form 做校验标题必填且不超过 100 字分类必选内容长度限制在 2000 字以内。图片上传组件配置是常见翻车点el-upload action/api/upload :headers{ Authorization: Bearer token } :limit3 list-typepicture-card :on-successhandleUploadSuccess /el-uploadaction 指向后端 /api/upload:headers 必须手工带上 token否则上传请求会被后端拦截器拦掉。limit 设为 3 限制了最大三张图片后端接口再校验一次文件大小和扩展名前后端双重限制。上传成功后的回调把返回的 url 存进表单的 attachmentList和意见内容一起提交业务表里只存文件 id 列表不直接拼 JSON。页面和路由的对应关系建议按下面这张表规划写论文时也直接复用页面路由主要组件登录/loginel-form意见列表/opinionel-table、el-pagination提交意见/opinion/addel-form、el-upload意见详情/opinion/:idel-descriptions、el-timeline统计面板/dashboardecharts4.5 ECharts 统计图表的挂载时机统计面板放两个图基本够用按分类分的环形图、按周提交量的折线图。数据从 /api/statistics 接口拿后端用 GROUP BY category 和 DATE_FORMAT(create_time, %Y-%m-%d) 聚合。ECharts 组件要等 DOM 挂载后再 init也就是 mounted 里初始化拿到接口数据后 setOption。如果写在 created 里此时 DOM 还没有渲染完图表容器宽度是 0画出来的图形是空白。这个坑非常典型答辩时主动讲“我踩过 created 和 mounted 的时序问题”比背概念真实得多。5. 校园意见箱论文写作与答辩演示的五个验证点5.1 论文结构和代码的对应关系论文最忌讳把功能实现章节写成代码逐行解释。第三章系统设计要放 ER 图、用例图和类图ER 图里的实体要和表一一对应第四章核心功能实现按“用户故事”来写而不是按“方法清单”来写。每写一个功能点就标注它对应的后端接口、前端页面和数据库表评阅老师随便抽一个功能都能在源码、论文、演示三处对得上。匿名意见的追溯机制建议单独写一小节这是整个系统里最有得讲的设计点。5.2 五个必验证场景清单答辩前把下面五个场景完整走一遍验证项操作通过标准登录鉴权未登录直接访问 /opinion自动跳转登录页匿名可见性学生A提交匿名意见学生B登录列表和详情都不显示姓名状态流转辅导员回复后点击办结状态由“已回复”变“已办结”并发防重双开页面同时点办结仅一个请求成功另一个提示刷新图片上传提交超过 2MB 的图片前端直接拦截不发请求验证完顺手把演示数据整理一下不要清一色的“测试123”分类错开、时间跨度拉长到两个月统计图表和列表页的演示效果会好非常多。这一点很多毕业生忽略现场才开始造数一定要提前准备。5.3 最容易翻车的三个技术问题第一个问题“密码为什么不加密存”。标准答法BCrypt 单向哈希加盐且每次哈希结果不同校验用 matches 方法比对数据库泄露也不会反推出明文。第二个问题“分页怎么实现的”。答MyBatis-Plus 分页插件拦截 SQL 自动拼接 LIMIT并执行 COUNT 查询总数。把这个说清楚就足够不要支支吾吾说“用了插件”。第三个问题“数据量大了要不要分表”。诚实的答法是毕设场景数据量小单表天然够用生产环境可以按分类或时间分表配合全文检索引擎做查询优化。承认边界比硬吹强老师要的是你对方案边界的理解不是万能答案。写实际代码时把状态流转写死在枚举里条件查询统一走 wrapper 拼接前端路由守卫把所有页面都盖上 meta.requiresAuth演示过程就不会出现“点进去白屏”的意外。整套链路跑通后源码里的每个接口都能在论文的用例表里找到对应行老师随便抽你都能顺着表结构讲到答辩结束。本文还有配套的精品资源点击获取