前后端分离的毕业设计里SpringBoot Vue 这套组合几乎被用到了“满大街”的程度。但真正能把一套售后管理系统从零搭起来、讲清楚设计理由、还能顺利通过答辩的人其实并不多。NUCT 是我给一套产品售后管理系统起的演示代号它解决的问题非常典型客户报修、维修单流转、处理记录留存、产品资料维护。这篇文章就围绕这套系统的完整实现思路展开从业务建模、数据库设计到后端接口、前端页面再到部署和答辩准备把你需要知道的点都捋一遍。如果你正在做类似的毕设、课设或者想找个练手项目吃透 Java MySQL Vue 的配合方式这篇内容可以直接作为参考。1. 售后管理到底管什么NUCT 项目的问题域与需求拆解很多人动手写代码之前最常犯的毛病是拿到“售后管理系统”五个字就开始建表结果表建了一堆页面做了一半才发现流程是拧巴的。我先花点篇幅把业务想清楚后面的代码才好写。1.1 核心角色和闭环流程售后系统的角色可以收敛成三类提交售后申请的客户、处理工单的客服或维修人员、维护基础数据的系统管理员。权限不用铺得太开三套页面就能撑住大部分场景。核心流程是一条闭环链路客户创建售后工单 → 客服受理并审核 → 维修人员接单处理 → 填写处理过程和结果 → 客户确认或系统关闭工单。中间还可以拆出“待受理、处理中、等待配件、已完成、已关闭”这些状态。这个状态流转是整个系统最值得讲给答辩老师听的部分因为它本质上是一个状态机每个状态能跳转到哪里都有明确边界。1.2 从业务需求拆页面和接口把上述流程落到页面上大概会得到这些模块模块页面核心接口登录认证登录页用户登录、获取用户信息工单管理工单列表、创建工单、工单详情工单分页查询、新增、状态变更、处理记录客户管理客户列表客户增删改查产品管理产品列表产品增删改查、保修期计算数据统计看板页工单数量统计、状态占比这个表看起来简单却是多数人容易漏掉的环节。很多初学者上来就先写用户管理、角色管理把框架搭得很重然后时间不够了售后服务本身反而没做透。我自己的建议是主流程优先用户管理只留必要的登录和简单的权限校验剩下时间都投到工单状态流转和页面流畅度上演示效果好说服力也强。2. 技术组合的选择逻辑Spring Boot Vue MySQL 的学习价值选技术栈的时候经常有人问为什么不用 Python为什么不用 Spring Cloud为什么数据库非要 MySQL这些问题在毕业设计答辩里几乎是被问烂了的你要能讲清楚“我为什么不选别的”比“我用了什么”更有价值。2.1 为什么选 Spring Boot 而不是其他后端框架Spring Boot 最核心的价值是“约定优于配置”它帮你把 Tomcat、依赖管理、自动配置都处理好了。你不需要像早期 SSM 那样手动拼一堆 XML 配置文件写一个启动类就能干活。对课设和毕设来说这意味着精力能花在业务代码上而不是环境搭建上。Java 本身又是学校里最常教的语言很多同学从大一就开始写 Java到做设计时语法已经熟悉了剩下的只是框架习惯问题。Spring Boot 配合 MyBatis-Plus 之后大部分 CRUD 都不用手写 SQL单表操作直接调用封装好的方法多表查询再写 XML。这样的组合既不会让你陷入“SQL 地狱”又能保住 SQL 基本功。2.2 Vue 3 Element Plus 对新手更友好的地方前端部分我建议使用 Vue 3 Vite Element Plus。对比 Vue 2Vue 3 的组合式 API 在组件复用上明显更顺手写一个表格页面时搜索条件、分页逻辑、数据请求可以拆成独立函数不会像选项式 API 那样全堆在 data 和 methods 里。Element Plus 的表格、表单、弹窗、分页组件开箱即用页面结构和后端接口结构很容易对应上。尤其是 el-table 配合 el-pagination写一个后台管理列表页大概只需要几十行模板代码。这个选型对不擅长写复杂 CSS 的同学来说非常友好稍微调一下间距和颜色页面就能达到演示标准。3. 数据库设计订单流转的一张表是系统的主心骨数据库设计在答辩里是“重灾区”因为老师很爱问你的表为什么这么建字段冗余了吗逻辑删除能不能讲清楚所以这块我写得细一点。3.1 核心表结构与字段说明售后系统不需要太多表核心就五张用户表、客户表、产品表、售后工单表、工单处理记录表。工单表几乎就是整个系统的中心周边表都是围绕它做信息补全的。售后工单表的大致结构可以这样设计CREATE TABLE after_sale_order ( id bigint NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 工单编号, customer_id bigint DEFAULT NULL COMMENT 客户ID, product_id bigint DEFAULT NULL COMMENT 产品ID, fault_desc varchar(500) DEFAULT NULL COMMENT 故障描述, status tinyint DEFAULT 0 COMMENT 0待受理 1处理中 2等待配件 3已完成 4已关闭, handler_id bigint DEFAULT NULL COMMENT 当前处理人ID, create_time datetime DEFAULT NULL, update_time datetime DEFAULT NULL, deleted tinyint DEFAULT 0 COMMENT 逻辑删除, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT售后工单表;注意这里我用了 order_no 做唯一索引而不是直接用自增主键展示给用户。原因很简单订单号要给别人看、要打印、要在电话里报读通常需要“NUCT 年月日 随机数”这种格式。自增 id 只留在数据库内部当主键就好。工单处理记录表也很重要它的作用是把每个环节的操作都留下来。字段包括记录ID、工单ID、处理人、处理备注、处理时间。这张表设计出来之后工单详情页就能顺畅地展示“这个工单经历了什么”演示的时候很有说服力。3.2 常见设计坑用户态、逻辑删除和状态机字段第一个坑是用户表设计。很多人一上来就做三张表的 RBAC用户表、角色表、用户角色关联表。这个设计本身没错但在售后场景里确实过度了。是管理员就处理管理端逻辑是客服/维修就处理工单权限粒度只需要到“角色”这一层就够了。我推荐直接用一张用户表加一个 role 字段角色枚举放在代码里精简又够用。第二个坑是删除问题。物理删除在演示系统里看似无害但一旦涉及到后续统计删除的数据会让报表对不上。比如你删了一个产品但历史工单里还引用着这个产品的名称一旦按产品维度统计就会缺数据。做法是用 deleted 字段做逻辑删除查询时自动过滤既保住了数据完整也方便恢复。第三个坑是状态字段。工单状态尽量用 tinyint 而不是字符串因为字符串容易拼错、体积也大。查询的时候用代码里的枚举做转换页面显示中文数据库存数字中间用 MyBatis 的枚举类型映射来接清晰又安全。4. 后端实现细节接口、鉴权与工单状态机的落地后端是这套系统里代码量最大、也最容易出 bug 的部分。我按实际开发顺序来讲先搭骨架再填业务最后处理安全和并发。4.1 项目分层与统一返回体项目包结构我建议按这种模式分com.nuct ├── common // 返回体、异常处理、常量 ├── config // 跨域、MyBatis-Plus、JWT配置 ├── controller // 接口层 ├── service // 业务逻辑层 ├── mapper // 数据访问层 ├── entity // 数据库实体 ├── security // JWT过滤器 └── NuctApplication.java前后端分离的项目接口返回格式最好全系统统一。我习惯用一个泛型返回体Data public class ResultT { private Integer code; private String msg; private T data; public static T ResultT success(T data) { ResultT r new Result(); r.code 200; r.msg 操作成功; r.data data; return r; } public static T ResultT error(String msg) { ResultT r new Result(); r.code 500; r.msg msg; return r; } }再加上一个全局异常处理器用 RestControllerAdvice 捕获业务异常和系统异常返回格式永远是统一的。前端 axios 拦截器拿到 code 不等于 200 时统一弹错误提示不用每个接口各写一套。4.2 JWT 登录鉴权一次请求怎么被拦截下来的登录过程很常规前端传用户名密码 → 后端用 BCrypt 校验密码 → 通过后生成 JWT 返回给前端。前端把 token 存到 localStorage 或内存里下次请求放在 Header 的 Authorization 字段。关键在后端过滤器。Spring Boot 中我习惯写一个 OncePerRequestFilter拦截所有 /api/** 请求放行登录、注册等白名单路径对带 token 的请求解析用户身份并放入 ThreadLocal方便 Service 层获取“当前登录用户是谁”。Component public class JwtAuthenticationFilter extends OncePerRequestFilter { Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String token request.getHeader(Authorization); if (token ! null token.startsWith(Bearer )) { token token.substring(7); // 解析 token如果有效则设置当前用户上下文 Long userId JwtUtil.parseUserId(token); if (userId ! null) { UserContext.set(userId); } } chain.doFilter(request, response); } }这个过滤器是很多学生最容易写错的地方有人把它写成了拦截所有静态资源导致 Swagger 文档都看不了有人忘记排除登录接口导致自己把自己锁在外面。建议在配置类里用 Whitelist 数组明确列出哪些路径不需要登录别写死在过滤器里。4.3 工单状态机的实现与并发安全性工单状态流转我建议用一个状态机枚举来做public enum OrderStatus { PENDING(0, 待受理), PROCESSING(1, 处理中), WAITING_PARTS(2, 等待配件), COMPLETED(3, 已完成), CLOSED(4, 已关闭); private final int code; private final String desc; }每个状态允许跳转到哪些下一个状态定义一张转换表。比如“待受理”只能转“处理中”或“已关闭”“等待配件”只能转“处理中”谁也不能从“待受理”直接跳成“已完成”。接口层在更新状态之前先校验当前状态和目标状态是否构成合法转换这句话能回答答辩老师相当多关于“流程控制”的提问。并发安全也是容易被追问的点。两个管理员同时点“接单”理论上可能导致重复处理。解决思路是在更新语句里加上条件UPDATE after_sale_order SET status 1, handler_id #{handlerId} WHERE id #{id} AND status 0如果影响行数等于 0说明状态已经被别人改掉了后面代码直接返回“工单已被处理”。这种乐观锁思路在课程设计里已经足够不用真的去上 Redis 分布式锁。5. 前端页面与接口联调Vue 3 Element Plus 的实践思路后端接口设计得再漂亮前端接不上都是白搭。前端我重点讲三件事请求封装、路由守卫、列表页联动。5.1 前端目录和 axios 封装Vue 3 项目里我通常按这样的目录组织src ├── api // 请求接口定义 ├── router // 路由 ├── stores // 用户状态管理登录信息、角色 ├── views // 页面 ├── utils // axios 封装等工具 └── components // 通用组件axios 封装的核心有两个请求拦截器自动携带 token响应拦截器统一处理后端返回的 code。这样页面代码里写一个接口调用只需要关心 data完全不用每次重复处理错误。import axios from axios const request axios.create({ baseURL: /api, 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( res { if (res.data.code 200) { return res.data.data } // 全局报错 return Promise.reject(new Error(res.data.msg)) }, error { // 401 时跳转登录页 } ) export default request前端和后端联调最烦的就是跨域。开发环境下我用 Vite 的 proxy 把 /api 代理到后端地址避免每个请求都面对 CORS 报错。生产环境部署到 Nginx 后同样用 location /api 反代跨域问题在网关层面就解决了。5.2 路由守卫与权限控制前端权限控制的核心是路由守卫。用户登录后把角色信息保存到 store 里每次路由跳转前判断当前页面要求的角色是否匹配不匹配就跳回登录页或 403 页面。路由的定义里给每个后台页面加一个 meta 属性里面放 roles。比如产品管理只允许管理员看工单列表所有角色都可以看。代码实现很简单router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (!token to.path ! /login) { next(/login) return } const role store.user.role if (to.meta.roles !to.meta.roles.includes(role)) { next(/403) return } next() })这里有个细节很多人会踩坑刷新页面时 store 里的用户信息会被清空但路由已经放行了。解决办法是进入页面之前先调一次“获取当前用户信息”的接口或者把用户基本信息也放进 localStorage刷新后重新读出来。做毕设时为了省事我建议把用户信息缓存到 localStorage权限判断才不会因为刷新就失效。5.3 工单列表页和数据联动工单列表页是最能体现前端功底的地方。模板里用 el-table 展示顶部放搜索条件工单号、客户姓名、状态底部用 el-pagination 分页。搜索、重置、分页、刷新这四个操作本质上都是触发同一个查询函数只是参数不同。创建工单页面要联动下拉框选择客户后自动带出客户联系方式选择产品后自动带出产品型号和保修期。这个功能需要后端在返回工单详情时提供关联对象的完整信息或者前端在创建页拉到全量客户和产品列表。毕设阶段我推荐后者数据量不大前端缓存一份全量列表页面交互体验更好。处理好“新增弹窗”和“编辑弹窗”共用一个表单组件是很加分的点。表单组件通过一个 visible 属性控制显示传入的初始值决定是新增还是编辑。这样代码少写一份也显得你有组件化思维。6. 打包部署与运行环境本地跑通、再上演示服务器很多同学在本地 IDE 里跑得飞快一到演示环境就崩。部署环节我建议自己提前完整走一遍别留到答辩前一天晚上。6.1 后端打 jar 包并连接 MySQL后端打包非常简单在项目根目录执行 Maven 打包命令mvn clean package -DskipTests打包后在 target 目录下会生成一个可执行的 jar 文件。部署时用java -jar nuct-after-sale-0.0.1.jar --server.port8080需要注意的是 application.yml 里的 MySQL 地址不要写死成 localhost要写实际数据库所在机器的 IP否则换台机器就连不上数据库。另外 MySQL 8.0 的驱动链接要加上时区参数spring: datasource: url: jdbc:mysql://127.0.0.1:3306/nuct_after_sale?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/Shanghai这里迟早会有一次报错是“服务器时区值无法识别”或者中文乱码根因就是 characterEncoding 和 serverTimezone 没设置正确。我在做项目时踩过这个坑印象很深。6.2 前端构建与 Nginx 代理配置前端打包之前先确认 axios 的 baseURL 用的是相对路径 /api而不是 http://localhost:8080这样代码挪到哪台机器都不用改前端。执行npm run buildbuild 成功后生成 dist 目录把它放到 Nginx 的 html 目录下再配置一段代理规则server { listen 80; server_name localhost; root /usr/share/nginx/html/dist; index index.html; }接口代理的部分单独用 location /api 转发同时处理 Vue Router 的 history 模式刷新问题location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }前面那句话解释了很多人“本地好好的部署后刷新 404”的困惑。Vue Router 用 history 模式时刷新某个路由路径会直接请求服务器服务器上没有这个物理文件就会 404。try_files 那条配置就是把这个请求重新导回 index.html 的关键。7. 答辩/展示时的高频追问与几个值得做的扩展方向做完整套系统只能说完成了一半另一半是“能讲、能圆、能加分”。7.1 高频追问与答复思路我把老师最爱问的问题列出来你提前把答案想清楚答辩就会从容很多。为什么选前后端分离回答思路是职责清晰、部署灵活、开发效率高前端专注交互后端专注数据和业务规则Vue 的工程化能力让页面维护成本更低。接口安全问题怎么考虑回答思路登录用 JWT 鉴权密码用 BCrypt 加密存储接口层面通过过滤器拦截未授权请求前端通过路由守卫控制页面访问。这个回答已经覆盖了“你能想到基本安全”这个层次。工单状态怎么防止并发改错回答思路状态机约束非法流转SQL 更新语句带条件判断当前状态影响行数为 0 时说明操作失效返回友好提示。老师听到“影响行数”就能知道你是真的写代码不是背概念。为什么用逻辑删除回答思路保留业务数据线索方便统计和审计查询统一过滤 deleted 0避免误删后无法追溯。7.2 值得做的扩展方向核心功能做完之后如果时间有余推荐按性价比排序去扩展通知提醒工单状态每变化一次对创建人做站内消息或短信通知。这块最不复杂但演示效果拔群。统计看板用 ECharts 画工单状态占比、每月工单量趋势。纯前端图表后端一个聚合查询就能支撑。文件上传故障图片、维修单附件上传用本地磁盘存储或对象存储。既体现细节又能应对“数据都是文本会不会太单薄”的质疑。工单导出用 EasyExcel 导出工单列表到 Excel。功能不难确实经常被办公场景提到。最后再说一个实操体会这类管理系统最忌讳把代码写得太“骨架化”列表页没有任何汇总信息、新增工单没有校验、删除按钮连个确认弹窗都没有。这些细节花不了多少时间却能让整个项目看起来像一个真正会被使用的产品而不是一堆表单纯拼接出来的 Demo。如果你做完核心功能还有半天时间优先打磨这些让演示更顺手的地方回报率比再加一张表高得多。