ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Spring Boot在线答疑系统毕设:文件上传与权限控制实战

Spring Boot在线答疑系统毕设:文件上传与权限控制实战 简介面向计算机专业毕业设计及课程设计的一套在线答疑系统完整源码后端采用 Spring Boot 框架配合 Java 1.8 环境、MySQL 5.7 数据库和 Tomcat 7 服务器项目按 Maven 标准组织可导入常用的集成开发环境直接运行。压缩包共 818 个文件整体约 23.57 兆包含 97 个 Java 后端源文件、100 个 Vue 前端页面、96 个编译后的 class 文件、40 个 JavaScript 脚本、24 个 XML 配置文件、3 个数据库初始化脚本及多种启动辅助文件能支撑从环境准备、数据建表、后端接口编码到前端页面联调的过程。目前已有 143 人学习下载适合作为毕业设计或课程设计的参考实现也适合用来梳理在线答疑场景下的数据交互流程。包内附有一键安装与启动脚本、前端组件备份和备份文件便于导入项目时对照排错并为二次扩展功能提供基础整体目录清晰、模块划分明确对 Java Web 实践入门者具有较好的参考价值。1. 在线答疑系统是什么这个毕设题目的真实分量与「文件」二字的双重含义拿到「Java毕业设计基于springboot的在线答疑系统文件的实现.zip」这个名字时很多人的第一反应是「又一个学生提问平台」。但对毕设来说这个题目真正的价值在于它踩中了三个高频考点Spring Boot 的 CRUD 基本功、角色权限的处理、以及文件附件上传下载这个最容易被轻视的模块。所谓「文件的实现」其实有两层意思——一是指整个系统工程的源码文件二是指答疑过程中提问者上传的图片、压缩包、文档等附件功能。不少同学把前者当成了全部答辩时被「你上传的文件存到哪、怎么防超限、别人能不能乱下」一问就卡壳。这篇文章把整套链路完整讲透表怎么建、接口怎么写、文件怎么存、上线前哪些坑必须躲开照着做能省下至少一周的返工时间。2. 先立骨架六张表与角色权限把答疑系统的数据关系定死2.1 六张核心表每个字段为什么存在答疑系统的业务模型不复杂最多六张表就能覆盖。我的习惯是先把表结构定死再写代码因为实体类、Service、接口全是围着表转的。下面这张表是经过几次迭代后最稳的版本表名用途关键字段user用户表id, username, password, role(0学生/1教师/2管理员), avatar, create_timequestion问题表id, user_id, title, content, status(0待答/1已答/2已采纳), view_count, create_timeanswer回答表id, question_id, user_id, content, is_accepted, create_timeattachment附件表id, business_type(question/answer), business_id, file_name, file_path, file_size, upload_timecategory分类表id, nameJava、数据库、算法等message站内信id, from_user, to_user, content, is_readcategory 和 message 属于加分项时间紧可以砍掉但前四张表必须完整。一个常被问到的设计为什么单独拆 attachment 表而不是直接给 question 加一个 file_url 字段因为回答也可以带附件而且一个问题可能挂多个附件。用 business_type business_id 这种多态关联一张附件表同时服务问题和回答查询时只需要一个字段区分归属。2.2 用 MyBatis Plus 生成建表 SQL脚本写法与更稳的手写路线很多人搜「mybatisplus根据java实体类生成创建表的sql语句」确实有这条路。MyBatis Plus 的 TableInfoHelper 能反射实体类拿到表名和字段映射拼出 CREATE TABLE。我写过一个最小脚本核心长这样// 用 TableInfoHelper 反射实体类拼出建表 DDL 骨架 TableInfo tableInfo TableInfoHelper.getTableInfo(Question.class); StringBuilder ddl new StringBuilder(); ddl.append(CREATE TABLE IF NOT EXISTS ).append(tableInfo.getTableName()).append( (\n); for (TableFieldInfo field : tableInfo.getFieldList()) { ddl.append( ).append(field.getColumn()).append( ); if (field.getPropertyType().equals(String.class)) { ddl.append(VARCHAR(255)); } else if (field.getPropertyType().equals(Integer.class)) { ddl.append(INT); } else { ddl.append(BIGINT); } ddl.append(,\n); } // 主键、索引、注释需要手动补脚本只负责字段部分这段代码用来验证「实体和表能对上」是够用的但我不推荐把建表这件事交给运行时反射——主键策略、索引设计、字段注释它都处理不了。更稳的路线是先手写 SQL再让实体类去对齐表结构。一个标准的 question 建表语句长这样CREATE TABLE question ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id bigint(20) NOT NULL COMMENT 提问人ID, title varchar(100) NOT NULL COMMENT 问题标题, content text COMMENT 问题描述支持富文本, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 0待答复 1已答复 2已被采纳, view_count int(11) NOT NULL DEFAULT 0 COMMENT 浏览量, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user_id (user_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT答疑问题表;两个关键点字符集必须用 utf8mb4 而不是 utf8——用户在问题里贴个 emojiutf8 表直接报错或变问号这种低级问题在答辩演示时特别丢分外键不要加MyBatis Plus 体系里靠逻辑层保证一致性数据库外键反而让测试数据清理变得麻烦。2.3 三种角色怎么落到接口上一个注解搞定权限控制user 表的 role 字段定死了三种角色0 学生、1 教师、2 管理员。权限控制不需要上 Spring Security毕设场景用它反而显得笨重——配置类、过滤链、密码加密器一套下来答辩时很难讲清楚。常见做法是自定义一个注解加拦截器三十行代码解决问题Target({ElementType.METHOD, ElementType.TYPE}) Retention(RetentionPolicy.RUNTIME) public interface RequireRole { int[] value(); }拦截器里从请求头拿到 token解析出当前用户的 role再和注解上的要求比对// 拦截器核心逻辑判断当前用户角色是否在注解允许的范围内 HandlerMethod handlerMethod (HandlerMethod) handler; RequireRole requireRole handlerMethod.getMethodAnnotation(RequireRole.class); if (requireRole ! null) { Integer userRole SessionUtil.getCurrentUserRole(request); boolean allowed Arrays.stream(requireRole.value()) .anyMatch(role - role userRole); if (!allowed) { throw new ForbiddenException(当前角色无权操作); } }这样做的好处是接口上写一行RequireRole({0, 1})就能控制「学生和教师可访问管理员不行」比在方法里写 if 判断清爽得多。答辩时如果老师问「为什么不直接用 Spring Security」你就说毕设系统角色只有三种自定义注解更轻量且核心逻辑一眼能看懂。3. Spring Boot 核心接口用状态字段管住提问、回答与被采纳3.1 提问接口校验、入库与「别在同步链路里发通知」提问是系统的入口接口设计上有一个容易犯的错在提问的同时给所有教师发站内信。这个操作如果同步执行会让请求变慢而且一旦有教师账号被禁用消息发送失败还会导致整个提问接口报错。我的做法是提问接口只做三件事校验参数、插入记录、返回 ID。PostMapping(/question) public ResultLong createQuestion(RequestBody QuestionDTO dto, HttpServletRequest request) { // 1. 从拦截器写入的 attribute 里拿当前用户 Long userId (Long) request.getAttribute(userId); if (StringUtils.isBlank(dto.getTitle())) { return Result.error(问题标题不能为空); } // 2. 标题去空格防止只输入空格的情况 Question question new Question(); question.setUserId(userId); question.setTitle(dto.getTitle().trim()); question.setContent(dto.getContent()); question.setStatus(0); question.setViewCount(0); questionService.save(question); // 3. 这里不要写发通知的逻辑放到事务提交后异步去发 return Result.ok(question.getId()); }参数说明DTO 用RequestBody接收 JSON前端富文本编辑器提交的 content 是一段 HTML入库前要做 HTML 转义或白名单过滤否则一个script标签就能让你的页面崩掉。校验放在 Controller 层是为了让非法请求快速失败不进入事务减少无谓的数据库连接占用。至于发通知正确姿势是事务提交后调用一个Async的方法失败只记日志不影响主流程。3.2 回答与采纳乐观锁状态流转避免并发下的一题多态回答接口的核心不是 insert而是对 question.status 的状态流转。一个常见 bug两个学生同时回答同一道题都读到 status0都插入成功然后各自把 status 更新成 1虽然数据没丢但状态更新里出现了不可控的中间态。答疑系统允许一题多答这里真正要保护的是「采纳」这个动作——一道题只能被采纳一次。PostMapping(/answer) RequireRole({0, 1}) public ResultLong createAnswer(RequestBody AnswerDTO dto, HttpServletRequest request) { Long userId (Long) request.getAttribute(userId); Question question questionService.getById(dto.getQuestionId()); if (question null) { return Result.error(问题不存在); } if (question.getStatus() 2) { return Result.error(该问题已被采纳不能再回答); } // 插入回答记录 Answer answer new Answer(); answer.setQuestionId(dto.getQuestionId()); answer.setUserId(userId); answer.setContent(dto.getContent()); answer.setIsAccepted(0); answerService.save(answer); // 如果问题还是待答状态更新为已答复 if (question.getStatus() 0) { question.setStatus(1); questionService.updateById(question); } return Result.ok(answer.getId()); }采纳动作单独写一个接口关键是那条条件更新。用 MyBatis Plus 的 lambdaUpdate 直接写成乐观锁风格// 采纳回答只有问题处于“已答复”状态才能被采纳且只能成功一次 boolean updated questionService.lambdaUpdate() .eq(Question::getId, dto.getQuestionId()) .eq(Question::getStatus, 1) .set(Question::getStatus, 2) .update(); if (!updated) { return Result.error(问题状态已变化请刷新后重试); } answerService.lambdaUpdate() .eq(Answer::getId, dto.getAnswerId()) .set(Answer::getIsAccepted, 1) .update();这里eq(Question::getStatus, 1)就是那把锁并发请求同时到达时数据库行锁保证只有一个 update 影响行数为 1另一个 updated 为 false直接返回错误。比先查再改的写法安全一个量级。参数上注意这个接口应该只允许提问人本人调用所以要再加一个userId.equals(question.getUserId())的判断。3.3 分页与搜索LambdaQueryWrapper 的标准写法与 N1 问题列表页是答疑系统访问量最大的地方分页加搜索是必考。MyBatis Plus 的 Page LambdaQueryWrapper 是最常见的组合// 问题分页查询支持关键字模糊搜索和按状态过滤 PageQuestion page new Page(pageNum, pageSize); LambdaQueryWrapperQuestion wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.isNotBlank(keyword), Question::getTitle, keyword) .eq(status ! null, Question::getStatus, status) .orderByDesc(Question::getCreateTime); IPageQuestion result questionService.page(page, wrapper);这里的技巧在第 4 行和第 5 行StringUtils.isNotBlank(keyword)作为条件参数keyword 为空时整条 like 条件自动跳过不用写 if 判断status 同理。返回给前端时还需要补两个字段提问人的昵称和头像。最直观的写法是 join 查询但 MyBatis Plus 的 LambdaQueryWrapper 不直接支持 join常见做法是先查出分页结果再拿到所有 user_id 一次性查用户信息内存中组装// 避免 N1 查询先查问题再批量查用户内存组装 ListLong userIds result.getRecords().stream() .map(Question::getUserId).distinct().collect(Collectors.toList()); MapLong, User userMap userIds.isEmpty() ? Collections.emptyMap() : userService.listByIds(userIds).stream() .collect(Collectors.toMap(User::getId, u - u)); result.getRecords().forEach(q - { User user userMap.get(q.getUserId()); q.setUserName(user ! null ? user.getUsername() : 已注销用户); });参数说明pageNum 从 1 开始pageSize 建议限制在 20 以内防止有人一次拉全表。这种「先分页再批量补关联数据」的模式在数据量到十万级之前都不会有性能问题也比拼接 SQL 优雅。很多毕设在这块直接用Select注解写一个连表分页查询本身没问题但一旦加条件就变得极难维护。4. 文件上传与访问权限把「文件的实现」做成答辩加分项4.1 本地存储还是 OSS毕设场景的选型判断「基于 springboot 的在线答疑系统文件的实现」这个题目里「文件」最容易出彩的部分就是附件模块。选型上我的建议很直接毕设答辩用本地磁盘存储不要碰 OSS。理由有三点演示现场不确定有没有外网OSS 需要 bucket 配置和密钥现场翻车率高本地存储可以让老师看到文件实实在在落在服务器目录里讲存储逻辑更有说服力OSS 的凭证、计费、跨域等问题会转移答辩焦点。但代码上要为日后切换留后路。定义一个存储接口// 文件存储抽象本地实现和 OSS 实现可以无缝切换 public interface FileStorage { String store(MultipartFile file, String bizType) throws IOException; void delete(String filePath); InputStream getInputStream(String filePath) throws IOException; }答辩时老师问「以后文件太多、单机磁盘不够怎么办」你只需要说「再写一个 OSS 实现类替换 Bean 就行业务代码不动」。这一句话就能展示你考虑了扩展性是很实用的加分话术。4.2 上传接口MultipartFile 双重校验与本地落盘实现上传接口是文件模块的核心要同时做类型校验和大小校验。类型校验靠后缀白名单大小校验靠 Spring 配置加业务层兜底。先看配置spring: servlet: multipart: max-file-size: 10MB max-request-size: 50MB这个配置里max-file-size 是单文件上限max-request-size 是一次请求的总大小多文件上传时算总和。配置不生效是后期最容易翻车的地方原因大多是 yaml 缩进错误或者干脆放错位置进了spring.http.multipart——那是 Spring Boot 1.x 的写法2.x 开始已经改了。上传接口的完整实现PostMapping(/file/upload) RequireRole({0, 1, 2}) public ResultFileVO upload(RequestParam(file) MultipartFile file, RequestParam String bizType, RequestParam Long bizId, HttpServletRequest request) { Long userId (Long) request.getAttribute(userId); // 1. 空文件校验 if (file.isEmpty()) { return Result.error(文件不能为空); } // 2. 后缀白名单校验防止上传可执行文件 String originalName file.getOriginalFilename(); String ext StringUtils.getFilenameExtension(originalName); SetString allowed new HashSet(Arrays.asList( jpg, png, gif, pdf, zip, rar, doc, docx, txt)); if (!allowed.contains(ext.toLowerCase())) { return Result.error(不支持的文件类型: ext); } // 3. 落盘并拿到访问路径 FileStorage storage SpringContextUtil.getBean(FileStorage.class); String path storage.store(file, bizType); // 4. 写附件记录 Attachment attachment new Attachment(); attachment.setBusinessType(bizType); attachment.setBusinessId(bizId); attachment.setFileName(originalName); attachment.setFilePath(path); attachment.setFileSize(file.getSize()); attachment.setUploaderId(userId); attachmentService.save(attachment); return Result.ok(FileVO.of(attachment)); }参数说明bizType 传question或answer标识这个附件挂在哪个业务下bizId 是问题或回答的 ID。这里有一个容易漏的校验bizId 对应的业务记录必须真实存在且当前用户有权操作否则任何人都可以往别人的问题上挂附件。代码里的allowed集合要按需放行答辩现场有人传个.jsp文件会非常尴尬。本地存储实现类注意落盘路径的设计Component public class LocalFileStorage implements FileStorage { Value(${file.storage-path:./upload}) private String storagePath; Override public String store(MultipartFile file, String bizType) throws IOException { // 按日期分目录避免单个目录文件过多 String dateDir LocalDate.now().toString().replace(-, ); String ext StringUtils.getFilenameExtension(file.getOriginalFilename()); // UUID 重命名解决中文文件名乱码和覆盖问题 String fileName UUID.randomUUID().toString().replace(-, ) . ext; Path dir Paths.get(storagePath, dateDir); if (!Files.exists(dir)) { Files.createDirectories(dir); } Path target dir.resolve(fileName); file.transferTo(target.getAbsolutePath()); // 返回相对路径不暴露服务器绝对路径 return /file/ dateDir / fileName; } }两个关键参数file.storage-path在 application.yml 里配置我习惯用绝对路径比如/data/qa-upload不要用相对路径./upload——因为相对路径取决于你从哪里启动 jar同一个 jar 在 IDE 里启动和命令行启动落盘位置可能完全不一样。文件名用 UUID 重命名一是避免两个用户上传同名文件互相覆盖二是避免中文名在下载时产生 URL 编码问题。4.3 文件下载接口用权限控制替代裸奔的静态映射很多教程让你把上传目录配成静态资源映射然后前端拼一个 URL 直接访问。这是在埋雷任何拿到 URL 的人都能下载文件你没法判断「这个用户是不是这个问题的参与者」。毕设系统里至少要做到——附件只允许问题提出者、回答者和教师管理员访问。我的做法是下载走接口先鉴权再返回文件流GetMapping(/file/{dateDir}/{fileName}) public ResponseEntityResource download(PathVariable String dateDir, PathVariable String fileName, HttpServletRequest request) { Long userId (Long) request.getAttribute(userId); // 1. 先查附件记录 Attachment attach attachmentService.lambdaQuery() .eq(Attachment::getFilePath, /file/ dateDir / fileName) .one(); if (attach null) { return ResponseEntity.notFound().build(); } // 2. 权限判断提问者、回答者、教师、管理员可访问 if (!canDownload(userId, attach)) { return ResponseEntity.status(403).build(); } // 3. 读文件并返回 Path path Paths.get(storagePath, dateDir, fileName); Resource resource new FileSystemResource(path); return ResponseEntity.ok() .contentType(MediaType.APPLICATION_OCTET_STREAM) .header(HttpHeaders.CONTENT_DISPOSITION, attachment; filename\ URLEncoder.encode(attach.getFileName(), UTF-8) \) .body(resource); }canDownload 的逻辑可以写简单点如果附件的上传者是自己放行如果附件挂在 question 下查 question 的 user_id 是否是自己role 为 1 或 2 的教师和管理员直接放行。注意下载文件名用URLEncoder.encode处理一下不然中文文件名在 Chrome 里会变成一坨乱码这个细节很能体现工程经验。4.4 附件表设计为什么不用 question 表的冗余字段关于「文件」最后一层设计决策附件到底存哪。有人习惯在 question 表加一个file_url字段问题不大但一旦遇到两种情况就尴尬了——回答也想带附件怎么办一个问题贴了三个参考文档怎么办冗余字段要么再加file_url2、file_url3要么用逗号分隔存一个字符串这两种都是坏味道。独立 attachment 表的优势在查询时体现得最明显。查一个问题的所有附件只需要ListAttachment qAttachments attachmentService.lambdaQuery() .eq(Attachment::getBusinessType, question) .eq(Attachment::getBusinessId, questionId) .list();如果还要把回答里的附件也一起查出来语句也清晰// 查问题和其所有回答下的附件避免循环里单查 ListLong answerIds answerService.lambdaQuery() .eq(Answer::getQuestionId, questionId) .list().stream().map(Answer::getId).collect(Collectors.toList()); ListAttachment attachments attachmentService.lambdaQuery() .and(wrapper - wrapper .eq(Attachment::getBusinessType, question) .eq(Attachment::getBusinessId, questionId)) .or(wrapper - wrapper .eq(Attachment::getBusinessType, answer) .in(Attachment::getBusinessId, answerIds.isEmpty() ? Collections.singletonList(0L) : answerIds)) .list();这段的细节在处理 answerIds 为空的情况in条件不能传空集合否则 MyBatis Plus 生成的 SQL 会变成IN ()直接报语法错误所以塞一个不存在的 0L 进去。这是很多人会在答辩前夜翻车的地方。独立附件表还有一个隐性好处管理后台做文件统计时一条 SQL 就能算出「本月上传文件总数、总体积」可以作为答辩时的数据展示点。5. 避坑手册Spring Boot 答疑系统最容易翻车的 5 个真实问题5.1 版本号玄学Spring Boot 3.x 的 javax/jakarta 包名迁移现象按照 Spring Boot 2.x 的教程写代码import javax.servlet.*在 3.x 项目里编译直接报「程序包不存在」。 原因Spring Boot 3 基于 Jakarta EE 9所有 javax.servlet 开头的包名换成了 jakarta.servlet连 Tomcat 的依赖都换了一整套。搜「springboot 版本太高」的人基本都卡在这一步。 解决毕设直接用 Spring Boot 2.7 这条线最稳教程最多、MyBatis Plus 兼容性最好、网上遇到的坑基本都有答案。如果已经建了 3.x 项目全局替换javax.servlet为jakarta.servletMyBatis Plus 也要换用mybatis-plus-spring-boot3-starter这个坐标。另外 Spring Boot 3 默认的 Java 版本是 17如果你的机器装的是 JDK 8启动就会报UnsupportedClassVersionError——这也是「版本太高」的连锁反应。5.2 文件上传 413Multipart 配置没生效的排查顺序现象小文件上传正常传一个大文件直接返回 413 Request Entity Too Large或者抛 MaxUploadSizeExceededException。 原因Spring Boot 2.x 的默认单文件上限是 1MB你没在 application.yml 里配置 multipart 参数。还有一种情况配置写了但放在 Nginx 层Nginx 默认 client_max_body_size 也是 1MB请求还没到 Spring Boot 就被挡了。 解决先看后端日志里有没有 MaxUploadSizeExceededException有说明 Spring 配置没生效或没写没有异常但请求被拒优先查 Nginx 的client_max_body_size。配置项注意是spring.servlet.multipart不是spring.http.multipart。还有一个容易忽略的点全局异常处理器里要捕一下 MaxUploadSizeExceededException返回一个友好的 JSON不然前端拿到的是 Tomcat 默认的 HTML 错误页很难看。5.3 中文乱码连接串、表字符集与驱动版本三件事现象插入的中文正常但换一台电脑部署后数据库中看到的是问号或者前端传进来的 emoji 直接报错。 原因字符集问题有三个层次缺一不可——数据库表字符集、JDBC 连接串字符集、驱动版本对字符集的处理。 解决建表一律用 utf8mb4连接串写成jdbc:mysql://localhost:3306/qa_db?useUnicodetruecharacterEncodingUTF-8serverTimezoneAsia/ShanghaiuseSSLfalse。注意这里不要写characterEncodingutf8mb4MySQL Connector/J 8.x 里 UTF-8 会映射到 utf8mb4写 utf8mb4 反而可能不被识别。如果已经建好的表是 utf8执行一句ALTER TABLE question CONVERT TO CHARACTER SET utf8mb4;可以补救但千万记得同时检查连接串和驱动版本三个环节有一个不对问题就还在。这类问题在答辩演示时尤其致命——你之前在本机好好的换到教室电脑就乱码多半就是 MySQL 版本或字符集变量不同。5.4 附件列表有记录、磁盘却没文件文件生命周期的一致性现象列表页能看到附件名称点击下载却 404去服务器目录看文件不见了。 原因最常见的是上传目录落在了target/classes或项目构建目录里每次mvn clean都会把文件一起清掉。另一个方向是删除操作没做干净——只删了 attachment 表记录磁盘文件残留或者只删了磁盘文件记录还在。 解决存储路径用绝对路径配置不要放在项目目录内启动时在日志里打印当前生效的存储路径方便排查。删除附件的方法要写成「先删磁盘文件再删数据库记录磁盘删除失败则记录日志并保留记录定时任务补偿」。上传同理文件先落盘再写数据库记录如果写库失败要立即删除刚落的文件。这套流程不复杂但能保证两个源始终一致答辩时老师问你「文件删了磁盘怎么办」你有完整回答。5.5 启动失败三板斧端口占用、数据源与 Banner 误导现象启动类一跑控制台满屏红色 stack trace或者干脆启动到一半停住不动。 原因九成是三类问题——端口被占用控制台报BindException: Address already in use数据源连接失败报Access denied for user或Communications link failure资源目录缺 application.yml 导致默认配置找不到数据源。 解决端口占用用netstat -ano | findstr 8080Windows或lsof -i:8080Mac/Linux查进程 PID杀掉或者改server.port。数据源失败先确认 MySQL 服务启动了、账号密码正确、数据库已创建再检查连接串里的serverTimezone——这个参数缺失在新版驱动里会直接启动报错。至于 Banner很多人用「springboot banner生成器」做了个花哨的启动图案贴进 resources结果里面有隐藏特殊字符打印出来满屏乱码误导你以为启动出错。我的习惯是直接关掉 Banner在 application.yml 里加spring.main.banner-modeoff日志干净排查问题省心。6. 最后冲刺把 Vue 打包进 Spring Boot一条命令交付演示6.1 前端 dist 合并进 resources/static项目结构变化与路由回退毕设做前后端分离很常见——基于 springboot vue 项目的结构前端独立跑 8081 端口开发上线时合并部署。很多人的「vue打包放进springboot中」的流程是这样的前端执行npm run build生成 dist 目录把 dist 下的所有文件复制到后端src/main/resources/static下重新mvn package得到单个可执行 jar。这样演示时只需要java -jar xxx.jar不用再开一个 Node 服务对答辩现场非常友好。这里有一个关键坑前端路由如果用的 history 模式直接访问http://localhost:8080/question/3会 404因为 Spring Boot 找不到这个路径对应的 Controller只有http://localhost:8080/index.html是通的。解决办法是加一个回退 Controller// 非 API 路径统一转发到 index.html交给前端路由处理 Controller public class PageForwardController { RequestMapping(value /{path:[^\\.]*}) public String forward() { return forward:/index.html; } }这段代码写在项目根 Controller 包下即可。注意正则[^\\.]*排除了带点的路径这样静态资源js、css、图片不会误伤。同时后端接口如果统一带有/api前缀就不会被这个转发规则拦截。合并部署后前端代码里的接口 baseURL 要改成完整的http://localhost:8080/api不要再依赖开发时的 proxy 代理这是合并后接口通不通的关键。6.2 交付前自测用一条 curl 链跑通核心链路答辩前夜我会用一组 curl 命令把核心链路完整跑一遍确认无死角。这比在页面上手工点靠谱得多因为 curl 能看到状态码和原始响应。一组典型命令如下# 1. 注册两个测试账号并登录取出 token curl -X POST http://localhost:8080/api/user/register \ -H Content-Type: application/json \ -d {username:stu01,password:123456,role:0} curl -X POST http://localhost:8080/api/user/login \ -H Content-Type: application/json \ -d {username:stu01,password:123456} | tee login.json # 手工从 login.json 里取出 token 变量继续后面的操作 # 2. 学生提问带一个附件文件 curl -X POST http://localhost:8080/api/question \ -H Authorization: Bearer $TOKEN \ -F titleSpring Boot 多数据源怎么配置 \ -F content如题求详细步骤 \ -F file./readme.txt # 3. 教师回答 curl -X POST http://localhost:8080/api/answer \ -H Authorization: Bearer $TEACHER_TOKEN \ -H Content-Type: application/json \ -d {questionId:1,content:在 pom 里加两个 DataSource 配置即可} # 4. 学生采纳回答 curl -X POST http://localhost:8080/api/answer/accept \ -H Authorization: Bearer $TOKEN \ -H Content-Type: application/json \ -d {questionId:1,answerId:1}跑完这四条你的系统核心业务就全部验证过了。中间任何一步返回非预期状态码就按错误信息去查日志这是最直接的验证方式。最后检查一下 upload 目录里文件是否真的落盘了再用浏览器访问一次附件下载链接确认权限控制生效整套交付就稳了。我自己早年做这类系统时把上传目录放在了target/classes下每次一mvn clean附件全没了答辩前夜差点翻车。后来所有文件相关的代码一律强制用绝对路径并在启动日志里打印出当前生效的存储路径。这个小习惯救了我很多次。希望这篇笔记能帮到你别在文件这个不起眼的模块上栽跟头——把它做扎实反而是整个毕设最亮眼的加分项。本文还有配套的精品资源点击获取
返回列表