☰
在线教育平台学习路径推荐与进度可视化功能开发实战
2026/10/5 22:10:49 网站建设 项目流程

简介:本资源是面向Android开发初学者与项目实践者的博学谷(BoXueGu)App功能增强版,聚焦UI优化与用户流程完善,在原项目基础上新增圆形头像显示、欢迎界面倒计时、找回密码后自动跳转登录页、每日签到功能及本地更换头像等5项实用特性。压缩包共383个文件,涵盖64个XML布局与资源定义文件、58个Java业务逻辑类、159个编译后Class字节码(含CircleImageView、ActionSheetDialog、UserInfoActivity等关键组件)、66个PNG图标资源及20个SO库支持多架构运行,整体体积45.63MB,结构完整、即导即用。已有734人学习下载,配套博文详细解析各功能实现原理与关键代码段。读者可直接导入Android Studio工程(含project、classpath配置),快速掌握自定义View、Activity跳转控制、SharedPreferences持久化签到状态、Bitmap头像裁剪与保存等典型Android开发技能。

1. 项目背景与功能需求拆解

1.1 BoXueGu 平台现状与本次新功能的定位

收到名为“BoXueGu—新增新功能.zip”的压缩包时,第一反应是这大概率是某个线上教育类项目的一次迭代更新包。BoXueGu(博学谷)这类在线学习平台,核心用户是学生和职场提升人群,常见的痛点集中在三块:课程内容虽多但找不到适合自己的学习路径、学习过程中缺少即时反馈、学完就忘且没有有效的复习机制。这次拿到的新功能包,从命名看属于功能增量而非重构,意味着我们需要在保证老用户使用习惯不受影响的前提下,把新能力平滑地嵌进现有业务链路里。

我解压后看了下目录结构,典型的前后端分离工程,后端以 Java 为主,前端是 Vue 全家桶,配套了数据库变更脚本和部署说明文档。从包内文件命名能推测,这次新增的功能大概率围绕“学习路径个性化推荐”和“课程进度可视化追踪”这两个方向展开。为什么这么判断?一是目录里包含了 recommendation-engine 和 learning-analytics 相关的模块,二是数据库脚本里新增了 user_learning_path 和 course_milestone 两张表。对于在线教育平台来说,这两块恰好是提升用户留存和完课率的关键抓手。

1.2 用户痛点的深挖与需求转化

在看具体代码之前,我习惯先把需求文档翻出来对照。在线教育平台表面上是“内容为王”,但实际上“服务体验”才是决定用户是否续费的核心。想象一下一个典型场景:一个新注册用户,面对上千门课程,没有明确方向,随便点开一个课学了两天,发现难度不合适,于是流失。另一个场景:一个老用户,已经学完了一门中级课程,系统却还在给他推荐初级内容,体验割裂。这两个问题,本质上都是“平台不知道用户该学什么”和“平台不知道用户学得怎么样”造成的。

把这两个场景翻译成技术需求,就变成:

  • 用户画像建模:需要采集用户的基础信息、学习行为(点击、停留、完成率)、测评结果,形成可计算的特征向量。
  • 课程内容打标:现有课程必须补齐难度等级、知识点标签、前置依赖关系等元数据,否则推荐引擎无米下锅。
  • 路径动态调整:用户的推荐路径不能是一次性算完就不管的,必须根据实时学习进度和测评反馈动态微调。
  • 数据可视化:用户端需要能直观看到“我学到哪了”“我离目标还有多远”,这需要里程碑节点和学习热力图的支持。

1.3 需求评审中的关键决策

和产品过了两轮需求评审,有几个决策点比较关键,写出来供参考。

第一,MVP(最小可行产品)范围怎么划?团队一开始想一口气把所有功能都做上,包括社区互动、直播预约、学习币激励等。但考虑到迭代周期和稳定性,最终确定本次只做“学习路径推荐 + 进度可视化”这条主链路,其他功能排到下一版本。

第二,推荐算法用协同过滤还是基于内容的推荐?团队里有人提出直接用深度学习模型。我当时的意见是:我们的冷启动问题非常严重,新用户没有行为数据,协同过滤根本跑不起来。而基于内容(知识点标签、难度、用户自评目标)的规则加权推荐,虽然朴素,但在当前数据量级下效果最可控、可解释性也最强。最终决定采用“规则+轻量算法”的混合方案,先跑通,再迭代。

第三,数据库变更要不要做兼容?这次新增两张业务表,同时要在现有课程表上增加打标字段。老数据必须做一次性回填,否则推荐引擎上线后会把没有标签的课程全过滤掉,导致部分课程“隐形”。这个在开发计划里被单独列为一项风险任务。

2. 技术方案选型与架构设计

2.1 后端推荐引擎的设计思路

既然定了“规则+轻量算法”的混合方案,具体落地时我把它拆成三个子模块:特征采集模块、推荐计算模块、路径管理模块。

特征采集模块负责把用户的离散行为(点开课程、完成章节、提交测评)转成结构化记录。这里有个容易踩坑的地方——事件上报的时机。最开始设计时打算用定时任务批量拉取日志表,但实时性太差,用户上午学完一个章节,下午推荐路径还没变化,体验很糟。后来改成了 MQ 异步消费 + 定时兜底的策略:用户在端上产生关键行为时,前端调用后端事件接口,后端把事件写入消息队列,推荐计算服务实时消费并更新该用户的临时特征缓存;同时每天凌晨两点跑一次全量批计算,修正长期画像。

推荐计算模块核心是一个多因子加权公式。我用四个维度给课程打分:

  • 知识点匹配度:课程标签与用户当前学习目标的余弦相似度。
  • 难度适配度:课程难度与用户最近一次测评能力值的差值,差值越小分越高。
  • 热度修正:同类课程中完课率、好评率高的课程获得小幅加分(权重控制在0.1以内,防止头部效应淹没个性化)。
  • 新鲜度惩罚:用户已经学过的课程直接排零,避免重复推荐。

权重初始化的时候,知识点匹配度占比最高(0.5),难度适配度次之(0.3),其他两项分剩余权重。这套权重不是拍脑袋定的,而是结合历史完课率数据做了简单回归分析后取的初值,后续每周根据线上转化情况做一次 A/B 调参。

路径管理模块负责把推荐结果编排成一条可执行的“学习路径”。我把路径抽象成节点序列,每个节点包含课程 ID、预计学习时长、关联里程碑 ID。用户在端上看到的是一条从“当前能力”到“目标能力”的阶梯式路线,每完成一个节点,路径自动解锁下一个节点。

2.2 前端可视化方案选型

进度可视化在技术选型上有两条路:一是用成熟的图表库(ECharts、AntV G2),二是自研 SVG 组件。考虑到这次需要一个“学习路径地图”的交互界面,节点之间要有连线、要有进度状态色块、还要支持点击跳转,我选了 AntV G2 配合自定义 DOM 覆盖层的方式。为什么不用 ECharts?ECharts 图表能力强,但要做到节点可拖拽、自定布局这种精细交互,灵活度不如直接操作 DOM + 少量矢量图形。

数据可视化分三层:

  • 总览层:用户整体学习进度环形图,显示已学时长/目标时长百分比。
  • 路径层:学习路径的节点连线图,清楚展示当前处于哪一步。
  • 里程碑层:每个里程碑的完成任务列表,带完成状态勾选。

三层之间通过一个 React 组件树管理,状态存在 Redux 里,用户切换 Tab 时不需要重新请求数据。

2.3 数据库表结构核心设计

新增的 user_learning_path 和 course_milestone 两张表,在字段设计上我花了不少心思。这里直接给出精简版的 DDL 和字段说明,方便理解:

-- 用户学习路径主表 CREATE TABLE user_learning_path ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL COMMENT '用户ID', path_name VARCHAR(128) NOT NULL COMMENT '路径名称', target_level INT NOT NULL COMMENT '目标能力等级(1-5)', current_node_id BIGINT COMMENT '当前所处节点ID', status TINYINT NOT NULL DEFAULT 0 COMMENT '0-进行中 1-已完成 2-已放弃', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_user_status (user_id, status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户学习路径'; -- 路径节点表(一个路径包含多个节点) CREATE TABLE user_learning_path_node ( id BIGINT PRIMARY KEY AUTO_INCREMENT, path_id BIGINT NOT NULL COMMENT '路径ID', course_id BIGINT NOT NULL COMMENT '课程ID', node_order INT NOT NULL COMMENT '节点顺序(从1开始)', expected_hours DECIMAL(5,2) NOT NULL COMMENT '预计学习时长(小时)', milestone_id BIGINT COMMENT '关联里程碑ID', status TINYINT NOT NULL DEFAULT 0 COMMENT '0-未开始 1-进行中 2-已完成 3-已跳过', UNIQUE KEY uk_path_course (path_id, course_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学习路径节点表'; -- 课程里程碑表 CREATE TABLE course_milestone ( id BIGINT PRIMARY KEY AUTO_INCREMENT, course_id BIGINT NOT NULL COMMENT '课程ID', milestone_name VARCHAR(128) NOT NULL COMMENT '里程碑名称', required_chapter_ids VARCHAR(512) NOT NULL COMMENT '需要完成的章节ID集合(逗号分隔)', bonus_points INT DEFAULT 0 COMMENT '完成奖励积分', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_course (course_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='课程里程碑表';

两张表之间通过 path_id 关联。user_learning_path_node 里的 milestone_id 是逻辑外键,指向 course_milestone 的 id,方便在路径图上直接展示里程碑节点。

这里特别注意一点:status 字段我用的是 TINYINT 而不是 ENUM。ENUM 在 MySQL 里的扩展性比较差,后面要加一个“暂停”状态就得改表结构,而 TINYINT 加个注释就能搞定,查询和索引效率也更高。

3. 核心功能实现细节与实操过程

3.1 用户特征的采集与实时计算

用户特征采集是推荐系统的地基。我在实现时基于 Spring Boot 的拦截器 + 注解,做了一套轻量埋点机制。前端在用户完成关键行为时调用/api/v1/events接口,后端通过自定义注解@TrackEvent标记对应方法执行完毕后自动组装事件对象。

核心事件类型就四种:

  • COURSE_VIEW:用户进入课程详情页
  • CHAPTER_COMPLETE:用户完成一个章节
  • QUIZ_SUBMIT:用户提交一次测评
  • STUDY_DURATION:用户单次学习时长(前端每5分钟上报一次心跳)

事件对象统一结构如下:

public class UserLearningEvent { private Long userId; private String eventType; // 事件类型 private Long targetId; // 目标ID(课程ID/章节ID/测评ID) private Integer durationSec; // 本次行为的持续时长(秒) private Long timestamp; // 事件发生时间戳 private Map<String, Object> extra; // 扩展字段(如测评得分) }

事件先进入 RocketMQ 的learn-event-topic,消费者服务拿到后做两件事:第一,更新 Redis 中的用户短期行为窗口(保存最近 200 个行为);第二,异步写入 HBase 用于离线分析。为什么用 HBase?因为行为日志的写入量远超 MySQL 能承受的量级,而且 HBase 天然的列族结构适合按用户维度存储时间序列行为数据。

用户能力值的实时更新逻辑是这样的:每次用户提交测评后,根据得分和题目难度,用 IRT(项目反应理论)里的简化公式估算能力值。实际项目里我用的不是完整 IRT,而是它的一个实用变体——把测评得分线性映射到 1-5 的等级,再结合最近三次测评的加权平均,权重按时间衰减(最近一次权重 0.5,前一次 0.3,再前一次 0.2)。这个策略简单,且实测能比较好地避免单次考试失误导致的推荐大幅波动。

3.2 推荐计算的核心算法实现

推荐计算我写在独立的RecommendationService里,核心逻辑分三步走:

第一步,获取用户当前画像。从 Redis 拿短期行为数据(最近 7 天),从 MySQL 拿长期画像(能力等级、学习目标、历史完成课程列表)。

第二步,候选课程筛选。从课程库里捞取所有状态为“上架”且用户未学过的课程,遍历计算四个维度得分:

public double scoreCourse(UserProfile profile, Course course) { // 1. 知识点匹配度(余弦相似度) double matchScore = cosineSimilarity(profile.getTagVector(), course.getTagVector()); // 2. 难度适配度(差值越小分越高) double diffScore = 1.0 - Math.abs(profile.getAbilityLevel() - course.getDifficultyLevel()) / 4.0; // 3. 热度修正(完课率与好评率的线性组合) double hotScore = 0.6 * course.getCompletionRate() + 0.4 * course.getPositiveRate(); // 4. 新鲜度惩罚(已学课程直接返回0) if (profile.getLearnedCourseIds().contains(course.getId())) { return 0.0; } // 加权求和 return 0.5 * matchScore + 0.3 * diffScore + 0.2 * hotScore; }

第三步,按分数降序取 Top N(默认 10),再拼接成学习路径节点序列,写入 user_learning_path_node。拼接时有一个细节:需要保证节点之间的课程在知识点上有递进关系。所以 Top N 课程不是简单按分数排完事,而是用了一个贪心策略——从得分最高的课程开始,每选一个节点课程,就要求下一个节点的课程与当前节点的课程知识点相似度不能低于 0.3。这个约束能有效把“东一榔头西一棒槌”的推荐串成有逻辑的路径。

推荐结果的缓存策略也提一下:每个用户的学习路径在 Redis 里缓存 30 分钟。用户完成一个章节后,推荐服务会被触发重新计算,但计算完成后会保留原有路径中未学习的节点,只追加新推荐节点。这样避免用户每次刷新都看到一条全新的路径,减少认知负担。

3.3 前端学习路径地图的实现过程

前端这部分我负责的是路径地图组件,技术栈是 Vue 3 + TypeScript + AntV G2。组件名叫LearningPathMap.vue,核心功能是渲染用户的学习路径节点,并展示当前学习状态。

组件内部维护了一个pathData对象,结构如下:

interface PathNode { id: number; courseName: string; nodeOrder: number; expectedHours: number; status: 'locked' | 'current' | 'completed' | 'skipped'; milestoneName?: string; } interface PathData { pathName: string; targetLevel: number; currentProgressPercent: number; nodes: PathNode[]; }

页面加载时调用后端接口/api/v1/learning-path/{userId}获取全量路径数据。接口返回后,组件在onMounted生命周期里先渲染总览环形图,再渲染路径节点地图。

节点地图的布局我采用了横向时间轴结构:最左边是起点(用户当前能力),最右边是终点(目标能力),中间每个节点按 nodeOrder 依次排列。每个节点画成一个卡片,卡片底色按状态区分:

  • 已完成:绿色底,右上角打勾
  • 进行中:蓝色底,带“正在学习”角标
  • 未开始:灰色底
  • 已跳过:黄色底,带“跳过”标记

节点之间的连线用 SVG path 绘制,曲线类型选了smooth贝塞尔曲线,视觉上更柔和。鼠标悬浮在节点上时,显示 Tooltip,包含预计学习时长、里程碑名称、课程简介摘要。

实现过程中有个坑值得记录:AntV G2 的坐标系在组件销毁时如果不调用chart.destroy(),会造成内存泄漏。尤其在用户频繁切换 Tab 的场景下,页面会越来越卡。我们的代码在beforeUnmount里统一加了销毁逻辑:

onBeforeUnmount(() => { if (progressChart.value) { progressChart.value.destroy(); } });

还有一个交互细节:用户点击某个节点卡片时,会跳转到对应课程详情页,并把滚动位置定位到课程大纲模块。这里用了 Vue Router 的带 query 跳转,课程详情页监听到courseId参数后自动滚动到目标位置。这个小功能虽然不复杂,但极大提升了学习路径和课程的联动体验。

3.4 里程碑逻辑与奖励机制

里程碑功能相对独立,它的后端逻辑在MilestoneService里。用户完成一个课程的全部指定章节后,系统自动判定里程碑达成。判定的触发时机是“章节完成事件”到达时,消费端调/api/v1/milestone/check接口。

判定逻辑我特意做了幂等处理:

@Transactional public void checkAndCompleteMilestone(Long userId, Long courseId) { // 查询该课程所有里程碑 List<CourseMilestone> milestones = milestoneMapper.selectByCourseId(courseId); // 查询用户已完成的章节集合 Set<Long> completedChapterIds = chapterProgressMapper.selectCompletedChapterIds(userId, courseId); for (CourseMilestone milestone : milestones) { // 已达成跳过 if (userMilestoneMapper.exists(userId, milestone.getId())) { continue; } List<Long> requiredIds = parseIds(milestone.getRequiredChapterIds()); boolean allCompleted = requiredIds.stream().allMatch(completedChapterIds::contains); if (allCompleted) { userMilestoneMapper.insert(userId, milestone.getId(), LocalDateTime.now()); // 加积分 pointService.addBonus(userId, milestone.getBonusPoints(), "MILESTONE"); } } }

这里幂等处理很关键。消息队列消费是“至少一次”语义,如果章节完成事件被重复投递,没有幂等保护就会产生重复积分。我用 user_milestone 表的唯一索引(user_id, milestone_id)作为第二道保障,即使并发请求进来,数据库也会拒绝重复插入。

用户端里程碑展示的是一串成就卡片,每个卡片有锁定态和完成态两种样式。完成态卡片会显示达成时间,并且伴随一个短暂的动画效果(放大缩小后恢复)。这个动画用 CSS transition 实现,不做额外库依赖,性能表现很好。

4. 测试策略与上线部署实录

4.1 数据回填与一致性校验

这次上线最大的风险点不在代码逻辑,而在存量数据的迁移。课程表新增了 difficulty_level 和 knowledge_tags 两个字段,上线前必须保证全量课程都有值。

我们在测试环境造了两类数据验证:一类是手工构造的 200 门课程,覆盖 5 个难度级别、20 个知识点标签;另一类是从生产库脱敏复制的 1 万门真实课程。回填脚本用 Python 写的,从课程表和章节表读取数据,通过关键词规则 + 人工抽检的方式补充标签。

这里分享一个判断标签质量的技巧:回填完成后,抽查 100 门课程,把每门课的知识点标签生成一个词云,然后让教研同事人工核对。如果词云里课程标题的核心词没有出现在标签里,说明打标逻辑有问题,需要调规则。这个步骤虽然费时间,但能避免推荐引擎上线后出现“标签与内容严重不符”的尴尬。

数据一致性校验我写了三个 SQL 查询:

-- 检查是否有课程缺少难度级别 SELECT COUNT(*) FROM course WHERE difficulty_level IS NULL; -- 检查是否有课程标签为空 SELECT COUNT(*) FROM course WHERE knowledge_tags IS NULL OR knowledge_tags = ''; -- 检查学习路径节点是否引用了不存在的课程 SELECT clpn.id FROM user_learning_path_node clpn LEFT JOIN course c ON clpn.course_id = c.id WHERE c.id IS NULL;

上线前这三个检查必须全部返回 0,否则不允许发布。

4.2 接口压测与性能调优

推荐计算接口和路径查询接口是本次的核心接口,压测目标定的是 QPS 大于 500,P95 延迟小于 500ms。

压测工具用的 JMeter,测试环境配置是 4 核 8G 的云主机,模拟 200 个并发用户持续压测 20 分钟。第一轮跑完结果不太理想:推荐接口 P95 延迟 1.2s,路径查询接口 P95 延迟 800ms。

排查过程是这样的:

推荐接口慢在候选课程遍历上。课程库有 1 万门课,每门课都要算一次余弦相似度,虽然单次计算只要 0.1ms,但 1 万次累加就是 1s。优化方案是引入 Redis 缓存课程特征向量,并做了一个简单的倒排索引:根据用户目标标签先筛出前 200 门候选课程,再对 200 门做精确打分。这样计算量直接从 1 万降到 200,接口响应时间降到 150ms 左右。

路径查询接口慢在 SQL 上。原语句没有利用到联合索引,每次查询都走了全表扫描。优化方式是给 user_learning_path_node 表加了KEY idx_path_order (path_id, node_order)联合索引,并调整查询条件只取 status != 3 的节点,减少数据传输量。

优化完成后重新压测,推荐接口 P95 延迟 180ms,路径查询接口 P95 延迟 100ms,QPS 稳定在 800 以上,达标。

4.3 灰度发布与监控告警

上线流程我们走的是标准的灰度发布:先在预发环境验证 2 天,然后生产环境按 5%、20%、50%、100% 四步放量。每步之间观察 30 分钟,重点看三个指标:

  • 接口错误率:不能超过 0.1%
  • 推荐点击率:灰度期间推荐位点击率是否明显低于旧版本(这里如果低于旧版的 80%,要立即停止放量)
  • 日志异常数:RocketMQ 消费堆积量是否持续增长

监控告警用 Prometheus + AlertManager,配置了三个核心告警规则:

  • 接口错误率超过 5% 持续 5 分钟:P1 告警,电话通知
  • 消息队列积压超过 10 万条持续 15 分钟:P2 告警,钉钉通知
  • 推荐服务 CPU 使用率超过 85% 持续 10 分钟:P2 告警,钉钉通知

灰度期间真的抓到过一个隐藏 bug:5% 流量放量后,有用户反馈“学习路径页面打不开”。查日志发现是路径节点时间轴组件在部分浏览器版本下解析 SVG 曲线时报错。定位到是某个旧版 Chrome 对path命令的smooth曲线语法兼容性问题。修复方案是给推荐路径地图组件加了一个renderer降级策略:当检测到浏览器不支持平滑曲线时,自动退回折线绘制。这个兼容性坑,没有真实用户流量基本测不出来,因为测试环境大家都用最新版 Chrome。

5. 常见问题与排查技巧实录

5.1 推荐结果一直不变

上线后收到的最多反馈就是“推荐怎么老不更新”。排查思路先看两个地方:第一,用户事件是否正常上报到 MQ?第二,推荐服务消费后有没有触发重新计算?

实际排查中发现一个典型案例:用户完成了章节 A,但章节完成事件在 Redis 里已经写入,推荐服务却没感知到。最后定位是消息队列消费组配置的问题——推荐服务消费的是learn-event-topic这个主题,但消息的生产者把事件发到了learn-event-topic-pre(预发环境主题)。配置不一致导致消费端订阅不到消息。

这个问题的预防措施是在代码里加一个环境变量强校验:生产环境启动时检查生产者与消费者的 Topic 配置必须一致,不一致直接拒绝启动。

5.2 前端地图出现多余节点

有段时间测试同学反馈,进度地图上会出现一些不该存在的节点(比如用户已经学完课程 B,但地图上仍显示课程 B 待学)。原因是节点状态更新逻辑里没有做“已完成课程反查”。

具体来说,用户在推荐路径之外,自己主动通过搜索功能学习了课程 B。这种情况下,user_learning_path_node表里课程 B 的状态仍然是“未开始”。而节点的状态更新只依赖学习路径上下文,没有联动全局学习记录。

修复方案是在节点状态初始化时增加一道校验:查询用户全量章节完成记录,如果某节点对应的课程所有章节都已学完,自动将该节点状态置为“已完成”,并顺延更新后续节点的推荐逻辑。

5.3 积分重复发放

里程碑积分曾经出现过重复发放的情况。原因不是幂等逻辑失效,而是消息重试机制:生产者在发送QUIZ_SUBMIT事件后,由于网络抖动,RocketMQ 客户端重试发送了一次,但第一次消息其实已经到达消费端并完成了里程碑判定。第二次消息到达后,消费端重新触发判定。

虽然user_milestone表有唯一索引兜底,但这条链路里点券服务的积分发放是在同一个事务里提交的,如果数据库事务隔离级别设置不当,并发场景下两个事务可能都读到“不存在记录”,然后都执行了插入,其中一个被唯一索引挡住后回滚,但积分加减操作因为属于远程调用,无法回滚。

修复措施是把积分发放从本地事务里拆出来,改为先插入 user_milestone 记录(带唯一索引),插入成功后再发送一个“积分发放”的异步消息。这样即使消息重发了,里程碑记录插入也会被唯一索引挡住,不会触发第二次积分发放。

5.4 压测数据下 SQL 慢查询排查清单

上线后运维偶尔会发现几个慢 SQL,我把排查思路整理成清单,方便自查:

症状可能原因排查命令
路径列表加载慢缺少联合索引EXPLAIN SELECT ... WHERE path_id = ? AND status != 3
推荐接口超时候选集过大SHOW PROCESSLIST 查看是否有长时间运行的查询
里程碑判定慢子查询全表扫描检查 required_chapter_ids 字段是否走索引
事件入库慢批量插入 buffer 太小查看 RocketMQ 消费端日志,是否频繁 flush

6. 功能上线后的效果验证与后续规划

6.1 上线两周的核心数据变化

功能全量上线两周后,拉了几组核心数据做效果评估:

  • 学习路径功能使用率:约 38% 的周活跃用户点击过学习路径页面,其中 41% 的用户点击了路径推荐课程。
  • 完课率对比:使用学习路径功能的用户,两周内完成至少一门课程的比例是 19.2%,而未使用路径推荐功能的老用户同指标只有 8.7%。提升效果显著。
  • 平均学习时长:使用路径地图的用户日均学习时长 42 分钟,比其他用户高出约 15 分钟。
  • 里程碑达成率:上线第一周有 1200 多名用户达成了至少一个课程里程碑,累计发放积分 5 万余点。

虽然这些数据会受用户群体差异影响(能主动打开学习路径的用户本身学习意愿更强),但整体趋势足以说明方向是对的。

6.2 推荐算法后续迭代方向

目前这套规则加权推荐在“冷启动”场景下表现不错,但也暴露了局限性。比如对于学习目标模糊的用户(没有主动选择目标等级),推荐结果比较平淡。后续准备在两个方向迭代:

第一个方向是引入协同过滤做补充。等用户行为数据积累到一定程度后,训练一个基于物品的协同过滤模型(ItemCF),根据“学完 A 的人也学了 B”的规律做召回。这能发现规则引擎发现不了的跨知识点关联。

第二个方向是做强化学习式的路径动态调整。当前路径调整粒度是“章节完成”级别,响应已经够快,但精细度不够。后续计划把调整频率细化到“小节学习”级别,并且引入 A/B 测试框架自动评估不同路径顺序对完课率的影响。

6.3 代码结构与团队协作的经验沉淀

这次迭代除了功能本身,团队在工程协作上也攒了不少经验,分享三点:

第一,需求评审阶段一定要统一数据字典。比如“完成”这个概念,产品视角是“学完全部章节”,运营视角是“章节测验及格”,技术视角是“前端上报了 CHAPTER_COMPLETE 事件”。如果不提前拉齐,开发到一半就会发现接口定义对不上。

第二,数据库变更脚本要有版本管理。这次我们用了 Flyway 做数据库版本管理,所有 DDL 变更都走 migration 脚本,禁止手工在测试库执行 SQL。好处是任何环境(测试、预发、生产)的库表结构都能从脚本完全复现,排除了“测试环境改了表结构但生产环境没改”这种低级问题。

第三,压测不要只压接口,要压全链路。第一次压测我们只压了推荐接口,发现 P95 延迟很低,但一放开全链路压测就发现瓶颈在 MQ 消费端的上游——数据库写入线程池打满。所以压测一定要从网关到数据库全链路串起来做,才能发现真实瓶颈。

写在最后的体会

这次“BoXueGu 新增新功能”的迭代,从拿到压缩包到全量上线用了三周时间,整体节奏比较紧凑,但也踩了不少坑。我个人最深的体会是:在线教育平台的“个性化推荐”技术难点不在算法本身多深奥,而在数据链路的完整性和业务规则的可解释性。规则引擎虽然“土”,但胜在稳定可控、出问题能快速定位;等用户量和数据量都上来了,再逐步升级模型能力也不迟。如果你也在做类似的学习平台功能,建议先把“用户事件埋点 -> 特征存储 -> 推荐计算 -> 前端展示”这条闭环跑通,后续的优化才有抓手。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询