Open edX 课程证书状态机解析:downloadable / notpassing / unavailable / unverified 的设计决策与实践
【免费下载链接】openedx-platformThe Open edX LMS & Studio, powering education sites around the world!项目地址: https://gitcode.com/GitHub_Trending/ed/openedx-platform
导读
在 Open edX 平台中,课程证书并非只有“有/无”两种存在形态,而是由一套明确的状态(status)枚举驱动:从生成、发放到失效、撤销,每一个业务动作都会将证书记录置于特定状态。本文以 lms/djangoapps/certificates/docs/decisions/004-cert-status.rst 这份已获采纳(Accepted)的架构决策记录(ADR)为核心,完整梳理当前课程证书代码实际写入的四种状态——downloadable、notpassing、unavailable、unverified,并结合仓库中CertificateStatuses模型、GeneratedCertificate的状态变更方法与证书生成链路,说明各状态的语义、触发条件与底层实现。读完本文,你将能准确理解 Open edX 证书模块的状态机设计,并能基于源码定位证书为何“拿不到”“被撤销”或“处于异常态”。
一、背景:证书状态为什么需要一个明确的集合
课程证书在 Open edX 中存储于GeneratedCertificate模型(见 lms/djangoapps/certificates/models.py)。一份证书在生命周期内可能经历“已生成、已发放、已失效、未通过、未验证”等多种情形,因此证书记录上有一个status字段,取值来自统一的枚举类CertificateStatuses(定义于 lms/djangoapps/certificates/data.py)。
对用户而言,一份证书只有处于downloadable(可下载)状态时,才真正“对用户可见、可获取”——学习者门户(dashboard)与证书展示页仅展示该状态的证书。其他状态要么表示用户尚未满足发证条件,要么表示证书已被撤销,用户看不到可用的证书。
这一判断标准贯穿证书模块各处:例如
CertificateStatuses.is_passing_status()仅将downloadable与generating视为“通过”状态,供成绩、退款等业务逻辑判断。
1.1 历史上的全部状态值
CertificateStatuses枚举完整保留了证书模块历史上出现过的所有取值(data.py 中CertificateStatuses类):
| 状态值 | 语义 |
|---|---|
deleted | PDF 证书已被删除 |
deleting | 已发起删除 PDF 证书的请求 |
downloadable | 用户已被授予证书,证书就绪且可获取 |
error | PDF 证书生成过程中发生错误 |
generating | 已发起生成 PDF 证书的请求,但尚未生成完成 |
notpassing | 用户未达到及格成绩 |
restricted | 用户被限制获取证书 |
unavailable | 证书已被作废(invalidated) |
auditing/audit_passing/audit_notpassing | 审计轨(audit track)用户的相关状态 |
honor_passing | 荣誉轨(honor track)用户且已及格 |
unverified | 用户没有获批且未过期的身份验证 |
invalidated | 证书无效 |
requesting | 已发起生成 PDF 证书的请求 |
1.2 决策核心:当前代码只写四种状态
ADR 004 的关键决策在于:尽管枚举类保留了上述全部取值,但当前课程证书代码只会写入四种状态,其余状态仅因历史原因与存量证书而保留:
downloadable—— 用户已获授证书,证书就绪可获取;notpassing—— 用户未达到及格成绩;unavailable—— 证书已被作废;unverified—— 用户没有获批且未过期的身份验证。
这一点在源码枚举类的 docstring 中有明确注释(data.py):四种状态分别由证书生成逻辑(generation.py)与GeneratedCertificate的mark_notpassing()、invalidate()、mark_unverified()方法写入。
二、决策:四种状态各自的写入场景与代码实现
2.1downloadable:生成(或更新)一份可获取的证书
当用户满足全部发证条件时,证书模块会生成或更新一份downloadable证书。生成入口是 generation.py 中的generate_course_certificate(),其内部通过_generate_certificate()调用GeneratedCertificate.objects.update_or_create():
- 若该用户在此课程 run 下尚无证书记录,则创建一条新记录;
- 若已存在证书记录,则复用其
verify_uuid(保证学习者原有证书链接继续有效)并更新记录。
生成后若证书状态属于通过类状态(PASSED_STATUSES = (downloadable, generating)),还会通过emit_certificate_event()发出created证书事件,供埋点统计使用。
2.2notpassing:用户未达到及格成绩
用户未通过课程时,若证书记录已经存在,其状态会被改写为notpassing。对应的实现是GeneratedCertificate.mark_notpassing()(models.py):
- 入参包括用户当前 enrollment mode、成绩快照(grade,十进制小数)与来源标识
source; - 内部调用统一的撤销方法
_revoke_certificate(),将状态置为CertificateStatuses.notpassing。
ADR 同时强调了一种典型场景:如果一份downloadable证书已存在,而系统收到该用户未及格的成绩信号(failing grade signal),且该用户不在 allowlist(白名单)中,证书状态就会被改写为notpassing。换句话说,即使曾经发过证,后续成绩被判定为未通过时,证书仍会被降级回收。
2.3unavailable:证书已被作废
证书一旦被作废(invalidate),状态即为unavailable。实现位于GeneratedCertificate.invalidate()(models.py):
- 若未显式传入
mode,会自动查询用户在当前课程 run 的 enrollment mode; - 记录日志后调用
_revoke_certificate(status=CertificateStatuses.unavailable, ...); - 作废动作会触发
COURSE_CERT_REVOKED信号,进而启动任务检查是否需要同步撤销该学习者的项目(program)证书;若被作废前状态为downloadable,还会额外发出edx.certificate.revoked追踪事件(见_revoke_certificate()注释,models.py)。
2.4unverified:身份验证未通过或已过期
当用户满足除“身份验证”外的全部发证条件、但没有获批且未过期的 ID 验证时,会生成一份unverified证书。实现位于GeneratedCertificate.mark_unverified()(models.py),同样走_revoke_certificate()将状态置为unverified。
在证书生成链路中,这一分支清晰可见:generate_course_certificate()在证书状态非通过类时,若发现状态为unverified,会调用cert.mark_unverified(mode=enrollment_mode, source='certificate_generation')(generation.py)。需要注意的是,该判断分支使用elif,即unverified的补写逻辑只针对未被判定为通过状态的证书。
三、后果:状态流转规则与关键边界
ADR 004 明确了四种状态之间的流转后果,结合源码可以归纳如下规则:
- 作废即
unavailable:证书一旦被作废,其状态必然为unavailable。 - 全部条件满足 →
downloadable:生成或更新一份可下载证书。 - 除身份验证外全部满足 →
unverified:生成一份未验证状态的证书。 - 除及格外全部满足且证书已存在 →
notpassing;或已有downloadable证书、收到未及格信号且用户不在 allowlist →notpassing。
3.1 状态之间不存在层级关系
ADR 特别强调:这四种状态并不构成一个层级(hierarchy)。例如,一份证书可以处于notpassing,即使该用户同样没有通过身份验证要求。各状态由不同的业务条件独立触发,不能根据某状态推断另一条件的满足情况。这提醒开发者在做证书相关判断时,必须显式检查具体状态值,而不是假定状态之间存在大小/先后关系。
3.2 辅助状态判断方法
CertificateStatuses提供了两个与退款/通过判断相关的类方法(data.py):
is_passing_status(status):状态是否为downloadable或generating;is_refundable_status(status):状态不在NON_REFUNDABLE_STATUSES = (downloadable, generating, unavailable)中时返回 True,即处于notpassing、unverified等状态的证书允许退款。
此外readable_statuses字典给出了面向用户展示的友好文案,例如downloadable → "Received"、notpassing → "Not Received"、unavailable → "Invalidated"。
四、配套依据:发证条件的完整要求
四种状态中的downloadable对应“全部条件满足”的结果,其具体条件由两份相邻 ADR 定义:
- 常规(非 allowlist)证书(002-cert-requirements.rst)要求:用户在该 course run 有 enrollment 且 enrollment mode 对证书有资格(无需处于 active);没有已作废的证书(
CertificateInvalidation模型);HTML(web)证书全局开启且该课程 run 开启;用户已通过课程;用户不是该 run 的 beta 测试者;该 run 不是 CCX 课程;若ENABLE_CERTIFICATES_IDV_REQUIREMENTWaffleFlag 开启,还需有获批且未过期的身份验证。 - Allowlist 证书(001-allowlist-cert-requirements.rst)大体一致,但将“用户已通过课程”替换为“用户在该 run 的 allowlist 中”(存储于
CertificateAllowlist模型,早期名为白名单CertificateWhitelist),且允许课程工作人员为未获证用户手动授予证书。
对照可见:“未验证”“未通过”正是常规发证条件中两个可独立缺失的环节——这也是unverified、notpassing两种状态存在的原因。而 allowlist 场景下,notpassing的降级规则还要求“用户不在 allowlist”,避免白名单用户被成绩信号误伤(详见 ADR 004 Consequences)。
五、源码验证:测试用例与状态变更链路
仓库测试对上述状态变更逻辑提供了直接验证(lms/djangoapps/certificates/tests/test_models.py):
test_invalidate/test_invalidate_find_mode/test_invalidate_no_mode:覆盖invalidate()显式传 mode、自动从 enrollment 查询 mode、无 enrollment mode 等分支;test_invalidate_no_profile、test_invalidate_with_verified_name:覆盖无用户 profile、启用 verified name 时的作废行为;mark_notpassing与mark_unverified亦有对应测试用例(如第 547 行、第 611 行附近的测试)。
这些测试共同确认了三点事实:三种“非下载”状态都经由_revoke_certificate()统一写库;invalidate()会自动补查 enrollment mode;状态变更会联动事件与信号(COURSE_CERT_REVOKED、edx.certificate.revoked)。
从源码结构可以推断,状态变更的核心收敛点就是_revoke_certificate()(models.py 起):它记录旧状态、清除download_uuid与download_url(主要影响 PDF 证书)、写入新状态,并触发信号与追踪事件——这保证了无论从哪个入口(成绩信号、验证失效、人工作废)发起,状态流转行为都保持一致。
六、结语
Open edX 的课程证书状态设计遵循“枚举收敛 + 统一流转”的原则:对外部保留完整历史状态值以兼容存量数据,对内只使用downloadable、notpassing、unavailable、unverified四个状态表达证书当前的可获取性与资格状况。理解这份 ADR,是在 Open edX 上排查证书发放问题、扩展新发证逻辑或对接证书 API 的必备基础。
关键源码入口速览:
- 状态枚举与判断方法:lms/djangoapps/certificates/data.py
- 证书模型与状态变更方法:lms/djangoapps/certificates/models.py
- 证书生成链路:lms/djangoapps/certificates/generation.py
- 常规证书要求 ADR:lms/djangoapps/certificates/docs/decisions/002-cert-requirements.rst
- Allowlist 证书要求 ADR:lms/djangoapps/certificates/docs/decisions/001-allowlist-cert-requirements.rst
- 状态变更测试:lms/djangoapps/certificates/tests/test_models.py
【免费下载链接】openedx-platformThe Open edX LMS & Studio, powering education sites around the world!项目地址: https://gitcode.com/GitHub_Trending/ed/openedx-platform
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考