2026/9/13 0:02:40

在线答疑系统Java毕设实战:Spring Boot前后端分离与状态流转

在线答疑系统Java毕设实战:Spring Boot前后端分离与状态流转 简介面向计算机相关专业需要完成毕业设计或课程设计的学生这套Java实战项目以某学院在线答疑系统为业务场景采用B/S架构并搭配MySQL数据库完整实现在线答疑、课程申请、知识库精选与交流互动等核心功能。项目源码共449个文件除Java源码与class编译文件外还包含XML配置、HTML页面、JavaScript/CSS前端资源、图片及SQL数据库脚本等压缩包约51.91MB便于离线查看与二次开发其中XML多用于框架配置HTML、CSS与JavaScript构成前端界面SQL脚本可快速完成数据库初始化。内容预览中出现多个Controller控制器后端采用经典分层结构便于理解请求流转、业务逻辑与数据访问之间的调用关系。资源附带演示视频配合源码和数据库脚本可快速还原运行环境目前已有105人浏览学习适合需要快速搭建同类型Web项目、准备答辩讲解或进行课程设计的读者参考。1. 拿到这份在线答疑系统毕设源码先看懂题目在考什么答辩现场最常见的一幕演示视频里学生提问、老师回答都很流畅但评委一问“回答说已采纳时数据库里哪几张表会变”现场就冷场。某学院在线答疑系统的 Java 毕业设计看起来只是把论坛里的发帖回帖换成问答实际上做完整至少要覆盖用户登录、提问、回答、采纳、通知和统计。对做 Java 项目实战的在校生来说它是最稳妥的一类题目业务封闭、边界清楚又不缺数据库设计和事务处理的分量。下面按“从压缩包里拿到源码到能解释清楚”的顺序把常见做法讲清楚。2. 在线答疑系统的技术选型Java 项目实战先定边界再谈源码结构2.1 前后端分离项目实战为什么比 JSP Servlet 更适合做在线答疑在线答疑系统的流程不算复杂学生登录后发布问题教师或同学在问题详情里回答提问者在若干答案中选择一个设为采纳双方收到站内通知。认领这类 Java 项目实战题目时我一般不会一上来写代码而是先把边界画出来哪些页面必须存在哪些角色必须区分哪些状态必须可流转。边界清晰后技术选型其实是顺着业务走的。用 JSP 加 Servlet 也能把问答做完但最近几年的常见做法是 Spring Boot 做后端接口Vue 做页面MySQL 做存储前后端分离开发。这样选型的好处不是性能而是开发期可以分开调试后端用 Postman 测接口前端单独调试页面最后用一份接口文档联调。答辩时评委问“前后端数据怎么传”你可以直接讲 JSON、HTTP 方法和 token 校验而不是讲 JSP 标签和页面跳转内容更清晰。登录、提问、回答、采纳这些动作都是典型的短事务操作单体应用完全够用。不要因为听到“高并发”就给项目加 Redis 缓存或 MQ在线答疑系统在毕业设计语境下最该证明的是业务闭环和代码可读性。如果确实要加分可以在浏览量统计上用 Redis 或直接数据库计数改动范围小也容易讲清楚。2.2 后端工程结构和核心依赖pom.xml 别照抄拿到源码后我一般会先看目录结构。常见后端工程长这样src/main/java/com/college/qa/ ├── controller/ # REST 接口 ├── service/ # 业务层事务写在这里 ├── mapper/ # MyBatis Mapper 接口 ├── entity/ # 数据库实体类 ├── config/ # JWT 拦截器、WebMvc 配置 └── common/ # 统一返回体、异常、枚举 src/main/resources/ ├── mapper/ # 连表查询 XML └── application.yml这个结构本身就能应付提问“分层怎么分”。controller 只做参数接收和结果返回service 处理业务和事务mapper 只负责数据访问。把事务放在 service 层而不是 mapper 层是为了保证“插入回答并更新问题状态”这两步要么都成功要么都不生效。核心依赖一般就四类dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId /dependencyspring-boot-starter-web 提供 REST 接口和内置 TomcatMyBatis-Plus 负责单表 CRUD省掉大量重复 Mapper 方法MySQL 驱动负责数据库连接JWT 用来做无状态登录校验。MyBatis-Plus 只擅长单表操作问答列表需要连三张表拿用户名、回答数、分类名这类查询还是要写在 resources/mapper 下的 XML 里答辩时被问“MyBatis-Plus 和 MyBatis 有什么区别”也能顺势回答。2.3 数据库脚本在源码包里的位置角色权限对照标题里的“数据库”通常对应一个 .sql 文件里面应该有建表语句和初始数据。我先不急着导入而是确认三张核心表的关系。下面这组角色权限是这类项目最常见的划分角色核心动作涉及表学生发布问题、回答问题、采纳答案question、answer教师回答问题、关闭问题、标记精华question、answer管理员用户管理、分类管理、删除违规内容user、category这个表不是让你背而是帮你确认 .sql 里有没有对应字段。比如学生和教师要能区分user 表里就要有 role 字段管理员要能删问题question 表里就要有 status 字段而不是直接物理删除。演示视频里只会看到页面操作但评委提问的落点基本都在数据库表上。提示导入数据库前先打开 .sql 文件看前面几条 CREATE TABLE 语句确认是 utf8mb4 字符集。如果写成 utf8emoji 昵称和特殊字符会插入失败。3. 从压缩包到能访问在线答疑系统数据库初始化、后端启动、前端启动的三段式操作3.1 数据库初始化先建库再导入别直接执行 CREATE DATABASE 之外的表解压后通常能看到 sql 目录里面有 qa_system.sql。导入前先手工建库避免 .sql 里没有建库语句时导入报错。mysql -u root -p -e CREATE DATABASE qa_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;建库时指定 utf8mb4 是为了兼容中文和特殊符号。接着导入表结构和初始数据mysql -u root -p qa_system sql/qa_system.sql导入后确认表数量和一个关键账号mysql -u root -p -e USE qa_system; SHOW TABLES; SELECT id, username, role FROM user LIMIT 5;如果 user 表没有初始数据说明 .sql 里只建表没插数据演示视频就登不进去。这种情况要自己补一条初始用户管理员、教师、学生各一个密码用 MD5 或 BCrypt具体看源码里登录校验怎么写的再决定。3.2 后端启动改 application.yml 里的三个参数再打 jar 包后端最常见的启动失败原因不是代码错而是配置文件和本地环境不一致。打开 application.yml先看数据源这段spring: datasource: url: jdbc:mysql://localhost:3306/qa_system?useUnicodetruecharacterEncodingutf8mb4useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 server: port: 8080 mybatis-plus: mapper-locations: classpath:mapper/*.xml global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0需要改的是 url 里得数据库名、username、password。serverTimezone 写成 Asia/Shanghai 可以避免 MySQL 8 时间相差 8 小时的问题。MyBatis-Plus 的逻辑删除配置表示所有 delete 操作都转成 update deleted1业务代码里就不会出现物理删除这也符合在线答疑系统保留问答记录的需求。配置改完后后端有两种启动方式。开发期用mvn spring-boot:run答辩打包演示用mvn clean package -DskipTests java -jar target/qa-system-0.0.1-SNAPSHOT.jarmvn package 会执行测试如果源码里有无效单元测试会把打包卡住所以加 -DskipTests 跳过测试。jar 包启动后用curl http://localhost:8080/api/questions看一眼是否返回 JSON能返回说明后端基本起来了。3.3 前端启动npm install 可能比后端还费时间前端如果是 Vue 项目启动命令通常是固定的三步npm config set registry https://registry.npmmirror.com npm install npm run serve第一步把 npm 源切到国内镜像避免下载依赖超时。第二步安装 package.json 里的依赖耗时取决于网络和 node_modules 是否完整。第三步启动开发服务器默认端口是 8080但和后端容易冲突。我一般会先看 package.json 里的 vue-cli 版本再决定 node 版本。Vue 2 项目配 Element UI 时node 版本太高会导致 node-sass 装不上Vue 3 项目相对省心一些。启动后如果页面打不开按下面顺序查现象检查点常见原因登录接口 401JWT 拦截器放行路径/api/auth/login 没有放行页面能开但列表 404前端接口转发配置转发目标端口写错SQL 报错数据库连接地址数据库名和 .sql 不一致如果前端把 /api 开头的请求转发到后端 8080而后端端口改成了 8081就会出现前端页面正常但数据加载不出来。演示视频里录的是哪套端口演示前就要按视频里的一致来启动。4. 核心业务实现在线答疑系统数据库设计、提问接口、回答与采纳状态流转4.1 在线答疑系统的数据库设计五张表可以画出完整 ER 图回答“有哪些表”是答辩必考题。一个完整的在线答疑系统至少要有这五张表表名用途关键字段user用户id、username、password、role、statuscategory问题分类id、name、sortquestion问题主表id、title、content、status、accept_answer_id、view_countanswer回答表id、question_id、content、is_accepted、create_bymessage站内通知id、from_user_id、to_user_id、type、is_readquestion 表的 status 字段是业务核心常见枚举是 0 待回答、1 已有回答、2 已采纳、3 已关闭。accept_answer_id 用来记录最终采纳的是哪条回答没有这个字段列表页就没法直接展示“已解决”状态。answer 表里必须有 question_id因为一个回答必须明确归属到某个问题这就是 ER 图里的一对多关系。建表语句可以参考CREATE TABLE question ( id BIGINT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(120) NOT NULL, content TEXT NOT NULL, category_id BIGINT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待回答 1已有回答 2已采纳 3已关闭, accept_answer_id BIGINT NULL, view_count INT NOT NULL DEFAULT 0, create_by BIGINT NOT NULL, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, deleted TINYINT NOT NULL DEFAULT 0, KEY idx_status_create_time (status, create_time) );status 和 create_time 建联合索引是因为首页通常查“待回答的最新问题”这条 SQL 会高频执行。deleted 字段配合 MyBatis-Plus 逻辑删除保证误删内容还能恢复这也是管理员操作时需要的。4.2 提问接口的 Service 层实现事务为什么放在这里提问看起来只是 insert 一条问题但实际至少要同时更新两个数据插入 question并且给提问者的发布数加一。这两个动作必须在一个事务里。常见实现是Transactional(rollbackFor Exception.class) public Long createQuestion(CreateQuestionRequest request, Long userId) { Question question new Question(); question.setTitle(request.getTitle()); question.setContent(request.getContent()); question.setCategoryId(request.getCategoryId()); question.setStatus(QuestionStatus.UNANSWERED.getCode()); question.setCreateBy(userId); questionMapper.insert(question); // 统计用户发布问题数保持数字与问题列表一致 userMapper.incrementQuestionCount(userId); return question.getId(); }Transactional 表示方法内的数据库操作要么全部成功要么全部回滚。这个项目里最典型的回滚场景是question 插入成功但用户统计更新失败如果不在同一事务里就会出现一个问题页面上能看到、个人中心计数却少一的脏数据。rollbackFor 指定 Exception 时哪怕抛的是运行时异常也会回滚这比默认配置更可靠。4.3 回答与采纳的状态流转防止重复采纳的写法回答操作不能只 insert answer还要更新 question 的状态。写成 service 方法Transactional(rollbackFor Exception.class) public Long createAnswer(CreateAnswerRequest request, Long userId) { Question question questionMapper.selectById(request.getQuestionId()); if (question null || QuestionStatus.CLOSED.getCode().equals(question.getStatus())) { throw new BizException(问题不存在或已关闭); } Answer answer new Answer(); answer.setQuestionId(request.getQuestionId()); answer.setContent(request.getContent()); answer.setCreateBy(userId); answerMapper.insert(answer); // 第一次有回答时把问题从待回答改成已有回答 if (QuestionStatus.UNANSWERED.getCode().equals(question.getStatus())) { questionMapper.updateStatus(request.getQuestionId(), QuestionStatus.ANSWERED.getCode()); } return answer.getId(); }这段代码解释了状态机问题初始是待回答有人回答后变为已有回答之后提问者可以采纳采纳后变为已采纳。如果问题已经关闭就不能再进入回答流程。采纳操作要解决“两个人同时点采纳”的问题。不能先查再更新要用一条条件更新语句保证只有一次成功int rows questionMapper.acceptQuestion(questionId, answerId, userId); if (rows 1) { answerMapper.markAccepted(answerId); messageMapper.insertAcceptMessage(questionId, answerId); }对应的 XML 更新语句是update idacceptQuestion UPDATE question SET status 2, accept_answer_id #{answerId} WHERE id #{questionId} AND create_by #{userId} AND status IN (0, 1) /update条件里带 status IN (0, 1) 是关键第一次更新成功时状态变成 2第二次执行时条件不再满足影响行为 0就不会重复采纳。先判断 rows 再写入通知避免用户收到两次已经无效的采纳提醒。通知表在这里的作用是把“谁回答了你的问题”“谁采纳了你的回答”记录下来这是演示视频里最容易被评委追问的一段关系。5. 答辩前自检用一组 curl 和 SQL 把在线答疑系统源码讲到评委心里的技巧演示视频只要对着流程走一遍就能录完但答辩提问不会按流程走。我一般会在演示前用 curl 替自己先做一轮接口回归把最容易出错的登录和提问过一遍curl -X POST http://localhost:8080/api/auth/login \ -H Content-Type: application/json \ -d {username:student01,password:123456}登录接口返回的 JSON 里会有 token把它拿出来继续提问curl -X POST http://localhost:8080/api/questions \ -H Content-Type: application/json \ -H Authorization: Bearer eyJhbGciOiJIUzI1NiJ9... \ -d {title:Java 集合和数组的区别,content:这个知识点常出现在 java 基础题里,categoryId:1}返回问题 id 后再调用回答接口和采纳接口。这套流程能帮你确认 token 传参、状态流转、通知生成是否都正常比手点页面更容易定位问题。数据库层也要自检。演示时如果数据量到了一万条以上评委一定会问查询慢怎么办。先看执行计划EXPLAIN SELECT * FROM question WHERE status 0 ORDER BY create_time DESC LIMIT 10;返回的 type 如果是 REF 或 RANGE说明联合索引生效如果是 ALL就要检查索引是否加到了 .sql 文件里。这条语句可以把话题从“能跑就行”引到“我考虑过查询代价”比背 java 面试八股文更贴合这个项目。演示视频我建议只录三段登录和角色切换、提问和回答、采纳和通知。每段控制在三十秒左右画面里同时露出浏览器地址和 SQL 日志。答辩时先放视频再打开源码讲对应的 service 方法评委多数会顺着代码问事务和状态流转反而容易提前准备。把 acceptQuestion 那条条件更新语句写在笔记里被问并发场景时现场画一张表解释比任何空泛说明都有说服力。本文还有配套的精品资源点击获取