知识付费产品的核心增长矛盾,往往集中在转化与留存两端。
LikeShop 知识付费系统原生已具备完整的专栏课程、音频视频交付、会员订阅、支付结算能力,但在实际运营中,两类需求尤为突出:一是转化侧,用户付费决策门槛高,需要免费试学降低决策成本;二是留存侧,用户购买后容易半途弃课,需要打卡解锁等游戏化机制提升完课率。如果直接在原有章节播放逻辑里硬加判断,很容易导致权限混乱、播放地址泄露、学习进度不同步等问题。
这类需求属于典型的业务扩展型二开,最佳实践是扩展章节权限字段 + 独立学习进度体系 + 统一播放权限校验入口,不推翻原生课程与订单架构,以最小侵入成本实现试学转化与打卡激励能力。
本文基于 LikeShop 知识付费 Java 版技术底座,从设计思路、表结构改造、核心权限逻辑、打卡解锁实现、上下游联动五个维度,给出完整的二开落地方案。
一、二开背景与核心目标
1.1 原生能力的局限
原生系统以「购买后解锁全部内容」为核心模型,能够满足基础交付需求,但在精细化运营场景下存在短板:
- 转化手段单一:用户只能看到课程简介,无法体验内容质量,付费转化率低;
- 学习激励缺失:用户购买后缺乏持续学习的动力,完课率低,复购意愿弱;
- 进度颗粒度粗:缺少章节级别的学习进度记录,无法做精细化的学习提醒;
- 权限控制简单:仅区分「已购买/未购买」,无法实现部分章节免费、部分付费的分级体验。
1.2 二开核心目标
- 章节试学能力:支持单章节设置免费试学,未付费用户可观看试学章节,付费后解锁全部内容,降低决策门槛;
- 打卡解锁机制:支持「学习完前一章 → 打卡 → 解锁下一章」的递进式学习,提升完课率与用户留存;
- 统一权限校验:所有播放请求走统一权限入口,防止绕过页面直接访问视频地址;
- 最小侵入原则:通过扩展字段与新增表实现,复用原生订单、会员、课程体系,不破坏原有主流程;
- 向下兼容:历史课程与订单不受影响,可平滑升级。
二、核心设计思路
2.1 章节权限模型
采用「三级权限」控制,优先级从高到低:
- 全局免费:课程整体免费,所有章节无需购买即可观看;
- 试学章节:课程整体付费,但指定章节设置为免费试学,未购买用户仅可观看试学章节;
- 付费解锁:购买课程或开通会员后,解锁全部章节内容。
核心原则:视频/音频的真实播放地址,必须经过后端权限校验后再返回,绝不能直接暴露在前端页面中,防止被抓包下载。
2.2 打卡解锁模型
两种主流解锁模式,可配置切换:
| 模式 | 规则说明 | 适用场景 |
|---|---|---|
| 顺序解锁 | 必须学完前一章并打卡,才能解锁下一章 | 体系化课程、训练营、进阶教程 |
| 每日解锁 | 每天固定解锁 N 章,打卡后获得当日学习权限 | 轻量专栏、每日听书、打卡营 |
核心规则:
- 每章设置「学习完成条件」,如观看时长达到 80%、完成课后习题;
- 满足条件后用户点击打卡,系统记录打卡时间,解锁下一章节;
- 支持补卡机制,可通过分享、积分等方式补解锁历史章节。
2.3 与原生体系的关系
- 复用原生课程表、章节表、订单表、会员表;
- 试学标记通过章节表扩展字段实现;
- 学习进度、打卡记录独立建表,不侵入原生业务表;
- 购买、会员开通等事件通过监听原生事件,同步更新用户课程权限。
三、数据库设计
遵循「扩展字段 + 新增表」的原则,尽量不修改原生表结构,降低升级冲突。
3.1 章节表扩展字段
在原生章节表上增加试学与解锁配置字段,不改变原有字段语义。
ALTER TABLE ls_course_chapter
ADD COLUMN is_free_trial TINYINT DEFAULT 0 COMMENT '是否免费试学:0否 1是' AFTER chapter_title,
ADD COLUMN unlock_type TINYINT DEFAULT 0 COMMENT '解锁方式:0直接解锁 1打卡解锁' AFTER is_free_trial,
ADD COLUMN study_duration_require INT DEFAULT 0 COMMENT '要求学习时长(秒),达到才算完成' AFTER unlock_type,
ADD INDEX idx_free_trial(is_free_trial);
3.2 用户课程权限表 ls_user_course_auth
记录用户对某门课程的权限状态,避免每次都关联订单表查询。
CREATE TABLE ls_user_course_auth (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
course_id BIGINT NOT NULL,
auth_type TINYINT NOT NULL COMMENT '权限类型:1试学 2购买 3会员',
expire_time DATETIME DEFAULT NULL COMMENT '权限过期时间,会员模式使用',
total_progress DECIMAL(5,2) DEFAULT 0.00 COMMENT '课程总进度百分比',
last_study_chapter_id BIGINT DEFAULT NULL COMMENT '上次学习章节',
last_study_time DATETIME DEFAULT NULL,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
UNIQUE KEY uk_user_course(user_id, course_id),
INDEX idx_user_id(user_id),
INDEX idx_course_id(course_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户课程权限表';
3.3 章节学习进度表 ls_user_chapter_study
章节级别的学习记录,是打卡解锁的判断依据。
CREATE TABLE ls_user_chapter_study (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
course_id BIGINT NOT NULL,
chapter_id BIGINT NOT NULL,
study_duration INT NOT NULL DEFAULT 0 COMMENT '已学习时长(秒)',
is_completed TINYINT NOT NULL DEFAULT 0 COMMENT '是否完成学习:0否 1是',
is_checked_in TINYINT NOT NULL DEFAULT 0 COMMENT '是否已打卡:0否 1是',
checkin_time DATETIME DEFAULT NULL COMMENT '打卡时间',
is_unlocked TINYINT NOT NULL DEFAULT 0 COMMENT '是否已解锁:0否 1是',
unlock_time DATETIME DEFAULT NULL,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
UNIQUE KEY uk_user_chapter(user_id, chapter_id),
INDEX idx_user_course(user_id, course_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='章节学习进度表';
3.4 打卡规则配置表 ls_course_checkin_rule
每门课程可配置独立的打卡解锁规则。
CREATE TABLE ls_course_checkin_rule (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
course_id BIGINT NOT NULL UNIQUE,
enable_checkin TINYINT NOT NULL DEFAULT 0 COMMENT '是否开启打卡解锁:0否 1是',
unlock_mode TINYINT NOT NULL DEFAULT 1 COMMENT '解锁模式:1顺序解锁 2每日解锁',
daily_unlock_count INT DEFAULT 1 COMMENT '每日解锁章节数',
enable_retroactive TINYINT DEFAULT 0 COMMENT '是否允许补卡:0否 1是',
retroactive_cost INT DEFAULT 0 COMMENT '补卡消耗积分',
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='课程打卡规则表';
四、核心业务代码实现
4.1 统一章节权限校验
这是整个模块的第一道防线,所有获取播放地址的请求必须经过此校验。
@Service
public class ChapterAuthService {
@Autowired
private UserCourseAuthMapper authMapper;
@Autowired
private CourseChapterMapper chapterMapper;
@Autowired
private CheckinRuleMapper ruleMapper;
/**
* 校验用户是否有权限观看该章节
*/
public boolean hasPermission(Long userId, Long chapterId) {
CourseChapter chapter = chapterMapper.selectById(chapterId);
if (chapter == null) return false;
// 1. 全局免费章节,直接放行
if (chapter.getIsFreeTrial() == 1) {
return true;
}
if (userId == null) {
return false; // 未登录且非试学章节,无权限
}
// 2. 查询用户课程权限
UserCourseAuth auth = authMapper.selectOne(userId, chapter.getCourseId());
if (auth == null) {
return false; // 无购买/会员权限
}
// 3. 已购买或会员,再校验打卡解锁规则
CourseCheckinRule rule = ruleMapper.selectByCourseId(chapter.getCourseId());
if (rule != null && rule.getEnableCheckin() == 1) {
UserChapterStudy study = studyMapper.selectOne(userId, chapterId);
return study != null && study.getIsUnlocked() == 1;
}
// 4. 无打卡限制,直接有权限
return true;
}
/**
* 获取可播放的视频地址(有权限才返回真实地址)
*/
public String getPlayUrl(Long userId, Long chapterId) {
if (!hasPermission(userId, chapterId)) {
throw new BizException("无权限观看该章节,请先购买课程");
}
CourseChapter chapter = chapterMapper.selectById(chapterId);
// 可增加签名、时效token,防止地址外泄
return buildSignedPlayUrl(chapter.getVideoUrl());
}
}
4.2 学习进度同步
用户播放过程中定时上报学习时长,达到阈值自动标记为完成。
@Service
public class ChapterStudyService {
@Transactional(rollbackFor = Exception.class)
public void syncStudyProgress(Long userId, Long chapterId, int duration) {
CourseChapter chapter = chapterMapper.selectById(chapterId);
Long courseId = chapter.getCourseId();
// 1. 更新章节学习时长
UserChapterStudy study = studyMapper.selectOne(userId, chapterId);
if (study == null) {
study = initStudyRecord(userId, courseId, chapterId);
}
int newDuration = study.getStudyDuration() + duration;
study.setStudyDuration(newDuration);
// 2. 达到要求时长,标记为完成
if (newDuration >= chapter.getStudyDurationRequire()
&& study.getIsCompleted() == 0) {
study.setIsCompleted(1);
}
studyMapper.updateById(study);
// 3. 更新课程总进度
updateCourseTotalProgress(userId, courseId);
}
}
4.3 打卡解锁核心逻辑
用户完成学习后打卡,解锁下一章节。
@Service
public class CheckinService {
@Transactional(rollbackFor = Exception.class)
public CheckinResultVO checkin(Long userId, Long chapterId) {
UserChapterStudy study = studyMapper.selectOne(userId, chapterId);
if (study == null || study.getIsCompleted() == 0) {
throw new BizException("请先完成本章学习后再打卡");
}
if (study.getIsCheckedIn() == 1) {
throw new BizException("今日已打卡");
}
CourseChapter chapter = chapterMapper.selectById(chapterId);
Long courseId = chapter.getCourseId();
CourseCheckinRule rule = ruleMapper.selectByCourseId(courseId);
// 1. 标记当前章节已打卡
study.setIsCheckedIn(1);
study.setCheckinTime(new Date());
studyMapper.updateById(study);
// 2. 解锁下一章节
CourseChapter nextChapter = getNextChapter(courseId, chapter.getSort());
if (nextChapter != null) {
unlockChapter(userId, nextChapter.getId());
}
// 3. 打卡奖励(可选:积分、成长值)
rewardService.giveCheckinReward(userId, courseId);
CheckinResultVO vo = new CheckinResultVO();
vo.setCheckedIn(true);
vo.setNextChapterId(nextChapter != null ? nextChapter.getId() : null);
vo.setRewardInfo(getRewardInfo(courseId));
return vo;
}
private void unlockChapter(Long userId, Long chapterId) {
UserChapterStudy study = studyMapper.selectOne(userId, chapterId);
if (study == null) {
study = initStudyRecord(userId, getCourseId(chapterId), chapterId);
}
if (study.getIsUnlocked() == 0) {
study.setIsUnlocked(1);
study.setUnlockTime(new Date());
studyMapper.updateById(study);
}
}
}
4.4 购买后初始化权限
监听订单支付成功事件,自动为用户开通课程权限并解锁第一章。
@Component
public class CourseOrderListener {
@Autowired
private UserCourseAuthMapper authMapper;
@Autowired
private CheckinService checkinService;
@EventListener
@Transactional(rollbackFor = Exception.class)
public void onOrderPaid(CourseOrderPaidEvent event) {
Long userId = event.getUserId();
Long courseId = event.getCourseId();
// 1. 创建或更新课程权限
UserCourseAuth auth = authMapper.selectOne(userId, courseId);
if (auth == null) {
auth = new UserCourseAuth();
auth.setUserId(userId);
auth.setCourseId(courseId);
auth.setAuthType(2); // 购买
authMapper.insert(auth);
}
// 2. 如果是打卡解锁模式,默认解锁第一章
CourseCheckinRule rule = ruleMapper.selectByCourseId(courseId);
if (rule != null && rule.getEnableCheckin() == 1) {
CourseChapter firstChapter = getFirstChapter(courseId);
if (firstChapter != null) {
checkinService.unlockChapter(userId, firstChapter.getId());
}
}
}
}
五、上下游模块联动
试学与打卡不是孤立功能,必须联动订单、会员、统计等模块,才能形成完整运营闭环。
5.1 订单与会员模块
- 购买课程、开通会员后,自动初始化用户课程权限;
- 会员过期时,同步更新权限有效期,过期后收回非试学章节的观看权限;
- 退款时,根据退款比例处理课程权限,支持关闭或保留。
5.2 内容播放模块
- 播放地址统一走权限校验接口,返回带时效签名的播放链接,防止外泄;
- 播放页根据权限状态,展示「试看结束,购买解锁」「打卡解锁下一章」等引导文案;
- 学习进度断点续播,下次进入自动回到上次观看位置。
5.3 营销与激励模块
- 打卡可配置积分、成长值奖励,提升用户动力;
- 支持分享补卡,利用社交裂变带来新用户;
- 学习排行榜、连续打卡天数展示,增强竞争感与成就感。
5.4 数据统计模块
- 课程维度:试学转化率、购买转化率、完课率、平均打卡天数;
- 用户维度:累计学习时长、连续打卡天数、已完成课程数;
- 运营后台看板直观展示,辅助优化课程内容与运营策略。
六、平滑升级与兼容方案
6.1 升级步骤
- 执行扩展字段与新增表 SQL,全部为增量操作,不修改原生核心数据;
- 部署权限校验、学习进度、打卡模块代码,原生播放与订单逻辑保持不变;
- 后台先灰度开启部分课程的试学与打卡功能,验证权限与解锁逻辑;
- 全量上线后,同步更新运营端配置指引。
6.2 历史数据兼容
- 历史已购买课程的用户,通过定时任务批量初始化课程权限记录;
- 历史章节默认关闭试学、关闭打卡解锁,不影响原有内容的观看;
- 学习进度数据从无到有,不追溯历史播放行为,新产生的学习行为正常记录。
6.3 官方版本升级兼容
- 核心改造以新增表、扩展字段为主,极少修改原生核心文件;
- 官方版本迭代时,重点核对章节表、订单支付事件是否有变动,同步适配即可;
- 独立的权限校验层将扩展逻辑与原生业务解耦,升级冲突成本极低。
七、常见坑与避坑建议
1. 播放地址泄露
坑:视频地址直接写在前端页面,用户通过抓包就能获取全部课程链接,付费体系形同虚设。
避坑:播放地址统一由后端接口返回,校验通过后生成带时效与签名的临时地址;重要课程配合视频加密、防盗链。
2. 打卡作弊刷进度
坑:用户通过脚本快速上报学习时长,瞬间完成所有章节打卡。
避坑:服务端校验时长合理性,单次上报时长不超过实际时间间隔;增加防刷机制,异常时长人工审核。
3. 权限判断不一致
坑:列表页显示可观看,点进去却提示无权限,体验割裂。
避坑:列表查询与详情播放共用同一套权限校验方法,保证判断口径一致。
4. 打卡解锁顺序错乱
坑:章节排序变动后,解锁逻辑出现跳章或死锁。
避坑:解锁严格依赖章节排序字段,排序变更后重新校验用户已解锁章节的连续性。
5. 会员过期权限不回收
坑:会员到期后,用户依然能观看所有付费章节。
避坑:权限校验时实时判断有效期,不依赖静态标记;定时任务扫描过期权限,同步更新状态。
结语
知识付费的试学与打卡,表面看是两个运营功能,本质是「转化漏斗」与「留存激励」两套体系的落地。
LikeShop 知识付费系统原生已经有扎实的课程交付与支付结算底座,二开时不用推翻重构,沿着「扩展权限字段 + 统一校验入口 + 事件联动上下游」的思路,就能以很低的侵入成本实现完整的试学与打卡能力,同时保留后续官方版本的平滑升级空间。
对知识付费类二开项目来说,核心原则永远是:权限收口、进度独立、事件联动。底层链路做扎实了,后续再加直播、作业、社群等更多玩法,也都是在这套骨架上做增量。