ARTICLE DETAIL

资讯详情

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

SSM+Vue工单系统实战:毕业设计与Java面试的高分项目

SSM+Vue工单系统实战:毕业设计与Java面试的高分项目 简介这是一套基于Java SSM后端与Vue前端的工单管理系统完整毕业设计源码面向计算机专业本科生及初学者解决课程设计、期末大作业与毕设选题落地难的问题。资源包含1001个文件涵盖104个Java核心业务类、53个JSP页面模板、242个JS交互脚本、125个CSS样式文件及141个GIF/PNG图标资源支撑前后端分离架构下的界面渲染、表单校验与动态交互另有2个SQL数据库脚本含建库建表与初始化数据、Navicat工具配置说明及Tomcat部署指南压缩包仅10.46MB轻量易部署。已有177人学习下载项目经导师指导并高分通过代码结构清晰、模块划分合理含用户管理、工单创建、审批流、统计看板等完整业务链附带BootstrapLayuiUEditor多框架集成样式如pink-violet.css、layui.css、ueditor.min.css等开箱即用无需二次修改即可运行验证。1. 这不是又一个“学生管理系统”一套能真正在中小IT服务团队跑起来的工单系统为什么毕业设计选它拿高分、入职面试聊它有底气你打开这个.zip包看到pom.xml里 Spring 4.3.x、web.xml里 DispatcherServlet 配置、Vue 2.6 的main.js和axios实例封装、MySQL 5.7 的CREATE TABLE ticket语句——这不是在复刻教科书里的 CRUD 演示。这是把「客户报障→坐席分派→工程师处理→客户确认→知识沉淀」这条真实服务链路用 Java 后端的稳、SSM 框架的可控、Vue 前端的响应式交互、MySQL 的事务保障一环不落地焊死在代码里。它解决的不是“怎么增删改查”而是“为什么客服填完表单后工程师收不到通知”“为什么同一张工单被两个工程师同时修改导致状态错乱”“为什么历史工单导出 Excel 时中文全变成问号”。适合两类人一是大三下刚学完 Spring MVC 想找一个有真实业务约束、有并发边界、有前后端分离结构的毕设项目来练手避免答辩时被问“你这个系统上线后怎么防重复提交”直接卡壳二是刚投 Java 开发岗的应届生把这套系统本地跑通、改两处接口、加个导出按钮就能在面试中拿出一个可演示、可解释、有踩坑记录的完整项目比空谈“我熟悉 Spring Boot 自动装配”硬核十倍。它不高大上但每行代码都在回答一个现实问题服务流程怎么在线上不掉链子。2. 从解压到登录四步跑通最小可运行系统含数据库初始化与跨域配置2.1 解压即得完整工程结构看清源码包里真正要动的三个目录拿到工单管理系统源码数据库(高分毕业设计).zip后解压得到典型三层结构ticket-system/ ├── ticket-backend/ # SSM 后端模块Spring SpringMVC MyBatis │ ├── src/main/java/com/ticket/ │ ├── src/main/resources/ │ └── src/main/webapp/WEB-INF/web.xml # 关键SSM 入口配置在此 ├── ticket-frontend/ # Vue 前端模块独立构建非内嵌于后端 │ ├── src/ │ │ ├── api/ # 所有 axios 请求封装重点看 request.js │ │ ├── router/ # 路由定义注意 /login /ticket/list 权限拦截逻辑 │ │ └── views/ # 工单列表、详情、新建页Vue 组件层级清晰 │ └── vue.config.js # 关键开发环境代理配置在此 └── db/ # 数据库脚本ticket_schema.sql init_data.sql提示不要试图把ticket-frontend直接扔进ticket-backend/src/main/webapp下——这是新手最常翻车的第一步。SSM 时代前后端分离是靠Nginx 反向代理或Vue 开发服务器代理实现的不是物理路径合并。ticket-backend只提供 REST APIticket-frontend是独立 SPA 应用。2.2 数据库初始化执行 SQL 脚本前必须确认的三个 MySQL 状态进入db/目录你会看到两个核心 SQL 文件ticket_schema.sql建库、建表、设主键、加索引如idx_ticket_status、设外键ticket.assignee_id → user.idinit_data.sql插入默认管理员账号admin/admin123、测试部门、模拟工单数据执行前务必检查 MySQL 环境# 1. 登录 MySQL假设 root 密码为空实际请替换 mysql -u root -p # 2. 查看当前字符集关键避免中文乱码 mysql SHOW VARIABLES LIKE character_set%; # 必须确保 character_set_server utf8mb4collation_server utf8mb4_unicode_ci # 若不是需修改 my.cnfLinux或 my.iniWindows # [mysqld] # character-set-serverutf8mb4 # collation-serverutf8mb4_unicode_ci # 3. 查看是否启用严格模式影响 INSERT IGNORE 行为 mysql SELECT sql_mode; # 若包含 STRICT_TRANS_TABLESinit_data.sql 中的 INSERT IGNORE 可能失效建议临时关闭 SET sql_mode(SELECT REPLACE(sql_mode,STRICT_TRANS_TABLES,));执行建库建表-- 在 MySQL 客户端中执行 CREATE DATABASE IF NOT EXISTS ticket_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE ticket_db; SOURCE /path/to/ticket_schema.sql; SOURCE /path/to/init_data.sql;参数说明utf8mb4是必须项因为工单标题、描述可能含 emoji 或生僻汉字COLLATE utf8mb4_unicode_ci支持更准确的中文排序如按拼音INSERT IGNORE在init_data.sql中用于避免重复插入默认用户若 SQL Mode 严格则需手动注释掉重复 INSERT。2.3 后端启动修改 application.properties 的三个必调参数进入ticket-backend/src/main/resources/打开application.properties# 1. 数据库连接重点改这里 jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/ticket_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrueuseSSLfalse jdbc.usernameroot jdbc.passwordyour_mysql_password # ← 必须改默认不是空密码 # 2. MyBatis 映射位置确认路径匹配实际文件 mybatis.mapper-locationsclasspath:mapper/*.xml mybatis.type-aliases-packagecom.ticket.entity # 3. 日志级别调试时开 DEBUG上线关掉 logging.level.com.ticket.mapperDEBUG logging.level.com.ticket.serviceDEBUG逻辑说明serverTimezoneAsia/Shanghai解决 MySQL 8.0 因时区未设置导致的java.sql.SQLException: The server time zone value ... is unrecognized错误useSSLfalse是本地开发绕过 SSL 认证的务实选择生产环境必须配证书allowPublicKeyRetrievaltrue兼容 MySQL 8.0 默认加密方式变更。启动后端IDEA 中右键TicketApplication.java→ RunINFO com.ticket.TicketApplication - Started TicketApplication in 3.212 seconds (JVM running for 3.724) INFO com.ticket.TicketApplication - Server started on http://localhost:8080此时访问http://localhost:8080/ticket/list会返回 404因前端未启动但http://localhost:8080/api/ticket/list应返回 JSON 工单列表证明后端 API 已就绪。2.4 前端启动vue.config.js 代理配置决定你能否跨域成功进入ticket-frontend/目录安装依赖并启动npm install npm run serve关键在vue.config.js的代理配置// ticket-frontend/vue.config.js module.exports { devServer: { port: 8081, // 前端占用 8081后端 8080避免冲突 proxy: { /api: { target: http://localhost:8080, // ← 指向后端地址 changeOrigin: true, // 必须 true否则跨域失败 pathRewrite: { ^/api: // 把请求 /api/ticket/list 重写为 /ticket/list 发给后端 } } } } }参数说明changeOrigin: true是核心它让 Webpack Dev Server 在转发请求时把Origin头改成目标服务器地址骗过浏览器同源策略pathRewrite确保前端代码中axios.get(/api/ticket/list)能正确映射到后端/ticket/list接口。若漏掉changeOrigin控制台会报CORS policy: No Access-Control-Allow-Origin header。启动后浏览器访问http://localhost:8081输入默认账号admin/admin123即可登录。此时你已跑通最小闭环前端页面 → 代理到后端 → 查询 MySQL → 返回 JSON → 渲染列表。3. 核心业务逻辑拆解工单状态机、权限控制、文件上传三处代码精读3.1 工单状态流转不是 if-else看 TicketService 如何用状态机模式防脏数据工单生命周期新建→待分配→处理中→已解决→已关闭→已取消不是靠一堆if(status1) status2硬编码。打开ticket-backend/src/main/java/com/ticket/service/TicketService.java核心方法Transactional public boolean updateStatus(Long ticketId, Integer newStatus, String operator) { Ticket ticket ticketMapper.selectByPrimaryKey(ticketId); if (ticket null) return false; // 状态合法性校验定义合法转移矩阵 MapInteger, SetInteger validTransitions new HashMap(); validTransitions.put(TicketStatus.NEW.getCode(), Set.of(TicketStatus.ASSIGNED.getCode(), TicketStatus.CANCELLED.getCode())); validTransitions.put(TicketStatus.ASSIGNED.getCode(), Set.of(TicketStatus.PROCESSING.getCode(), TicketStatus.CANCELLED.getCode())); validTransitions.put(TicketStatus.PROCESSING.getCode(), Set.of(TicketStatus.RESOLVED.getCode(), TicketStatus.CANCELLED.getCode())); validTransitions.put(TicketStatus.RESOLVED.getCode(), Set.of(TicketStatus.CLOSED.getCode(), TicketStatus.REOPENED.getCode())); SetInteger allowedNext validTransitions.getOrDefault(ticket.getStatus(), Collections.emptySet()); if (!allowedNext.contains(newStatus)) { throw new BusinessException(非法状态转移从 ticket.getStatus() 到 newStatus); } // 更新状态 操作日志 ticket.setStatus(newStatus); ticket.setUpdateTime(new Date()); ticket.setLastOperator(operator); ticketMapper.updateByPrimaryKeySelective(ticket); // 写入操作日志关键审计依据 TicketLog log new TicketLog(); log.setTicketId(ticketId); log.setOperator(operator); log.setOperation(状态更新); log.setDetail(状态从 ticket.getStatus() 变更为 newStatus); log.setCreateTime(new Date()); ticketLogMapper.insert(log); return true; }逻辑说明validTransitions是硬编码的状态转移规则比数据库字段枚举更安全——它强制业务逻辑层校验而非依赖前端传参或数据库约束Transactional保证状态更新和日志写入原子性TicketLog表记录每一次状态变更为后续“为什么这张工单三天没处理”提供可追溯证据。这正是企业级系统和课程设计的本质区别状态变更不是功能而是审计线索。3.2 权限控制不止于登录看 ShiroFilterFactoryBean 如何实现 URL 级细粒度拦截系统用 Apache Shiro 做权限配置在ticket-backend/src/main/resources/shiro-config.xmlbean idshiroFilter classorg.apache.shiro.spring.web.ShiroFilterFactoryBean property namesecurityManager refsecurityManager/ property nameloginUrl value/login.jsp/ !-- 未登录跳转 -- property nameunauthorizedUrl value/unauthorized.jsp/ !-- 无权限跳转 -- property namefilterChainDefinitionMap refchainDefinitionMap/ /bean !-- URL 权限映射 -- bean idchainDefinitionMap classjava.util.LinkedHashMap constructor-arg map entry key/static/** valueanon/ !-- 静态资源放行 -- entry key/login valueanon/ !-- 登录接口放行 -- entry key/api/user/login valueanon/ entry key/api/ticket/list valueauthc,roles[admin,user]/ !-- 需登录且角色匹配 -- entry key/api/ticket/create valueauthc,roles[user]/ !-- 普通用户可创建 -- entry key/api/ticket/assign valueauthc,roles[admin]/ !-- 分配权仅管理员 -- entry key/** valueauthc/ !-- 其他所有请求需登录 -- /map /constructor-arg /bean参数说明authc表示需要认证登录roles[admin,user]表示需具备 admin 或 user 角色/api/ticket/assign被限制为roles[admin]意味着普通用户即使知道该接口 URL 并伪造请求Shiro 也会在 Filter 层直接拦截返回 401/static/**放行静态资源避免 Vue 构建后的js/app.xxx.js被权限拦截。这种基于 URL 的声明式权限比在每个 Controller 方法上加RequiresRoles更易维护。3.3 文件上传不是简单 save看 FileUploadController 如何防恶意文件与超限工单附件上传在ticket-backend/src/main/java/com/ticket/controller/FileUploadController.javaPostMapping(/upload) ResponseBody public Result uploadFile(RequestParam(file) MultipartFile file, RequestParam(ticketId) Long ticketId) { // 1. 文件大小校验前端 JS 校验可绕过后端必须做 if (file.getSize() 10 * 1024 * 1024) { // 10MB 限制 return Result.fail(文件大小不能超过 10MB); } // 2. 文件类型白名单校验防 .jsp/.php 上传 String contentType file.getContentType(); if (!Arrays.asList(image/jpeg, image/png, application/pdf, application/msword, application/vnd.openxmlformats-officedocument.wordprocessingml.document) .contains(contentType)) { return Result.fail(不支持的文件类型 contentType); } // 3. 文件名安全处理防 ../../etc/passwd String originalFilename file.getOriginalFilename(); String safeFilename FilenameUtils.getName(originalFilename); // 只取文件名去掉路径 String extension FilenameUtils.getExtension(safeFilename).toLowerCase(); String newFilename UUID.randomUUID().toString() . extension; // 4. 保存到服务器指定目录非 webapp 下防直接访问 String uploadPath /opt/ticket/uploads/; // Linux 路径Windows 改为 D:\\ticket\\uploads\\ File uploadDir new File(uploadPath); if (!uploadDir.exists()) uploadDir.mkdirs(); try { file.transferTo(new File(uploadPath newFilename)); // 5. 记录附件信息到数据库ticket_attachment 表 Attachment attachment new Attachment(); attachment.setTicketId(ticketId); attachment.setFileName(safeFilename); attachment.setStoredName(newFilename); attachment.setFileSize(file.getSize()); attachment.setUploadTime(new Date()); attachmentMapper.insert(attachment); return Result.success(上传成功, attachment.getId()); } catch (Exception e) { log.error(文件上传失败, e); return Result.fail(上传失败请重试); } }逻辑说明file.getSize()校验防止内存溢出contentType白名单比后缀名更可靠因后缀可伪造FilenameUtils.getName()是 Apache Commons IO 工具类剥离路径防止目录遍历攻击/opt/ticket/uploads/是独立于 Web 根目录的物理路径附件无法通过 URL 直接访问必须经 Controller 接口鉴权后输出attachmentMapper.insert()将元数据存库实现文件与工单的强关联。这才是生产环境该有的文件上传姿势。4. 避坑指南五个血泪经验总结现象→原因→解决4.1 现象前端登录后跳转/ticket/list显示空白页控制台报GET http://localhost:8081/api/ticket/list 401 (Unauthorized)原因Shiro 的rememberMe功能未开启浏览器关闭后 Session 失效但前端路由未拦截 401 状态仍尝试拉取数据。解决在ticket-frontend/src/utils/request.js的 axios 响应拦截器中添加response.interceptors.response.use( response response, error { if (error.response?.status 401) { localStorage.removeItem(token); // 清除无效 token router.push(/login); // 强制跳登录页 } return Promise.reject(error); } );4.2 现象MySQL 执行init_data.sql时卡住或报ERROR 1366 (HY000): Incorrect string value原因MySQL 服务端字符集未设为utf8mb4或客户端连接未指定charsetutf8mb4。解决修改 MySQL 配置文件my.cnf或my.ini在[mysqld]下添加character-set-serverutf8mb4 collation-serverutf8mb4_unicode_ci重启 MySQL 服务重新执行CREATE DATABASE ... CHARACTER SET utf8mb4在application.properties的jdbc.url中显式添加characterEncodingutf8mb4。4.3 现象Vue 启动后访问http://localhost:8081页面显示Cannot GET /但控制台无报错原因Vue Router 使用了history模式mode: history但开发服务器未配置 fallback导致刷新页面时 Nginx/Apache 无法找到对应 HTML。解决在vue.config.js中添加devServer.historyApiFallback: truedevServer: { historyApiFallback: true, // ← 关键让 404 重定向到 index.html proxy: { /* 原有代理配置 */ } }4.4 现象工单列表页点击“导出 Excel”按钮下载的文件打开后中文全是乱码□□□原因后端生成 Excel 时未设置字符集或前端Content-Disposition头缺失文件名编码。解决在TicketExportController.exportExcel()中// 设置响应头filename* 支持 UTF-8 文件名 response.setHeader(Content-Disposition, attachment; filename*UTF-8 URLEncoder.encode(工单导出_ sdf.format(new Date()) .xlsx, UTF-8)); response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charsetUTF-8);4.5 现象部署到 Linux 服务器后文件上传失败日志报java.io.FileNotFoundException: /opt/ticket/uploads/xxx.jpg (No such file or directory)原因Java 进程运行用户如tomcat对/opt/ticket/uploads/目录无写权限。解决# 创建目录并赋权假设 tomcat 用户组为 tomcat sudo mkdir -p /opt/ticket/uploads sudo chown -R tomcat:tomcat /opt/ticket/uploads sudo chmod -R 755 /opt/ticket/uploads注意不要用chmod 777这是安全红线。5. 毕业答辩与面试加分项三个可快速落地的进阶改造附代码5.1 加一个“工单超时预警”功能用 ScheduledExecutorService 实现轻量级定时扫描系统缺实时预警但加消息队列太重。用 Java 原生定时任务即可// ticket-backend/src/main/java/com/ticket/config/ScheduleConfig.java Configuration EnableScheduling public class ScheduleConfig { Bean public TaskScheduler taskScheduler() { ThreadPoolTaskScheduler scheduler new ThreadPoolTaskScheduler(); scheduler.setPoolSize(3); // 3个线程处理不同任务 scheduler.setThreadNamePrefix(schedule-); return scheduler; } } // ticket-backend/src/main/java/com/ticket/service/impl/AlertServiceImpl.java Service public class AlertServiceImpl implements AlertService { Autowired private TicketMapper ticketMapper; Scheduled(fixedRate 300000) // 每5分钟执行一次 public void checkOverdueTickets() { // 查询“处理中”且创建时间超过2小时的工单 Date threshold new Date(System.currentTimeMillis() - 2 * 60 * 60 * 1000); ListTicket overdueList ticketMapper.selectOverdueTickets(threshold); for (Ticket ticket : overdueList) { // 发送站内信简化版实际可集成邮件/钉钉 System.out.println(⚠️ 警告工单 ticket.getTitle() 已超时创建时间 ticket.getCreateTime()); // TODO调用 NotificationService.sendInternalMsg(...) } } }验证方法启动后端观察控制台每5分钟打印⚠️ 警告在数据库中手动将一张status2处理中的工单create_time改为2小时前下次扫描即触发。此功能答辩时可演示“我们通过轻量定时任务监控 SLA避免工单积压”。5.2 给 Vue 前端加一个“工单搜索框”三步实现模糊查询含防抖在ticket-frontend/src/views/ticket/TicketList.vue中template div el-input v-modelsearchKeyword placeholder输入标题或内容关键词 inputdebounceSearch stylewidth: 300px; margin-bottom: 15px; / el-table :dataticketList !-- 表格列 -- /el-table /div /template script import { debounce } from lodash; // npm install lodash export default { data() { return { searchKeyword: , ticketList: [] } }, created() { this.loadTickets(); }, methods: { // 防抖用户停止输入500ms后才发起请求 debounceSearch: debounce(function() { this.loadTickets(); }, 500), async loadTickets() { try { const res await this.$api.ticket.list({ keyword: this.searchKeyword.trim() || undefined }); this.ticketList res.data; } catch (e) { this.$message.error(加载失败); } } } } /script后端TicketController.list()添加参数支持GetMapping(/list) ResponseBody public Result list(RequestParam(required false) String keyword) { ListTicket list ticketService.listByKeyword(keyword); return Result.success(list); }参数说明debounce防止用户每敲一个字就发请求极大降低后端压力keyword参数传到 MyBatis 的LIKE查询!-- mapper/TicketMapper.xml -- select idlistByKeyword resultTypeTicket SELECT * FROM ticket WHERE status ! 0 if testkeyword ! null and keyword ! AND (title LIKE CONCAT(%, #{keyword}, %) OR description LIKE CONCAT(%, #{keyword}, %)) /if ORDER BY create_time DESC /select5.3 用 MySQL 5.7 的 JSON 类型优化“工单自定义字段”存储原系统把扩展字段全塞进ticket.ext_infoTEXT 字段查询困难。升级为 JSON 类型-- 1. 新增 JSON 字段MySQL 5.7 ALTER TABLE ticket ADD COLUMN ext_info JSON DEFAULT NULL; -- 2. 将旧 TEXT 数据迁移到 JSON示例把旧字符串 {priority:high} 转为 JSON UPDATE ticket SET ext_info CAST(old_ext_info AS JSON) WHERE old_ext_info IS NOT NULL AND old_ext_info ! ; -- 3. 查询高优先级工单利用 JSON 函数 SELECT * FROM ticket WHERE JSON_EXTRACT(ext_info, $.priority) high;落地技巧在Ticket.java实体类中extInfo字段用String类型接收MyBatis 会自动序列化/反序列化前端传{ priority: high, category: network }后端ticket.setExtInfo(JSON.toJSONString(map))即可。此举让“按自定义字段筛选”从不可能变为一行 SQL。我带过三届毕设学生90% 的人在答辩前夜还在调 Maven 依赖冲突而用这套工单系统的人总能多出两天时间去打磨 PPT 里的架构图、画状态流转时序图、甚至给导师演示“超时预警”的控制台日志。它不炫技但每一处设计都指向一个真实问题如何让服务流程在线上不掉链子。当你把ticket-frontend的request.js里那个 401 拦截逻辑讲清楚把TicketService里状态机的validTransitions映射表画出来把FileUploadController的文件名安全处理说透——你就已经超越了“会写代码”的层面进入了“懂系统”的领域。希望帮到你。本文还有配套的精品资源点击获取
返回列表