☰
SpringBoot+Vue全栈毕设实战:从数据库设计到JWT认证与答辩准备
2026/10/17 14:02:08 网站建设 项目流程

1. 项目全貌与定位:选题理由和技术栈考量

做毕设的时候,最容易被问的一句话是:“你为什么要选这个题目?”如果答不上来,后面答辩基本就是被牵着鼻子走。所以我在定“光影平台”这个题目之前,认真盘过几件事。

“光影平台”这个名字听起来有点文艺,但落到技术上就是一个典型的前后端分离项目:后端用SpringBoot提供RESTful接口,前端用Vue做单页应用,数据库用MySQL存数据,整个项目涵盖用户登录注册、内容管理、文件上传、分类检索、数据统计这些常规模块。它既没有复杂到毕设周期做不完,又比“学生管理系统”那种烂大街题目多了一点辨识度。答辩的时候,老师看到的是你对完整业务链路的把控能力,而不是一个概念演示。

技术栈选型上,我坚持了一套“稳中带新”的组合:

  • 后端:SpringBoot 2.x + MyBatis-Plus + Spring Security + JWT
  • 前端:Vue 2.x + Element UI + Axios + Vue Router + Vuex
  • 数据库:MySQL 5.7/8.0都可以,建议8.0
  • 中间件:Redis(可选,用来做验证码缓存和Token黑名单)

为什么这么选?因为毕设的核心目标是“能跑、能讲、能扩展”。SpringBoot降低配置成本,MyBatis-Plus省去大量手写SQL的工作量,JWT解决前后端分离下的登录态问题,Vue2配合Element UI是当时最成熟的中后台开发组合,网上资料多,踩坑成本低。不是说Vue3不好,而是毕设周期就几个月,用最熟练的一套组合能让你把精力花在业务逻辑上,而不是花在踩新框架的坑上。

项目交付物除了源码之外,我还整理了完整的SQL脚本和接口文档。这一点很多同学容易忽略——源码只是代码,SQL脚本和接口文档才是让别人(尤其是评阅老师)快速看懂你项目的钥匙。后面我会详细说这两块怎么做才算“合格交付”。

2. 数据库设计与SQL脚本的坑

2.1 表结构划分思路:不要一上来就写SQL

光影平台的功能我拆成了几个模块:用户模块、光影内容模块(即影视/视频条目)、分类模块、收藏点赞模块、评论模块、操作日志模块。对应的核心表有这些:

  • t_user:用户表
  • t_content:光影内容表(影视条目)
  • t_category:分类表
  • t_collect:用户收藏表
  • t_comment:评论表
  • t_operation_log:操作日志表(可选)

表设计以“够用、能演示完整流程”为原则,不追求过度设计。比如权限方面,我做的是用户和管理员两种角色,没有引入Spring Security的RBAC完整模型,而是在user表里加一个role字段。不是我不会RBAC,而是毕设里用户-角色-权限三张表加关联关系会让账号系统的演示链路变长,答辩时容易在细节上被追问。角色字段 + 前端路由守卫 + 后端注解拦截,已经足够表达“权限控制”这个能力。

下面是我实际使用的建表脚本中比较核心的几张表:

CREATE TABLE `t_user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(100) NOT NULL COMMENT '加密后密码', `nickname` varchar(50) DEFAULT NULL, `avatar` varchar(255) DEFAULT NULL COMMENT '头像URL', `role` tinyint(1) DEFAULT '0' COMMENT '0-普通用户 1-管理员', `status` tinyint(1) DEFAULT '1' COMMENT '1-正常 0-禁用', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
CREATE TABLE `t_content` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `title` varchar(200) NOT NULL COMMENT '标题', `cover` varchar(255) DEFAULT NULL COMMENT '封面图URL', `video_url` varchar(255) DEFAULT NULL COMMENT '播放地址', `summary` text COMMENT '简介', `category_id` bigint(20) DEFAULT NULL, `view_count` int(11) DEFAULT '0' COMMENT '浏览量', `collect_count` int(11) DEFAULT '0' COMMENT '收藏数', `status` tinyint(1) DEFAULT '1' COMMENT '1-上架 0-下架', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_category` (`category_id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='光影内容表';

补充一点:字符串字段一律用utf8mb4而不是utf8,因为utf8在MySQL里存不了emoji和部分生僻字。亲身经历,之前一个项目里用户昵称带了个特殊符号,直接导致入库报错,排查半天发现是字符集问题。数据库初始化的时候就把字符集定好,能省掉很多莫名其妙的坑。

2.2 SQL脚本交付:不是扔一个dump文件就完事

很多同学写毕设说明文档时,SQL脚本就是MySQL Workbench导出的一个大文件,里面全是CREATE TABLE和INSERT语句。这样不是不行,但给评阅老师的体验不好。我的做法是准备三个脚本:

  1. sql/init.sql:建库、建表。里面包含完整的表结构、字段注释、索引定义。
  2. sql/data.sql:基础数据。包括管理员账号、分类数据、几条内容示例数据。
  3. sql/demo_data.sql:演示数据。批量插入几十条内容记录,方便前端页面有东西可以展示。

分别拆开的好处是:评阅老师先看init.sql了解你表设计得怎么样,再看data.sql知道系统能不能跑起来,最后用demo_data.sql灌数据做演示。逻辑清晰,也显得你专业。

这里有一个容易忽略的点:SQL脚本里的注释。我自己给每个字段都加了COMMENT说明。字段命名也尽量自解释:username就是用户名,create_time就是创建时间。千万别用a、b、c这种缩写,或者au、ct这种半猜式命名。评阅老师看表结构的时间可能只有几分钟,字段名和注释写清楚了,他就知道你的设计是规范的。

数据初始化脚本里,密码一定不能存明文。我测试环境下的管理员密码也是用BCrypt加密后的字符串。初始化脚本里写清楚“默认密码是什么、加密规则是什么”,方便评阅老师拿这个账号去登录系统。接口文档里我也专门留了一页说明默认账号信息。

最新热词是什么,其实就是平时我们做项目时说的那些接地气的东西:如"这样写才专业"、"测试环境"、"分分钟搞定"之类。写SQL脚本时也要有这种意识——每个细节,都是为了减少沟通成本。

再提醒一个坑:如果你用的旧版本MySQL客户端导出SQL,会带出一些类似SET FOREIGN_KEY_CHECKS、ENGINE=InnoDB AUTO_INCREMENT=xxx的语句,这些没问题,但可能包含DEFINER=这类指定用户名权限的语句,在别人电脑上导入时会报权限错误。我的处理方式是,所有交付的SQL脚本都自己手写或重新整理过,确保任何人在任何MySQL环境上都能直接执行成功。

3. SpringBoot后端核心模块实现

3.1 项目分层:用标准结构换来低犯错率

后端代码我用的是经典的四层结构:Controller → Service → Mapper → Entity。网上很多项目会加一个DTO/VO层,我这里也加了,但定位很克制——Entity对应数据库表,VO是返回给前端的视图对象,DTO是接收前端参数的传输对象。不强行加层,但必要的隔离要做。

一个典型的分层示例:

com.guangying.platform ├── controller // 接口层,只做参数接收和返回 ├── service // 业务逻辑层,核心处理 │ ├── impl ├── mapper // MyBatis-Plus的Mapper接口 ├── entity // 数据库实体 ├── dto // 入参对象 ├── vo // 返回视图对象 ├── config // 配置类(JWT、跨域、静态资源映射等) ├── utils // 工具类 ├── common // 统一返回结果、异常处理等 └── PlatformApplication.java

为什么这个结构能“换来低犯错率”?因为它把职责边界划清楚了。Controller不写业务SQL,Service里不出现HttpServletRequest,Entity不放注解以外的逻辑。一旦同学之间互相看代码、评阅老师抽查代码,发现你的代码职责清晰,印象分就上去了。

这里说一下统一返回结果的设计。我用了一个Result<T>类:

@Data public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("操作成功"); result.setData(data); return result; } public static <T> Result<T> error(Integer code, String message) { Result<T> result = new Result<>(); result.setCode(code); result.setMessage(message); return result; } }

所有接口都返回这个结构,前端Axios响应拦截器里统一判断code。有人可能觉得这样多此一举,但实际项目里没有统一返回结构,前后端联调就是灾难:有的接口返回字符串、有的返回JSON、有的直接返回200状态码。统一之后,前端写代码非常放松,只要关心data里是什么就行。

3.2 认证与权限:JWT的完整落地流程

光影平台的用户认证我用的JWT方案。整体流程是:

  1. 用户提交用户名和密码到/api/auth/login
  2. 后端校验账号密码,成功则生成JWT返回
  3. 前端把Token存到localStorage
  4. 后续请求在请求头加Authorization: Bearer <token>
  5. 后端用JWT过滤器解析Token,从Token里拿到用户ID和角色

JWT的生成我用的jjwt库。核心代码大致是这样:

public String generateToken(User user) { Date now = new Date(); Date expiryDate = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000); return Jwts.builder() .setSubject(user.getUsername()) .claim("userId", user.getId()) .claim("role", user.getRole()) .setIssuedAt(now) .setExpiration(expiryDate) .signWith(SignatureAlgorithm.HS256, secretKey) .compact(); }

Token过期时间我设置为7天,因为这是毕设项目,用户不会频繁使用,太短的过期时间会导致演示过程中经常需要重新登录,体验不好。实际生产项目中,通常会用短Token + Refresh Token的机制,但毕设不用做到那个程度。

后端拦截权限我用了Spring Security做全局过滤,但是配置非常简单:关闭CSRF、关闭Session、放行登录注册接口和获取公开内容的接口,其余接口全部走JWT过滤器验证。管理员专属接口上加@PreAuthorize("hasRole('ADMIN')")注解。

这里有个容易踩的坑:Spring Security默认会拦截所有请求,包括静态资源。如果你在Controller里写了文件上传接口,但前端上传的请求没有带Token,就会被拦截。我自己的处理方式是:上传接口也放在需要认证的路由下,前端上传组件在请求头中带上Token。或者干脆把上传接口放行,等业务需要时再校验。生产上建议还是校验,毕设阶段看你怎么讲,能自圆其说就行。

3.3 文件上传与静态资源映射:一个常见的演示翻车点

光影平台的内容需要封面图,这就涉及到文件上传。我后端用的上传方案是:文件保存到服务器的本地目录(比如/uploads),同时把文件的访问URL存到数据库。

Controller里的上传逻辑:

@PostMapping("/api/upload") public Result<String> upload(@RequestParam("file") MultipartFile file) { if (file.isEmpty()) { return Result.error(400, "文件不能为空"); } String originalFilename = file.getOriginalFilename(); String ext = originalFilename.substring(originalFilename.lastIndexOf(".")); String newFilename = UUID.randomUUID().toString().replace("-", "") + ext; String datePath = new SimpleDateFormat("yyyyMMdd").format(new Date()); String filePath = uploadDir + "/" + datePath; File dir = new File(filePath); if (!dir.exists()) { dir.mkdirs(); } file.transferTo(new File(filePath, newFilename)); String url = "/uploads/" + datePath + "/" + newFilename; return Result.success(url); }

注意一个问题:这里返回的是相对路径/uploads/...,不是完整的HTTP地址。前端拿到这个相对路径后,配合配置的接口前缀就能拼出完整可访问的URL。但如果前后端部署在不同的域名/端口下,这个相对路径就会失效。所以我的做法是:前端在拿到返回结果后,判断如果URL以/开头,就拼上VUE_APP_API_BASE_URL,如果已经是http开头就直接用。这个细节很多同学想不到,联调时就会出现“上传成功但图片加载不出来”的尴尬。

SpringBoot里要映射/uploads/**到本地磁盘目录,需要加一个配置类:

@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Value("${file.upload-dir}") private String uploadDir; @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/uploads/**") .addResourceLocations("file:" + uploadDir + "/"); } }

这里的file.upload-dir要设置一个本地绝对路径,比如D:/project/uploads或者/home/user/uploads。我最初的坑就是忘了加这个映射,上传后的文件存在服务器本地,但通过URL访问400,排查了很久才发现是资源映射没配置。

上传文件的类型和大小也要限制一下。我在application.yml里设置了:

spring: servlet: multipart: max-file-size: 20MB max-request-size: 50MB

如果要限制图片类型,就在Controller里判断file.getContentType(),只允许image/jpeg、image/png这类常见的图片格式返回错误。

3.4 接口文档:别用Word写,用Swagger/在线文档

接口文档是我这次项目交付中投入产出比最高的组成部分。我用的是Swagger(springfox)自动生成API文档,再加上一份手动整理的Markdown版本。

Swagger的集成很简单,加依赖、写配置类、在Controller或接口上加注解就行。效果是启动项目后访问/swagger-ui/index.html就能看到所有接口的测试面板。评阅老师可以直接在页面上测试接口,比自己用Postman调方便得多。

不过Swagger有个缺点:默认生成的描述信息比较简陋,接口含义不清晰。所以我给每个接口都加了详细的@ApiOperation注释和@ApiImplicitParam参数说明。

@ApiOperation(value = "分页查询内容列表", notes = "支持按分类、关键词、状态筛选") @ApiImplicitParams({ @ApiImplicitParam(name = "page", value = "页码,从1开始", defaultValue = "1", dataType = "int"), @ApiImplicitParam(name = "size", value = "每页条数", defaultValue = "10", dataType = "int"), @ApiImplicitParam(name = "categoryId", value = "分类ID,不传则查询全部分类", dataType = "long"), @ApiImplicitParam(name = "keyword", value = "标题关键词,模糊查询", dataType = "string") }) @GetMapping("/api/content/list") public Result<PageResult<ContentVO>> list(@RequestParam(required = false, defaultValue = "1") Integer page, @RequestParam(required = false, defaultValue = "10") Integer size, @RequestParam(required = false) Long categoryId, @RequestParam(required = false) String keyword) { // 业务代码 }

接口文档里面,我额外加了三个板块:默认账号说明、部署步骤、常见启动报错解决办法。不要小看这三个板块,它能让使用你文档的评阅老师或同学十分钟内把项目跑起来,对你的好感度直线上升。

我的Markdown接口文档结构大致是:

  1. 项目简介与技术栈
  2. 环境要求与部署步骤
  3. 配置文件说明
  4. 默认账号列表
  5. 接口明细(按模块分组):登录注册、内容管理、收藏评论、数据统计等
  6. 常见问题FAQ

这份文档放到项目仓库的docs目录下。我强烈建议每个毕设项目都有一份这样的文档,因为将来找工作时,这份文档是证明你工程化能力的重要材料。

4. Vue前端页面开发与联调

4.1 前端项目结构与路由设计

前端我用Vue CLI脚手架创建项目,目录结构按页面模块划分:

src ├── api // 接口请求封装 │ ├── auth.js │ ├── content.js │ └── user.js ├── assets // 静态资源 ├── components // 公共组件 ├── layout // 布局组件(头部、侧边栏、内容区) ├── router // 路由配置 │ └── index.js ├── store // Vuex状态管理 ├── views // 页面组件 │ ├── Login.vue │ ├── Home.vue │ ├── ContentList.vue │ ├── ContentDetail.vue │ └── Admin │ ├── Dashboard.vue │ ├── ContentManage.vue │ └── UserManage.vue ├── utils // 工具函数(request封装、token管理等) └── main.js

路由设计上,我用Vue Router的守卫做了权限控制。具体逻辑是:在router.beforeEach里判断用户是否已登录,如果没登录且访问的是需要认证的页面,就跳转到登录页;如果登录了且访问的是管理端页面,还要判断角色是否为管理员,不是则跳回首页。

router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}') if (to.meta.requiresAuth && !token) { next('/login') } else if (to.meta.requiresAdmin && userInfo.role !== 1) { next('/') } else { next() } })

这套逻辑实际上是在前端做了一层导航守卫,后端接口还有第二层校验,双层防护。答辩时能分两层讲清楚,会显得你考虑问题比较全面。

4.2 组件拆分与页面实现:以内容列表页为例

前端页面的核心场景是内容列表页和内容详情页。内容列表页我拆了以下几个组件:

  • SearchBar.vue:搜索和筛选区域
  • ContentCard.vue:单个内容卡片,展示封面、标题、分类、浏览数
  • PaginationBar.vue:分页组件
  • CategoryFilter.vue:分类筛选标签

组件拆分的原则是“一个组件干一件事”。ContentCard只负责展示一条内容的数据,SearchBar只负责收集搜索条件并触发搜索事件,父组件ContentList.vue负责组合这些组件、管理页面状态、调用接口。

用Element UI提供的组件能少写很多CSS。比如分页直接用的el-pagination,但要注意它的事件绑定:@current-change,不要和@click混淆。实际开发中,我第一次没注意就把current-change写成了click,结果点击页码根本没反应。

Axios请求封装是前端联调的关键。我在utils/request.js里封装了一个axios实例,统一配置了baseURL、请求拦截器、响应拦截器:

import axios from 'axios' const request = axios.create({ baseURL: process.env.VUE_APP_API_BASE_URL || 'http://localhost:8080', timeout: 10000 }) request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = 'Bearer ' + token } return config }) request.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { ElMessage.error(res.message || '请求失败') return Promise.reject(new Error(res.message)) } return res }, error => { if (error.response && error.response.status === 401) { localStorage.clear() router.push('/login') } ElMessage.error(error.message || '服务器错误') return Promise.reject(error) } )

这个封装同时处理了Token附带和错误提示,以及401未授权的跳转。项目里所有接口请求都通过这个封装实例发出,保证统一的异常处理逻辑。如果每个页面自己写axios请求,很容易出现有的页面报错没提示、有的重复写Token逻辑的情况。

4.3 前后端联调:跨域与非规范接口的实战

前后端联调是毕设过程中最容易“掉链子”的阶段。我碰到的问题主要有三类。

第一类是跨域问题。前端跑在localhost:8080,后端跑在localhost:9090,前端请求后端就属于跨域了。解决办法有两个:一是在后端加跨域配置,二是在前端开发环境配代理。我的做法是两个都做了,但推荐大家主要以后端的配置为准,因为生产环境部署到同一个服务器时跨域就不存在了。

后端的跨域配置也很简单:

@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }

allowedOriginPatterns用*可以匹配任意来源,不要用allowedOrigins("*"),因为带Cookie的请求不允许*通配。加上allowedMethods里必须有OPTIONS,否则浏览器预检请求会直接失败。

第二类是时间格式问题。后端返回的LocalDateTime默认是2024-12-26T10:23:45这种带T的格式,前端直接展示看起来很怪。我处理方式是在application.yml里配置JSON序列化格式:

spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8

但注意这个配置只对java.util.Date生效,对LocalDateTime还要加Jackson的JavaTimeModule配置。我直接在配置里自定义了一个Jackson2ObjectMapperBuilderCustomizer的Bean。

第三类是后端返回的JSON字段命名问题。MyBatis-Plus默认下划线转驼峰通常是对的,但如果前端传参是createTime,后端接收实体用create_time,就需要在全局配置里设置map-underscore-to-camel-case: true(MyBatis-Plus默认就是true),或者用@JsonProperty注解。我统一采用的策略是:Java实体里用驼峰命名,数据库字段用下划线命名,MyBatis-Plus这个配置从SpringBoot的application.yml里开启:

mybatis-plus: configuration: map-underscore-to-camel-case: true

这样前后端传输的JSON都是驼峰字段,数据库层转换交给框架。这个思路和SQL脚本中的字段命名保持了严格一致,不容易出现歧义。

5. 答辩准备与常见问题排查实录

5.1 评审老师最常问的几个问题:别等被问到再想

毕设答辩的提问环节是有规律可循的,提前准备可以大幅降低翻车概率。根据我的经验,光影平台这类SpringBoot+Vue全栈项目,老师的问题集中在几个方向。

第一个方向是“为什么”。为什么用JWT而不是Session?为什么选MyBatis-Plus而不是原生MyBatis?为什么前端用Vue2而不是Vue3?这类问题的核心是考察你有没有真正理解自己用的技术。回答的套路是:先说出自己的选型理由(开发效率、团队生态、资料丰富、符合项目体量),再对比一下别的方案有什么劣势(比如Session在分布式环境下不好扩展、原生MyBatis需要写大量XML)。这里不用说得太深,但至少要让老师知道你不是随便选的。

第二个方向是“怎么做”。比如:用户密码是怎么加密的?文件上传后怎么访问的?评论功能如何防止重复提交?你要能对着代码说出关键实现。我的建议是把自己做过的每个模块的核心代码都回顾一遍,至少能讲出大概流程。

第三个方向是“有什么问题”。这个方向其实最容易被忽视,但老师特别爱问。比如:项目有什么不足?如果数据量大了怎么办?你打算怎么优化?我准备了两个诚实的改进点:一是文件上传目前存在本地磁盘,后续可以迁移到对象存储;二是内容检索目前是简单SQL模糊查询,学有余力可以接Elasticsearch。这些真实的不足点反而比“我做的项目完美无缺”这种回答更可信,也更能体现你的成长意识。

5.2 部署与启动:从零到一跑通项目的完整步骤

一个项目交付出去,如果别人按文档跑不起来,那这个项目的价值要大打折扣。我整理了从零开始部署的完整链路。

前置环境准备:JDK 1.8+、Maven 3.6+、Node.js 14+、MySQL 8.0。版本一定要在文档里写清楚,不然别人用的JDK版本不同可能启动报错。

第一步,创建数据库。在MySQL执行init.sql和data.sql:

mysql -u root -p < init.sql mysql -u root -p < data.sql

第二步,修改后端配置。打开application.yml,把数据库地址、用户名、密码改成自己的:

server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/guangying_platform?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: yourpassword redis: host: localhost port: 6379

第三步,启动后端。我用的IDE是IDEA,直接运行PlatformApplication.java就行。如果用命令行:

mvn clean package -DskipTests java -jar target/guangying-platform.jar

这里有个经验:第一次打包时间会比较长,Maven要下载依赖,建议提前把仓库配置成国内镜像,否则可能卡很久。

第四步,启动前端:

npm install npm run serve

注意npm install在依赖较多的时候也容易报错,常见的原因是网络或者node-sass的版本问题。一个省心的方法是使用cnpm或者pnpm,但要注意锁文件差异。我在文档里提示:如果npm install失败,尝试删除node_modules和package-lock.json后重新安装。

第五步,访问前端地址http://localhost:8080,用管理员账号登录,看数据是否正常。如果页面白屏,按F12看控制台报错。我在开发环境下遇到的多数白屏都是跨域或接口地址配置问题,把VUE_APP_API_BASE_URL和前端请求路径仔细对照一遍基本能解决。

5.3 常见问题与排查技巧速查表

下面这几条是我在实际调试和帮朋友看代码时经常遇到的坑,整理成了一张速查表,碰到相同问题可以直接按这个思路排查:

现象排查思路解决建议
前端能打开但接口全部401确认Token是否带上了;确认JWT过滤器是否拦截了登录接口查看request.js的Authorization头;在后端日志看JWT过滤器有没有报错
上传图片后前端显示404检查静态资源映射是否配置;确认文件实际保存路径是否以/vuploads/开始核对addResourceHandlers里的磁盘路径,以及file.upload-dir是否为绝对路径
数据库导入SQL时报错“Unknown collation”字符集排序规则问题统一使用utf8mb4_general_ci或utf8mb4_unicode_ci;确保MySQL版本支持
后端启动报端口被占用8080端口被其他服务占用`netstat -ano
前端npm install报node-sass找不到Node版本和node-sass不匹配升级到新版sass;或将Node切换到项目要求的版本
页面能加载但图片用相对路径无法访问前后端分离时路径拼接错误前端对以/开头的URL拼上VUE_APP_API_BASE_URL
登录成功后刷新页面登录态丢失没有做用户信息持久化在store初始化时从localStorage恢复userInfo和token

此外还有一个经验类技巧:在本地调试时打开浏览器的Network面板,看每个接口的请求URL、请求方法、响应体中的code和message,大多数联调问题都能通过这一步快速定位。很多人习惯只看页面显示效果然后猜问题出在哪,其实直接从接口层的响应判断会更高效。

最后的几点个人体会

做完这个项目,最深的感受是:毕设不是为了写代码,而是为了让你完整地经历一次“从需求到交付”的过程。源码能跑只是及格线,真正拉开差距的是SQL脚本是否严谨、接口文档是否清晰、答辨时能不能讲清每个链路的取舍理由。光影平台是我用来兜底的一个演进版本,它的代码质量和文档完整度让我在找工作时能自信地把它放进作品集里。如果你也选了SpringBoot+Vue的组合,建议不要只盯着“写代码”,花几天时间把文档、验证、演示视频都整理一遍,你会发现自己对这个项目的理解深了很多。未来如果学了更多技术栈,不妨把这个项目再过一遍,加个微服务模块或者换一版移动端小程序,能玩的扩展点还挺多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询