简介:这是基于SpringBoot与Vue前后端分离架构的企业级在线学习平台源码包,面向教育培训机构、独立开发者及高校实训项目,解决课程管理、学员管理、考试测评、直播互动、作业提交、成绩统计和证书生成等全链路教学管理需求。压缩包共701个文件,约31.2MB,以170个Vue页面组件、146个Java后端类、162个SVG图标及PNG/JPG素材为主,辅以JS、CSS、XML配置和数据库脚本,同时包含多终端适配的静态资源,便于直接编译部署或二次开发。目前已有122人学习/下载。通过该资源可获取完整的项目工程结构、前后端分离实现思路、用户与权限管理方案、数据统计与可视化展示模块,以及移动端适配的样式与交互设计;对正在开发在线教育产品、或需要参考SpringBoot+Vue企业级实践的中高级开发者,具有较高参考价值。
1. 教育培训小程序:一套 SpringBoot + Vue 前后端分离的全链路在线学习平台到底值不值得下
做教育类项目最怕什么?不是功能不够多,而是课程管理、学员管理、考试测评、直播互动、作业提交、成绩统计、证书生成这一整条链路,散落在七八个系统里,数据互相不通。这套基于 SpringBoot 和 Vue 技术栈的前后端分离项目,就是把在线教育平台的主干功能一次性凑齐了,而且天然适配微信小程序、H5 和 PC 多终端。对正在做毕业设计、接私活、或者公司要快速搭教育产品 demo 的人来说,这份资源的价值在于:不用从零设计表结构,不用纠结权限模型怎么建,直接拿着改就能跑。SpringBoot 负责后端接口和业务逻辑,Vue 负责管理后台和前端展示,小程序端通过 API 对接同一套后端,属于典型的互联网行业主流前后端分离架构。适合有一定 Java 和 Vue 基础、想快速看到完整项目效果的人,纯新手需要先补一补 SpringBoot 注解和 Vue 组件通信的基础知识再看代码,否则容易被目录结构劝退。
2. 拆解项目骨架:SpringBoot 后端分层设计与 Vue 前端工程化结构
2.1 后端分层架构:Controller-Service-Mapper 三层如何支撑教育业务
这套项目的后端采用标准的 SpringBoot 分层架构,Controller 层只做参数接收和结果封装,Service 层承载业务规则,Mapper 层通过 MyBatis 操作 MySQL 数据库。代码包结构大致是controller、service、mapper、entity、dto、config、util这几类。实际开发教育类项目时,我一般会在 Service 层再拆出impl子包,避免接口和实现混在一起——这个习惯在企业级项目里几乎是标配,因为后期做单元测试和 AOP 日志切面时,面向接口编程会顺手很多。
学员注册登录这块用的是 JWT(JSON Web Token)无状态鉴权方案,用户登录成功后后端签发 token,前端存储后每次请求在请求头里带上。相比 Session 方案,JWT 天然适合前后端分离和多终端场景,小程序端、Web 端共用同一套认证逻辑。需要注意jjwt库的版本兼容性问题,0.9.x 和 0.11.x 的 API 有差异,比如setClaims和setSubject在 0.11.x 中被ClaimsBuilder取代,依赖注入方式也变了。如果你拿到手的是 0.11.x 版本,私钥必须用Keys类生成,不能直接传字符串。
课程模块是整个平台的核心。课程表course和章节表chapter是一对多关系,章节里挂视频资源 URL,前端通过播放器加载。课时表lesson再挂在章节下面,形成三级结构。这种设计符合主流在线教育产品的内容组织方式,比如慕课网、腾讯课堂都是这个思路。后端在返回课程详情时,如果一次性全查出来,SQL 会写得很难看,我一般会分成两个接口:getCourseInfo查课程基本信息,getChapterTree查章节和课时列表,前端分开请求、按需加载。
@Service public class CourseServiceImpl implements CourseService { @Autowired private CourseMapper courseMapper; @Autowired private ChapterMapper chapterMapper; @Override public CourseDetailVO getCourseDetail(Long courseId) { Course course = courseMapper.selectById(courseId); if (course == null) { throw new BusinessException(ResultCode.COURSE_NOT_EXIST); } // 查询章节树,包含课时列表 List<ChapterVO> chapterTree = chapterMapper.selectTreeByCourseId(courseId); CourseDetailVO vo = new CourseDetailVO(); BeanUtils.copyProperties(course, vo); vo.setChapterTree(chapterTree); return vo; } }这段代码有几个点值得留意:BusinessException是自定义异常,配合全局异常处理器@RestControllerAdvice统一返回{code: xxx, message: xxx}格式,前端拿到非 0 的 code 直接弹提示。BeanUtils.copyProperties是 Spring 自带的属性拷贝工具,可以少写十几个 setter,但要注意字段名必须一致,courseId和course_id这种下划线风格在 Java 属性里不会被识别,需要靠 MyBatis 的mapUnderscoreToCamelCase配置来转换。
2.2 Vue 管理后台与小程序端:同一套 API 如何支撑多终端
前端部分管理后台用的是 Vue + Element UI,页面结构非常典型:侧边栏菜单 + 顶栏面包屑 + 主内容区。路由懒加载通过import()函数实现,这是 Vue 项目性能优化的基础操作,否则首屏加载会把所有 js 包全拉下来,白屏时间能到三四秒。这个项目把路由拆成了constantRoutes和asyncRoutes,前者是登录页、404 页这种不需要权限的,后者根据用户角色动态挂载——这个设计在做权限控制时很好用,后端返回角色标识,前端通过addRoutes动态注入。
小程序端如果是基于 uni-app 写的,那代码里会大量出现<view>、<text>这种跨端组件,一套代码可以编译到微信小程序、H5、App。如果用的是原生微信小程序,那wx.request封装请求是基本功,需要在success回调里统一处理 token 过期跳转登录页的逻辑。这里要特别注意小程序端的域名白名单问题:微信公众平台后台必须配置 request 合法域名,否则真机调试时请求直接失败,而且开发工具里勾选「不校验合法域名」只能在开发阶段生效,上线前必须在 mp 后台把 HTTPS 域名配上。
多终端的核心是 API 层复用。后端接口通过 CORS 配置允许跨域,前端用 axios 实例统一管理 baseURL,小程序端用uni.request或wx.request各自封装。三个端共用同一套后端接口,意味着任何一个接口变动,三个端都要回归测试,所以后端接口设计时一定要考虑兼容性——比如新增字段用@JsonInclude(Include.NON_NULL)注解,null 值不返回,前端就不会因为多了字段而报错。
2.3 数据库设计:教育业务的核心表结构拆解
数据库这块是这套资源最值钱的部分。课程表、用户表、订单表这三张是基础,但真正体现教育行业特点的是这几张表:exam_paper(试卷表)、exam_question(题目表)、exam_record(考试记录表)、homework_submission(作业提交表)、user_certificate(用户证书表)。
考试模块的表设计有个容易踩坑的点:试卷和题目是多对多关系,很多新手会直接建一张关联表就完事,但这套项目里还多了一张exam_paper_question中间表,额外记录了每题的分值score和排序sort_order。原因很简单——同一道题在不同试卷里可能分值不一样,你必须把分数存在关联关系上,而不是存在题目表里。考试记录表exam_record记录了学员的答题开始时间、结束时间、得分和答题详情,答题详情一般用 JSON 格式存储,字段类型是text,内容包含题目 ID、用户答案、是否正确。这个设计在数据量上来之后会有点尴尬——想按题目维度统计分析答题正确率,得先把 JSON 解析出来,做不了高效的 SQL 聚合。所以如果项目要长期运营,建议增加一张exam_answer_detail明细表,一行一道题的作答结果,后续出学情分析报告会方便得多。
3. 跑通业务流程:学员从选课到拿证书的完整链路实现
3.1 课程下单与学员管理:订单状态机与权限校验
课程购买流程是一个典型的状态机:待支付、已支付、已取消、已退款。下单接口创建订单记录并返回订单号,前端唤起微信支付,支付结果通过回调通知后端,后端更新订单状态并给学员开通课程权限。项目里如果内置了模拟支付逻辑,通常在PayService里有一个mockPay方法,本地环境直接调用接口模拟支付成功。需要注意的点是:幂等处理必须做,支付回调可能因为网络原因重试多次,后端要以order_no作为唯一键做去重校验,防止重复开通课程权限。
学员管理模块的权限校验逻辑要围绕「课程访问权」来做。常见做法是建一张user_course关联表,记录学员和课程的绑定关系,当学员访问课程详情或播放视频时,后端校验该学员是否在关联表里有有效记录。这里容易漏掉的场景是:免费课程和付费课程的权限判断路径不同,免费课程谁都能看,付费课程必须先查订单状态再查关联关系。如果项目里把这层逻辑写在同一个接口里,一定会有 if-else 分支处理课程类型,写代码时要注意分支覆盖率,测试用例要覆盖三种情况:免费课程、已购付费课程、未购付费课程。
@GetMapping("/course/{courseId}/access") public Result<Boolean> checkAccess(@PathVariable Long courseId, @RequestHeader("token") String token) { Long userId = jwtUtil.getUserIdFromToken(token); Course course = courseService.getById(courseId); if (course.getIsFree() == 1) { return Result.success(true); } UserCourse userCourse = userCourseMapper.selectByUserIdAndCourseId(userId, courseId); return Result.success(userCourse != null && userCourse.getStatus() == 1); }这段代码把免费课程分支放在最前面直接返回,逻辑直观。@RequestHeader("token")从请求头里取 token,再由jwtUtil解析出用户 ID——这是前后端分离项目的标准姿势。真正生产环境里,token 解析这一步骤通常会做成拦截器或 AOP 切面,避免每个 Controller 方法里重复写解析代码。项目里如果使用了 Spring MVC 的HandlerInterceptor,登录校验和权限校验会统一在preHandle方法里处理,Controller 层只需要关注业务参数。
3.2 直播互动与作业提交:实时消息和文件上传的落地方案
直播互动模块包含直播房间管理、聊天消息、签到、连麦等功能。聊天消息如果用轮询方案实现,前端每隔几秒请求一次后端接口,代码简单但体验差——消息延迟高、服务器压力大。主流方案是 WebSocket 长连接推送,SpringBoot 里可以用 Spring WebSocket 或 Netty 实现。如果项目用的 Netty,那需要关注断线重连、心跳检测、消息序列化这几个点;如果用的 Spring WebSocket,在 SpringBoot 2.7.x 版本下WebSocketConfigurer注册拦截器的方式比较直接,但 3.x 版本里 Spring Security 的配置方式有调整,保安规则写错会导致握手失败。
作业提交模块的核心是文件上传。视频、文档、图片,这些文件的存储不能放在应用服务器本地,否则打包部署时文件丢失、磁盘扩容麻烦。生产环境优先考虑阿里云 OSS、腾讯云 COS 或 MinIO。项目里如果用的是 OSS,上传流程通常是前端直传 OSS——前端从后端获取临时签名 URL,然后直接 PUT 文件到 OSS,好处是文件不经过应用服务器,减轻带宽和内存压力。如果项目里后端是通过 MultipartFile 接收文件再转存到 OSS,代码层面要注意文件大小限制的配置:
spring: servlet: multipart: max-file-size: 200MB max-request-size: 500MB我的建议是给作业附件设置 200MB 上限,课程视频不通过这个接口上传,而是走独立的视频上传通道。文件存储路径的命名规范也要提前定好:按业务类型/日期/随机文件名分目录,避免所有文件堆在一个目录下——OSS 虽然不担心目录性能,但按日期分目录方便后续做冷热分层存储清理。
3.3 成绩统计与证书生成:数据聚合与 PDF 导出的实现细节
成绩统计模块要完成三个维度的数据展示:学员个人成绩趋势、课程平均分、题目正确率。SQL 聚合查询是核心手段,典型的写法是GROUP BY课程维度汇总平均分和通过率。这里有个常见的性能陷阱:如果按学员查全量考试记录再做内存计算,学员数量一上来内存直接爆掉。SQL 层的聚合是最优解,数据库引擎做分组统计的效率远高于应用层循环。
SELECT c.course_name, COUNT(DISTINCT er.user_id) AS student_cnt, AVG(er.score) AS avg_score, SUM(CASE WHEN er.score >= 60 THEN 1 ELSE 0 END) / COUNT(*) AS pass_rate FROM exam_record er JOIN course c ON er.course_id = c.id WHERE er.exam_time BETWEEN #{startTime} AND #{endTime} GROUP BY er.course_id ORDER BY avg_score DESC这条 SQL 通过JOIN course拿到课程名称,通过COUNT(DISTINCT ...)统计参考人数规避重复记录,通过SUM(CASE WHEN ...)计算及格人数占比——这种写法比先查所有记录再到 Java 里算要高效得多。唯一要注意的是:成绩表数据量大之后,BETWEEN时间过滤条件需要走索引,建议在exam_time字段上建普通索引,避免全表扫描。如果项目里还提供了导出 Excel 的功能,通常用 EasyExcel 或 POI,模板需要固定格式的话建议用 EasyExcel 的复杂模板填充能力,导出的大文件考虑异步生成、生成完成后推送下载链接,避免同步导出阻塞网关线程。
证书生成模块用的是 PDF 方案。学员完成课程且考试通过后,后端调用证书生成服务,把学员姓名、课程名称、证书编号渲染到 PDF 模板上,输出到 OSS,同时在user_certificate表插入一条记录。证书编号的生成规则一般是日期 + 随机数 + 课程 ID,要保证全局唯一,直接用 UUID 最简单但不够美观,用雪花算法生成纯数字 ID 更正式。PDF 模板渲染用 iText 或 Adobe Acrobat 预制的表单模板,需要注意中文字体问题——iText 默认不支持下中文字体,必须加载系统中文字体文件,比如宋体或黑体 TTF,否则证书上中文会全部变成方块。
4. 多终端适配实施:小程序端和后台管理端的联调要点
4.1 小程序端请求封装与登录态同步
小程序端联调是前后端分离项目最容易翻车的环节。第一个坑就是请求封装,原生小程序wx.request不支持拦截器,但可以通过包装一个request.js工具函数来实现统一处理。核心逻辑是:请求前检查本地 token,请求中在 header 里带上Authorization字段,响应后统一判断状态码。
function request(url, method, data) { return new Promise((resolve, reject) => { const token = wx.getStorageSync('token') wx.request({ url: getApp().globalData.baseUrl + url, method: method, data: data, header: { 'Content-Type': 'application/json', 'Authorization': token ? 'Bearer ' + token : '' }, success: (res) => { if (res.data.code === 0) { resolve(res.data.data) } else if (res.data.code === 401) { // token 过期,清空本地登录态,跳转登录页 wx.removeStorageSync('token') wx.navigateTo({ url: '/pages/login/login' }) reject(res.data) } else { wx.showToast({ title: res.data.message, icon: 'none' }) reject(res.data) } }, fail: (err) => { wx.showToast({ title: '网络异常', icon: 'none' }) reject(err) } }) }) }这段封装代码可以说很关键,统一处理了 401 未授权的情况,避免每个页面都写一遍 token 过期跳转登录。getApp().globalData.baseUrl是全局配置的小程序后端地址,在app.js里维护,切换测试环境和生产环境时只需要改这一个地方。真机调试时要注意的是,本地开发的 SpringBoot 服务监听的是localhost,手机访问不到,必须把后端部署到服务器或使用内网穿透工具暴露公网地址,然后在微信开发者工具「详情 - 本地设置」里勾选「不校验合法域名」才能调通。
4.2 后台管理端权限控制与动态路由
管理后台的权限控制如果只做了菜单隐藏,那只能叫「界面屏蔽」——真正安全的权限控制必须在后端接口上做校验。项目里如果使用了 Spring Security 或 Shiro,接口级别的权限通过@PreAuthorize("hasRole('ADMIN')")这类注解来控制。管理后台前端部分,菜单是根据用户角色动态生成的:登录接口返回角色标识,前端根据角色标识过滤路由表,再用router.addRoutes动态挂载菜单。这里有一个系列问题:addRoutes添加的路由刷新页面后会丢失,因为 Vue Router 的路由表是内存态,刷新后重新加载了空的静态路由。常见的解决思路是把路由表存到sessionStorage,刷新时重新挂载,或者在main.js入口处每次启动都请求一次用户信息再生成路由表。
需要注意权限变更的处理:用户角色被修改后,前端已经挂载的动态路由不会自动移除,必须通过resetRouter清空重新挂载。如果项目里没有这个逻辑,权限降级的用户仍然能看到高权限菜单——这个属于安全隐患,企业级项目一定会处理。
4.3 多终端适配的联调技巧:统一错误码与 Mock 数据
三个端共用一个后端,错误码规范必须统一。最常见的规范是code = 0表示成功,非 0 表示业务异常,401表示未登录或 token 过期,500表示系统异常。前端各端只认 code,不认 HTTP 状态码——这个设计的好处是:后端返回业务错误时 HTTP 状态码仍然是 200,但 code 是 40001 之类,前端能统一处理弹 toast,不会被全局异常拦截器干扰。
联调阶段的 Mock 数据可以这么处理:后端接口还没开发完时,前端在request.js里加一个开关控制是否走本地 mock 数据,比如拦截 URL 匹配/mock/前缀的请求。避免低级错误:mock 数据的数据结构和真实接口不一致,字段名大小写不一样,导致联调时前端取值取不到。我的习惯是 mock 数据直接放在 JSON 文件里,字段名严格按后端接口文档来,至少保证结构对得上。
5. 避坑指南:这套教育平台项目最常见的 8 个运行问题
5.1 程序包org.springframework.transaction.annotation.Transactional不存在
现象:Maven 编译报错,引不到@Transactional注解的包。原因:项目里使用了spring-boot-starter-web依赖,但事务相关的spring-tx没有显式依赖,某些 SpringBoot 版本里默认不是全量引入。解决:在pom.xml里显式添加spring-boot-starter-jdbc依赖,它会传递引入spring-tx,或者直接添加spring-tx依赖。此外注意检查是否 JDK 版本太高(17+),高版本 JDK 可能需要升级 SpringBoot 版本,SpringBoot 2.x 在 JDK 17 下运行常出现 CGLIB 代理相关报错。
5.2 小程序端请求报ERR_CERT_COMMON_NAME_INVALID
现象:真机调试时,所有请求都失败,错误信息提示证书域名不匹配。原因:小程序要求的合法域名必须是在微信公众平台配置过的,本地开发用的 IP 访问不在白名单里,或者 HTTPS 证书是自签名的。解决:开发阶段在详情 - 本地设置勾选「不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书」,调试完成上线前务必将后端域名配置为已备案且绑定正规证书的域名,并通过微信公众平台添加 request 合法域名。不能图省事把「不校验」勾选带到生产环境,用户会全部请求失败。
5.3 前端 npm run dev 启动失败,vue-cli-service不是内部或外部命令
现象:克隆项目后npm run dev直接报错,提示找不到vue-cli-service。原因:项目依赖没有安装完整,尤其是 node_modules 目录缺失,或者是 Node 版本和vue-cli-service要求的版本不兼容,node-sass在 Node 16+ 下基本都会编译失败。解决:先执行npm install(建议用淘宝镜像源npm config set registry https://registry.npmmirror.com),再把 Node 切换到 14.x LTS 或 16.x(Node 18 有些老项目确实跑不起来)。如果node-sass报错,换成sass或sass-loader的兼容版本。
5.4 视频播放到一半卡住或加载失败
现象:前端播放器加载视频 URL 时,PC 端正常,小程序端不能播;或者拉流地址只能播几分钟。原因:小程序播放视频需要的m3u8地址必须支持 HTTPS,且播放器组件有格式限制。视频拉流用video标签的 HLS 格式,PC 端 Chrome 对 HLS 的原生支持有限。解决:选择支持 HLS 的播放器库,PC 端用hls.js播放 m3u8,小程序端用<video>组件加载 m3u8 链接。后端视频存储地址要保证跨域访问正常,OSS 需要设置 CORS 规则允许GET请求,否则播放器请求视频分片时会被浏览器拦截。在国际化版本或大视频处理场景下,可以考虑用腾讯云点播或者阿里云 VOD 做转码,m3u8 地址会稳定很多。
5.5 考试提交后成绩为 0,但exam_record表有记录
现象:学员答题完成,提交试卷后成绩显示 0 分,查表发现记录存在但score字段为 0。原因:提交试卷时,判分逻辑遍历的是试卷题目列表,但前端提交的答题 JSON 里字段名不匹配——比如前端传的是question_id,后端实体用的是questionId,导致后端读取答案失败,每题都判错得 0 分。解决:提交时后端用 DTO 接收参数,DTO 上标注@JsonProperty("question_id")映射前端字段,或者要求前端统一用驼峰命名传参。建议在判分代码后打印一条日志:log.info("submit exam: userId={}, paperId={}, answered={}, score={}", userId, paperId, answeredCount, score);这样成绩异常时可以直接查日志定位。
5.6 直播聊天室消息堆积,页面卡死
现象:直播间在线人数超过 50 人之后,前端聊天区出现明显卡顿,消息延迟 5 秒以上。原因:前端实时渲染全部消息 DOM,每条消息都插入一个节点,不做列表裁剪。WebSocket 后端给所有人广播全量消息,在线人数越多消息吞吐越大。解决:前端聊天列表做虚拟滚动或只保留最近 200 条消息,超出部分从 DOM 中移除。后端增加消息合并策略:高频消息按 500ms 时间窗口合并成一条批量消息发送,避免逐条广播造成的 IO 压力。
5.7 作业附件上传成功,但预览打不开
现象:作业附件上传后,列表里能看到文件名,点击预览报 403 或 404。原因:文件上传到了后端本地磁盘目录,但临时目录被系统清理了;或者 OSS 文件的访问权限设置了私有读写,没有生成签名 URL。解决:如果文件存 OSS,上传完成后用bucket.signUrl(fileUrl, 3600)生成临时访问链接供前端预览,链接有效期根据业务需求设定,作业批改场景一般 1-2 小时足够。如果文件存本地磁盘,把文件路径配置成静态资源映射,通过WebMvcConfigurer.addResourceHandlers暴露访问路径,但这种方案只适合测试环境,生产环境不建议用。
5.8 证书生成后中文显示成乱码方块
现象:PDF 证书导出后,学员姓名和课程名称里的中文全部显示为「□□□□」。原因:iText 渲染 PDF 时没有注册中文字体,找不到支持中文的字体资源时就会用默认字体兜底,默认字体不包含中文字符集。解决:在项目resources/fonts目录放一个simhei.ttf或msyh.ttf,代码里显式注册:
BaseFont bf = BaseFont.createFont( "/fonts/msyh.ttf", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED); Font font = new Font(bf, 12f, Font.NORMAL);BaseFont.IDENTITY_H表示使用文字的水平标识编码,支持 Unicode 全字符集,NOT_EMBEDDED表示字体不嵌入 PDF 文件,文件体积更小。这一步不做,证书功能直接是废的。
6. 上线前的验证清单:用自动化脚本把整套流程跑一遍
拿到一套完整项目,很多人习惯先npm run dev启动前端,看到页面能打开就认为项目跑通了。实际上页面打开只是第一步,完整的业务流程验证至少需要覆盖十个以上关键路径。我一般会写一个验证脚本逐个调用后端接口,把整条用户链路跑通,而不是靠浏览器点点点。
首先验证注册登录链路:调用注册接口创建测试学员,调用登录接口获取 token,用一个jwt.sh脚本把 token 存到环境变量,后续所有接口都带Authorization: Bearer。
#!/bin/bash BASE_URL=http://localhost:8080/api # 注册账号 curl -X POST $BASE_URL/auth/register \ -H "Content-Type: application/json" \ -d '{"mobile":"13800138000","password":"123456","nickname":"testuser"}' # 登录获取 token TOKEN=$(curl -X POST $BASE_URL/auth/login \ -H "Content-Type: application/json" \ -d '{"mobile":"13800138000","password":"123456"}' \ | python3 -c "import sys,json; print(json.load(sys.stdin)['data']['token'])") echo "TOKEN=$TOKEN" > .env这段脚本里curl的-d参数是 JSON 请求体,| python3 -c的作用是从响应的 JSON 结构里取出 token 字段。日常调试时我一般会加上-s静默参数去掉进度条,输出更干净。echo把 token 写入.env文件,后续脚本用source .env引入。
接着验证核心业务链路:选课下单、模拟支付、课程访问、考试提交、成绩查询、证书生成。每个接口调用后检查响应 code 是否为 0,不为 0 就打印日志退出,实现一个最小可用的断言逻辑:
#!/bin/bash source .env BASE_URL=http://localhost:8080/api # 下单课程 ORDER_NO=$(curl -X POST $BASE_URL/order/create \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"courseId":1,"payType":"mock"}' \ | python3 -c "import sys,json; print(json.load(sys.stdin)['data']['orderNo'])") # 模拟支付 curl -X POST $BASE_URL/order/mockPay \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d "{\"orderNo\":\"$ORDER_NO\"}" # 验证课程访问权限 ACCESS=$(curl -X GET $BASE_URL/course/1/access \ -H "Authorization: Bearer $TOKEN" \ -H "Authorization: Bearer $TOKEN" \ | python3 -c "import sys,json; print(json.load(sys.stdin)['data'])") if [ "$ACCESS" != "true" ]; then echo "ERROR: course access check failed" exit 1 fi echo "PASS: course access authorized"这段脚本里$TOKEN是从.env文件引入的变量,ORDER_NO是下单接口返回的订单号。我习惯把每个验证点都打PASS或ERROR标记,哪一步失败了日志里一眼能看到。注意一个细节:-H "Authorization: Bearer $TOKEN"在多个接口里重复出现,可以把公共请求头抽成变量简化脚本,但这里为了直观起见保持展开写法。
证书生成是最后一步,验证证书编号是否唯一、PDF 是否可下载:
CERT_NO=$(curl -X POST $BASE_URL/certificate/generate \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"courseId":1}' \ | python3 -c "import sys,json; print(json.load(sys.stdin)['data']['certNo'])") curl -X GET $BASE_URL/certificate/download/$CERT_NO \ -H "Authorization: Bearer $TOKEN" \ -o /tmp/cert_$CERT_NO.pdf ls -lh /tmp/cert_$CERT_NO.pdf这里-o参数把 PDF 保存到本地,ls -lh确认文件有没有正常生成。如果文件大小低于 10KB,大概率是生成异常只输出了空模板。
整套脚本跑完之后,再从后端日志里排查有没有异常堆栈——重点看[ERROR]级别的日志,确认没有悄悄吞掉的异常。从那以后我做任何前后端分离项目,都强制在部署前走一遍这个接口链路验证脚本,而不是只靠前端页面点点点。这样既能发现接口字段不匹配、SQL 报错、权限失控这些问题,也能把联调成本压到最低,省下来的时间用来处理真正的业务细节。希望这套流程对你有帮助。
本文还有配套的精品资源,点击获取