
毕业设计选城市公交查询系统说句实在话是Java方向里性价比很高的一个题目。它不是那种烂大街的图书管理、学生管理系统带了一点信息展示和查询优化的复杂度又比电商、秒杀这类项目简单不少非常适合用来做毕设或者课设。整个系统的业务逻辑够清晰技术栈又能覆盖SpringBoot、Vue、MySQL、MyBatis这些主流技能点不管是写论文还是答辩演示都拿得出手。这篇文章我会完整拆解这个系统的设计和实现过程从前端页面到后端接口从数据库表设计到联调部署把一个可运行、能演示、禁得住答辩追问的完整项目讲清楚。这当中还包括我在实际开发中踩过的坑、解决过的典型报错以及毕业答辩时老师最爱问的连环追问该怎么接住。1. 为什么选这个系统做毕设先看清需求边界和技术难点做毕设最忌讳的事情就是一上来就写代码。你先把你要做的系统的边界画清楚哪些功能必须有哪些功能可以没有心里要有数。这样后续开发才不会被各种不切实际的想法带偏。1.1 教师视角下的毕设题目考察点毕设答辩的时候老师在意的不是你实现了多少花哨的功能而是这三点需求分析是否到位、技术选型是否合理、系统能否真正跑起来。城市公交查询系统天然适配这三点需求非常明确谁都会用公交查询理解零门槛讲解起来不费力涉及BS架构、前后端分离、数据库设计、接口编写、数据图表展示等常见企业级开发要素扩展性强加入站点规划、线路推荐等算法也不算突兀有余力可以做加分项换句话说这个题目的考察点覆盖了JavaWeb全链路但又不会像电商秒杀系统那样把并发、缓存、消息队列全部堆上来。难度梯度合理这就是它适合做毕设的根本原因。1.2 B/S架构选型的真正逻辑题目里点出了keywordsbs架构。B/S架构其实不需要花太多篇幅去长篇大论理解成所有功能都通过浏览器访问服务器负责核心业务处理和数据存储就够了。选B/S架构的考量在于用户不需要安装任何客户端打开浏览器输入地址就能用这对公交车查询这种面向公众的场景非常友好系统升级只需要改服务器端代码客户端永远拿到的是最新版本不需要像C/S架构那样逐台机器更新基于SpringBoot内置的Tomcat打包成一个jar就能跑部署成本极低在论文里描述架构选型时不要只是说本系统采用B/S架构就结束要加上对比逻辑C/S适合专用高频率操作场景B/S适合公共查询和信息发布场景而公交查询系统恰恰属于后者。这个对比分析写进论文的需求分析或技术选型章节是很标准的写法。1.3 核心功能模块的边界划分我最后敲定的功能结构是这样的你可以根据自己的安排增删功能模块子功能说明用户模块注册、登录、个人信息修改区分普通用户和管理员角色公交线路查询按线路查询、按站点查询、换乘查询核心业务演示时优先展示线路收藏常用线路收藏与取消提升功能完整性公告信息系统公告列表、详情管理员可发布后台管理线路管理、站点管理、车辆管理、用户管理仅管理员角色可见可操作数据可视化线路数量、站点数量的统计图表加分项答辩效果很好对于毕设来说模块不是越多越好而是要让每个模块之间逻辑关联紧密。比如用户收藏线路这个功能一方面涉及用户表、线路表、收藏表的关联查询另一方面也能在答辩时说本系统具备简单的个性化服务能力这就为论文的系统功能创新点提供了支撑。1.4 技术栈版本选择和理由我用的是这套组合JDK 1.8严格说其实叫JDK 8 SpringBoot 2.7.x MyBatis-Plus 3.5.x MySQL 5.7 / 8.0均可 Vue 2.6 Vue Router Vuex Element-UI 2.15 Axios Maven 3.6 IDEA VSCode这里我想重点提醒一下技术选型的两个坑关于SpringBoot版本不要用3.x虽然3.x已经出了很久了但是很多教程、资料、插件还停留在2.x生态而且3.x强制要求JDK17如果你机器上装的是JDK8很多老旧的JDK配置教程会直接失效。毕设阶段求稳为主选中2.7.x这个版本网上随便搜都能找到对应的配置解决方案。关于JDK版本绝大多数学校机房和老师的机器上最稳妥的环境就是JDK8。你答辩演示的时候如果环境有问题那是灾难性的。所以不要追求新要追求稳。2. 数据库表设计业务落地的第一块基石很多同学数据库表设计特别潦草随便建两张表就开始写代码写着写着发现关联查不出来或者字段不够用又回头改表结构浪费时间不说还容易憋出bug。这部分的功夫做到了后面的开发效率会成倍提升。2.1 核心数据表规划按照系统的功能边界我设计了这几张表。你可以在Navicat或者命令行里直接执行-- 用户表 CREATE TABLE sys_user ( id bigint(20) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 登录用户名, password varchar(100) NOT NULL COMMENT 密码存入的是BCrypt加密后的密文, nickname varchar(50) DEFAULT NULL COMMENT 昵称, phone varchar(20) DEFAULT NULL COMMENT 手机号, role varchar(20) DEFAULT USER COMMENT 角色ADMIN/USER, 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; -- 公交线路表 CREATE TABLE bus_line ( id bigint(20) NOT NULL AUTO_INCREMENT, line_name varchar(50) NOT NULL COMMENT 线路名称如1路、K2路, start_station varchar(100) DEFAULT NULL COMMENT 起始站, end_station varchar(100) DEFAULT NULL COMMENT 终点站, first_bus_time varchar(10) DEFAULT NULL COMMENT 首班车时间如06:00, last_bus_time varchar(10) DEFAULT NULL COMMENT 末班车时间, price decimal(4,1) DEFAULT 2.0 COMMENT 票价, mileage decimal(6,1) DEFAULT NULL COMMENT 里程公里, line_type tinyint(1) DEFAULT 1 COMMENT 线路类型1普通 2快速公交 3旅游专线, status tinyint(1) DEFAULT 1 COMMENT 状态1运营中 0已停运, remark varchar(500) DEFAULT NULL COMMENT 备注, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT公交线路表; -- 站点表 CREATE TABLE bus_station ( id bigint(20) NOT NULL AUTO_INCREMENT, line_id bigint(20) NOT NULL COMMENT 所属线路ID, station_name varchar(100) NOT NULL COMMENT 站点名称, sort_no int(11) NOT NULL COMMENT 站点在线路中的顺序从1开始, latitude decimal(10,6) DEFAULT NULL COMMENT 纬度, longitude decimal(10,6) DEFAULT NULL COMMENT 经度, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT站点表线路-站点 一对多;这里有个设计要点站点信息和线路信息是分开的两张表原因是一条线路会经过多个站点一个站点也会被多条线路经过它们是多对多关系。我在这里选择把线路-站点关联表和站点本身的信息表合并成了bus_station这张表每一条记录代表某条线路上的某个站点通过line_id和sort_no来唯一确定顺序。这种方式在公交系统里叫顺序表结构对于毕设来说逻辑清晰且查询方便。此外还需要两张表-- 线路收藏表 CREATE TABLE bus_favorite ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id bigint(20) NOT NULL, line_id bigint(20) NOT NULL, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_user_line (user_id,line_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT线路收藏表; -- 公告表 CREATE TABLE bus_notice ( id bigint(20) NOT NULL AUTO_INCREMENT, title varchar(100) NOT NULL, content text, publisher varchar(50) DEFAULT NULL, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT公告通知表;2.2 给管理员的初始化数据写代码测试之前一定要先造几组真实感强的数据。数据质量直接决定了演示效果——你演示的时候线路表里只有3行数据点开一看全是测试1、测试2答辩老师对系统的第一印象就没了。我当时造数据时用的城市是本市的真实公交线路格式比如1路火车站 → 汽车客运中心 → 人民公园 → 市政府 → 大学城全程12.5公里票价2元首班06:00末班21:30旅游1号线高铁站 → 古城墙遗址 → 博物馆 → 湖心公园全程15.8公里票价3元首班08:00末班18:00数据要体现出多样性普通线路、快速线路、旅游专线各准备几条覆盖工作日高峰和平峰时间段这样答辩演示换乘查询的时候才有说服力。2.3 密码加密与角色权限的设计思路用户密码我在项目里用的是BCrypt加密把密文存进数据库。这里给准备做毕设的同学一个重要的建议不要在论文里写密码以明文形式存储这是安全常识问题写上去容易被老师点名批评。用SpringSecurity自带的BCryptPasswordEncoder做加密总共就几行代码既安全又能作为论文里的一个技术亮点。角色权限我用了最简方案sys_user表里加一个role字段USER或者ADMIN。前端根据登录用户角色动态显示或隐藏菜单后端用SpringBoot的拦截器校验请求中携带的token所对应的用户角色。没上SpringSecurity那套完整的过滤链因为毕设阶段没必要把复杂度堆那么高但是你必须能解释清楚为什么不用SpringSecurity标准答法是为降低系统复杂度只涉及两个角色自定义拦截器足以满足需求。2.4 为什么加站点坐标字段设计bus_station表时我额外加了latitude和longitude两个字段。这里涉及一个功能扩展性问题如果后期想做线路走线地图或者附近站点查询就必须要有坐标数据。答辩时老师如果问你系统里站点信息能不能在地图上展示你至少可以说数据层已经预留了经纬度字段前端可以使用Leaflet或高德地图API做展示这是一个可扩展点。这个回答会显得你有全局设计思维而不是只为了交差。3. 后端代码结构SpringBoot分层的落地实践后端写出来的代码要有清晰的层次结构这是毕设代码评分的重要标准之一。很多同学把代码全写在Controller里几百行一个方法整个项目没法维护。分层设计在这个系统里我认为主要做好controller、service、mapper这三大块职责分离。3.1 后端项目包结构我的后端工程目录长这样com.example.bus ├── BusApplication.java // 启动类 ├── common │ ├── Result.java // 统一返回结果包装类 │ ├── ResultCode.java // 返回码枚举 │ ├── BusinessException.java // 自定义业务异常 │ └── GlobalExceptionHandler.java // 全局异常处理器 ├── config │ ├── CorsConfig.java // 跨域配置 │ ├── MybatisPlusConfig.java // MyBatis-Plus分页插件配置 │ └── WebMvcConfig.java // 拦截器注册 ├── interceptor │ └── AuthInterceptor.java // 登录权限拦截器 ├── controller │ ├── AuthController.java // 登录、注册接口 │ ├── LineController.java // 线路查询接口 │ ├── StationController.java // 站点查询接口 │ ├── FavoriteController.java // 收藏接口 │ └── AdminController.java // 后台管理接口 ├── service │ ├── LineService.java │ ├── impl/LineServiceImpl.java │ └── ... ├── mapper │ ├── LineMapper.java │ ├── StationMapper.java │ └── ... ├── entity │ ├── SysUser.java │ ├── BusLine.java │ ├── BusStation.java │ └── ... └── dto / vo ├── LoginDTO.java ├── LineQueryDTO.java ├── LineVO.java // 包含站点列表的线路视图对象 └── ...这套结构在论文的系统设计章节里很好描述表现层controller接收请求和参数校验业务层service处理具体业务逻辑数据访问层mapper与数据库交互。用了MyBatis-Plus之后很多单表CRUD不需要自己写SQL但多表查询仍然要自己编写XML文件中的SQL语句。3.2 统一返回结果类所有接口的响应规范后端和前端交互的第一件事是把返回结构定下来。我自定义了一个Result类所有接口统一返回这个格式Data public class ResultT { private Integer code; // 200表示成功500表示失败401表示未登录 private String message; // 提示信息 private T data; // 实际返回的数据 public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(操作成功); result.setData(data); return result; } public static T ResultT error(String msg) { ResultT result new Result(); result.setCode(500); result.setMessage(msg); return result; } public static T ResultT unauthorized(String msg) { ResultT result new Result(); result.setCode(401); result.setMessage(msg); return result; } }这个类的意义在于不管查询成功还是失败前端axios都能统一处理响应体。配合Axios的响应拦截器当code为401时自动跳转到登录页当code为500时弹出错误提示。这套设计是毕设项目中非常体现工程素养的一个点很多速成班出来的学生根本没这个意识你在答辩时主动提一句所有接口通过统一返回对象约束响应格式老师的印象分会不一样。3.3 核心查询接口的实现细节线路查询是这个系统的核心。我设计了三个主要查询维度按线路名查、按站点名查、按换乘查。其中换乘查询是难点也是亮点这里重点讲。按线路名查询的逻辑相对简单Override public LineVO getLineDetail(Long lineId) { BusLine line lineMapper.selectById(lineId); if (line null) { throw new BusinessException(线路不存在); } QueryWrapperBusStation wrapper new QueryWrapper(); wrapper.eq(line_id, lineId).orderByAsc(sort_no); ListBusStation stations stationMapper.selectList(wrapper); // 组装成视图对象返回 LineVO vo new LineVO(); BeanUtils.copyProperties(line, vo); vo.setStations(stations); return vo; }按站点名查询线路就需要多表联查在StationDao.xml中写SQLselect idfindLinesByStationName resultTypecom.example.bus.vo.LineVO SELECT DISTINCT l.id, l.line_name, l.start_station, l.end_station, l.first_bus_time, l.last_bus_time, l.price FROM bus_line l INNER JOIN bus_station s ON l.id s.line_id WHERE s.station_name LIKE CONCAT(%, #{stationName}, %) AND l.status 1 /select换乘查询是整个系统含金量最高的功能。我用了最简单有效的一次换乘算法查询从站点A到站点B先找出所有经过A站的线路集合linesA再找出所有经过B站的线路集合linesB两个集合取交集如果交集非空说明可以直达否则遍历linesA中的所有线路对每条线路的每个站点C判断是否有线路同时经过C站和B站如果有就形成乘坐线路X从A到C换乘线路Y从C到B的方案。这个思路还原了现实中最多换乘一次的逻辑代码实现也不复杂核心就是把集合操作用好。答辩时这段代码的价值极大因为它涉及了图的广度遍历思想面试官或答辩老师会认为你有算法基础不是纯调包选手。3.4 基于JWT的登录认证实现前端登录后后端发一个token给前端前端存到localStorage里。之后每次请求都在Header里带上token后端拦截器解析token确认用户身份。核心代码就三块生成token、拦截器校验、获取当前登录用户。// 生成token的工具类使用io.jsonwebtoken的Jwts public static String generateToken(Long userId, String role) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim(role, role) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() 24 * 60 * 60 * 1000)) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); } // 拦截器中的核心校验逻辑 Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录注册接口 if (request.getRequestURI().contains(/auth/login) || request.getRequestURI().contains(/auth/register)) { return true; } String token request.getHeader(Authorization); if (StringUtils.isBlank(token)) { response.setStatus(401); response.getWriter().write({\code\:401,\message\:\未登录或登录已过期\,\data\:null}); return false; } try { Claims claims Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token.replace(Bearer , )).getBody(); // 从token中解析出userId存入request域中方便后续接口使用 request.setAttribute(userId, Long.valueOf(claims.getSubject())); request.setAttribute(role, claims.get(role)); return true; } catch (JwtException e) { response.setStatus(401); response.getWriter().write({\code\:401,\message\:\token无效或已过期\,\data\:null}); return false; } }这段代码在演示时非常直观用户不登录直接访问收藏接口系统返回401提示登录后再访问一切正常。这个过程中先拦截再校验再放行的处理思路也值得写进论文。3.5 跨域问题前后端分离的第一个大坑前后端分离开发时前端跑在localhost:8080后端跑在localhost:9090两个端口不同就会触发浏览器的跨域限制。你在Chrome的F12控制台会看到经典的报错Access to XMLHttpRequest at http://localhost:9090/api/user/info from origin http://localhost:8080 has been blocked by CORS policy解决方法多种多样最常用的是后端加上跨域配置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); } }这里有个细节如果你同时使用了拦截器和跨域配置预检请求OPTIONS请求会被拦截器拦下来返回401导致实际请求发不出去。解决办法是在拦截器里放行OPTIONS请求if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; }这个坑我印象太深了第一次联调的时候卡了整整一个下午。你在配置拦截器时一定记得把OPTIONS请求放行。写进论文里就是一句系统在跨域配置时需额外放行预检请求否则会导致前端无法正确获取接口数据显得你实操经验丰富。4. 前端实现Vue 2 Element-UI怎么把页面做得干净好用前端这块很多同学习惯用现成的后台管理模板比如vue-element-admin然后改改就完事了。我的建议是模板可以借鉴但页面结构你自己要能说清楚。答辩时老师如果指着页面问你这个侧边栏是怎么渲染的路由是怎么配置的你答不上来就很尴尬。4.1 前端项目创建与目录结构用Vue CLI创建项目后我做了以下目录规划src ├── api // 接口请求模块 │ ├── auth.js // 登录注册接口 │ ├── line.js // 线路查询接口 │ ├── favorite.js // 收藏相关接口 │ └── admin.js // 后台管理接口 ├── assets // 静态资源 ├── components // 公共组件 │ ├── HeaderBar.vue // 顶部导航栏 │ └── LineCard.vue // 线路信息卡片组件 ├── router // 路由配置 │ └── index.js ├── store // Vuex状态管理 │ └── modules/user.js ├── utils │ └── request.js // axios封装 ├── views // 页面组件 │ ├── Login.vue │ ├── Register.vue │ ├── home │ │ ├── Home.vue // 系统首页公告快捷查询 │ │ ├── LineSearch.vue // 线路查询页 │ │ ├── StationSearch.vue // 站点查询页 │ │ ├── TransferQuery.vue // 换乘查询页 │ │ ├── Favorite.vue // 我的收藏 │ │ └── Profile.vue // 个人中心 │ └── admin │ ├── Dashboard.vue // 后台数据概览含图表 │ ├── LineManage.vue // 线路管理增删改查 │ ├── StationManage.vue // 站点管理 │ ├── NoticeManage.vue // 公告管理 │ └── UserManage.vue // 用户管理这里有一个隐性的架构设计问题哪些页面放views/home哪些页面放views/admin。这其实就是路由权限控制的体现。前端的路由分为公共路由和管理员路由管理员页面是动态添加的登录时根据用户角色判断是否需要注册这些路由。这一点可以在答辩时说本质上是一种前端路由级权限控制。4.2 axios封装统一处理token和响应码所有前端请求都走封装好的request.js不要直接在每个组件里裸写axios调用。严格说这是为了避免重复代码也是工程化的基本要求。// utils/request.js import axios from axios import { Message } from element-ui import router from /router const service axios.create({ baseURL: /api, // 通过Vue CLI的代理转发到后端避免在代码里写死后端地址 timeout: 10000 }) // 请求拦截器自动携带token service.interceptors.request.use(config { const token sessionStorage.getItem(token) if (token) { config.headers[Authorization] Bearer token } return config }, error { return Promise.reject(error) }) // 响应拦截器统一处理业务状态码 service.interceptors.response.use(response { const res response.data if (res.code 200) { return res } else if (res.code 401) { sessionStorage.removeItem(token) router.push(/login) Message.error(登录已过期请重新登录) } else { Message.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } }, error { Message.error(网络异常请稍后重试) return Promise.reject(error) }) export default service与此同时在vue.config.js里配置开发代理module.exports { devServer: { port: 8080, proxy: { /api: { target: http://localhost:9090, changeOrigin: true // 不需要重写路径后端接口正好以/api开头 } } } }这个代理配置解决了一个非常要命的问题如果前端直接请求http://localhost:9090/api/xxx浏览器会产生跨域但用代理后前端请求的是http://localhost:8080/api/xxx同源浏览器不拦截代理服务器再去请求后端9090端口。开发环境用代理生产环境用Nginx反向代理这是一个标准的全链路方案。4.3 路由权限与菜单渲染路由配置使用Vue Router这里给出一个精简的核心配置思路const routes [ { path: /login, component: Login, meta: { public: true } }, { path: /register, component: Register, meta: { public: true } }, { path: /, component: Layout, redirect: /home, children: [ { path: home, component: Home, meta: { title: 首页 } }, { path: lineSearch, component: LineSearch, meta: { title: 线路查询 } }, { path: stationSearch, component: StationSearch, meta: { title: 站点查询 } }, { path: transferQuery, component: TransferQuery, meta: { title: 换乘查询 } }, { path: favorite, component: Favorite, meta: { title: 我的收藏, auth: true } }, ] }, { path: /admin, component: Layout, meta: { role: ADMIN }, children: [ { path: dashboard, component: Dashboard }, { path: lineManage, component: LineManage }, // ... ] } ] // 全局前置守卫 router.beforeEach((to, from, next) { if (to.meta.public) { next() return } const token sessionStorage.getItem(token) if (!token) { next(/login) return } // 角色校验 const role sessionStorage.getItem(role) if (to.meta.role to.meta.role ! role) { next(/home) return } next() })这里demo里有一个隐含的操作细节后端返回登录成功时把token和role都存在了sessionStorage里。当用户访问/admin下的页面时前置守卫发现角色不是ADMIN就跳回首页。这套机制属于前端路由守卫后端接口拦截双保险前端保证不显示页面后端保证即使直接调接口也拿不到数据。4.4 线路查询和换乘查询的页面交互设计线路查询页我用的是比较传统的布局顶部是查询区域输入关键字或线路名点按钮下面是表格或卡片列表展示查询结果。点击某条线路后展开该线路经过的所有站点并用Element-UI的Timeline时间轴组件来展示站点的先后顺序。这个展示方式特别直观答辩时老师一眼就能看出这个线路经过了哪些站。换乘查询页稍微复杂一点用户选择或输入起点站和终点站后端返回以下几种结果之一直达方案、一次换乘方案、暂未找到方案。直达方案直接用卡片展示从起点站上车乘坐X路在终点站下车全程X站。一次换乘方案用两步卡片展示起点站A → 乘坐1路经过5站→ 换乘站C → 改乘K2路经过3站→ 终点站B为了让页面更好看我还在换乘查询结果中显示了估算时间和票价预计时间约35分钟按每站3分钟估算 总票价4元1路2元 K2路2元这个每站3分钟票价累加的逻辑虽然简单但结合UI展示效果非常好。答辩的时候可以说我们模拟了真实换乘场景下的时间和费用预估。4.5 后台管理页面的实现要点后台管理部分核心就是四个列表页线路管理、站点管理、公告管理、用户管理。每个管理页都是标准的表格增删改查套路。这里我用一个例子讲一下线路管理页的关键代码思路围绕它们用Element-UI的el-table、el-dialog、el-form三件套el-table展示线路列表操作列放编辑删除按钮点击新增按钮弹出el-dialog里面是el-form表单填线路名称、起始站、终点站、首末班时间、票价等信息提交时调用后端接口成功后刷新表格删除时弹出确认框this.$confirm确认后调删除接口后台管理页面特别容易出问题的是站点和线路的关联在线路管理里要能维护这条线路下的所有站点顺序。我用了一个PageDialog的方式点击站点管理按钮打开线路的站点列表里面可以添加站点名称并调整sort_no顺序。这块对前端基本功要求不低主要是表格增删改查的代码量大逻辑也不算难跟着Element-UI的文档把几个常用组件的用法跑一遍就通了。4.6 数据可视化图表的引入为了让系统看起来更有含金量我在管理员首页增加了一个数据可视化的区域展示线路类型分布、各线路站点数量等统计信息。用的是ECharts的Vue封装vue-echarts。实现思路特别简单后端写一个统计接口比如SELECT line_type, COUNT(*) AS cnt FROM bus_line GROUP BY line_type返回一个Map的List前端用ECharts的PieChart渲染成饼图。核心代码如下this.chart echarts.init(this.$refs.chartRef) this.chart.setOption({ title: { text: 公交线路类型分布 }, tooltip: {}, series: [{ type: pie, data: this.chartData }] })这块在答辩演示时的视觉效果比任何文字说明都强。老师看到你系统里有图表统计直观感受就是这个学生做的系统比较完整。5. 核心难点的排查与处理那些你一定会遇到的报错毕设开发最大的拦路虎不是写功能而是报错不会排查。这里我把项目开发过程中最容易遇到、几乎所有人都躲不过的问题整理出来每一条都是我用调试时间换来的经验。5.1 MyBatis-Plus分页查询不生效很多同学用MyBatis-Plus分页时发现Page对象返回的records永远是所有数据分页没起作用。原因很可能是没有配置分页插件。MyBatis-Plus的分页插件默认不生效需要显式声明Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }这个问题在答辩前没有暴露出来的话后台管理的表格会一直是全量数据一旦数据量大了页面就会卡死。装好分页插件后每页只取10条体验完全不同。5.2 日期时间字段的JSON格式化问题SpringBoot默认的Jackson序列化对Java的LocalDateTime类型默认序列化出来是一长串数字时间戳前端拿到后没法直接显示。解决方式可以是在application.yml里配全局格式化spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8但这里有个坑这个配置只对java.util.Date生效对LocalDateTime不生效。如果你数据库的时间字段用了LocalDateTime还是乖乖地在字段上加注解JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8) private LocalDateTime createTime;这个细节很多教程不会提我是在前端发现时间显示成数字时排查了半天才解决的。5.3 前端请求404接口路径对不上前后端联调时最常见的报错是404。排查链路是这样的看后端控制台有没有收到请求没有收到说明路径或代理有问题看浏览器Network里实际请求的URL是什么比对后端RequestMapping注解的路径确认后端项目有没有配置context-path配置了的话你访问路径会多一层前缀比如后端接口是RequestMapping(/api/line)前端axios的baseURL是/api那请求地址应该是/api/line/list而不是/line/list。这种错误多出现几次慢慢就会形成肌肉记忆但第一次碰到时真的能卡很久。5.4 Vue打包后刷新404用Vue Router的History模式时本地开发一切正常一打包部署到服务器刷新某个二级页面就404。原因是Nginx没有做try_files配置把请求直接按文件路径找找不到就返回404。在Nginx里加上server { listen 80; server_name localhost; location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } # 后端接口反向代理 location /api/ { proxy_pass http://127.0.0.1:9090; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }部署时把Vue打包生成的dist目录里的文件放到Nginx的html目录下把后端SpringBoot的jar启动起来整个系统就能通过Nginx对外提供服务了。这是毕设演示的最终形态一个前端地址80端口一个后端服务9090端口非常清晰。5.5 数据库datetime默认值用Navicat图形化建表给create_time字段设置了默认值CURRENT_TIMESTAMP但MySQL有可能报错或者插入时默认值不生效。这个问题的本质是MySQL版本或表引擎的问题。可以试试建表时直接写SQL语句来创建表结构不要在图形化界面里点来点去避免一些坑。6. 接口测试与演示答辩前必须准备好的流程答辩演示是十分钟到十五分钟的完整走查你需要把整个系统从前到后串联起来让老师看到一个完整的业务闭环。这里我拆解一下我的演示脚本。6.1 用Swagger/接口文档自测为了让后端接口自测和答辩展示更专业我引入了springdoc-openapiSpringBoot 2.x对应的版本是springdoc-openapi-ui。导入依赖后访问http://localhost:9090/swagger-ui.html就能看到所有接口的文档页面。每写一个接口顺手在Swagger里测试一遍确保参数能打通。这个工具在答辩现场的作用当老师问你后端有哪些接口你打开Swagger页面所有接口一目了然当场测一个查询接口参数填进去就有数据返回这比光放PPT讲接口列表有说服力得多。6.2 演示前要准备好的账号和数据一个合格的演示必须包括两套账号权限的演示账号角色演示内容user / 123456普通用户线路查询、站点查询、换乘查询、收藏线路admin / 123456管理员登录后看到后台管理菜单进行线路增删改查演示顺序我建议这样安排先不登录直接使用线路查询、站点查询、换乘查询说明系统支持游客查询公交查询系统的核心是公开信息查询注册新账号或登录user账号登录后进行收藏操作退出登录用admin账号登录进入后台管理演示线路新增一条、修改票价、删除一条测试数据回到前台刷新线路列表看到刚才管理员做的修改已经生效说明前后台数据实时联动这套流程大约8-10分钟走完逻辑衔接严密老师跟着你的思路走不会觉得乱。6.3 演示前容易翻车的几个小点确保MySQL服务已启动命令行里能正常连接。演示时数据库连不上是最尴尬的场景没有之一。确保后端端口没有被占用。如果9090被占启动会失败。稳妥的推荐做法是写一个启动脚本一下全搞定。确保前端页面用无痕窗口打开。避免登录态缓存影响演示流程。准备一份测试数据脚本。如果演示到一半数据被误删了你可以快速重置数据库。这些点平时开发时总觉得不可能出问题但到了答辩现场紧张状态下什么都可能发生。提前演练两遍把所有能准备的都准备到位是唯一正确的策略。7. 关于权限拦截和异常处理的深入答辩追问的应对思路答辩几乎必问的几个技术问题如果你理解不透很容易当场卡壳。这一章我直接把可能出现的问题和思路整理出来供你参考。7.1 你这个系统安全性怎么样这个问题的回答思路其实就在项目里已经实现了四层保障第一层前端路由守卫控制页面访问权限第二层后端拦截器校验token第三层接口数据层面收藏查询接口会校验收藏记录属于当前登录用户第四层密码加密存储数据库泄露也不会直接泄露明文密码。如果老师追问token过期了怎么办你可以答系统设置了24小时有效期过期后前端会收到401并自动跳转登录页用户重新登录后获取新token这是一个标准的会话管理机制。如果老师追问token被别人窃取了怎么办这是一个进阶问题你只需要答出核心思路即可所有敏感操作管理员的增删改都会经过拦截器校验角色权限业务层还会再校验一次即使token泄露也只会影响该用户自己的数据。HTTPS也可以防止传输过程中的明文泄露这部分在企业生产中是标配。7.2 线路查询高峰期并发访问怎么办正常答法系统本身是查询为主的系统读多写少。MySQL本身可以支撑一定量级的并发查询如果需要进一步提升可以在热点线路的查询接口加一层Redis缓存缓存线路信息设置合理的过期时间减少数据库压力。如果老师接着问你怎么设计缓存你可以答// 先查缓存缓存没有再去查数据库 String key bus:line:detail: lineId; String json redisTemplate.opsForValue().get(key); if (StringUtils.isNotBlank(json)) { return JSON.parseObject(json, LineVO.class); } LineVO vo getLineDetailFromDb(lineId); redisTemplate.opsForValue().set(key, JSON.toJSONString(vo), 30, TimeUnit.MINUTES); return vo;30分钟过期怎么定的公交线路和站点信息属于低频变化数据短期缓存不会出现明显的数据不一致。这就是一个基于数据特性设计缓存策略的回答思路能体现你对业务的理解。7.3 如果用户从A站到B站需要换乘两次以上你的系统怎么处理我的系统目前只支持一次换乘也就是直达或最多换乘一次因为现实中绝大多数公交出行一次换乘已经覆盖大部分场景。如果要扩展到两次换乘思路是把一次换乘产生的每个换乘站点C继续作为起点再递归做一次换乘查询直到找到B站或达到换乘次数上限。这其实就是BFS广度优先搜索的层数上限控制。这个目前不做但有清晰的扩展方案的答复在答辩里是非常稳妥的。7.4 为什么用MyBatis-Plus而不是JPA适合JavaWeb课设和毕设的项目MyBatis-Plus使用更广泛学习资料多出现问题容易排查。从技术对比上JPA对复杂SQL支持比较吃力而MyBatis-Plus兼容MyBatis的XML写法复杂查询写SQL更直观代码可控性强。对这个系统来说大量的多表关联查询、条件动态SQL用MyBatis-Plus或XML更合适。8. 写在最后给做毕设的同学的操作建议整个项目从零到跑通从前面的章节你就可以看到涉及到的东西其实非常多数据库设计、后端接口、前端页面、联调、部署、答辩准备。在学校里能独立做完这套流程你的JavaWeb水平会有一次很明显的提升。如果你现在准备开始做我的建议是拆好节奏第1周把需求分析和数据库表设计做好画出用例图、E-R图这是论文的重要素材。第2周搭好前后端骨架实现登录注册和线路查询功能打通联调流程。第3周完成所有核心功能重点攻关换乘查询同步开始写论文的详设章节。第4周做后台管理和数据可视化整理测试用例准备答辩PPT。开发过程中遇到问题优先用控制台看报错搜索引擎找解决方案这套思路解决。一个报错你花十分钟能搞定就不要自己死磕两小时把时间花在理解业务逻辑和数据设计上收益会大得多。最后再分享一个非常实用的经验把项目完整跑通之后再重新从零部署一遍。删掉数据库、关闭后端、清空前端构建文件然后按照你自己的项目说明文档一步步重新把环境搭起来。这个过程能暴露出的问题比写代码时多得多。我见过很多同学开发时一切正常答辩前突然起不来了原因就是环境依赖忘记了、配置没写进文档。真正能顺畅重装一次的项目才是真正做完了。