ARTICLE DETAIL

资讯详情

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

基于SpringBoot+Vue+MySQL的家教管理系统开发全流程详解

基于SpringBoot+Vue+MySQL的家教管理系统开发全流程详解 开头这篇博文想聊聊我完整做过的“基于SpringBootVueMySQL的家教管理系统”——一套典型的Java Web毕业设计项目。如果你正在选毕设题目或者已经选了类似的“XX管理系统”但还没想清楚怎么下手这篇文章应该能帮你省下好几周的摸索时间。我会从需求拆分、表结构设计、后端接口实现、前端页面联调一直讲到最后的打包部署把我会踩的坑和后来复盘总结出来的经验一并写下来。先说说这个项目本身能做什么一套完整的家教管理系统覆盖了注册登录、家长发布家教需求、学生/老师在线接单、课程预约排期、课时费计算、订单评价、后台数据管理等常见业务流程。技术栈就是标题里那三件套——SpringBoot做后端服务、Vue做前端界面、MySQL做数据存储。这个组合是目前高校毕业设计里出现频率最高的一套方案原因是它每一层都很“标准”SpringBoot有大量现成生态Vue前后端分离的开发模式清晰MySQL入门门槛低、资料多。对于需要在一个学期内完成设计、开发、论文、答辩的学生来说这套组合的性价比确实是最高的。本文适合的读者主要是正在做毕设的本科生尤其Java方向其次是想快速上手前后端分离项目的开发者。文中涉及的具体代码片段、配置文件和SQL语句都是可以直接参考的我不光会写“怎么实现”还会解释“为什么这么做”毕竟毕设答辩时老师最喜欢问的就是设计理由。1. 开始前的准备技术栈选型与环境搭建1.1 技术栈选型与版本搭配很多人一上来就问“用什么版本好”这其实比想象中重要。版本搭配不当会导致很多莫名其妙的问题而且这类问题在百度上往往搜不到有效答案。我最后定的版本组合是后端SpringBoot 2.7.x我用的是2.7.18JDK1.8或者JDK 8但注意SpringBoot 2.7兼容JDK 8到JDK 17都可以前端Vue 2.6.x Element UI 2.15.x构建工具Maven 3.8.x npm 6.x或8.x都可以MySQL8.0.x也可以用5.7但8.0的JSON字段等功能会让设计更从容ORMMyBatis-Plus 3.5.x也可以用JPA但MyBatis-Plus在毕设里更常见、SQL可控性更强选SpringBoot 2.7而不是3.x的原因很实诚当时的MyBatis-Plus、某些文档教程仍然以2.x为主网上能搜到的解决方案大部分针对2.xSpringBoot 3.0要求JDK 17起步虽然新但生态支持没那么稳。毕设的核心诉求是稳定出活不是追新。Vue选择2.x而不是3.x是因为Element UI对Vue 2的支持是最成熟的绝大多数毕设前端教程都是基于这个组合写的。Vue 3配Element Plus也成熟了但如果你时间紧、对前端又不是特别熟选Vue 2 Element UI的容错率更高出了问题搜教程最方便。1.2 开发环境配置与常见版本坑环境配置这一块儿很多人都栽过我把它拆成容易出问题的几个点详细说说。后端的JDK安装不用说了关键是Maven仓库的配置。国内直接拉Maven中央仓库经常卡住甚至失败时间都耗在等下载上。解决方案是修改settings.xml把镜像指到阿里云mirror idaliyunmaven/id mirrorOfcentral/mirrorOf name阿里云公共仓库/name urlhttps://maven.aliyun.com/repository/public/url /mirrorMaven配好之后用IDEA新建SpringBoot项目时要注意spring-boot-starter-parent的版本号是否和自己本地Maven仓库里已下载的版本一致不一致的时候IDEA会重新下载依赖这个等待过程容易给人“卡死”的错觉其实只是慢。MySQL这里有个经典坑值得单拎出来说如果你装的是MySQL 8.0以上版本连接驱动名和URL配置跟5.7不一样。很多教程还停留在5.x时代直接抄会报ClassNotFoundException或者SSL相关错误。正确的8.0配置是spring.datasource.driver-class-namecom.mysql.cj.jdbc.Driver spring.datasource.urljdbc:mysql://localhost:3306/tutor_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue spring.datasource.usernameroot spring.datasource.password你的密码serverTimezoneAsia/Shanghai这个参数尤其重要不设置的话连数据库会报时间相关的异常。useSSLfalse和allowPublicKeyRetrievaltrue两个参数是针对MySQL 8.0的加密认证机制补充的少了也有可能连接失败。前端的Node环境配置相对简单不过有一个小问题很多新手会遇到——npm下载依赖慢。建议先在命令行执行npm config set registry https://registry.npmmirror.com再把全局镜像换掉后续创建Vue项目npm install的时候就能快很多。创建Vue工程我用的是vue create注意提前安装Vue CLInpm install -g vue/cli vue create tutor-front创建过程中会问选哪种配置选“Manually select features”然后勾上Router就行其他的可以后面再加。这里建议勾上Router是因为管理系统几乎都需要多页面跳转晚点自己配置router虽然也不难但一开始就生成好更省事。环境准备阶段的最后一个建议先做一个最简单的“Hello World”通一遍全流程——后端写一个/hello接口前端写一个展示页面前后端能调通再开始正式写业务。这个“最小闭环”花不了多长时间但它能提前把环境里90%的问题暴露出来后续做业务功能时就不会反复被环境问题打断。2. 需求分析与数据库设计把业务理清楚2.1 角色与核心业务流程拆解写代码之前最关键的一步是分析需求数据表设计得合理不合理直接决定后面开发的效率。家教管理系统最核心的参与者是三类家长发布家教需求、筛选老师、预约试听、支付课时费、评价老师教师查看需求、提交申请、管理课程表、确认上课、查看收入管理员审核教师资质、管理用户、处理投诉、统计平台数据围绕这三类角色核心的业务流程可以归纳成一条主线家长发布需求→教师申请→家长确认→形成订单→预约排课→完成课程→家长确认完成→课时费结算→互相评价。这里有个容易犯的设计错误就是一上来想“系统要支持XX功能”然后直接把功能列表丢给数据库去设计结果表之间关系混乱。正确做法是先画业务流程图理清顺序关系再反推数据表。比如“订单”和“预约排课”看起来像一件事实际上应该分开设计因为一个订单可能包含多次课程预约按课时结算的场景很常见这也和实际业务逻辑一致。系统采用前后端分离架构大致分层如下前端Vue工程页面渲染、交互逻辑通过Axios调用后端接口获取数据后端SpringBoot工程提供RESTful API处理业务逻辑、权限校验、数据持久化数据库MySQL存储用户、需求、订单、预约等核心数据前端和后端之间通过JSON交换数据这样分离的好处是前端可以独立开发和测试后端也可以先写好接口给前端Mock数据联调。2.2 数据库表设计与核心字段解析数据库设计是这个项目里最值得花时间的部分之一。表结构设计得好后面写业务代码会非常顺设计得不合理经常写着写着发现要加字段、改关系连带前后端一起返工。我最终的库表结构核心部分如下这里给出我认为比较健壮的一个版本用户表user字段名类型说明idbigint主键自增usernamevarchar(50)登录名唯一passwordvarchar(255)加密存储BCryptreal_namevarchar(50)真实姓名phonevarchar(20)手机号roletinyint角色1家长 2教师 3管理员statustinyint状态0禁用 1正常avatarvarchar(255)头像URLcreate_timedatetime创建时间MySQL 8.0的自增主键用bigint完全够用别用int虽然这个项目数据量不大但养成好习惯就行。需求表demand字段名类型说明idbigint主键parent_idbigint发布人ID关联user表titlevarchar(100)需求标题subjectvarchar(50)科目gradevarchar(50)年级areavarchar(100)区域teach_typetinyint1上门 2线上salarydecimal(10,2)期望课时费detailtext描述详情statustinyint0待接单 1已接单 2已完成 3已关闭create_timedatetime发布时间这里把parent_id直接关联用户表查询“我的发布”时只需一条WHERE parent_id ?。订单表order字段名类型说明idbigint主键order_novarchar(32)订单号全局唯一demand_idbigint关联需求表teacher_idbigint接单教师parent_idbigint家长total_course_countint总课时数finished_countint已完成课时total_feedecimal(10,2)总金额statustinyint0待付款 1进行中 2已完成 3已取消create_timedatetime下单时间订单号的生成我建议用yyyyMMddHHmmss 随机数的格式避免用户直接通过ID推断业务量这属于比较常规的做法。课时预约表course_schedule字段名类型说明idbigint主键order_idbigint关联订单course_datedate上课日期start_timetime开始时间end_timetime结束时间subjectvarchar(50)科目contentvarchar(255)本节课内容statustinyint0待上课 1已完成 2已取消remarktext备注设计这套表时我踩过一个坑一开始把课时直接塞进订单表里用一个course_date字段记录所有上课日期结果要用逗号分隔存储查询“某天某老师有没有课”变得非常痛苦最后老老实实拆成独立的预约表。这也是数据库设计里最核心的一个原则一张表只描述一种业务实体。除了这些核心表之外还应有评价表、公告表、管理员操作日志表等。日志表在答辩时是一个加分项能体现出你考虑了系统的可追溯性。关于权限设计由于这个系统的角色只有三种且互相独立用role字段区分就够了不需要引入Spring Security那套权限模型角色表、权限表、角色权限关联表否则反而增加了复杂度。对这一类“管理型系统”来说最简单直接的方案往往最合适。3. 后端落地SpringBoot核心实现全解3.1 项目结构划分与统一返回格式后端项目结构上我倾向于按模块分包而不是按技术层分包这样代码的可维护性更高找东西也方便com.example.tutor ├── controller # 接口层只做参数接收和结果返回 ├── service # 业务逻辑层写核心流程 │ └── impl # 接口实现类 ├── mapper # MyBatis-Plus的Mapper接口 ├── entity # 数据库实体类 ├── dto # 数据传输对象比如前端传过来的表单数据 ├── vo # 视图对象返回给前端的数据结构 ├── config # 配置类如跨域、拦截器 ├── common # 公共类如返回结果封装、异常处理 ├── utils # 工具类如JWT工具 └── TutorApplication.java # 启动类entity直接映射数据库表dto接收前端参数vo返回前端数据——这三者的区分非常重要不要图省事全程用MapString, Object传参那样代码会变得不可维护而且答辩时老师看到清晰的VO结构会更容易认可。接口返回格式建议统一封装我使用的是经典的“三件套”结构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 message) { ResultT result new Result(); result.setCode(500); result.setMessage(message); return result; } public static T ResultT error(Integer code, String message) { ResultT result new Result(); result.setCode(code); result.setMessage(message); return result; } }有了统一返回格式之后前端不管调什么接口处理逻辑都一致的先看code是不是200是再取data不是就弹message提示。这里不用Result的泛型的话前端不好做类型推导但用泛型在Java里稍显繁琐换来的是接口语义更清晰值得。3.2 登录鉴权与角色权限控制登录模块是几乎所有管理系统的基础这里的选择很多Session、JWT、第三方框架Spring Security JWT。毕设阶段我个人推荐直接使用JWT原因有三它是目前企业项目的主流方案答辩时有内容可讲前后端分离场景下JWT不需要处理Session共享问题实现起来也没有想象中复杂。我使用的JWT工具类简化版本如下Component public class JwtUtils { Value(${jwt.secret}) private String secret; Value(${jwt.expire}) private Long expire; // 生成Token public String generateToken(Long userId, String username, Integer role) { Date now new Date(); Date expireDate new Date(now.getTime() expire * 1000); return Jwts.builder() .setHeaderParam(typ, JWT) .setSubject(username) .claim(userId, userId) .claim(role, role) .setIssuedAt(now) .setExpiration(expireDate) .signWith(SignatureAlgorithm.HS256, secret) .compact(); } // 解析Token public Claims parseToken(String token) { return Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); } }登录成功之后后端把token返回给前端前端存储在localStorage里之后每次请求都在请求头带上Authorization: Bearer {token}。后端的拦截器统一校验Component public class JwtInterceptor implements HandlerInterceptor { Autowired private JwtUtils jwtUtils; Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录接口 if (request.getRequestURI().contains(/api/auth/)) { return true; } String authHeader request.getHeader(Authorization); if (authHeader null || !authHeader.startsWith(Bearer )) { throw new BusinessException(未登录或Token已过期); } String token authHeader.substring(7); Claims claims jwtUtils.parseToken(token); request.setAttribute(userId, claims.get(userId)); request.setAttribute(role, claims.get(role)); return true; } }拦截器做好之后需要注册到WebMvc配置中别漏掉这一步。另外注意区分“登录接口不用校验”和“某些接口必须某种角色才能访问”的逻辑。我在角色校验上采用了自定义注解的方式Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface RequireRole { int[] value(); }然后拦截器解析HandlerMethod上是否有这个注解再和JWT里的role比对if (handler instanceof HandlerMethod) { HandlerMethod handlerMethod (HandlerMethod) handler; RequireRole requireRole handlerMethod.getMethodAnnotation(RequireRole.class); if (requireRole ! null) { Integer role (Integer) request.getAttribute(role); boolean allowed Arrays.stream(requireRole.value()).anyMatch(roleValue - roleValue role); if (!allowed) { throw new BusinessException(权限不足); } } }这个方案比Spring Security轻量得多但又能完整覆盖毕设的权限需求。答辩时如果老师问“你是怎么控制权限的”你就可以把注解拦截器这套讲清楚逻辑链完整。密码存储方面必须强调绝对不要明文存数据库。Spring Security虽然不引框架但可以单独引spring-security-crypto依赖来使用BCrypt加密。注册时BCrypt.hashpw(password, BCrypt.gensalt())登录时BCrypt.checkpw(rawPassword, encryptedPassword)。BCrypt每次生成的盐都不一样所以即使两条相同密码存储值也不同安全性更好这也是答辩时的加分回答。3.3 核心业务接口的实现细节核心业务接口里面最有代表性的是家长发布需求、教师响应需求、确认订单和排课这几个接口它们的实现逻辑我展开讲讲。发布需求接口权限家长PostMapping(/api/demand/publish) public ResultVoid publish(RequestBody DemandDTO demandDTO) { // 从请求中获取当前登录用户 Long userId (Long) request.getAttribute(userId); Integer role (Integer) request.getAttribute(role); if (role ! 1) { return Result.error(403, 只有家长才能发布需求); } demandService.publish(demandDTO, userId); return Result.success(null); }注意这里role判断我写在了Service层之前实际上拦截器已经做了粗略的登录校验具体的角色判断在接口层做更直观。当然也可以完全通过注解来做我是为了演示两种方式的区别故意保留了其中一种写法。Service层实现里要注意对DTO字段做校验标题不能为空、科目不能为空、课时费必须大于0。参数校验可以用Validated注解配合NotBlank等实现也可以手写if判断。手写虽然啰嗦但在答辩时更好讲两者各有优势选自己熟练的即可。教师响应需求接口权限教师的流程稍微复杂一点校验需求状态是“待接单”校验当前需求不是该教师自己发布的理论上教师不会同时是家长但保险起见写入申请表teacher_apply记录教师ID、需求ID、申请时间提交申请后家长可以在“需求详情”页面看到申请教师的列表选择一个教师确认后系统自动创建订单。我把“确认接单”作为一个独立接口处理逻辑集中在事务里是很稳妥的Transactional public void confirmTeacher(Long demandId, Long teacherId) { Demand demand demandMapper.selectById(demandId); if (demand null || demand.getStatus() ! 0) { throw new BusinessException(需求不存在或已被接单); } // 更新需求状态 demand.setStatus(1); demandMapper.updateById(demand); // 创建订单 Order order new Order(); order.setOrderNo(generateOrderNo()); order.setDemandId(demandId); order.setTeacherId(teacherId); order.setParentId(demand.getParentId()); order.setStatus(0); orderMapper.insert(order); }Transactional这个注解必须加不然“更新需求状态”和“创建订单”之间如果发生异常数据就会不一致。这里我踩过坑第一版没加事务测试时手动模拟网络超时结果需求显示被接单了但订单表里没有记录。后来加上事务之后这个问题就消失了。课时预约与排课是业务的核心。我设计的逻辑是订单创建后家长和教师可以共同维护排课表。排课接口要处理的校验主要有三点同一时间段教师不能有重复课程上课时间不能早于当前时间已经完成的课时不能重复排排课冲突校验SQLSELECT COUNT(*) FROM course_schedule WHERE teacher_id #{teacherId} AND course_date #{courseDate} AND ( (start_time #{endTime} AND end_time #{startTime}) )只要COUNT0就说明有冲突拒绝插入。这个重叠区间判断是一个经典的时间段冲突判定公式面试和答辩都可以拿出来强调一下——start_time 新endTime AND end_time 新startTime。订单完成与结算的逻辑也不复杂每一次课程完成finished_count加1当等于total_course_count时订单状态改为“已完成”。课时费结算可以简化成总金额在订单开始时已经确定课程全部完成后管理员在后台手动标记“结算完成”也可以直接用定时任务自动结算。毕设里建议做手动结算避免引入复杂的定时任务调度逻辑但可以把“定时结算”作为论文里“可扩展功能”提一嘴体现你的思考。4. 前端落地Vue Element UI 工程化实践4.1 前端工程目录与插件选型前端工程我按Vue CLI的标准结构来自己的补充规划如下src ├── api # 按模块封装的接口请求 │ ├── auth.js # 登录注册相关 │ ├── demand.js # 需求相关 │ ├── order.js # 订单相关 │ └── user.js # 用户相关 ├── router # 路由配置 ├── store # Vuex状态管理 ├── views # 页面组件 │ ├── login.vue │ ├── home.vue │ ├── parent/ # 家长端页面 │ ├── teacher/ # 教师端页面 │ └── admin/ # 管理端页面 ├── components # 公共组件 ├── utils # 工具函数request封装等 └── App.vueAxios封装是必须做的一步不要在每个页面里直接axios.get那样改起来会很想死。比较推荐的封装方式import axios from axios; import { Message } from element-ui; import router from ../router; const request axios.create({ baseURL: /api, timeout: 10000 }); // 请求拦截器统一携带Token request.interceptors.request.use(config { const token localStorage.getItem(token); if (token) { config.headers.Authorization Bearer ${token}; } return config; }, error Promise.reject(error)); // 响应拦截器统一处理错误码 request.interceptors.response.use(response { const res response.data; if (res.code 200) { return res; } if (res.code 401) { Message.error(登录已过期请重新登录); localStorage.clear(); router.push(/login); return Promise.reject(new Error(res.message)); } Message.error(res.message || 请求失败); return Promise.reject(new Error(res.message || 请求失败)); }, error { Message.error(网络异常请稍后重试); return Promise.reject(error); }); export default request;统一封装的另一个好处是后期加“请求加载动画”之类的全局功能时只需改拦截器一处不用全页面动刀。路由部分需要做登录守卫router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (to.path /login) { next(); return; } if (!token) { next(/login); return; } // 根据角色判断路由权限放行或拒绝 const role localStorage.getItem(role); if (to.meta.roles !to.meta.roles.includes(role)) { next(/403); return; } next(); });角色权限的判断放在前端路由守卫里可以优化用户体验——没有权限的人看不到入口但后端接口的RequireRole依然不可省略前端只是体验优化后端才是安全底线。4.2 核心页面与状态管理登录页面虽然简单却是整个系统的门面有几个细节值得注意。登录表单校验用Element UI自带的rules就可以了密码字段要设置show-password登录成功之后把token、role、username这些信息存进localStorage并跳转到对应的首页。很多新手会在这里犯一个错误在登录页面里用this.$router.push(/home)硬跳然后发现刷新页面后状态丢失。正确做法是首页组件在created钩子里根据localStorage里的角色动态区分显示内容。家长端的核心页面是“发布需求”和“我的订单”。发布需求表单我会用el-form控制校验el-form refform :modelform :rulesrules label-width80px el-form-item label标题 proptitle el-input v-modelform.title placeholder请输入需求标题/ /el-form-item el-form-item label科目 propsubject el-select v-modelform.subject placeholder请选择科目 el-option label数学 value数学/ el-option label英语 value英语/ el-option label物理 value物理/ !-- 其他科目 -- /el-select /el-form-item el-form-item el-button typeprimary clicksubmitForm发布/el-button /el-form-item /el-form表单校验规则写在data的rules里rules: { title: [{ required: true, message: 请输入标题, trigger: blur }], subject: [{ required: true, message: 请选择科目, trigger: change }] }这里的prop必须和v-model绑定的字段名一致否则校验不生效——这也是Element UI表单一个特别容易踩的坑。教师端的核心页面是“需求广场”和“我的排课”需求广场用el-card列表展示需求卡片每个卡片上有“申请”按钮。申请成功后按钮变灰避免重复申请。排课页面我用了日历组件el-calendar但Element UI的日历组件支持能力有限毕设场景其实完全够用——在日期上展示课程信息点击某一天弹出当天课程列表。这里有个需要注意的小细节日历组件有时区问题展示的时间可能比实际少一天或多一天建议在后端统一返回yyyy-MM-dd格式字符串即可规避时区歧义。管理员的页面主要是表格类操作用户管理、需求管理、订单管理、数据统计。表格加搜索、分页是标配素材上我直接使用el-tableel-pagination。搜索建议用独立的请求参数传递而不是在前端把所有数据一次拉回来再过滤——虽然数据量小的时候后者也能跑但不符合正规项目习惯答辩容易被追问。4.3 Vue 组件化与复用技巧管理系统里很多页面结构相似组件化可以避免大量重复代码。我在开发中提炼了两个比较通用的组件。第一个是分页组件。虽然每个页面都有分页但大家写的分页逻辑都差不多当前页、每页条数、总条数、切换页码时重新请求。所以我直接封装了一个Pagination组件接收total、page、limit三个参数内部通过v-model同步当前页。这样每个列表页面只需要维护一个对象data() { return { pageParams: { page: 1, limit: 10, total: 0 } } }第二个是空状态组件。有些页面在数据为空时要显示“暂无数据”提示。Element UI的el-table自带empty-text属性可以基本满足但如果是非表格页面比如需求广场就需要自己写一个空状态提示。我封装了EmptyState组件支持传入自定义文案和图标。组件化的另外一层价值在于答辩演示时你可以直接说“我开发了两个公共组件减少了XX%的重复代码”这比单纯说“我用了Element UI”要有说服力得多。5. 前后端联调与部署上线5.1 跨域问题与开发环境配置前后端分离项目最典型的问题就是跨域。前端在localhost:8080后端在localhost:8088前端直接请求后端接口会触发浏览器同源策略限制报错信息通常是No Access-Control-Allow-Origin header is present on the requested resource。解决办法有两个常用路径我推荐先在开发环境用Vue CLI的代理功能解决在vue.config.js中添加module.exports { devServer: { port: 8080, proxy: { /api: { target: http://localhost:8088, changeOrigin: true, pathRewrite: { ^/api: /api } } } } }这个配置的作用是前端请求/api/login时开发服务器会把请求转发到http://localhost:8088/api/login同时浏览器的请求源不会变化跨域问题直接绕开。注意pathRewrite一般不需要改写/api前缀因为后端接口本身就带了这个前缀。如果不用代理也可以在后端开启全局CORS配置如下Configuration public class CorsConfig { Bean public WebMvcConfigurer corsConfigurer() { return new WebMvcConfigurer() { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOrigins(http://localhost:8080) .allowedMethods(GET, POST, PUT, DELETE) .allowedHeaders(*) .allowCredentials(true); } }; } }allowCredentials(true)这个配置和allowedOrigins(*)不能同时使用如果设了allowedOrigins(*)就不能携带Cookie认证信息这是浏览器安全策略的硬性要求。我一般建议直接使用代理方案因为开发环境用代理更优雅生产环境前端的请求都走同源也不会跨域。5.2 前端构建与后端打包开发完成后部署前端要执行构建npm run build这个命令会在项目根目录生成dist文件夹里面是编译后的静态文件HTML、CSS、JS。dist目录可以直接用Nginx部署也可以让SpringBoot直接托管。我们在毕设中最省事的部署方案是把前后端合并部署到同一个服务先把后端打成Jar包再把前端的dist目录复制到SpringBoot项目的src/main/resources/static目录下最后重新打包。这样启动一个SpringBoot应用就能同时提供后端接口和前端页面无论是本地演示、答辩还是放到服务器上都极其方便。后端打包前先确认pom.xml里的打包方式为jar然后执行mvn clean package -DskipTests生成的Jar包在target目录下启动命令java -jar tutor-system.jar如果想让端口固定可以在application.properties里指定server.port8088生产环境的数据库地址、账号密码最好通过启动命令传入避免把配置写在Jar包里java -jar tutor-system.jar --spring.datasource.passwordxxx这种启动方式即便在答辩现场临时换环境也能快速适应有一定的灵活度。Nginx部署也是一个不错的选项它还可以做反向代理和负载均衡。但对于毕设项目来说直接用SpringBoot托管Static目录更简单也少一个需要讲解的环节。我自己最终选了合并部署因为论文里少写一章Nginx配置也可以减少一个潜在问题点。5.3 数据库初始化与种子数据系统第一次启动时数据库是空的直接演示会显得很空。建议准备一套初始化SQL里面包含必要的管理员账号、测试家长账号、测试教师账号以及一部分模拟数据需求、订单、排课、评价等。管理员账号我习惯初始化为INSERT INTO user (username, password, real_name, phone, role, status, create_time) VALUES (admin, $2a$10$...BCrypt加密后的密码..., 管理员, 13800000000, 3, 1, NOW());注意密码一定用BCrypt加密后的字符串不要直接写明文。测试数据能覆盖一到两条完整业务链就行——一条已完成的订单包含课程预约和评价一条进行中的订单一条待接单的需求。这样答辩时打开系统就能直观展示不同状态不用现场造数据。6. 常见问题与排查技巧实录再稳的项目开发过程中都会遇到各种问题我把自己实际踩过的坑整理成一份排查清单希望能帮你省去一部分搜索时间。问题现象可能原因解决方法前端请求接口报404Vue代理中的路径和后端接口路径不一致或后端接口没有加RequestMapping核对路径用浏览器Network面板查看实际请求URL登录后刷新页面用户信息丢失只存了Token没有在created钩子里重新拉取用户信息增加getUserInfo接口刷新时调用或把用户基本信息存localStorage数据库中文乱码数据库连接URL缺少characterEncodingutf8或表结构字符集不是utf8mb4修改连接URL建库时指定DEFAULT CHARSETutf8mb4MySQL连接报Access denied for user用户名密码错误或账号只允许localhost连接核对配置执行ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 密码上传的图片无法显示图片存储路径不对或没有配置静态资源映射后端添加静态资源映射registry.addResourceHandler(/upload/**).addResourceLocations(file:路径)Vue打包后页面白屏publicPath配置错误在vue.config.js中设置publicPath: ./相对路径提交订单时提示事务错误没有加Transactional或事务方法内部调用了同类的方法导致事务失效在需要原子性操作的方法上标注Transactional并确保方法被外部调用时间字段显示不正确时区设置问题JDBC连接添加serverTimezoneAsia/Shanghai前端显示时格式化除了表格里的这些我再补充一个很实用的问题排查思路——学会看日志。SpringBoot的日志默认会打印在控制台当接口报错时把我上面提到的拦截器逻辑、业务逻辑里的异常信息都打印出来。很多新手看到报错就慌其实大多数错误日志的第一行就写明了原因。比如500错误日志里通常紧跟着一条Caused by: xxxx信息顺着看就找到了根因。如果完全看不懂日志就复制最后两行关键报错去搜索搜索时去掉明显属于你项目的包名路径加关键词“SpringBoot”或“MyBatis-Plus”命中率会高很多。有一个容易被忽视的“坑”是MyBatis-Plus的字段映射规则。如果你的实体类字段叫createTime数据库字段叫create_timeMyBatis-Plus默认是开启了下划线转驼峰的所以可以直接映射。但如果你老老实实把数据库字段起名叫createTime不带下划线MyBatis-Plus反而映射不起来。这是因为约定走的是create_time风格不是大小写敏感匹配。我第一版就是因为数据库字段风格不一致导致查出来全是null排查了很久才意识到是这个原因。前端还有一个小问题很容易忽略Element UI的el-date-picker返回的时间默认是Date对象直接提交给后端时JSON序列化格式可能不是后端想要的比如是时间戳或者带时区的字符串。稳妥做法是在前端先格式化再提交const formattedDate this.$moment(date).format(YYYY-MM-DD);或者在后端对应字段上用JsonFormat注解JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8) private LocalDateTime createTime;两种方案选其中一个就够了但一定要统一不然后端接到的日期格式五花八门排查起来非常痛苦。结尾最后再分享一个我做完这个项目之后最大的体会毕业设计的难点其实不在于某个具体的框架用法而在于“把一堆零散的需求变成一套能跑通的完整系统”这件事本身的工程协调能力。你得同时管好数据库设计、后端接口、前端页面、权限控制、异常处理、数据初始化这些线而任何一条线断了整个系统都没法演示。我写这篇指南时把每个环节的关键坑都尽量标出来了希望能让你少走一些弯路。如果时间允许强烈建议你在这个基础上再加一个“亮点功能”比如基于ECharts的管理员数据看板、基于WebSocket的站内消息通知、或基于定时任务的自动结算。这些功能单看都不难但它能让你的系统在答辩时明显区别于其他同学的“增删改查”也给了论文“创新点”一栏足够的写作素材。我自己加的是数据看板和消息通知两个功能最后答辩时老师主要追问的就是这两个部分——可见老师虽然不会细看每一行代码但一个功能是否有含金量一眼就能看出来。
返回列表