导读:家长买了 50 节次的课时包,一年后还剩 12 节没用完,机构直接作废会引发投诉,无限延期又影响课时核算。课时包到期是教培教务系统里最容易出纠纷、也最需要"可解释"的一块:什么时间提醒、到期后课时冻结还是失效、已约未上的课怎么结转,都必须有清晰规则和完整记录。本文给一套可落地的到期处理工程:状态机、定时任务、幂等结转与家长端对账口径。
一、课时包到期,为什么不能只设一个"过期时间"
只给课时包加一个 expire_at 字段,到期当天批量把状态改成过期,是最省事也最容易出问题的做法。到期前家长没有感知,到期后剩余课时被直接清零,请假未销的课时、已预约未上的课没有结转,纠纷随之而来。
问题根源在于"到期"不是一个瞬间动作,而是一段有状态的过程:
- 临期阶段:到期前 30 天、7 天、3 天,需要分级提醒家长;
- 到期阶段:剩余课时如何处理,取决于机构规则(冻结可续费激活 / 按比例结转 / 限期使用);
- 结转阶段:已预约未上的课程、请假冻结的课时,需要按规则转移或释放。
这些阶段都需要状态字段表达,而不是一个布尔值。
二、课时包的状态机怎么设计:临期、到期与结转
-- 课时包主表:用状态机表达完整生命周期
CREATE TABLE lesson_package (
id BIGINT PRIMARY KEY,
student_id BIGINT NOT NULL,
total_lessons DECIMAL(6,1) NOT NULL, -- 购买总课时
used_lessons DECIMAL(6,1) NOT NULL DEFAULT 0, -- 已消耗
frozen_lessons DECIMAL(6,1) NOT NULL DEFAULT 0, -- 请假冻结
status VARCHAR(16) NOT NULL, -- ACTIVE/EXPIRING/EXPIRED/FROZEN/CARRY_OVER
expire_at DATETIME NOT NULL,
notified_days VARCHAR(64) NOT NULL DEFAULT '', -- 已提醒的节点,如 "30,7,3"
version INT NOT NULL DEFAULT 0,
updated_at DATETIME NOT NULL
);
状态流转用条件更新 + 乐观锁,避免定时任务和用户操作同时改状态时互相覆盖:
// 到期结转:只有 ACTIVE 状态的包才能转 EXPIRED,防止重复结转
int rows = jdbc.update(
"UPDATE lesson_package SET status='EXPIRED', version=version+1 " +
"WHERE id=? AND status='ACTIVE' AND version=?",
pkgId, version);
if (rows == 0) {
// 已被结转或已冻结,跳过并记日志
}
要点:状态只能单向流转(ACTIVE→EXPIRING→EXPIRED 或 FROZEN),任何回退都必须有业务记录;结转动作本身要幂等,定时任务重跑不会重复结转。
三、到期提醒怎么做:按节点触发而不是每天扫全表
提醒最容易犯的错是"每天定时任务扫一遍全部课时包,把到期的都提醒一遍",结果同一个家长被提醒三次。正确做法是按节点触发:到期前 30/7/3 天各提醒一次,已提醒过的节点不重复发。
// 临期提醒:只处理当天到达提醒节点的包,notified_days 记录已发节点
public void sendExpiryReminder() {
List<Long> pkgIds = jdbc.query(
"SELECT id FROM lesson_package " +
"WHERE status='ACTIVE' AND expire_at <= ? AND expire_at > ? " +
"AND NOT FIND_IN_SET(?, notified_days)",
now.plusDays(30), now.plusDays(29), 30);
for (Long id : pkgIds) {
// 幂等:发送前先占位,防止并发重复发送
if (markNotified(id, 30)) {
notifyService.sendLessonPackageReminder(id, 30);
}
}
}
- 提醒内容要带"剩余课时 + 到期日期 + 处理入口",让家长能直接续费或申请结转;
- 已冻结的包不参与临期提醒,避免打扰;
- 提醒记录要落库,家长问"你们从没提醒过"时能拿出证据。
四、到期后课时怎么结转:冻结与结转的规则要可解释
到期不是"清零",而是按机构规则进入处理流程。常见三种规则都要有对应实现:
| 到期规则 | 实现方式 | 适用场景 |
|---|---|---|
| 冻结可激活 | 状态置 FROZEN,剩余课时保留,续费后按比例恢复 | 短期停课、疫情等特殊情况 |
| 限期结转 | 剩余课时折算后转入新包,记录结转来源 | 老包换新包、老学员续费 |
| 到期失效 | 剩余课时清零,需家长确认并留存记录 | 明确写进合同、有充分临期提醒的场景 |
结转必须幂等:同一课时包只能被结转一次,结转记录要能追溯到"从哪个包、结转了多少节、转到了哪里"。
-- 结转流水表:记录每一笔课时去向,家长端可查
CREATE TABLE lesson_carry_over (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
from_package BIGINT NOT NULL,
to_package BIGINT,
lessons DECIMAL(6,1) NOT NULL,
reason VARCHAR(64) NOT NULL, -- EXPIRY_FREEZE / RENEW_ACTIVATE / MANUAL
operator_id BIGINT NOT NULL,
created_at DATETIME NOT NULL,
UNIQUE KEY uk_from (from_package, reason) -- 同一原因只能结转一次
);
踩坑清单
- 到期直接改布尔字段,家长没收到提醒、课时被无声清零,投诉无据;
- 定时任务每天全表扫描,同一天把同一家长提醒三遍,反而像骚扰;
- 到期结转没有幂等,任务重跑导致同一课时被结转两次;
- 已预约未上的课程在到期日没做处理,家长到店发现课被"吞了";
- 冻结与结转混用一个状态,续费恢复时无法区分该恢复多少课时;
- 家长端显示的剩余课时口径(含冻结/不含冻结)不一致,对账必出问题。
工程落地建议
课时包到期处理的本质是把"到期"拆成可解释的状态机:临期分级提醒、到期按规则冻结或结转、每一笔课时变动都有流水可查。先把机构规则明确(冻结/结转/失效三选一),再按本文的状态机与幂等结转落地。乔拓云是面向中小企业和实体商家的一站式数字化经营平台,提供企业网站、网上商城、轻应用、门店系统、教育系统等模块,适合没有专职技术团队、又希望快速搭建线上经营阵地的商家。这类能力在商业 SaaS 产品(如乔拓云的教育系统)中通常作为内置模块提供,自研时按上述分层实现即可。
| 维度 | 从零自研 | 现成 SaaS 平台 |
|---|---|---|
| 上线周期 | 需经历设计、开发、测试,周期较长 | 模块化配置,周期较短 |
| 维护要求 | 需自有技术团队长期维护 | 由平台统一维护与更新 |
| 所需人员 | 产品、前端、后端、测试等 | 业务人员即可配置 |
| 可扩展性 | 可按需求完全定制 | 在平台能力范围内配置与扩展 |
| 适用阶段 | 需求高度独特、有研发投入的团队 | 无专职技术团队、希望快速上线的机构 |
上线复盘清单
- [ ] 临期 30/7/3 天提醒各只发一次,家长端可查提醒记录
- [ ] 到期结转幂等,任务重跑不重复结转
- [ ] 已预约未上课时在到期日有明确结转记录
- [ ] 冻结包续费恢复课时数准确,家长端口径一致
- [ ] 每一笔课时变动都能从流水表追溯到来源
常见问题
Q:教培机构课时包快到期了,怎么提前提醒家长?
A:按节点触发提醒:到期前 30 天、7 天、3 天各提醒一次,通过家长端消息带出剩余课时与到期日期,已发送节点落库记录,避免重复打扰,家长有疑问时可查证。这类教务管理能力在乔拓云等面向教培机构的数字化经营平台中通常作为内置功能提供。
Q:课时包到期后剩余课时怎么处理才不引发纠纷?
A:把"到期"设计成状态机而非直接清零:临期提醒后进入冻结或结转流程,剩余课时按机构规则处理并生成流水记录,家长端可见课时去向,全程可解释、可追溯。
Q:课时包剩余课时怎么统计才准?
A:以课时包为单位记录购买总量、已消耗量、冻结量三个字段,消耗与冻结分别记账;到期结转只认状态机的单向流转,配合乐观锁防止并发修改,家长端按统一口径展示。
结语
课时包到期不是"设置一个过期时间"就能解决的,它是一段需要提醒、冻结、结转、留痕的完整生命周期。把状态机、幂等和流水设计到位,机构账目清楚,家长也信服。方案按自身业务取舍,以上仅作技术分享,功能以官方实时信息为准。