☰
基于SpringBoot与Vue的软件缺陷管理系统设计与实现
2026/10/12 20:30:35 网站建设 项目流程

简介:这是一套面向计算机专业本科生的毕业设计级软件缺陷管理系统,采用SpringBoot+Vue前后端分离架构,解决中小型开发团队或教学场景中缺陷提交、状态跟踪、优先级调度与数据可视化等核心管理需求。资源包共706个文件,含136个JavaScript前端逻辑文件、53个Java后端业务代码、33个Vue组件、276张界面截图(PNG/JPG)及3个MySQL建库建表SQL脚本,CSS样式文件覆盖8种主题配色,整体压缩包仅3.52MB,轻量易部署。目前已有32人学习下载,适合Java与前端初学者开展全栈实践,尤其利于理解RESTful接口设计、Vue组件通信、数据库第三范式建模及缺陷生命周期管理流程。所有Java类与Vue组件均含规范注释,SQL脚本适配MySQL 8.0+,配套环境要求明确(JDK 11+、Node.js 14+),可直接导入IDE运行调试。

1. 项目概述与核心价值

1.1 这个系统到底解决什么问题

软件缺陷管理系统,说白了就是Bug管理系统。很多人觉得它不就是“记录bug、改bug”的工具吗?但真正开发过的人都知道,一个缺陷从被发现到最终关闭,中间要经过提交、指派、修复、验证、关闭好几个环节,每个环节都涉及不同角色的人,如果没有一套系统做流转记录,光靠微信群和Excel表格,项目到了后期Bug数量一多,谁改的、改没改完、改完有没有验证,完全是一笔糊涂账。

我之前带过一个6人小组的中型项目,团队用共享Excel记录缺陷,结果上线前统计缺陷状态时发现同一批Bug被重复记录了三次,还有几个已经修好的Bug因为没人更新状态,测试又提了一遍。这就是手工管理缺陷的典型痛点。所以这个基于SpringBoot与Vue的软件缺陷管理系统,本质上是把“缺陷的发现→指派→修复→验证→关闭”这条流程线上所有动作记录下来,并让不同角色各司其职,系统自动保留操作痕迹,最终还能根据这些数据统计出质量报表。

1.2 为什么这套技术栈值得参考

SpringBoot加Vue这套组合,在目前国内中小型项目里的占有率实在太高了。SpringBoot负责后端接口,Vue负责页面交互,MySQL存数据,Redis做缓存(可选,这个毕设里不加也行),前后端通过JSON交互。对于毕业设计来说,这套技术栈最大的优势是“生态成熟、资料多、面试也常问”。你需要演示什么功能都有人实现过,源码参考丰富,不至于卡死在某个冷门技术上。而且SpringBoot自带的Tomcat、自动配置机制,可以让开发者把精力集中在业务逻辑而不是环境配置上,Vue的组件化开发则让页面复用和维护都省心很多。

这个系统适合谁来参考?计算机相关专业做毕业设计的同学是最主要的群体。其次,想学习前后端分离开发完整流程的初中级开发者也可以照着搭一遍。如果你是在校生,花一周时间把核心模块理清楚,答辩的时候能把表结构设计、权限设计、状态流转这几个亮点讲明白,就足够拿到一个不错的分数了。

2. 整体功能设计思路与模块划分

2.1 用户角色与权限模型

缺陷管理系统最核心的不是写代码,而是权限模型的设计。bug谁能提交、谁能分配、谁能关闭,必须规定清楚,否则系统上线后会乱套。

按照最常见的团队配置,我设计了四类角色:

角色核心权限说明
管理员用户管理、项目管理、系统配置拥有所有权限,可查看全部数据
测试人员提交缺陷、验证修复、评论核心使用者,负责发现和验证Bug
开发人员查看被指派的缺陷、提交修复、评论只能修改分配给自己的缺陷
项目负责人项目统计、指派缺陷、关闭缺陷一般由项目经理担任,掌握全局

这个权限模型在实现上不复杂,用一张user表存角色编号,后端接口用拦截器鉴权,前端再根据角色控制按钮显示就行。注意一点:角色权限不要在前端做唯一校验,前端只是隐藏按钮,真正的权限判断必须在后端接口里做。我见过有的毕设只做了前端按钮隐藏,接口直接裸奔,任何人调接口都能删数据,这在答辩时也是硬伤。

2.2 六大核心功能模块

系统整体分为六大模块:登录认证模块、项目管理模块、缺陷管理模块、统计分析模块、用户管理模块、操作日志模块。这里重点讲一下各模块的职责和关联关系。

登录认证模块负责用户登录、JWT令牌签发和注销操作。项目管理模块比较简单,就是项目的增删改查,项目存在的意义是把Bug归类,一个项目下挂多个缺陷,项目负责人可以对项目成员进行维护。缺陷管理模块是核心中的核心,包含缺陷的提交、指派、修复、验证、关闭五种状态操作,外加查询、筛选、评论、附件上传等功能。统计分析模块是根据缺陷表的数据做聚合统计,比如按严重级别分组统计、按处理人统计、按状态统计,并给出图表展示。用户管理模块负责账号的CRUD和角色分配。操作日志模块则记录了关键动作(登录、提交Bug、修改状态等)的操作人、操作时间、操作内容,方便溯源。

2.3 缺陷状态流转流程设计

缺陷状态是整个系统最关键的领域模型。我参考了业界比较通用的Bug生命周期模型,结合毕业设计的演示需求做了简化,设计了五个状态:

新建(New)→ 已指派(Assigned)→ 已修复(Fixed)→ 已验证(Verified)→ 已关闭(Closed)

状态流转的操作方要卡死:

  • 测试人员提交缺陷后,状态为“新建”,系统自动记录提交人和提交时间。
  • 项目负责人或管理员看到新建的缺陷,指派给某个开发人员,状态变为“已指派”,同时记录指派人和指派时间。
  • 开发人员修改代码后,将缺陷状态改为“已修复”,写明修复说明,此时系统要记录修复人和修复时间。
  • 测试人员对修复结果进行回归测试。如果验证通过,状态变为“已验证”。如果验证不通过,可以直接退回“重建”(Reopen),重新走“已指派”流程。
  • 项目负责人确认缺陷确实已经解决,执行“关闭”操作,缺陷生命周期结束。

我做过调查,很多毕设系统里这块容易做成“状态字段随便改”,没有任何流转校验。答辩老师可能会提出一个经典问题:如果测试人员直接把新建的Bug改成已验证怎么办?你的系统如果允许这个操作,就会显得设计不严谨。所以我在后端一定要加状态机校验,只允许合法状态的迁移,非法操作直接抛异常。

3. 数据库设计与表结构实现

3.1 数据库建模思路

数据库是这个系统的地基,一开始建模没想清楚,后面写代码会一路踩坑。我按照“用户表—项目表—缺陷表—评论表—附件表—日志表”这6张核心表来设计,表关系如下图(用文字描述):用户表与缺陷表存在提交人、指派处理人、修复人三种关联关系;项目表与缺陷表是一对多关系;缺陷表与评论表是一对多关系;缺陷表与附件表是一对多关系。

这里有一点特别重要:缺陷表里不要直接用外键约束去关联用户表,而是用逻辑外键(只存userId)来关联。理由有两点:第一,删数据时不会因为外键约束导致各种报错;第二,MyBatis-Plus联合查询时用逻辑外键更灵活。这在答辩时也可以当作一个设计亮点来提。

3.2 核心表结构脚本

下面直接给出核心的SQL建表脚本,这几张表基本就能覆盖系统所有功能需求:

-- 用户表 CREATE TABLE `sys_user` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `username` VARCHAR(50) NOT NULL UNIQUE COMMENT '登录账号', `password` VARCHAR(100) NOT NULL COMMENT '密码(MD5加密存储)', `nickname` VARCHAR(50) DEFAULT NULL COMMENT '姓名', `role` TINYINT NOT NULL DEFAULT 3 COMMENT '角色:1管理员 2项目负责人 3开发人员 4测试人员', `email` VARCHAR(100) DEFAULT NULL, `phone` VARCHAR(20) DEFAULT NULL, `status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:1启用 0禁用', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP ) COMMENT '用户表'; -- 项目表 CREATE TABLE `project` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `name` VARCHAR(100) NOT NULL COMMENT '项目名称', `description` VARCHAR(500) COMMENT '项目描述', `manager_id` INT COMMENT '负责人ID', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) COMMENT '项目表'; -- 缺陷表 CREATE TABLE `bug` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `project_id` INT NOT NULL COMMENT '所属项目ID', `title` VARCHAR(200) NOT NULL COMMENT '缺陷标题', `description` TEXT COMMENT '缺陷详细描述', `severity` TINYINT NOT NULL COMMENT '严重级别:1提示 2一般 3严重 4致命', `priority` TINYINT NOT NULL DEFAULT 2 COMMENT '优先级:1低 2中 3高', `status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:1新建 2已指派 3已修复 4已验证 5已关闭 6重新打开', `reporter_id` INT NOT NULL COMMENT '提交人ID', `assignee_id` INT DEFAULT NULL COMMENT '指派人ID', `fixer_id` INT DEFAULT NULL COMMENT '修复人ID', `verifier_id` INT DEFAULT NULL COMMENT '验证人ID', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) COMMENT '缺陷表'; -- 缺陷评论表 CREATE TABLE `bug_comment` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `bug_id` INT NOT NULL COMMENT '缺陷ID', `user_id` INT NOT NULL COMMENT '评论人ID', `content` VARCHAR(1000) NOT NULL COMMENT '评论内容', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP ) COMMENT '缺陷评论表'; -- 附件表 CREATE TABLE `attachment` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `bug_id` INT NOT NULL, `file_name` VARCHAR(255) NOT NULL, `file_url` VARCHAR(500) NOT NULL, `uploader_id` INT NOT NULL, `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP ) COMMENT '附件表'; -- 操作日志表 CREATE TABLE `operation_log` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `user_id` INT NOT NULL, `action` VARCHAR(200) NOT NULL COMMENT '操作描述', `ip` VARCHAR(50) DEFAULT NULL, `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP ) COMMENT '操作日志表';

3.3 关于字段设计的几个关键细节

严重级别和状态字段,我用的都是TINYINT数字类型,而不是直接用字符串。这样可以减少存储空间,并且在后端代码里用枚举或常量进行映射,前端显示时再通过翻译函数转成文字。这里有一个容易踩的坑:如果你的状态直接存字符串“新建”“已指派”,后期如果要调整状态名称或者加状态,就得写一大堆SQL更新语句,而且代码里到处是魔法字符串,很容易写错。用数字常量统一管理,改起来只需要动枚举或者配置文件一处地方。

create_time字段我用的是DATETIME默认值CURRENT_TIMESTAMP,这个细节可以省掉很多后端手动set时间的代码。MyBatis-Plus插入数据时如果实体类的该字段为null,数据库会自动填充当前时间,省心。另外update_time加上了ON UPDATE CURRENT_TIMESTAMP,当记录被更新时自动刷新,这样就能省去“每次改数据都要手动维护更新时间”的麻烦。

4. 后端SpringBoot核心实现细节

4.1 项目分层结构与初始化配置

后端项目采用经典的四层结构:Controller层、Service层、Mapper层、Entity实体层。Controller只负责接收参数和返回结果,Service层写业务逻辑,Mapper层通过MyBatis-Plus操作数据库。此外还有config包(配置类)、common包(统一返回结果)、utils包(工具类)、interceptor包(拦截器)。

项目创建建议使用Spring Initializr,Java版本用JDK 8或11(面试和毕设环境最稳),SpringBoot版本不要一味求新,2.5.x系列就够了。第一次做毕设的同学很容易踩“版本太高导致各种依赖不兼容”的坑,比如SpringBoot 3.0以上要求JDK 17,而且javax包改为jakarta包,网上很多老代码直接不能用,折腾半天还在配置阶段打转。

pom.xml里核心依赖如下:

<dependencies> <!-- SpringBoot Web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- MyBatis-Plus --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.2</version> </dependency> <!-- MySQL驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <!-- JWT --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> <!-- Lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!-- Hutool工具类 --> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.18</version> </dependency> </dependencies>

MyBatis-Plus强烈推荐,它让我少写至少60%的CRUD代码。单表查询直接用baseMapper.selectList、selectPage这些现成的方法,复杂查询再手写XML,比纯MyBatis高效太多了。我在毕设里用MyBatis-Plus的分页插件做分页查询,远比自己在SQL里拼Limit方便。

4.2 统一返回结果与异常处理

前后端分离的项目,接口返回值一定要统一格式。我定义了一个R类作为统一返回结果:

@Data public class R<T> { private Integer code; // 200成功, 401未登录, 403无权限, 500错误 private String message; private T data; public static <T> R<T> ok() { return new R<T>(200, "操作成功", null); } public static <T> R<T> ok(T data) { return new R<T>(200, "操作成功", data); } public static <T> R<T> error(String message) { return new R<T>(500, message, null); } public static <T> R<T> error(Integer code, String message) { return new R<T>(code, message, null); } }

这个类看起来很普通,但在实际开发中作用很大。接口返回的数据结构一致,前端Axios响应拦截器就能统一处理错误提示,不用每个请求都写一遍错误分支。有个细节:code我用了HTTP风格的数字,而不是“true/false”这种布尔值,这样前端可以根据code值判断是未登录跳登录页、还是没有权限弹提示框,语义更清晰。

全局异常处理也要配上,用@RestControllerAdvice注解统一捕获业务异常和未知异常,返回R.error格式,避免一报错就把整个Tomcat的异常堆栈直接抛给前端浏览器。

4.3 JWT登录鉴权实现

登录鉴权我用了JWT方案。用户登录成功之后,后端生成一个令牌字符串返回给前端,前端存在localStorage里,每次请求在请求头里带上Authorization: Bearer token,后端拦截器再解析验证。

JWT工具类的核心代码:

public class JwtUtils { private static final String SECRET = "abcdefghijklmnopqrstuvwxyz0123456789"; // 生成令牌 public static String generateToken(Integer userId, String username) { return Jwts.builder() .setSubject(username) .claim("userId", userId) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } // 解析令牌 public static Claims parseToken(String token) { return Jwts.parser() .setSigningKey(SECRET) .parseClaimsJws(token) .getBody(); } }

拦截器在这里只做令牌有效性校验,用HandlerInterceptor前置拦截所有非登录接口。由于我后面还要做角色权限控制,所以拦截器里会顺便解析出userId和role,放到ThreadLocal或直接放入request的attribute中,Controller层就能直接拿到当前用户信息。

关于密码存储:这里千万不要明文存密码,我用的是MD5加盐(实际生产建议BCrypt),在用户表里存的是加盐后MD5的密文。答辩时老师如果问密码安全怎么做,你答出“MD5加盐存储”和“BCrypt”这两个词,至少能证明你不是照着网上源码无脑抄的。

4.4 缺陷状态流转的业务校验

这里展示一下核心的状态流转逻辑。我在Bug实体中定义了一个状态字段,然后控制器里写一个通用的状态更新方法:

@PostMapping("/bug/updateStatus") public R<?> updateStatus(@RequestBody UpdateStatusDTO dto) { // 1. 校验缺陷是否存在 Bug bug = bugMapper.selectById(dto.getBugId()); if (bug == null) { return R.error("缺陷不存在"); } // 2. 状态流转合法性校验(核心) Integer currentStatus = bug.getStatus(); Integer targetStatus = dto.getTargetStatus(); boolean isValid = checkStatusTransition(currentStatus, targetStatus, currentUser.getRole()); if (!isValid) { return R.error("非法的状态流转"); } // 3. 更新状态字段,同时记录操作人信息 bug.setStatus(targetStatus); if (targetStatus == 2) { bug.setAssigneeId(dto.getUserId()); } else if (targetStatus == 3) { bug.setFixerId(currentUser.getId()); } else if (targetStatus == 4) { bug.setVerifierId(currentUser.getId()); } bugMapper.updateById(bug); // 4. 记录操作日志 operationLogService.log(currentUser.getId(), "更新缺陷状态: " + bug.getId()); return R.ok(); } private boolean checkStatusTransition(Integer oldStatus, Integer newStatus, Integer role) { // 只有合法迁移才放行 switch (oldStatus) { case 1: return newStatus == 2 && role == 2; // 新建->指派, 负责人操作 case 2: return newStatus == 3 && role == 3; // 指派->修复, 开发人员操作 case 3: return newStatus == 4 || newStatus == 6; // 修复->验证 或 修复->重新打开 case 4: return newStatus == 5; // 验证->关闭 case 6: return newStatus == 2; // 重新打开->重新指派 default: return false; } }

这里有个设计心得:如果你把状态流转校验逻辑写成一长串if-else或者switch,后期状态一旦变多会非常难看。我后来在另一个项目里直接用了枚举加状态机映射表,代码更整洁。如果你时间充裕,也可以用Map提前初始化好“当前状态 -> 可到达状态集合”的映射,扩展起来更方便。

4.5 文件上传与静态资源映射

缺陷截图是Bug信息中很重要的部分,没有截图,开发人员有时候根本猜不出问题出在哪。所以系统要支持图片上传,前端用上传组件把图片传给后端,后端保存到本地磁盘,再把访问URL返回给前端。

SpringBoot配置文件里设置上传路径:

spring: servlet: multipart: max-file-size: 10MB max-request-size: 20MB # 自定义上传路径 upload: path: D:/upload/

上传接口的核心代码:

@PostMapping("/upload") public R<String> upload(@RequestParam("file") MultipartFile file) { if (file.isEmpty()) { return R.error("文件不能为空"); } // 生成唯一文件名,防止重名覆盖 String originalFilename = file.getOriginalFilename(); String suffix = originalFilename.substring(originalFilename.lastIndexOf(".")); String newFileName = UUID.randomUUID().toString().replace("-", "") + suffix; // 创建目录 File dir = new File(uploadPath); if (!dir.exists()) { dir.mkdirs(); } // 保存文件 File dest = new File(uploadPath + newFileName); file.transferTo(dest); // 返回访问路径 return R.ok("/files/" + newFileName); }

然后写一个配置类实现WebMvcConfigurer接口,把/files/**路径映射到本地目录:

@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/files/**") .addResourceHandler("file:" + uploadPath); }

5. 前端Vue核心实现细节

5.1 Vue项目结构与开发环境准备

前端使用Vue2加Element UI这套组合(Vue3加Element Plus也行,但对毕设而言Vue2的参考资料更丰富,我建议新手用Vue2)。用Vue CLI脚手架创建项目之后,我习惯把src目录按下面的方式组织:

src/ ├── api/ // 接口请求封装 │ ├── bug.js │ ├── user.js │ └── project.js ├── assets/ // 静态资源 ├── components/ // 公共组件 ├── layout/ // 后台布局(侧边栏+头部) ├── router/ // 路由配置 ├── store/ // Vuex状态管理 ├── utils/ // 工具函数(axios封装) └── views/ // 页面组件 ├── login.vue ├── dashboard.vue ├── bug/ │ ├── bugList.vue │ ├── bugDetail.vue │ └── bugForm.vue ├── project/ └── user/

关于安装Vue环境,常见的坑是node版本与脚手架版本不匹配。建议使用Node 16.20.2版本搭配Vue CLI 5.0以上,安装命令就是npm install -g @vue/cli,安装完用vue -V验证一下。npm安装卡住的时候,推荐切换国内镜像源,这个网上教程很多,这里不展开了。

5.2 Axios请求封装与拦截器

前端和后台交互最重要的一层就是Axios封装。如果不封装,每个页面里都得写token获取、错误弹窗、加载动画这些重复逻辑,非常痛苦。我的统一封装是这样的:

import axios from 'axios' import { Message } from 'element-ui' import router from '@/router' // 创建axios实例 const service = axios.create({ baseURL: '/api', timeout: 10000 }) // 请求拦截器:自动携带token service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = 'Bearer ' + token } return config }, error => { return Promise.reject(error) }) // 响应拦截器:统一处理code service.interceptors.response.use(response => { const res = response.data if (res.code === 200) { return res } else if (res.code === 401) { // 未登录或token过期 localStorage.removeItem('token') router.push('/login') return Promise.reject(new Error('登录已过期')) } else { Message.error(res.message || '系统错误') return Promise.reject(new Error(res.message)) } }, error => { Message.error('网络异常') return Promise.reject(error) }) export default service

有了这层封装,接口文件里只需要写请求路径和参数就行。比如bug.js:

import request from '@/utils/request' // 分页查询缺陷列表 export function getBugList(params) { return request({ url: '/bug/list', method: 'get', params }) } // 提交新缺陷 export function createBug(data) { return request({ url: '/bug/create', method: 'post', data }) } // 更新缺陷状态 export function updateBugStatus(data) { return request({ url: '/bug/updateStatus', method: 'post', data }) }

这样每个页面的请求代码都只有几行,代码量大大减少,而且排查问题时能快速定位。

5.3 缺陷列表页与表单弹窗

缺陷列表页是整个系统前端最核心的页面。我用Element UI的el-table展示缺陷列表,配合el-pagination做分页,el-select做状态筛选,el-date-picker做时间范围选择。表格要显示缺陷标题、所属项目、严重级别、当前状态、提交人、指派人和提交时间。严重级别用el-tag,不同级别配不同颜色(致命红色、严重橙色、一般蓝色、提示灰色),一眼就能看出最紧急的缺陷是哪些。

搜索条件的写法也要注意:用户选了状态、项目、时间范围,点击“查询”按钮后,需要把筛选条件拼成请求参数传给后端。这里有个小坑:日期范围选择组件返回的是一个数组,需要拆分成beginTime和endTime两个字段再传给后端,不然MyBatis-Plus的查询条件会拿不到正确的时间范围。

缺陷提交表单我用了el-dialog嵌套el-form,字段包括缺陷标题、所属项目(下拉选择)、严重级别(单选)、优先级(单选)、详细描述(textarea)、上传截图附件(el-upload)。提交前的表单校验规则不能省,标题必填、项目必选、严重级别必选,这些用el-form的rules配置即可。

5.4 状态操作按钮的权限控制

前端做权限控制的核心是v-if加角色判断。比如在缺陷列表的操作列,需要根据当前用户的角色和缺陷当前状态决定显示哪些按钮:

<el-table-column label="操作" width="240"> <template slot-scope="scope"> <!-- 详情按钮:所有人都有 --> <el-button size="mini" type="primary" @click="showDetail(scope.row)">详情</el-button> <!-- 指派按钮:项目负责人,且缺陷状态为新建 --> <el-button v-if="role === 2 && scope.row.status === 1" size="mini" type="warning" @click="showAssignDialog(scope.row)">指派</el-button> <!-- 修复按钮:开发人员且被指派给自己,状态为已指派 --> <el-button v-if="role === 3 && scope.row.assigneeId === userId && scope.row.status === 2" size="mini" type="success" @click="handleFix(scope.row)">修复</el-button> <!-- 验证按钮:测试人员,状态为已修复 --> <el-button v-if="role === 4 && scope.row.status === 3" size="mini" type="info" @click="handleVerify(scope.row)">验证</el-button> <!-- 关闭按钮:项目负责人,状态为已验证 --> <el-button v-if="role === 2 && scope.row.status === 4" size="mini" type="danger" @click="handleClose(scope.row)">关闭</el-button> </template> </el-table-column>

这里强调一遍:前端按钮隐藏只是用户体验层面的处理,后端接口必须做同样的权限校验,否则绕过页面直接调接口就全完了。

6. 常见问题与排查技巧实录

6.1 跨域请求被拦截

前后端分离项目第一个遇到的坑几乎都是跨域。前端跑在8080端口,后端跑在8081端口,浏览器会拦截跨域请求。解决办法有两种:一是在后端加CORS配置类,二是通过前端Vue CLI的proxy代理转发。我推荐第二种方案,因为生产部署时前后端大概率会用Nginx做同域代理,配置方式更接近真实场景。

Vue CLI中在vue.config.js里配置代理:

module.exports = { devServer: { port: 8080, proxy: { '/api': { target: 'http://localhost:8081', changeOrigin: true, pathRewrite: { '^/api': '' } } } } }

这样前端请求/api/bug/list时,会被代理转发到http://localhost:8081/bug/list,因为路径重写把/api前缀去掉了。后端Controller里就不需要加/api前缀。如果后端也加CORS配置,要注意是否和代理配置冲突,一般二选一即可,不要同时使用。

6.2 分页数据查询始终返回全部数据

这个问题常见于MyBatis-Plus的分页插件配置不正确。如果你发现调selectPage方法返回的是全表数据,分页根本没生效,那说明没有配置MyBatis-Plus的分页插件拦截器。需要在配置类中加入:

@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }

这个配置不能漏,漏了就分不了页。同时注意前端传参名:MyBatis-Plus默认接收current和size两个参数,而不是pageNum和pageSize。如果前端传的是pageNum,后端接口需要手动转换为current。我在第一次联调时就在这里栽了跟头,后端一直取不到分页参数。

6.3 时间字段从数据库读出后相差8小时

这是时区问题。MySQL连接串需要指定时区:

jdbc:mysql://localhost:3306/bug_system?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai

同时后端在返回时间字段给前端时,可以加统一配置让Jackson格式化:

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

配置完这两个地方,时间显示就正常了。如果还不够,检查一下是不是前端在传日期时用了UTC字符串,导致时间在转换时又被当作零时区偏移了。

6.4 文件上传提示Tomcat连接超时

图片太大或上传网速慢时,可能触发Tomcat的连接超时。如果图片超过10MB,建议在前端先做压缩,或者适当调大后端的超时时间:

spring: servlet: multipart: max-file-size: 20MB max-request-size: 50MB server: tomcat: connection-timeout: 30000

另外一个更隐蔽的坑:本地开发时上传的图片路径是D:/upload/xxx.jpg,但部署到Linux服务器后,路径就失效了。所以上传路径最好配置化,在application.yml里通过upload.path配置,部署时改成服务器路径。同时要注意Linux下创建目录的权限,权限不对会导致mkdirs()失败。

6.5 前端路由懒加载报错

用import()语法做路由懒加载时,打包后会出现“chunk加载失败”的报错。这通常是因为旧版本浏览器不支持动态导入,或者构建到生产环境后文件名带hash,刷新后找不到对应的js文件。解决方案:路由表中加了懒加载后,构建时检查webpack配置,Vue CLI 5默认已经支持code splitting,一般没问题。如果确实报错,可以降低浏览器兼容标准,或者把懒加载改成常规的组件引用方式,毕竟毕设系统页面数量有限,不用为了性能牺牲稳定性。

6.6 数据库连接数不足报错

答辩演示时如果同时打开了多个页签,后端可能出现Connection is not available, request timed out的错误。根因是连接池最大连接数太小。SpringBoot默认的HikariCP连接池最大连接数是10,我建议加大一点:

spring: datasource: hikari: maximum-pool-size: 20 minimum-idle: 5

这个配置在演示场景下能有效避免并发请求打爆连接池。当然,如果系统上线,连接数的设置还要考虑数据库服务器本身的性能,不是越大越好。

7. 部署与扩展建议

7.1 本地快速启动流程

到这一步,前后端代码基本就写完了。我总结一下本地跑通整个系统的流程,方便你在写论文时参考这五大步骤:

  1. 创建数据库:执行配置中提供的建库SQL,创建bug_system数据库和全部数据表。
  2. 启动后端:修改application.yml中的数据库账号密码,运行SpringBootApplication主类。
  3. 启动前端:在vue项目根目录执行npm install安装依赖,再执行npm run serve。
  4. 访问系统:浏览器打开http://localhost:8080,用预置的admin账号登录。
  5. 测试核心流程:创建一个项目,再提交一条缺陷,走一遍“提交→指派→修复→验证→关闭”完整流程。

这里提醒一下,数据库连接串里的数据库名要跟实际创建的库名一致,账号密码也必须改成本地的MySQL信息,这是第一次启动最常见的报错原因。

7.2 从毕设到生产环境还差什么

如果你想让这个项目不只是停留在毕设层面,而是能真正在团队中用起来,下面几个方向值得扩展:

  • 用Redis保存JWT令牌和用户会话,实现登录状态可注销和控制。现在JWT无状态,用户改密码后旧token仍然有效,存在安全隐患。
  • 增加缺陷导入导出功能,支持Excel批量导入和导出,方便和遗留系统迁移数据。
  • 增加邮件通知功能:缺陷指派给开发人员时,自动发邮件通知,提高响应速度。
  • 在统计分析模块中增加缺陷趋势图:用ECharts展示每日缺陷新增量和修复量,这个图表在答辩演示时视觉效果很好。
  • 将前端构建后的文件部署到Nginx,后端打包成Jar包部署到Linux服务器上,并使用域名访问。这篇博文里已经包含了Linux相关的配置思路,实际部署时要注意数据库和文件上传目录的路径问题。

我自己做这个系统时,最深刻的体会是:毕业设计的技术实现只占一半,另一半在于能不能把系统设计思路完整、有逻辑地讲出来。数据库表为什么这么设计,状态流转为什么要加权限校验,JWT相比Session有什么优势,这些问题你如果能对着代码讲出个一二三,那这套东西就真正内化成自己的能力了。最后再分享一个实用小技巧:写论文或画图时,用draw.io画一张系统功能结构图加一张数据库ER图,答辩时往大屏幕上一放,老师对你这个系统的整体认知会顺畅很多。

本文还有配套的精品资源,点击获取

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

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

立即咨询