SpringBoot+Vue在线教育后台管理系统:数据库设计到前后端分离实战
做了这么多年后台管理系统我越来越觉得所谓设计和实现这两件事是真正拉开差距的地方设计没想清楚代码写多少返工多少。前阵子帮一家职业技能培训机构整理在线教育管理后台时我的第一反应不是先写接口而是把角色、课程、订单这些核心实体在纸面上画了一遍。这套系统基于SpringBootVue实现后端用JavaMySQLMyBatis前端用Vue3Element Plus整体源码是完整可复现的。如果你正在做课程管理、培训后台、知识付费类项目或者想找一套前端分离的实战源码来练手这篇文章就是这套系统从业务拆解到代码落地的全过程记录。这套系统最初的需求并不复杂机构有十来个专职讲师录好的视频需要一个地方承载学员要能选购课程后台要能管理上下架、分类、订单、评论。但真实业务一旦展开细节远比想象得多——一门课被讲师误操作重复上架怎么办学员重复下单怎么办课程没配置视频能不能上架这些看似边缘的问题实际上决定了数据库表和接口要怎么写。接下来我会按项目全景、技术选型、数据库设计、后端链路、前端落点、联调部署、源码复现的顺序把每个决策和踩过的坑都说清楚。1. 项目全景拆解这套在线教育管理系统到底要管什么1.1 它是管理端不是学习端很多人看到在线教育系统第一反应是做一个像慕课网那样的视频学习平台但标题里写得很明确——管理系统。这两者的差别非常大学习端面向学员核心是选课、播放、学习进度管理端面向机构运营人员核心是维护数据、审核内容、查看订单。本项目重心在管理端它解决的是机构内部内容管理和交易管理的问题而不是解决学员怎么把课程看完的问题。管理端需要一个清晰的角色边界。系统里我把用户区分成三类管理员、讲师、普通学员。管理员看见的菜单最全用户管理、分类管理、课程审核、订单查询、评论删除都能操作讲师能维护自己的课程和章节但看不到全局订单明细学员在管理端基本只读实际上学员的主要行为发生在前台购买端管理端只是提供用户列表和禁用能力。这个三权分离的模型不需要引入重型的权限框架一张user表加一个role字段就够用。1.2 功能模块矩阵一个后台该有的功能清单需求落到模块上我梳理出的功能矩阵大概是这样功能模块核心功能实现要点登录认证账号密码登录、JWT签发登录后返回token后续请求携带用户管理用户分页、启用禁用、角色查看状态位控制是否可登录课程分类两级分类树的管理parent_id自关联删除前校验课程管理新增课程、编辑、上下架、章节维护上下架前校验课程完整性订单管理订单分页、状态筛选金额后端计算不能信任前端评论管理评论审核、删除状态字段控制前台展示轮播管理首页Banner配置排序字段 启停用数据统计课程数、用户数、订单金额汇总SQL聚合即可这套功能清单几乎覆盖了中小型在线教育机构管理后台的全部刚需。设计时我刻意把评论和轮播也放进来了因为很多演示项目只做课程和用户一旦面试官问内容安全怎么处理就答不上来。评论审核和Banner管理就是很好的补充点代码量不大但业务完整度上了一个档次。1.3 业务流程上的两个关键闭环管理后台的业务逻辑虽然看起来是增删改查但至少存在两个需要认真对待的闭环。第一个是课程发布闭环讲师创建课程 - 添加章节 - 添加小节视频 - 提交上架 - 管理员审核通过 - 前台可见。在这个链路里课程状态的变化不是随心所欲的必须有明确的状态机。我在course表里用status字段表示0草稿、1待审核、2已上架、3已下架。讲师只能把课程从草稿提交到待审核管理员才能做上架或驳回。这样设计的好处是前台永远不用考虑课程到底能不能展示这种业务问题。第二个是订单闭环学员下单 - 生成待支付订单 - 支付回调 - 订单已支付 - 学员获得课程观看权限。管理端不需要自己接支付通道但要预留支付入口并按状态流转来展示订单。订单状态用0待支付、1已支付、2已取消来表示。这里最容易出问题的不是状态本身而是重复下单和金额信任这两点我会在数据库设计和后端实现部分重点展开。2. 技术选型推演为什么是SpringBootVueMySQLMyBatis这套组合2.1 后端版本选型稳定优先不盲目追新SpringBoot的版本选择是这类项目第一个容易踩坑的地方。到项目落地时SpringBoot 3.x已经发布但它强制要求JDK17很多学习环境和机构服务器还在用JDK8。我最终选的是SpringBoot 2.7.18对应JDK8和JDK11都能跑Spring生态的资料、依赖兼容性都是最成熟的。选2.7而不是3.x还有一个原因MyBatis、PageHelper、JWT等常用库在2.7上基本无脑兼容不需要处理任何版本迁移问题。这不是说3.x不好而是对管理系统这种以业务逻辑为主的项目来说工具的稳定性比版本新鲜感重要得多。依赖清单上我加了Lombok来减少实体类getter/setter代码加了spring-boot-starter-validation做参数校验JWT选的是jjwt库。Hutool这种工具库看个人习惯我用它主要是为了生成订单号和日期处理避免重复造轮子。这些都不是必须的但能让代码保持简洁。2.2 前端选型Vue3的生态已经足够成熟前端我选择了Vue3 Vite Element Plus Pinia Vue Router的搭配。很多老项目教程还在教Vue2 Element UI因为历史包袱但新项目再开Vue2的坑是不划算的。Vue3的setup语法写起来比Options API更简洁Vite的启动速度和热更新体验也比Vue CLI时代好一个量级。Element Plus的组件库对于后台管理场景非常完整表格、表单、对话框、分页都有成熟组件能省下大量UI时间。如果你是从零开始配Vue环境我的建议是用npm create vitelatest命令初始化项目然后手动安装element-plus、axios、vue-router、pinia。之所以不推荐官方脚手架一键生成完整后台模板是因为那种模板会带进来大量用不到的代码和目录结构反而不利于理解路由、状态管理、请求封装这些核心机制。自己动手装一遍依赖才知道每个包是干什么用的。2.3 数据访问层原生MyBatis和MyBatis-Plus的取舍数据访问层是很多开发者争论的焦点。MyBatis-Plus确实能把单表CRUD缩短到一行代码很多人直接选它。但我在这套系统里用的是原生MyBatis XML写SQL为什么因为管理系统的价值恰恰集中在复杂查询上课程分页要关联分类表订单分页要关联用户表和课程表统计面板要聚合汇总。MyBatis-Plus对标准单表操作很友好但一旦涉及多表关联和动态条件最后还是得手写SQL那为什么不一开始就让SQL清晰可控呢另外一个私心是面试和招聘场景里原生MyBatis的动态SQL本身就是考点用XML写清楚比依赖框架的LambdaQueryWrapper更有示范价值。当然这并不意味着所有语句都要手写。user表、banner表这类简单实体的基础增删改查我在Mapper里用了注解方式course、order这类复杂查询则统一走XML。两条腿走路既不啰嗦也不失控制力。MyBatis的map-underscore-to-camel-case配置记得打开数据库的create_time才能自动映射到实体的createTime属性少写很多resultMap。2.4 数据库与工具链MySQL8是底线MySQL我建议直接上8.0版本5.7在字符集排序、窗口函数、JSON支持上都力不从心。连接驱动用com.mysql.cj.jdbc.Driver连接串务必带上serverTimezoneAsia/Shanghai和characterEncodingutf8否则日期差8小时和控制台乱码这两个问题会一起找上门具体原因后面联调章节细说。数据库管理工具我用Navicat比较多也可以用MySQL Workbench或命令行对这套系统来说只要能执行SQL脚本、看表结构就够了。字符集统一用utf8mb4因为评论内容里完全可能出现Emoji字符utf8会报错。3. 数据库设计把教育业务拆成不打架的表结构3.1 用户与权限单表加角色字段是管理端的最优解用户表设计我见过不少方案有的把管理员、讲师、学员拆成三张表有的引入Shiro的权限模型但对于管理端来说单表加role字段是最务实的。理由很简单同一套登录逻辑、同一套密码校验拆表只会增加联表和注册流程的复杂度。user表的核心字段包括id、username、password、nickname、avatar、phone、email、role、status、create_time。role取值admin/teacher/studentstatus取值0禁用1正常。密码字段存储的是加密后的密文绝不允许明文落库。为了贴近真实项目我这里用BCrypt加密如果演示环境想简单点用MD5加盐思路一样只是安全性差一个档次。给username建立唯一索引同时给role加普通索引因为后台列表页最常做的筛选就是按角色查用户。还需要注意一点删除用户要慎重。用户一旦产生过订单删除就破坏了订单表的关联完整性所以管理端对用户的操作应该以禁用为主不做物理删除。这是业务规则对表设计的一种约束如果代码里用了外键约束甚至应该在数据库层面禁止直接删除有订单记录的用户。3.2 课程内容模型分类、课程、章节、小节四级结构课程领域是这套系统的核心所以表结构要格外仔细。course_category表用来存课程分类parent_id自关联实现两级分类parent_id为0的是顶级分类比如Java开发子分类比如SpringBoot实战的parent_id指向顶级分类id。这样前端下拉树和后端分页筛选的实现都很直接。sort字段控制排序status控制前台是否展示。course表和分类表关联字段包括teacher_id、category_id、title、cover_url、description、price、original_price、status、view_count、buy_count、create_time、update_time。price用decimal(10,2)这是和金额相关的硬性要求——不要用double或float存钱二进制浮点数无法精确表达金额哪怕在Java里用double相加会出现0.10.20.30000000000000004这类问题数据库里同样不行。buy_count和view_count是冗余统计字段每次有人购买、有人浏览时单独更新避免统计面板实时count大表。课程表下面还有两级内容结构chapter章表section节表。chapter表字段是course_id、title、sortsection表字段是chapter_id、title、video_url、duration、sort。为什么课程内容要分成章和节两层因为真实教学场景里课程是按章节组织的视频挂在节上学员购买后学的是某门课的某章某节。如果不分层课程内容就是一长串视频列表既不符合用户习惯也无法支持章节维度的学习进度扩展。两层已经足够再加一层单元往往过度设计。3.3 订单与支付流水一笔订单怎么记才不糊涂订单表是管理后台里最容易设计出问题的表。course_order表的核心字段有order_no、user_id、course_id、total_amount、pay_type、status、create_time、pay_time。order_no是业务单号格式类似202507011030001234由代码生成展示给用户数据库层面加唯一索引。为什么不用自增id做订单号因为自增id外泄会暴露平台的日订单量运营数据不该这样泄露。这里有一个容易被忽视的关键约束user_id和course_id必须加联合唯一索引。这句话的意思是同一个用户对同一门课只能生成一单不管用户手速多快、前端点了几次下单按钮数据库层面就能挡掉重复订单。没有这个索引就只能靠代码查一遍再插入而并发场景下两次同时查询都查不到已有订单然后各插一条重复订单就这样产生了。联合唯一索引是幂等的底线代码里的判断只是优化体验。total_amount字段的值必须由后端从course表读取计算前端传过来的金额直接丢弃。3.4 运营位和评论审核类表设计的两个要点banner轮播图表和comment评论表看起来简单但它们体现了管理系统内容审核的设计思路。banner表字段有title、image_url、link_url、sort、status这个表的业务逻辑是首页展示什么内容、按什么顺序展示。运营人员调整sort字段就能改顺序调整status就能让某个Banner下线完全没有必要做删除操作。comment表字段是course_id、user_id、content、reply_content、status、create_time。status在这里承担审核功能0待审核、1通过、2驳回。前台只查status1的评论管理后台看到的是全部评论。这个设计的好处是不需要删除也可以实现不让用户看到某条评论——把status改为2即可数据还在审计可查。如果有营销风险内容需要彻底下架status置为2比物理删除更安全。评论和用户、课程表关联查询时用外连接展示nickname和course_title这是管理后台列表页的常规操作。3.5 核心表结构一览把上面几张核心表的职责关系汇总一下表名核心字段设计要点userusername, password, role, status统一三角色status控制登录course_categoryparent_id, name, sort, status两级分类树courseteacher_id, category_id, title, price, statusprice用decimalchaptercourse_id, title, sort课程下的章sectionchapter_id, title, video_url, duration视频挂在节上course_orderorder_no, user_id, course_id, total_amount, status联合唯一索引防重commentcourse_id, user_id, content, statusstatus做审核bannerimage_url, link_url, sort, status顺序和启停这套表结构大概只需要二十来行核心建表语句就能搭起来但每一张表的字段都是围绕业务闭环设计的。比起一开始就铺开几十张表我更倾向于让每张表都对应一个明确的业务概念后面扩展学习进度、优惠券、讲师分成时再增加表也不会伤到现有结构。4. 后端核心链路实现认证、课程与订单是怎么串起来的4.1 统一返回对象与全局异常前后端联调的第一块基石后端接口开发我习惯先定一套通用响应协议而不是写一个接口就自定义一堆字段。Result对象统一封装code、message、data三个字段成功返回Result.ok(data)失败返回Result.error(code, message)。code约定200成功400参数错误401未登录403无权限500系统异常。前端axios拦截器只需要判断code就能统一处理成功和错误分支不需要每个页面自己try-catch一堆业务异常。public class ResultT { private Integer code; private String message; private T data; public static T ResultT ok(T data) { ResultT r new Result(); r.code 200; r.message success; r.data data; return r; } public static T ResultT error(Integer code, String message) { ResultT r new Result(); r.code code; r.message message; return r; } }配合全局异常处理类用RestControllerAdvice统一捕获异常。业务异常直接抛出BizException框架异常和系统异常也在这里收口转成Result格式。这样做有一个很实际的好处接口永远不会返回非JSON结构前端处理逻辑单一日志里也能统一记录异常栈排查问题时一目了然。4.2 JWT登录鉴权的完整流程登录接口接收username和password校验通过后生成JWT返回前端。JWT的payload里只放userId、role两个必要信息不要放密码、手机号这类敏感数据。有效期我设置为2小时前端管理后台的使用强度足够过期后让用户重新登录。很多初学者会把token有效期设置成7天甚至30天对管理后台来说这是安全隐患管理员的身份凭证有效期越短越好。JWT校验我用拦截器实现注册到WebMvcConfigurer里。preHandle方法从请求头Authorization读取token解析失败或者过期直接返回401不放行。放行规则上/api/auth/login放行其余/api/**都拦截。为了业务代码里方便获取当前用户拦截器解析出userId后存入ThreadLocal后续Service层可以直接调用UserContext.getUserId()。这里有一个很多人容易忽略的点JWT的Header头在跨域请求时会有预检OPTIONS请求拦截器必须对OPTIONS请求直接放行否则前端跨域调接口会一直报403。public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } String token request.getHeader(Authorization); if (token null || !token.startsWith(Bearer )) { throw new BizException(401, 未登录或登录已过期); } Long userId JwtUtil.parseToken(token.replace(Bearer , )); UserContext.set(userId); return true; } }角色权限我用了更轻量的方案在需要管理员权限的接口上手动判断当前用户角色即可。比如课程删除接口里非admin直接抛403。对管理后台来说角色数量就三个引入Spring Security的复杂权限体系反而增加学习成本。4.3 课程分页查询的动态SQL多条件筛选的正确写法课程管理页是后台最常用的页面筛选项有课程名称、分类、状态、讲师。这种需求如果用逐字段判断来拼SQL代码会非常丑陋用MyBatis的XML动态SQL是最自然的方案。核心查询关联分类表动态拼接where条件然后分页。select idselectCoursePage resultTypecom.edu.entity.CourseVO SELECT c.id, c.title, c.price, c.status, c.cover_url, c.view_count, c.buy_count, c.create_time, cc.name AS categoryName, u.nickname AS teacherName FROM course c LEFT JOIN course_category cc ON c.category_id cc.id LEFT JOIN user u ON c.teacher_id u.id where if testkeyword ! null and keyword ! AND c.title LIKE CONCAT(%, #{keyword}, %) /if if testcategoryId ! null AND c.category_id #{categoryId} /if if teststatus ! null AND c.status #{status} /if /where ORDER BY c.create_time DESC LIMIT #{offset}, #{pageSize} /select写动态SQL时有两个细节值得注意。第一模糊查询不要直接写LIKE %${keyword}%要用CONCAT函数拼接参数因为${}直接拼字符串会有SQL注入风险。第二统计总条数的count查询和列表查询的where条件要完全一致所以要复制同样的动态SQL片段。我在Mapper里单独写了selectCourseCount虽然SQL看起来重复但分页总数必须精确省这一步后面前端页码就会错乱。4.4 课程上下架的级联事务状态机与校验规则课程上下架不是简单的update语句它涉及一个完整的事务校验课程内容是否完整、更新课程状态、可能还要刷新前台缓存或更新统计字段。我在Service层把上架逻辑拆成三步第一步查课程是否存在状态是否合法第二步查这门课下面是否有章章下面是否有节每个节是否有视频URL第三步才把状态改成已上架。如果第二步校验不通过直接抛异常整个事务回滚。为了保证这三步是一个原子操作Service方法必须加Transactional。这里要特别警惕事务失效的三个经典场景类内部方法自调用导致代理失效、方法不是public、异常被try-catch吞掉。前两个涉及Spring AOP原理第三个最隐蔽——如果业务代码里catch住异常没有重新抛出事务管理器根本感知不到异常回滚自然不会触发。我在代码里对所有Service层方法都贯彻一个原则业务异常用BizException抛出框架异常不做本地catch全部交给全局异常处理器。4.5 订单创建的幂等与金额校验后端必须守住的钱袋子订单创建接口是管理后台里最需要严谨对待的逻辑。接口流程是先根据courseId查出课程课程不存在或未上架则返回失败然后用当前userId和courseId查已存在订单查到就直接返回请勿重复下单最后创建待支付订单金额取course表里的price字段。这四步中查询已存在订单这个动作是用户无感知的幂等屏障配合数据库的联合唯一索引双保险。课程下架后还能下单吗不能需要先校验status。用户被禁用后还能下单吗不能需要在流程最前面校验用户状态。这些校验放在Service层统一处理Controller保持轻薄。支付回调的代码我会再校验一次订单状态只有待支付订单才能变成已支付已支付订单遇到重复回调直接忽略。这套状态机虽然简单但能保证账目不出错。5. Vue前端的关键落点请求封装、路由守卫与课程管理页5.1 工程初始化与目录结构前端工程用create-vite初始化选Vue3 JavaScript。TypeScript当然也是好选择但对以业务为主的中后台项目JS的直白和低心智负担更合适源码看起来门槛也更低。安装完基础依赖后src目录下我按api、router、store、views、components、utils来组织。api目录统一放请求函数views目录放页面components放复用组件。这样的目录结构看一眼就知道该找哪个文件对于后期维护和交给别人接手都很重要。main.js里注册router、pinia、element-plus。Element Plus建议完整引入虽然打包体积大一点但省去按需引入的配置折腾。管理后台本身就很少关心首屏体积完整引入的稳定性和便利性更值得。5.2 axios封装token注入与401统一处理axios不封装直接用到了联调阶段会非常痛苦因为每个请求都要手动塞token、手动处理错误提示。我在utils/request.js里创建axios实例baseURL设为/api请求拦截器自动从localStorage取token并设置Authorization头响应拦截器统一处理业务码。import axios from axios import { ElMessage } from element-plus import router from /router 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( response { const res response.data if (res.code 200) { return res.data } ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) }, error { if (error.response?.status 401) { localStorage.removeItem(token) router.push(/login) } ElMessage.error(网络异常或登录过期) return Promise.reject(error) } ) export default request这里有两个细节响应拦截器直接返回res.data这样页面里拿到的就是纯数据不用每页再剥一层code401时统一跳登录页面代码不用关心登录过期逻辑。开发期配好Vite的proxy之后前端请求路径统一写/api完全不需要知道后端在哪台服务器。5.3 路由守卫未登录拦截和角色控制Vue Router的路由守卫是前端权限控制的骨架。我在routes的meta字段上声明两个属性requiresAuth表示这个页面是否需要登录roles数组表示允许访问的角色。router.beforeEach里按顺序做判断目标页是登录页直接放行没有登录且需要登录就重定向到/login登录了但角色不在允许列表里就重定向到首页。核心判断逻辑如下router.beforeEach((to, from, next) { const token localStorage.getItem(token) const role localStorage.getItem(role) if (to.path /login) { next() return } if (!token to.meta.requiresAuth) { next(/login) return } if (token to.meta.roles !to.meta.roles.includes(role)) { next(/dashboard) return } next() })为什么用localStorage而不是把状态放在内存里因为页面刷新后内存清空用户身份也就丢了体验很差。token存localStorage有XSS风险但只要项目里不用v-html渲染不可信内容这个风险是可控的。路由守卫和axios的401跳转形成双保险一个管页面入口一个管接口响应。5.4 课程管理页表格、弹窗、校验的完整交互课程管理页的操作流程是进入页面自动拉取分页列表 - 在表格顶部操作栏选择筛选条件 - 点击新增按钮弹出Dialog表单 - 填写课程信息并保存 - 列表刷新。表格我用el-table配el-pagination筛选区是el-input、el-select的组合。新增和编辑复用同一个Dialog表单组件只是打开时初始数据不同表单校验规则在data里用rules声明必填项分别是课程标题、归属分类、价格、状态。完整性上要注意一个体验点分页组件切换页码时搜索条件必须随当前状态一起更新。有些实现里只要搜索就重置为第1页而翻页又丢失搜索条件这都属于交互细节没打磨到位。我做了一个搜索表单对象和一个页码状态搜索时pageNum重置为1翻页时带上搜索表单参数再次请求。表单提交后刷新列表时保持当前页码不清零避免用户编辑完一条数据后跳回第一页又要翻很久才能回到刚才的位置。6. 联调部署踩坑记录跨域、上传、时区一个都没跑掉6.1 跨域开发期交给Vite proxy生产期交给Nginx前后端分离项目联调阶段最常见的问题就是跨域。开发期不要在前端代码里把axios的baseURL写成http://localhost:8080而应该保持/api的相对路径然后在vite.config.js里配置proxy把/api转发到后端服务export default defineConfig({ server: { port: 3000, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })这样配置之后浏览器的请求始终是同源的http://localhost:3000/api/xxx由Vite开发服务器转发到8080端口彻底规避了浏览器跨域限制。开发期我甚至不需要在后端写CORS配置这能少踩很多坑。生产部署时前端npm run build生成的dist目录交给Nginx托管然后同样配置一个反向代理把/api转发到后端服务。后端jar包监听8080Nginx监听80前端页面和接口都走同一个域名同样是同源访问。这里我踩过一次的坑是代理路径配置错误导致接口404后来确定转发规则为location /api/ { proxy_pass http://127.0.0.1:8080; }记得proxy_pass结尾不加路径直接把原路径带过去才能保证后端接口路径一致。6.2 文件上传本地目录还是对象存储管理端涉及两个上传场景课程封面图片和视频文件。开发阶段最简单的方案是后端接收MultipartFile校验文件大小和类型用UUID重命名文件后保存到本地目录然后返回可访问的URL。图片大小限制我设为2MB视频根据机构上传的内容时长可能比较大但管理端演示阶段一般也不会传超大文件所以限制在200MB以内即可。项目里把上传保存目录配置在application.yml的upload.path字段里通过自定义静态资源映射把该目录暴露成URL访问。上生产环境就不建议放本地磁盘了有两个原因服务器磁盘空间有限且单机存储难以扩展。更稳妥的方案是接入对象存储服务逻辑不变只是把保存文件换成上传到存储桶。如果要完全自建MinIO是很合适的替代品社区版就够用。这里有一个我在真实项目里踩过的坑不要把上传目录放在Linux服务器的/tmp下面系统会定期清理该目录过几天用户头像就全部变成404了。正确做法是放在应用家目录下的data目录并单独配置磁盘空间监控。6.3 数据库时区与JSON序列化日期差8小时的两个修复点日期时间问题是这种系统几乎必踩的坑。数据库连接串没带serverTimezone时数据库默认使用服务器时区国内服务器和应用有时差时查询出来的时间就会差8小时。我的解决方案是在jdbc连接串里明确写上serverTimezoneAsia/Shanghai。另一个坑在Jackson序列化后端返回的LocalDateTime默认序列化格式是数组或ISO格式前端显示成2025-04-01T10:30:00非常难看。在application.yml里配置统一的日期格式spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8配置完这两处前后端传时间就不会再出现凌晨两点新建的课程显示在昨天这种诡异现象。排查这类问题最快的方式是直接在MySQL控制台执行SELECT NOW()看一下数据库当前时间是否和服务器一致定位到底是数据库时区问题还是Java序列化问题。6.4 部署流程和Nginx配置模板后端部署就是传统的jar包启动。执行mvn clean package打包得到target目录下的jar上传到服务器后用java -jar命令启动。我习惯加上nohup和让进程后台运行并把日志重定向到应用日志文件生产环境还可以用systemd来管理进程实现开机自启和异常重启。前端部署更简单npm run build后把dist目录上传到Nginx的html目录即可。server { listen 80; server_name edu.example.com; location / { root /opt/edu/dist; index index.html; 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; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }Nginx配置里最容易错的是history模式下刷新页面404所以try_files指令就派上用场了请求路径找不到对应文件时统一回退到index.html交给前端路由处理。这是Vue Router用history模式部署在Nginx上必须配置的否则用户一刷新浏览器就白屏。7. 从零到一复现源码环境配置与启动排错清单7.1 环境清单与版本建议想把这套源码完整跑起来建议先核对环境版本。JDK8或JDK11都行Maven用3.6以上MySQL用8.0Node最好16以上IDEA或者VSCode取决于你的习惯。这几个版本组合是我实测下来兼容性最好的不需要额外处理奇怪的环境问题。7.2 数据库初始化执行脚本是第一步拿到源码后第一步不是启动后端而是初始化数据库。创建数据库db_edu字符集选utf8mb4然后执行项目里的sql/init.sql脚本。这个脚本里建了表结构并预置了基础数据一个admin账号、一个讲师账号、若干个学员账号、几门测试课程、几条测试订单。执行成功的标志是在Navicat或命令行里能看到全部表并且user表里有管理员账号。如果脚本执行报错优先检查字符集设置和SQL文件编码比较常见的是Windows下SQL文件被以GBK编码读取导致中文乱码。7.3 后端启动步骤改配置、启动、看日志修改application.yml里的数据库地址、账号、密码确认配置无误之后直接运行启动类。启动成功的标志是控制台出现Started Application in xx seconds。如果启动过程中报错按这个思路排查数据库连不上先ping通网段再确认用户名密码端口被占用检查8080是不是被其他进程占了或者直接换个端口MyBatis的XML映射找不到确认mybatis.mapper-locations是classpath:mapper/*.xml且文件确实在resources/mapper目录下。启动之后别急着调前端先用Postman或浏览器访问http://localhost:8080/api/auth/login传一个正确账号密码如果返回JSON里有token说明后端已经通了。这一步单独验证很有价值能把后端问题在前端联调之前先隔离掉。7.4 前端启动步骤装依赖、配代理、访问页面进入admin-ui目录先执行npm install安装依赖。国内网络环境下建议加registry镜像源否则下载要好几分钟甚至失败。安装完成后执行npm run devVite会启动开发服务器并打印访问地址。打开浏览器访问该地址如果能打开登录页用admin账号登录成功后进入仪表盘和管理菜单。第一次登录成功后建议把常见业务链路走一遍新增课程分类、创建一门课程、添加章节和小节、把课程上架、在订单列表看到测试订单。走完这一套你基本就把后端事务、前端表格交互、权限路由这些关键代码都过了一遍。如果某个环节卡住优先看浏览器Network面板里接口返回的JSON错误信息那是定位问题最直接的方式。7.5 启动排错速查表症状可能原因修复方向后端启动报数据库连接失败数据库地址/账号/密码错误、MySQL没启动检查application.yml配置确认MySQL已启动前端页面接口403没有登录或token已过期清掉localStorage重新登录前端页面接口404Vite proxy或Nginx代理路径不对检查proxy配置和context path日期相差8小时连接串缺serverTimezone或Jackson未配置时区补上Asia/Shanghai配置上传文件后无法访问静态资源映射未配置或目录被系统清理自定义资源映射目录避免/tmpnpm install超时网络原因或registry问题配置阿里镜像源后重试这些报错我在帮别人排查时遇到的次数非常多基本上90%的问题集中在配置项代码本身反而很少出问题。这也侧面说明这一版源码的稳定性还是可以的。回到最初那句话这类管理系统真正难的不是增删改查而是把状态、幂等、事务这些底层的约束吃透。我写这套系统时最深的体会就是表结构就是系统的骨架骨架正了后面的代码怎么写都比较顺骨架歪了接口写一百个也难受。建议拿到源码之后不要只满足于跑起来试着把下单接口并发打两遍看看数据库联合唯一索引到底怎么挡住重复订单再试着把上架流程里的章节校验去掉看看事务回滚是不是真的生效。这些操作比单纯看代码更有价值也更有意思。往后要扩展时可以加上学习记录表、优惠券表、讲师分账表技术栈完全不用换这套骨架就能撑起一个真正商用的在线教育后台。

相关新闻

汽车贴膜服务哪家合适?2026年度牡丹江门店工艺对比与选购技巧解析

汽车贴膜服务哪家合适?2026年度牡丹江门店工艺对比与选购技巧解析

汽车贴膜服务的行业认知基础汽车贴膜早已不是简单的贴一层塑料,而是一项融合材料科学、施工工艺与售后保障的系统工程。近年来,随着车主对车漆保护、车内隐私与驾驶舒适性的需求不断提升,贴膜服务从少数车主的选择逐渐演变为大众化的用车升级…

2026/10/11 8:59:44 阅读更多 →
SpringBoot宿舍管理系统:从需求分析到部署上线的完整实战

SpringBoot宿舍管理系统:从需求分析到部署上线的完整实战

在这个行当里泡久了,每年到毕业季,总有学弟学妹拿着同一个问题来找我:“学长,我毕设题目是宿舍管理系统,用SpringBoot,应该怎么做?”大家的问题往往出奇的一致:不是不知道要做什么&a…

2026/10/11 8:59:44 阅读更多 →
eCall 多认证组合测试可行性,认证办理路径梳理

eCall 多认证组合测试可行性,认证办理路径梳理

做欧盟车载项目的,常卡在一个问题:eCall 到底是单独办,还是能顺着整车认证一起走。这问题问得对。eCall 从根上长在欧盟整车型式批准(WVTA)体系里,它不是一张能脱离整车单独存在的证,但核心部件…

2026/10/11 8:59:44 阅读更多 →

最新新闻

集成学习7天实战:Bagging、Boosting与Stacking全攻略

集成学习7天实战:Bagging、Boosting与Stacking全攻略

简介:由杰森布朗利编写的《Python集成学习算法7天速成》中文翻译版,源自MachineLearningMastery系列,面向已有Python、NumPy和scikit-learn基础的开发者,用七节课快速补齐集成学习核心技能。资源共1个PDF文件,压缩包大…

2026/10/11 9:43:47 阅读更多 →
impeccable项目拆解:从零搭建代码质量检查工具与工程实践

impeccable项目拆解:从零搭建代码质量检查工具与工程实践

1. 一个词引发的产品思维:为什么“impeccable”值得单独拿出来做第一次看到“impeccable”这个词被单独拎出来当作项目标题,我的直觉是:这要么是一个强迫症级别的代码规范工具,要么是一个追求极致体验的产品设计系统。不管是哪种&…

2026/10/11 9:43:47 阅读更多 →
本地大模型部署全攻略:从 0 到 1 玩转 Ollama 与 TaoToken 统一接入

本地大模型部署全攻略:从 0 到 1 玩转 Ollama 与 TaoToken 统一接入

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 9:43:47 阅读更多 →
【合作期刊推荐 | AP出版 | 线上会议 | 高录用快见刊,知网谷歌学术 | 主题不设限,管理、计算机等人文社科交叉主题均可】第六届管理科学和软件工程国际学术会议(ICMSSE 2026)

【合作期刊推荐 | AP出版 | 线上会议 | 高录用快见刊,知网谷歌学术 | 主题不设限,管理、计算机等人文社科交叉主题均可】第六届管理科学和软件工程国际学术会议(ICMSSE 2026)

第六届管理科学和软件工程国际学术会议(ICMSSE 2026) The 6th International Conference on Management Science and Software Engineering 2026年10月23日(线上会议) 高录用快见刊,知网&谷歌学术 | 主题不设限,管理、计算…

2026/10/11 9:43:47 阅读更多 →
小米手环数据自动化导出:从SQLite到CSV的完整方案

小米手环数据自动化导出:从SQLite到CSV的完整方案

简介:面向小米手环用户与数据取证爱好者的自动化导出工具包,通过解包Zepp Life软件直接读取底层数据库,帮助查看手环记录的具体健康数据,并支持后续二次分析与归档。资源共7个文件,由6个Python脚本和1个带注释的JSON5数…

2026/10/11 9:43:47 阅读更多 →
金融知识图谱构建实战:Neo4j+Python+Cypher完整指南

金融知识图谱构建实战:Neo4j+Python+Cypher完整指南

简介:一份面向金融领域的知识图谱构建项目源码包,基于Neo4j图数据库、Python与Cypher查询语言完成。项目代码完整、结构清晰,包含从数据采集到知识存储的完整链路,适合高校计算机、人工智能、金融科技等相关专业学生用于期末大作业…

2026/10/11 9:42:47 阅读更多 →

日新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/11 0:00:27 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/11 0:00:27 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/10 5:23:50 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/9 21:32:20 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/10 10:38:42 阅读更多 →