微信小程序+SSM在线课堂项目全流程拆解:从设计到部署
接手“在线课堂微信小程序SSM”这套组合是很多开发者绕不开的一道坎。小程序端负责跟用户直接打交道SSMSpring SpringMVC MyBatis在后台顶住业务逻辑和数据存储一路从登录、刷课程、看视频到后台管理全链路跑通。做这个项目既能把微信生态的开发流程摸一遍又能实打实练到 Java 后端三件套的配合所以不管你是拿来当课程设计还是作为毕设底子这套链路都非常有代表性。这篇文章我会用实际做一个在线课堂项目的角度把项目怎么拆、模块怎么定、表怎么建、接口怎么出、代码写到什么程度、踩过哪些坑全部捋一遍。核心围绕“一个用户怎么通过小程序看到课程并完成学习”这条业务线展开后端部分则重点讲 SSM 环境下接口和数据库怎么落地。无论你是刚学完 JavaWeb 准备做第一个完整项目还是已经有过一点开发经验想看看这类项目的完整结构这篇文章都能给你一套可以直接照着干的路径。1. 项目整体设计与思路拆解1.1 这类项目到底在做什么在线课堂微信小程序说白了就是一套“课程商城 学习平台”的轻量级结合体。用户通过小程序端看到课程分类、课程列表、课程详情可以报名购买课程然后在小程序里直接观看视频、记录学习进度。管理员则通过后台管理课程上下架、分类维护、订单查看、用户管理等操作。放在技术层面它要处理的并不是某个很难的功能点而是一条完整且闭环的业务链微信登录态获取 openid - 课程数据展示 - 订单创建 - 支付/模拟支付 - 学习记录 - 后台管理。这条链路把常见的业务场景都覆盖到了这也是它为什么总被选作教学项目和考核项目。对你来说做完这一整套东西相当于把“会调接口”升级成了“能设计并落地一个完整系统”。这类项目最容易犯的错误是一上来就堆功能。今天加个评论明天加个直播后天再加个积分商城结果每一个功能都是半成品。我的建议是先跑通“浏览课程 - 下单 - 学习记录”这条最小闭环再把扩展功能当作二期迭代。核心链路稳了项目就已经立住了一大半。1.2 技术选型为什么是“小程序 SSM”先说小程序端。选择微信小程序而不是传统 Web 页面原因很直接对于在线教育类应用用户最需要的是低门槛入口微信扫一扫或者搜索就能打开不需要下载 App也不需要通过浏览器输入网址。小程序的代码包体积小、加载快原生框架内置了 video 组件播放视频类内容非常方便。对于在线课堂这个场景原生小程序是足够胜任的。相比之下如果选 uni-app 这类跨端框架虽然可以同时输出 H5 和 App但会多出一层编译和兼容成本而且样式细节在小程序原生组件上还不如直接写原生来得顺手。除非你有明确的多端发布需求否则这类项目直接用原生微信小程序就是最稳的方案。再说后端。SSM 虽然比 Spring Boot 显得“老”但它是很多学校教学和考核中的标准技术栈覆盖了 Java 后端最核心的三块Spring 负责对象管理和解耦SpringMVC 负责请求路由和参数绑定MyBatis 负责数据库访问和 SQL 控制。把这三块分开理解再组合起来你对后端框架的理解深度会比直接上手 Spring Boot 更扎实。有人可能会问为什么不直接上 Spring Boot如果你是用于个人作品展示我当然推荐 Spring Boot配置少、起步快。但如果项目背景是课程设计或毕业设计SSM 往往更符合题目预期而且写文档的时候可以铺开的点更多。技术本身没有高下匹配场景才是关键。SSM 的复杂度主要在于配置文件多但它恰恰能让你理解 Spring 和 SpringMVC 的底层机制。1.3 系统角色与核心模块在线课堂项目按角色拆天然分成两端用户端小程序微信登录、浏览/搜索课程、查看课程详情、报名下单、观看视频、维护个人学习记录。管理端后台网页或文档中定义好的管理接口课程分类管理、课程信息管理、轮播图管理、订单管理、用户管理、学习数据统计。用户端的核心是“找课”和“学习”管理端的核心是“商品管理”和“订单查看”。两个角色不交叉但共享同一套数据库和接口规范。从模块清单来看主要包含这些模块核心功能涉及数据表登录模块获取 openid、生成 token、维护会话user首页模块轮播图、分类导航、课程推荐banner、course_category、course课程模块课程列表、课程详情、视频信息course、course_video订单模块创建订单、支付/模拟支付、取消订单orders学习模块用户-课程关系、学习进度记录user_course、study_record后台管理课程/分类/订单/用户维护对应各表这样划分之后开发顺序也就清楚了先建库建表再做登录再做课程展示然后做订单最后补学习记录。管理和统计功能可以穿插在最后统一完善因为它依赖前面所有业务数据。模块划分的最大价值是让团队或者你自己在动手写代码之前就知道每个接口该放到哪一层、每个表该服务谁。很多代码写乱不是因为水平不够而是因为一开始就没把边界画清楚。2. 核心功能模块解析与数据库设计2.1 核心用户流程拆解用户第一次打开小程序第一件事一定是登录。这里要注意微信小程序里用户点“授权登录”真正到后端能拿到的不是用户手机号而是一个临时的 code。后端拿着这个 code 去微信接口换 openid 和 session_keyopenid 是用户在当前小程序下的唯一标识这个值就是你这套系统识别用户的凭据。拿到 openid 之后后端要做的第一件事是去 user 表里查一下这个用户存不存在不存在就自动注册一条。这个过程对用户是无感的用户不需要填手机号、设置密码打开即用。这也是小程序类项目最大的体验优势。登录完成后用户进入首页。首页通常展示轮播图、课程分类和推荐课程列表。这些数据都来自后端接口小程序端通过 wx.request 发起请求拿到 JSON 数据后渲染到页面上。用户点击某个分类可以刷出该分类下的课程列表点击某个课程卡片进入课程详情页。课程详情页是信息密度最高的页面课程标题、封面、讲师、价格、课程介绍、视频列表、报名按钮。如果用户还没购买视频列表里可以展示试看片段购买之后所有视频解锁点开任意一集直接播放。这里非常关键的一点是视频播放权限不能只靠前端隐藏按钮来保证一定要在后端接口里校验当前用户是否已购买该课程否则前端可以直接绕过页面发起请求。用户点击报名后进入订单流程。系统创建一条订单记录状态为“待支付”。支付环节在真实项目中是接入微信支付统一下单接口但在课设项目中为了方便演示和避免繁琐的商户号申请绝大多数人会做成“模拟支付”点击支付按钮前端请求一个模拟支付接口后端直接把订单状态改成已支付同时把课程写入用户的“我的课程”。这个做法对系统逻辑没有影响反而把支付环节的关注点从“申请商户号”转移到了“订单状态流转”上。支付成功后用户可以在“我的课程”里看到已购课程点进去播放视频。播放过程中前端监听视频进度隔一段时间向后端上报学习进度。下次再进来可以直接从上次停下的位置继续看。至此一条核心业务闭环就完整了。2.2 数据库表设计要点数据库设计直接决定后面写 SQL 是舒服还是痛苦。在线课堂项目核心表大概七到八张下面是实际项目中我常用的设计方式用户表user字段类型说明idint自增主键openidvarchar(64)微信 openid唯一索引nicknamevarchar(64)昵称avatarvarchar(255)头像地址phonevarchar(20)手机号可选create_timedatetime注册时间openid 一定要建唯一索引因为用户重复进入小程序后后端判断“是否新用户”就是靠查 openid 是否已存在。如果遇到并发注册唯一索引能挡住重复数据的产生。课程分类表course_categoryid、name、sort、status。分类表结构非常简单但它是课程列表筛选的基础。注意 sort 字段控制显示顺序前端排序、后台拖拽排序都用得上。课程表course字段类型说明idint主键category_idint关联分类表titlevarchar(128)课程标题covervarchar(255)封面图introtext课程介绍teachervarchar(32)讲师名pricedecimal(10,2)价格单位元original_pricedecimal(10,2)原价划线价statustinyint1上架 0下架salesint虚拟销量create_timedatetime创建时间price 用 decimal 而不是 double/float是因为浮点数在精度上会出问题。比如 19.9 这个价格用 float 存储和运算很可能得到 19.900000xxx后续做金额汇总时全是坑。decimal 是精确保存做课程这类金额字段必须用。另外建议增加一个 sales 字段作为虚拟销量。为什么因为真实销量需要从订单表统计每次查询课程列表都要做 count 聚合数据量大了以后性能差而且很多课程刚上线没有销量页面显得冷清。用虚拟销量字段后台想调多少调多少展示效果好接口也快。课程视频表course_videoid、course_id、title、video_url、duration、sort。一个课程对应多集视频按 sort 排序播放。video_url 建议存完整的可访问地址不要把相对路径和 IP 拼一半留一半小程序端的 video 组件需要的是完整 URL缺了 https 前缀会直接加载失败。订单表orders字段类型说明idint主键order_novarchar(32)业务订单号唯一user_idint用户 idcourse_idint课程 idamountdecimal(10,2)实付金额statustinyint0待支付 1已支付 2已取消pay_timedatetime支付时间create_timedatetime创建时间订单号不要用自增 id因为订单号在业务层面会暴露订单量而且自增序号在更复杂的场景下容易冲突。这里用日期加随机数拼出 order_no比如 202606011035120001 随机四位基本可保证唯一。用户课程表user_courseid、user_id、course_id、buy_time。这张表用来记录用户与已购课程的关系。用户创建订单且支付成功后后端要做的关键动作就是往这张表插入一条记录。之后用户查看“我的课程”查询的就是这张表而不是订单表。学习记录表study_recordid、user_id、course_id、video_id、progress、update_time。记录用户看到某节课的哪个时间点单位可以是秒。用 video_id 精确到单集这样即使一个课程有多集视频也能分别记录各自进度。数据库的关联关系上我建议只在 Java 代码和 SQL 中体现外键逻辑不要真的去建外键约束。真实项目中物理外键会影响插入和删除性能还会带来看似安全实则僵化的数据约束。课程、订单、用户之间用索引加逻辑关联就够了。2.3 接口设计与统一返回格式前后端分离的项目接口设计做得舒服联调阶段就能少掉一半头发。在线课堂项目接口不需要多花哨按 RESTful 风格来但要做到路径清晰、返回格式统一。通常我会定义一个 Result 类所有接口都返回这个结构public class ResultT { private Integer code; // 200 成功500 失败401 未登录 private String msg; private T data; public static T ResultT success(T data) { ResultT r new Result(); r.code 200; r.msg success; r.data data; return r; } public static T ResultT error(String msg) { ResultT r new Result(); r.code 500; r.msg msg; return r; } }前端拿到这个结构后只需要判断 code 是否为 200再取 data 渲染。如果 code 是其他值直接弹出 msg 给用户提示。这样比后端直接甩一堆散乱字段要规范得多排查问题也快。接口清单大致如下接口方法功能/api/user/loginPOST传入 code获取 openid返回 token/api/course/listGET课程分页列表支持分类筛选/api/course/detailGET课程详情含视频列表/api/order/createPOST创建订单/api/order/payPOST模拟支付支付成功后写入 user_course/api/study/progressPOST上报学习进度/api/study/progress/getGET获取某节课的学习进度/api/user/coursesGET当前用户已购课程列表分页参数统一用 pageNum 和 pageSize前端传入后端返回总条数和当前页数据列表。分页这件事看着简单但如果不统一前端写多了就会乱。建议响应里返回一个 PageResult 结构total、list、pageNum、pageSize。这样前端做滚动加载和分页控件时直接取字段就行。3. 实操过程与核心环节实现3.1 搭建 SSM 后端骨架新建 Maven 项目后首先要解决依赖。SSM 需要导入的核心依赖包括 spring-webmvc、mybatis、mybatis-spring、druid 连接池、mysql-connector-java、jackson 等。写 pom.xml 的时候注意锁定版本特别是 spring 版本和 mybatis-spring 版本版本不匹配会导致启动时找不到类。项目结构按分包划分com.example.educourse ├── controller // 接口层接收前端请求 ├── service // 业务逻辑层 │ └── impl ├── mapper // MyBatis 接口 ├── entity // 实体类对应数据库表 ├── common // 通用类如 Result、PageResult └── config // 配置类如拦截器SSM 配置最麻烦但也最重要。核心有四个配置点jdbc.properties数据库连接地址、账号密码、连接池参数。spring-mvc.xml开启注解扫描、配置视图解析、JSON 转换器。spring.xml配置 service 层扫描、数据源、MyBatis 的 SqlSessionFactory、事务管理器。mybatis-config.xml配置驼峰映射、日志输出、别名。其中有一个非常容易踩坑的点就是 MyBatis 的驼峰映射。数据库字段名习惯用下划线比如 create_timeJava 实体类习惯用驼峰比如 createTime。默认情况下 MyBatis 并不知道这两者对应查询结果里 createTime 就会是 null。解决方法是开启驼峰映射settings setting namemapUnderscoreToCamelCase valuetrue/ /settings这个配置一开省掉 80% 的字段映射烦恼。3.2 用户登录与 openid 获取登录接口是小程序端的第一个请求也是所有业务请求的前提。流程是小程序端调用 wx.login() 拿到 code然后通过 wx.request 把 code 传到后端。后端拿到 code 后调用微信的 jscode2session 接口换取 openid 和 session_key。后端的核心代码大致是这样的Service public class UserServiceImpl implements UserService { Value(${wx.appid}) private String appid; Value(${wx.secret}) private String secret; Override public String wxLogin(String code) { String url https://api.weixin.qq.com/sns/jscode2session?appid appid secret secret js_code code grant_typeauthorization_code; String result HttpUtil.get(url); // 发送 GET 请求 JSONObject json JSONObject.parseObject(result); String openid json.getString(openid); User user userMapper.findByOpenid(openid); if (user null) { user new User(); user.setOpenid(openid); user.setCreateTime(new Date()); userMapper.insert(user); } // 生成自定义 token String token UUID.randomUUID().toString().replace(-, ); redis.set(token, user.getId().toString(), 7 * 24 * 60 * 60); // 有效期7天 return token; } }这段代码里要特别注意两点。第一wx.login 拿到的 code 是一次性的用一次就失效而且有效期很短所以后端必须及时处理不能把 code 存在数据库里等下回再用。第二不要拿 session_key 直接当作登录态更不要把它返回给前端存起来。session_key 是用来解密手机号、获取用户敏感信息的钥匙用途不同。这里我们自己生成一个 UUID token再把它和用户 id 关联存起来前端后续每次请求带上这个 token后端就认出是谁了。实际开发中高并发场景建议把 token 存到 Redis 并设置过期时间如果项目里没引入 Redis也可以用一个内存 Map 临时管理但系统重启后登录态会全部失效。如果只是想快速跑通流程内存方案完全够用。3.3 课程列表与分类筛选课程列表接口是首页和分类页统一复用的关键接口。前端传参包括 categoryId、pageNum、pageSize后端根据参数做条件查询并分页。在 MyBatis 的 mapper XML 里用动态 SQL 处理可选条件select idselectCourseList resultTypecom.example.educourse.entity.Course SELECT * FROM course WHERE status 1 if testcategoryId ! null AND category_id #{categoryId} /if ORDER BY create_time DESC LIMIT #{offset}, #{pageSize} /selectLIMIT 的 offset 由后端计算不要依赖前端传 pageNum 后直接拼到 SQL 里。通常在 service 层这样处理int offset (pageNum - 1) * pageSize; ListCourse list courseMapper.selectCourseList(categoryId, offset, pageSize);同时还要查总条数用来计算总页数select idcountCourseList resultTypeint SELECT COUNT(*) FROM course WHERE status 1 if testcategoryId ! null AND category_id #{categoryId} /if /select这里有个实战建议把 list 集合的查询和 count 查询分两个 mapper 方法写不要用一条 SQL 同时又查数据又查数量那样维护起来麻烦也不利于后期做缓存优化。小程序端的渲染逻辑相对简单用 wx.request 请求接口拿到 list 后通过 wx:for 渲染课程卡片。滚动到底部时继续请求下一页并把新数据 append 到当前列表末尾实现上拉加载更多。每个课程卡片绑定课程 id点击后跳转到详情页。需要注意列表页返回数据不要包含课程介绍这种大字段只需要 id、标题、封面、价格、销量、讲师名等卡片展示需要的信息。课程详细介绍在详情接口里单独返回能显著减小数据传输量。3.4 订单创建与模拟支付用户点击报名前端把 courseId 传给后端。后端先校验当前用户登录态再查课程是否存在且上架然后判断用户是否已经买过该课程买过就直接返回提示不要再生成订单。这个防重复下单的判断非常关键否则用户连点两次按钮会生成两条重复订单甚至出现支付后回了两次“我的课程”的操作。创建订单的 service 逻辑可以这样组织Transactional public Long createOrder(Long userId, Long courseId) { Course course courseMapper.selectById(courseId); if (course null || course.getStatus() ! 1) { throw new RuntimeException(课程不存在或已下架); } int cnt userCourseMapper.countByUserIdAndCourseId(userId, courseId); if (cnt 0) { throw new RuntimeException(您已购买过该课程); } Order order new Order(); order.setOrderNo(generateOrderNo()); order.setUserId(userId); order.setCourseId(courseId); order.setAmount(course.getPrice()); order.setStatus(0); // 待支付 order.setCreateTime(new Date()); orderMapper.insert(order); return order.getId(); }Transactional 是必须加的因为整个流程里存在多步写库操作任何一步失败都应该回滚不能出现订单创建了但用户课程写入失败这种脏数据。订单号生成逻辑我习惯这样写private String generateOrderNo() { SimpleDateFormat sdf new SimpleDateFormat(yyyyMMddHHmmss); String time sdf.format(new Date()); int random (int) ((Math.random() * 9 1) * 1000); return time random; }日期取到秒加上四位随机数基本不会重复。如果担心极端情况可以在 order_no 字段上建唯一索引万一真撞上了再重新生成一次。模拟支付的接口就简单了拿到订单号后把订单状态改成已支付同时把记录写到 user_course 表Transactional public void pay(String orderNo, Long userId) { Order order orderMapper.selectByOrderNo(orderNo); if (order null || !order.getUserId().equals(userId)) { throw new RuntimeException(订单不存在); } if (order.getStatus() 1) { return; // 已经支付过幂等处理 } order.setStatus(1); order.setPayTime(new Date()); orderMapper.updateStatus(order); UserCourse uc new UserCourse(); uc.setUserId(userId); uc.setCourseId(order.getCourseId()); uc.setBuyTime(new Date()); userCourseMapper.insert(uc); }幂等处理是这里的一个重点。如果用户支付成功后网络抖动前端重复点击支付按钮后端要能识别出订单已经支付过了直接返回成功而不是再次写入 user_course 导致重复。上面的代码里订单状态为 1 时直接 return就是最简单的幂等方案。如果是接入真实微信支付后端会多一步调用微信支付统一下单接口拿到 prepay_id返回给小程序端小程序端再调起支付收银台。支付完成后微信服务器会回调后端配置的回调地址后端收到回调后做同样的订单状态修改操作。回调地址必须是公网可访问本地开发时可以用内网穿透工具但安全性要注意。整个流程比模拟支付复杂不少如果不是必须接入真实支付建议先用模拟支付把业务跑通。3.5 学习进度记录用户进入课程播放页后小程序端的 video 组件持续播放。为了记录进度前端可以监听视频的 timeupdate 事件这个事件在播放过程中会频繁触发如果每次都向后端发请求会打出大量无意义请求。实际处理办法是节流每隔 5 秒或者 10 秒上报一次当前播放时间或者只在暂停、退出页面时上报。比如onVideoTimeUpdate(e) { this.currentTime e.detail.currentTime; } pauseAndReport() { wx.request({ url: baseUrl /api/study/progress, method: POST, header: { token: this.token }, data: { courseId: this.courseId, videoId: this.videoId, progress: Math.floor(this.currentTime) } }); }onHide 和 onUnload 生命周期里都调用 pauseAndReport保证用户退出页面时进度能保存下来。后端收到请求后按 user_id 和 video_id 去更新进度记录不存在就插入存在就更新update idupdateProgress UPDATE study_record SET progress #{progress}, update_time NOW() WHERE user_id #{userId} AND video_id #{videoId} /update更好的做法是先查后插或者用 INSERT ... ON DUPLICATE KEY UPDATE但简单项目里先查后插就够了。关键点是进度字段用秒数存储单位统一。前端拿到秒数后在页面 load 时直接定位到对应位置this.videoContext.seek(progressFromServer);这样用户下次进来就能续播。深入想一层就会发现学习进度上报其实是“写多读少”的场景所以上报接口不用太复杂但一定要在写库时加上唯一索引约束避免同一个用户同一节课出现多条进度记录。3.6 后台管理模块后台管理如果做完整 Web 页面工程量会大不少。很多课程设计项目里后台管理只需要提供接口配合一个简单管理页面即可。核心功能包含管理员登录、课程列表及上下架操作、课程分类维护、订单查询、用户查询。管理员登录和用户端登录分开设计。管理员表单独建账号密码字段直接存数据库。密码不能明文存储至少用 MD5 加盐或者用 BCrypt。虽然课设项目里 MD5 常见但我还是建议用 BCrypt代码量差别不大安全性高一个档次。后台接口要校验管理员身份可以在拦截器里统一处理。拦截器检查请求头中的 token判断是否是管理员的 token不是就直接返回 401。拦截器机制值得花时间理解因为它是 SSM 项目里做登录态校验的标准姿势。4. 常见问题与排查技巧实录4.1 请求发不出去、报域名错误这是小程序开发遇到最多的第一个坑。小程序为了安全所有请求域名必须在微信公众平台配置到白名单里。开发阶段可以在开发者工具里勾选“不校验合法域名”真机调试则要打开调试模式。但要注意这只是临时方案一旦要发布正式版本必须去后台配置 request 合法域名域名还必须是 HTTPS并且 ICP 备案。排查请求问题时我建议按以下顺序操作打开开发者工具的 Network 面板确认请求是否真的发出看请求头和响应状态码。看后端控制台有没有收到请求日志。没收到大概率是域名配置或网络问题收到了但返回异常就是后端代码问题。看响应体结构确认 Result 里的 code 和 msg前端再决定是弹错误还是正常渲染。另外一个容易踩的坑是跨域。小程序端请求后端接口如果后端是本地跑在 8080 端口小程序开发工具默认允许访问但上线后域名不同后端必须开启 CORS 跨域支持否则请求会被浏览器拦截。SpringMVC 里用 CrossOrigin 注解或者配置 CorsFilter 都能解决。4.2 登录态丢失、token 解析失败用户在使用过程中突然退出登录或者某些接口返回 “未登录”大多数情况不是微信登录失效而是 token 传递或解析除了问题。常见原因有前端每次请求没有带上 token比如写死了某个 header 名但后端拦截器读的是另一个 header 名。token 过期Redis 里过期时间设太短。一般建议 7 天起步在线教育类应用用户会频繁回来太短影响体验。后端服务重启如果用的是内存 Map 存储 token重启后所有 token 都失效。这种情况要么使用 Redis要么接受重启后重新登录。排查技巧在后端拦截器里加日志打印每个请求的 token 和最终解析结果。看到日志后问题基本一目了然。小程序端也要注意 wx.request 的 header 字段名严格一致大小写都要匹配。4.3 MyBatis 映射和 SQL 相关报错SSM 项目里最多的报错集中在 MyBatis 这块典型的有三类XML 文件没被扫描到pom.xml 里如果没有加入 resources 配置src/main/java 下的 XML 文件不会被打包进 target 目录运行时就报 Invalid bound statement。解决方法是把 mapper XML 放到 resources 目录下并在 pom.xml 里配置 resource 包含。namespace 写错mapper 接口的全限定名和 XML 的 namespace 必须完全一致否则启动时会报绑定错误。resultType 写错resultType 要写实体类的全限定名或者配置 typeAliasesPackage 后才能写类名。查询结果字段对不上时先检查驼峰映射有没有开启。遇到这类问题先不要慌着改代码第一时间看控制台完整日志。MyBatis 报错一般都会把问题的 SQL 片段和原因一起打出来顺着提示定位比瞎猜快得多。4.4 视频加载失败、播放黑屏在线课堂项目里视频是核心内容播放失败对用户来说是致命体验问题。视频播放失败常见原因有三类视频 URL 不是 HTTPS微信小程序正式环境中video 组件要求资源地址为 HTTPS。开发时用 HTTP 的本地视频地址可以看上线前必须替换。防盗链限制有些视频服务商设置了 Referer 检查小程序播放器请求视频时 Referer 头不符合要求就会拒绝返回内容。解决方法是换用支持自定义白名单的视频云服务或者把视频转存到自己服务器/CDN。压缩包单文件超限小程序代码包限制主包 2MB但视频文件不能打进包里。视频必须放在服务端或云存储上小程序端只放视频地址。还有一个业务层面的坑用户没购买课程时前端把视频按钮置灰但如果你没有在后端做播放权限校验懂技术的人完全可以绕过按钮直接请求视频地址。所以核心视频接口上一定要校验 user_course 表里有没有对应记录。4.5 重复下单和订单状态不一致用户连点两次“报名”按钮或者支付回调重试都会带来重复数据问题。解决方案前面提到过核心是两步创建订单前查 user_course判断用户是否已购。支付接口做幂等订单已支付就直接返回成功不再重复写入用户课程关系。订单状态不一致的问题往往是事务没加或者事务配置没生效。Spring 的事务默认只在抛出 RuntimeException 时回滚如果你的代码里 catch 住了异常但没有重新抛出事务就不会回滚。排查时可以打印事务管理器日志确认方法确实被代理了。5. 这套项目还能往哪些方向延伸5.1 功能扩展课程评论和评分。用户学完课程之后可以留言打分评论关联 user_id 和 course_id展示在课程详情页。这个功能不难加一张表加两个接口的事但对完成度提升很大尤其答辩时能讲的点更多。后台管理界面完善。如果只是接口答辩的时候只能对着 Postman 演示。建议花两天时间做一个简单的 Vue 后台管理页面课程上下架、订单查看用现成的后台模板改改就行。哪怕样式普通也会比纯 API 项目观感好很多。数据统计。管理端增加一个首页仪表盘展示注册用户数、课程总数、订单总金额、热门课程 Top10。这些统计用 SQL 聚合就能搞定但能体现出你对数据可视化有思考。引入 Redis 缓存。首页数据是典型的读多写少场景用 Redis 缓存课程列表和轮播图缓存更新策略可以选“课程增删改时主动清除对应缓存”。面试时聊到缓存这会让你的项目有明显亮点。5.2 服务端能力扩展就算是 SSM 项目也可以逐步向 Spring Boot 迁移。先保持业务逻辑不变把 XML 配置改成 Spring Boot 自动配置你会发现改完之后的代码量少很多但底层原理你已经在 SSM 阶段掌握了迁移过程本身也是学习。学习进度和课程推荐也可以做得更聪明。比如记录用户观看完成率、停留时长统计用户偏好分类再在“推荐课程”里优先展示用户喜欢分类下的高销量课程。这一步不需要上复杂算法一条 SQL 加一个排序规则就能做出效果。如果未来想部署上线后端打包成 jar/war 放到云服务器小程序端在微信公众平台申请账号并配置合法域名流程并不复杂。不要把部署想得太神秘一台基础云服务器跑一个 HTTP 服务再配一个 MySQL这套项目就能稳定跑起来。说句实在话我经手过不少类似的项目也见很多同学在“微信小程序 SSM”这个组合上栽过跟头回头复盘几乎都是同一个原因前期设计没想清楚直接上手写代码结果写到中间发现表和接口对不上来回改。如果你正在做这个选题我最大的建议是老老实实先把表设计好、把接口清单列出来哪怕花上两天都是值得的。流程理顺之后后面的代码其实都是熟练活儿。还有一个细节值得单独拿出来说项目里所有对外暴露的接口都要考虑“如果用户传入非法参数会怎样”。课程 id 传负数、pageNum 传 0、token 传一个乱码字符串这些极端情况在你的接口里怎么响应直接决定了系统健壮性。答辩时考官特别爱抽这种边界问题与其到时候现想答案不如写代码时就养成校验的习惯。最后分享一个小技巧项目文档里画一张清晰的系统架构图把“小程序端 - 后端接口 - Service 层 - Mapper 层 - 数据库”的调用关系标出来再在每张表旁边写好字段说明。等到答辩或者复盘的时候这张图就是你介绍项目的骨架照着它讲逻辑性和完整度都会比临场发挥强很多。动手做吧这套项目做完你对整个前后端链路的感觉会和以前完全不一样。

相关新闻

医学大语言模型分类与选型实战:从基础概念到落地避坑指南

医学大语言模型分类与选型实战:从基础概念到落地避坑指南

如果你在医疗信息化、医学AI或者临床科研这条线上工作,最近两年应该有一个明显感受:医学大语言模型这个词出现的频率,已经从偶尔听到变成了每周必刷。打开任何技术平台都是各种医学大模型的发布消息、榜单刷新、微调教程。这个领域热度高是好…

2026/10/13 11:27:50 阅读更多 →
大语言模型快速启动指南:从模型选型到本地部署

大语言模型快速启动指南:从模型选型到本地部署

这份指南写给你——一个刚接触大语言模型、想快速上手跑通一个真实项目的人。它不教你从头训练模型,那需要超算和团队;它教你怎么在一天内把现有的大模型用起来,跑通基础调用、动手部署、调好提示词,让你具备独立完成一个LLM小项目…

2026/10/13 11:27:50 阅读更多 →
医学大语言模型选型与落地实践:从分类到部署的完整指南

医学大语言模型选型与落地实践:从分类到部署的完整指南

最近有很多朋友在问医学领域的大语言模型到底该怎么选、怎么用,正好我手上整理过一份相对完整的梳理资料,干脆抽时间写成一篇总结性的文章。这篇不是纯列名字的罗列文,而是按场景、按定位把主流几条技术路线拆开讲清楚,包括它们各…

2026/10/13 11:27:50 阅读更多 →

最新新闻

RAG检索后处理实用指南:重排、压缩与校正,让结果直接可用

RAG检索后处理实用指南:重排、压缩与校正,让结果直接可用

1. 检索结果“看着相关”和“真正可用”之间,隔着一整条流水线先从一个很常见的现象说起。你做一个知识库问答系统,检索部分做得不错了,向量召回、BM25 混合检索都上了,Embedding 模型也换过好几版,离线测评指标一直在…

2026/10/13 12:14:01 阅读更多 →
KernelSU GKI 模式与 LKM 模式完整选型指南:Android 内核 Root 从安装到卡砖救援一次跑通

KernelSU GKI 模式与 LKM 模式完整选型指南:Android 内核 Root 从安装到卡砖救援一次跑通

KernelSU GKI 模式与 LKM 模式完整选型指南:Android 内核 Root 从安装到卡砖救援一次跑通 【免费下载链接】KernelSU A Kernel based root solution for Android 项目地址: https://gitcode.com/GitHub_Trending/ke/KernelSU KernelSU 是一款运行在内核里、直…

2026/10/13 12:14:01 阅读更多 →
Scrapy Shell调试技巧:提升爬虫开发效率的交互式工具

Scrapy Shell调试技巧:提升爬虫开发效率的交互式工具

1. 为什么Scrapy Shell值得你花时间掌握刚接触Scrapy那会儿,我最烦的就是改一行代码跑一次爬虫。每次调试XPath或者CSS选择器,都要重新发起请求、重新解析页面,一个简单的选择器调半天,效率低得让人抓狂。后来被一位做数据采集的老…

2026/10/13 12:14:01 阅读更多 →
如何用3升和4升的桶,量出2升的水?

如何用3升和4升的桶,量出2升的水?

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

2026/10/13 12:14:01 阅读更多 →
VC++2010学习版实战:Win32 GUI、静态库与C运行时深度开发

VC++2010学习版实战:Win32 GUI、静态库与C运行时深度开发

简介:VC2010学习版是专为C语言初学者及计算机二级考试备考者打造的轻量级开发环境,完整集成Visual Studio 2010 Express C版本,提供编译、调试、项目管理一体化支持,有效降低Windows平台下C/C编程入门门槛。资源包共58个文件&…

2026/10/13 12:14:01 阅读更多 →
Linux匿名管道原理与实战:fork进程通信的零配置IPC机制

Linux匿名管道原理与实战:fork进程通信的零配置IPC机制

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

2026/10/13 12:13:00 阅读更多 →

日新闻

物联网系统集成避坑指南:从选型到验收的关键要点

物联网系统集成避坑指南:从选型到验收的关键要点

做了这么多年物联网系统集成的项目,我最大的感受是,大部分坑不是出在技术前沿,而是出在一些看起来很平常的地方。设备选了便宜的、协议没问清楚、现场没摸透、验收标准写得含糊——这些细枝末节,最后都会变成延期、超支和扯皮的大…

2026/10/13 0:00:57 阅读更多 →
ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

如果你玩ESP32有一阵子了,很难不碰到舵机控制这道坎。做智能小车要转方向,做机械臂要抬关节,做云台要偏航俯仰,背后都是同一个东西在干活:舵机。而我见过太多人卡在“代码能编译、舵机就是不听话”这个阶段&#xff0c…

2026/10/13 0:00:57 阅读更多 →
CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

1. 为什么CAN总线至今仍是工业现场的“硬通货”?——从汽车底盘到智能温室的真实需求你有没有拆过一辆老款燃油车的中控台?或者调试过某高校实验室里那台跑着Modbus协议却频频丢帧的PLC控制柜?又或者,在某个农业物联网项目里&…

2026/10/13 0:00:57 阅读更多 →

周新闻

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

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

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

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

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

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

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

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

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

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 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/12 21:05:09 阅读更多 →
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/12 14:48:49 阅读更多 →
黑夜航拍船只数据集训练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/12 18:11:17 阅读更多 →