岗位培训的难点不是把课程列出来,而是让员工按照岗位要求完成一组有顺序、有标准、可追踪的学习活动。织码在线教育系统没有额外引入 learning_path 或“能力地图”实体,而是复用培训任务模型:用 Task 表示路径,用 TaskPhase 表示阶段,用 TaskPhasePeriod 表示阶段内的学习单元,再由 studyMode、必修/选修属性和完成标准共同定义进阶规则。

一、模型边界:复用任务模型,而不是新增路径实体
把岗位路径直接映射到任务模型,可以避免路径、任务、课时三套实体之间的数据同步。但这也意味着路径能力必须遵循任务已有边界:内容编排在三级结构里,参与人关系在 task_user,过程数据在 task_user_study,证书配置在任务字段中。
| 层级 | 实体/表 | 主要职责 | 典型字段 |
|---|---|---|---|
| 路径 | Task/task | 名称、归属、完成规则 | studyMode、certId |
| 阶段 | TaskPhase/task_phase | 阶段名称和阶段标准 | taskId、completionStandard |
| 课时 | TaskPhasePeriod/task_phase_period | 具体学习活动 | periodType、periodAttribute |
| 参与人 | TaskUser/task_user | 谁被分配到任务 | 用户、任务状态 |
| 学习过程 | TaskUserStudy/task_user_study | 完成与进度 | 课时、状态、时间 |
PeriodTypeEnum 将学习单元区分为 MATERIAL(10)、LIVE(20)、EXAM(30)、JOB(40)。因此,岗位路径可以把规范阅读、直播培训、考试和作业放进同一条可执行链路。
二、完成规则:必修、选修与任务聚合

periodAttribute=1 表示必修,periodAttribute=0 表示选修;阶段和任务的 completionStandard 则负责表达“至少完成多少个选修单元”或“达到多少分”。完成判定不应只读取客户端传来的一个布尔值,而应在服务端重新聚合学习明细。
public boolean phaseCompleted(TaskPhase phase, List<TaskUserStudy> studies) {
long requiredDone = studies.stream()
.filter(s -> s.isRequired() && s.isCompleted()).count();
long optionalDone = studies.stream()
.filter(s -> !s.isRequired() && s.isCompleted()).count();
return requiredDone == requiredCount(phase)
&& optionalDone >= phase.getCompletionStandard();
}
解锁模式 studyMode=2 的关键是把前置完成结果转化为后续可访问性:查询课程目录时返回锁定状态,打开课时时再次由服务端校验,提交考试或作业时再次校验前置条件。多处校验可以防止用户绕过前端直接调用接口。
三、解锁状态与并发:从展示状态到服务端约束
推荐把课时状态划分为 LOCKED、AVAILABLE、IN_PROGRESS、COMPLETED 和 FAILED。状态变更由学习事件驱动,例如素材达到有效学习时长、直播签到完成、考试达到及格线或作业审核通过。
@Transactional
public void completePeriod(Long userId, Long periodId) {
TaskPhasePeriod period = periodRepo.mustGet(periodId);
TaskUser userTask = taskUserRepo.mustGet(userId, period.getTaskId());
unlockService.assertAvailable(userTask, period);
studyRepo.upsertCompleted(userId, periodId);
phaseService.recalculate(userId, period.getPhaseId());
}
学习完成事件可能因重试重复到达,因此 upsertCompleted 应具备幂等语义,完成时间和完成状态不能被旧事件覆盖。阶段重算和解锁通知最好放在同一事务内提交,再异步刷新缓存或发送消息。
四、岗位分发:部门展开、用户去重与增量指派

assignDept 和 assignUser 只是指派请求,真正的参与关系要展开到 task_user。部门指派通过 feignUsers.listByDeptIds(deptIds) 获取部门用户,再与显式用户合并去重。建议使用 (task_id, user_id) 唯一索引或数据库 upsert,避免重复提交生成重复参与记录。
CREATE UNIQUE INDEX uk_task_user ON task_user(task_id, user_id);
CREATE INDEX idx_study_user_task ON task_user_study(user_id, task_id);
增量指派还要定义边界:新加入部门的员工是否自动补发任务,已经完成任务的员工是否保持完成状态,部门移除后是否只停止后续学习还是保留历史记录。这些规则应由任务状态和审计记录表达,而不是删除历史关系。
五、自动发证:完成事件、查重和异步生成

任务完成后,如果 certSet=true,系统调用 autoIssueCert 生成 CertUser,并异步生成证书图片。发证接口需要同时满足三点:完成条件已由服务端确认、同一任务同一用户只生成一条有效证书、证书图片生成失败可重试。
public void issueOnce(Long userId, Long taskId) {
String key = "cert:issue:" + taskId + ":" + userId;
if (!lock.tryLock(key, 10, TimeUnit.SECONDS)) return;
try {
if (certUserRepo.exists(userId, taskId)) return;
certUserRepo.insert(userId, taskId, IssueType.AUTO.getCode());
eventPublisher.publishCertificateRender(userId, taskId);
} finally { lock.unlock(key); }
}
证书的生成事件和图片渲染可以异步化,但证书主体记录应先落库。这样即使渲染服务短暂不可用,系统仍能追踪待生成状态并执行补偿。
六、数据查询与运营观测
路径首页通常需要同时返回阶段进度、课时锁定状态和任务总进度,建议服务端采用批量查询后组装,避免逐课时 N+1 查询。对于岗位看板,可按部门、任务、完成状态建立组合索引,并记录解锁失败、重复提交、考试不及格和证书生成失败等事件。
可观测指标包括任务分配成功率、阶段完成率、解锁拦截次数、课时完成事件重复率、自动发证成功率和证书渲染延迟。这些数据能帮助培训管理员判断问题究竟出在路径设计、内容质量还是系统执行链路。
七、总结
岗位学习路径在这套系统中不是一张静态地图,而是一条由任务结构、完成规则、参与关系和学习状态共同驱动的执行链路。三级编排负责表达内容,解锁模式负责表达顺序,服务端聚合负责判断达标,部门/用户指派负责落地,幂等发证负责形成可验证结果。
这种“复用既有任务模型”的方式适合先快速落地岗位培训,再逐步扩展能力标签、岗位版本和 O*NET 类标准化能力模型,而不会在早期引入过多孤立实体。
如需私有化部署报价、远程产品演示,可访问官网 https://www.weavecodes.com/ ,私信作者领取企业落地案例。

800

被折叠的 条评论
为什么被折叠?



