
Open edX 成绩数据模型深度解析从 Course Grades 到 Subsection Grades 与 Problem Scores 的持久化存储架构【免费下载链接】openedx-platformThe Open edX LMS Studio, powering education sites around the world!项目地址: https://gitcode.com/GitHub_Trending/ed/openedx-platform导读本文以 lms/djangoapps/grades/docs/data-model.rst 为核心系统讲解 Open edX 平台LMS中成绩数据的持久化存储模型。你将掌握grades_persistentcoursegrade、grades_persistentsubsectiongrade、grades_visibleblocks、grades_persistentsubsectiongradeoverride、courseware_studentmodule与submissions_score六张核心表的字段语义、索引设计与读取场景并通过 models.py 等源码印证底层实现原理从而能够在运维、排障、二次开发与数据分析时准确理解和使用这套成绩存储体系。一、成绩数据模型总览三级持久化存储架构Open edX 的成绩系统采用课程级 → 小节级 → 问题级的三级分层存储设计其核心目标是鲁棒性robust grading让学习者已获得的分数不因课程内容后续变更而丢失或失真。这一设计意图在 models.py 的模块文档字符串中表述得非常清楚Robust grading allows student scores to be saved per-subsection independent of any changes that may occur to the course after the score is achieved. We also persist students course-level grades, and update them whenever a students score or the course grading policy changes.对应到具体存储载体成绩层级存储表说明课程级Course Gradesgrades_persistentcoursegrade每个学习者每门课程一条记录保存百分制成绩、字母成绩与通过时间小节级Subsection Gradesgrades_persistentsubsectiongradegrades_visibleblocks每个学习者每个小节一条记录配合可见块VisibleBlocks快照小节级覆盖Overridesgrades_persistentsubsectiongradeoverride教员/工作人员对小节成绩的覆盖及历史审计表问题级Problem Scorescourseware_studentmodule普通题目或submissions_scoreORA 开放性题目记录具体题目的得分从源码结构看这三级模型分别对应 models.py 中的PersistentCourseGrade、PersistentSubsectionGrade、VisibleBlocks、PersistentSubsectionGradeOverride四个 Django Model问题级数据则分别位于 lms/djangoapps/courseware/models.py 的StudentModule以及 ORA 子应用的submissions相关表中。二、课程成绩grades_persistentcoursegrade2.1 表结构与字段详解表名grades_persistentcoursegrade表说明保存学习者课程成绩的持久化值Persistent values for learners course grades。唯一约束派生索引(course_id, user_id)即每个学习者每门课程至多一条成绩记录同时派生course_id单列索引。附加索引user_id支撑学习者的课程仪表盘查询course_id, passed_timestamp支撑课程完成度统计字段明细字段名类型说明是否包含在数据包Data Package中course_idCourseKey所属课程的课程键。示例course-v1:orgcourserun新式课程或org/course/run旧式课程Yuser_idInteger学习者用户 ID。示例41446Ycourse_edited_timestampDateTime成绩计算时课程的最后编辑时间戳。当前仅用于调试目的。示例2016-12-21 15:50:23.645000Ncourse_versionString (255)成绩计算时课程在 Split Modulestore 中的版本号。当前仅用于调试。注意旧版 Mongo modulestore 不支持版本概念因此该字段对于此类课程为 NULL应改用course_edited_timestamp理解课程内容的日期信息。示例58ff632f00d9e7501e0148c4Ngrading_policy_hashString (255)课程评分策略grading policy的 SHA-1 摘要用于在策略变化时检测并更新成绩。示例NiGhcAFSrpyijXbow/XKE1Cp1GAYpercent_gradeFloat按评分策略计算的课程成绩小数百分比。示例0.91即 91%Yletter_gradeString (255)按评分策略计算的字母成绩如 A→D、Pass。若学习者成绩为 Fail 或 F此字段为空。示例Pass或AYpassed_timestampDateTime学习者首次通过课程的时间。若为空表示从未通过若非空但letter_grade为空表示学习者从通过状态转为未通过状态。注意由于成绩由平台异步计算外部评分器、ORA 评分等该时间与触发通过的题目提交时间之间存在延迟。示例2017-05-02 15:51:04.395055YcreatedDateTime该用户该课程成绩首次计算的时间。注意回填Backfilled的成绩此值会被设置为最终计算并回填的时间。YmodifiedDateTime该用户该课程成绩最后更新的时间。回填成绩同样取回填时间。Y2.2 索引设计意图与源码印证从源码 models.py 的PersistentCourseGrade.Meta可以看到索引设计注释与文档一一对应unique_together提供(course_id, user_id)用于查询单个成绩(course_id)隐式创建便于教员查看课程全部成绩显式索引(passed_timestamp, course_id)用于追踪首次及格时间(modified, course_id)用于按时间段查找更新的成绩。这与数据模型文档中的附加索引完全一致。预期的读取使用场景读取场景功能/团队所需索引进度页展示学习者课程成绩及成绩分解信息LMS Progress Pagecourse_id, user_id课程仪表盘展示学习者每门已注册课程的成绩LMS Student Dashboarduser_id成绩报告为课程内每个学习者生成含课程成绩的 CSVLMS Grade Reportcourse_id统计课程完成情况Analytics/Course Completioncourse_id, passed_timestamp2.3 写入路径update_or_create 与通过事件课程成绩的持久化入口是PersistentCourseGrade.update_or_createmodels.py其核心逻辑包括以(user_id, course_id)为查找键执行update_or_create将course_version缺省值规范为空字符串首次通过处理若本次计算标记为passedTrue且该成绩尚无passed_timestamp则发送COURSE_GRADE_PASSED_FIRST_TIME信号、写入当前时间戳并发送COURSE_GRADE_PASSED_UPDATE_IN_LEARNER_PATHWAY信号该信号定义于 lms/djangoapps/grades/signals/signals.py支撑学习者路径Learner Pathway等下游功能发送course_grade_calculated事件events.course_grade_calculated更新请求级缓存grades_cache.{course_id}发送 Open edX 标准事件PERSISTENT_GRADE_SUMMARY_CHANGED事件类型org.openedx.learning.course.persistent_grade_summary.changed.v1携带完整的成绩快照数据供事件总线消费者使用见 models.py。2.4 grading_policy_hash 的生成原理grading_policy_hash是课程成绩中一个容易被忽略但非常关键的字段——它是检测评分策略变更、触发成绩重算的依据。其生成逻辑位于 transformer.pyordered_policy json.dumps( course.grading_policy, separators(,, :), # 去除空格以获得更紧凑的表示 sort_keysTrue, ) return b64encode(sha1(ordered_policy.encode(utf-8)).digest()).decode(utf-8)即将课程的grading_policy字典先按键排序序列化为紧凑 JSON再取 SHA-1 摘要并 Base64 编码。由于序列化是确定性的任何策略变化都会导致哈希值变化从而让系统能够检测到策略变更并据此重新计算、更新成绩。运行时该值通过 course_data.py 的CourseData.grading_policy_hash属性从块结构Block Structure或课程对象上获取测试用例可在 lms/djangoapps/grades/tests/test_transformer.py 中看到具体哈希值如ChVp0lHGQGCevD0t4njna/C44zQ的断言验证。三、小节成绩Subsection Grades 的两表协同设计小节成绩由两张表协同工作Subsection Gradegrades_persistentsubsectiongrade与Visible Blocksgrades_visibleblocks。此外教员/工作人员可以覆盖小节成绩最近的覆盖记录保存在Subsection Grade Override表中覆盖历史保存在Subsection Grade Override History表grades_historicalpersistentsubsectiongradeoverride由 django-simple-history 自动生成中用于审计。3.1 Subsection Grade 表grades_persistentsubsectiongrade表名grades_persistentsubsectiongrade表说明保存学习者小节成绩的持久化值。唯一约束派生索引(course_id, user_id, usage_key)course_idcourse_id, user_idcourse_id, user_id, usage_key附加索引visible_blocks_hash外键引用 VisibleBlocks 的哈希列字段明细字段名类型说明是否包含在数据包DP中course_idCourseKey所属课程的课程键。示例course-v1:orgcourserun新式或org/course/run旧式Ycourse_versionString (255)成绩计算时课程在 Split Modulestore 中的版本号。当前仅用于调试。示例58ff632f00d9e7501e0148c4NcreatedDateTime该用户该小节成绩首次计算的时间。回填成绩取最终回填时间。Yearned_allFloat该小节中用户聚合的total_weighted_earned得分即小节内所有题目weighted_earned值之和。Yearned_gradedFloat小节内所有**计分graded**题目的weighted_earned值之和。Yfirst_attemptedDateTime用户在小节内首次尝试题目的时间。若用户未尝试过该小节则不存在该小节的记录。回填成绩会尽力推导该值——取小节内可用题目已尝试分数的created日期最小值。YmodifiedDateTime该用户该小节成绩最后更新的时间。回填成绩取回填时间。Ypossible_allFloat小节内所有题目的weighted_possible值之和总分。Ypossible_gradedFloat小节内所有计分题目的weighted_possible值之和。Ysubtree_edited_timestampDateTime成绩计算时小节内容或其任意后代内容最后编辑的时间戳。当前仅用于调试。示例2016-12-21 15:50:23.645000Nusage_keyUsageKey小节的用途键别名module_id、location。示例block-v1:orgcourseruntypesequentialblock1234新式课程或i4x://org/course/sequential/1234旧式课程Yuser_idInteger学习者用户 ID。示例41446Yvisible_blocksVisibleBlocks指向grades_visibleblocks表的外键。N预期的读取使用场景读取场景功能/团队所需索引与之前成绩比较判断是否需要条件性更新如提高分数重算 Rescore to IncreaseRescore to Increasecourse_id, user_id, usage_key详细成绩报告为课程内每个学习者生成含小节成绩的 CSVLMS Grade Reportcourse_id进度页展示学习者小节成绩分解LMS Progress Pagecourse_id, user_id源码印证PersistentSubsectionGrademodels.py除了上述字段外还有几个值得注意的实现细节主键使用UnsignedBigIntAutoField无符号大整数自增源码注释明确指出该表主键需要足够大usage_key对旧式 Mongo 课程可能未填充 run 值因此提供了full_usage_key属性将 run 补齐后再比较models.py模型还定义了(modified, course_id, usage_key)与(first_attempted, course_id, user_id)两个组合索引见 models.py分别支撑按时间段/课程/小节查询更新成绩与查询用户在某课程中所有已尝试小节两类场景update_or_create_grade与bulk_create_grades提供单条与批量两种写入路径写入时会先经由VisibleBlocks.cached_get_or_create/bulk_get_or_create确保可见块记录存在再以visible_blocks_id即哈希值直接落库避免多余查询。3.2 Visible Blocks 表grades_visibleblocks表名grades_visibleblocks表说明保存计算小节成绩时该学习者在小节内可见块的有序列表。多个学习者很可能共享同一份可见块列表因此这份数据被独立存放供 Subsection Grade 表中的多行记录引用。唯一约束派生索引(hashed)hashed附加索引course_id字段明细字段名类型说明是否包含在数据包DP中course_idCourseKey所属课程的课程键。NhashedString (100)blocks_json值的 SHA1 哈希。Nblocks_jsonLongText包含以下信息的 JSONversion数据格式版本号的整数course_key所属课程的序列化 CourseKeyblocks小节内用户可访问的所有块block的序列化 UsageKey 有序列表。注意blocks 字段保存的是计算小节成绩时用户可见的全部块的使用键列表。当用户对小节内容的访问权限发生变化时分班 cohort 变更、角色变更、课程团队增删单元/题目等该值会随之改变并在表中创建带新哈希值的新行。N设计精妙之处由于grades_visibleblocks以blocks_json的 SHA1 哈希作为唯一键相同的可见块集合只存一行多个学习者同一分班、同一角色可共享同一条记录从而显著减少冗余。其 JSON 结构中的 version 字段则用于支持未来数据格式的演进。源码印证VisibleBlocksmodels.py内部使用BlockRecordList与BlockRecord两个工具类来序列化/反序列化可见块数据BLOCK_RECORD_LIST_VERSION 1见 models.py。BlockRecord是包含locator、weight、raw_possible、graded四个字段的命名元组记录了块在参与成绩计算时的定位符、权重、原始满分与是否计分。序列化时采用separators(,, :)与sort_keysTrue生成紧凑、确定性 JSON再计算 base64 编码的 SHA-1 摘要hash_value。该模型还实现了完整的请求级缓存体系bulk_read、cached_get_or_create、bulk_create、bulk_get_or_create以visible_blocks_cache.{course_key}.{user_id}为键避免同一请求周期内重复读写数据库。3.3 Subsection Grade Overrides成绩覆盖与审计表名grades_persistentsubsectiongradeoverride表说明保存指定小节最近一次的覆盖记录。在成绩计算中覆盖值取代持久化的小节成绩汇总。其历史版本表grades_historicalpersistentsubsectiongradeoverride保存此前各次覆盖的滚动记录用于审计目的。唯一约束派生索引(id)id附加索引created、modified、grade_id字段明细字段名类型说明idint(11)覆盖记录的自增 ID。createddatetime(6)覆盖首次创建的时间。modifieddatetime(6)覆盖最后修改的时间。earned_all_overridedouble被覆盖的总得分含计分与不计分题目。注意该字段的实际用法尚不明确因为某些情况下其为 NULL且不参与成绩计算。possible_all_overridedouble小节的总可能得分含计分与不计分题目。earned_graded_overridedouble小节的被覆盖得分。possible_graded_overridedouble小节的计分题目总可能得分。grade_idbigint(20) unsigned与grades_persistentsubsectiongrade.id一一对应指明该覆盖作用于哪条成绩。override_reasonvarchar(300)教员提供的覆盖原因。示例Student bribed me with doughnuts so Im increasing their score.systemvarchar(100)执行覆盖的系统来源。示例GRADEBOOK、grade-import源码印证PersistentSubsectionGradeOverridemodels.py实现要点通过OneToOneField与PersistentSubsectionGrade关联related_nameoverride一条成绩至多一条覆盖使用django-simple-history的HistoricalRecords自动维护审计历史且通过apps.app_configs判断避免在 CMSStudio环境中因 grades app 未安装而导致的历史记录连接失败update_or_create_override接收requesting_user并将该用户挂到_history_user非字段属性上使历史记录能正确标注操作人_prepare_override_params定义了字段白名单映射earned_all_override ← earned_all、possible_all_override ← possible_all、earned_graded_override ← earned_graded、possible_graded_override ← possible_graded调用方未显式指定的覆盖字段会回退到原成绩的对应值保证覆盖记录的完整性。四、问题得分Problem Scores 的两种存储路径学习者在具体题目上的得分依据题目类型存储在两张不同的 SQL 表中常规题目存储在courseware_studentmoduleORA开放型题目存储在submissions_score。4.1 通用用户态存储courseware_studentmodule表名courseware_studentmodule表说明面向任意 xBlock/xModule不限于题目类型的用户特定状态的通用存储。除用户状态外还设有独立字段保存可计分块scorable blocks的 earned 与 possible 成绩。唯一约束派生索引(student, module_id, course_id)studentstudent, module_idstudent, module_id, course_id附加索引module_type、module_id、course_id、grade、done、created、modified字段明细字段名类型说明studentUser指向 User 表的外键。stateString自由格式字符串由对应 xBlock 按上下文解释如题目作答状态。module_typeString (32)xBlock 的块类型例如problem、video、html、chapter 等。module_idUsageKey (255)xBlock 的用途键usage key。modifiedDateTime行最后修改时间。max_gradeFloat用户提交题目时该题目的raw_possible得分。持久化此值可保证题目内容后续变化不影响用户在该题上的历史得分。gradeFloat用户在该题目上的raw_earned得分。doneString可能取值Not Applicable不适用、Finished已完成、Incomplete未完成。createdDateTime行创建时间。course_idCourseKey (255)xBlock 所属课程的课程键。源码印证StudentModule实现在 lms/djangoapps/courseware/models.py。其中module_state_key字段以module_id作为数据库列名db_columnmodule_id与文档描述一致done字段实际存储为三字符缩写na/f/i分别对应 NOT_APPLICABLE、FINISHED、INCOMPLETE见 models.py。此外还提供了两个实用类方法all_submitted_problems_read_only(course_id)按课程过滤出所有module_typeproblem且 grade 非空的已提交题目记录若环境配置了只读副本则自动路由到只读副本查询using(read_replica)支撑成绩报告等重读场景save_state/get_state_by_params状态保存与批量查询的封装保存时使用update_or_create保证幂等。4.2 ORA 开放型题目得分submissions_score表名submissions_score表说明ORAOpen Response Assessment开放型互评提交系统一组表结构中的一员专门保存 ORA 题目的得分。唯一约束派生索引(id)id附加索引student_item_id、submission_id、created_at字段明细字段名类型说明created_atDateTime行创建时间。points_earnedPositive Integer用户在该题目上的weighted_earned得分。points_possibleFloat用户提交题目时该题目的weighted_possible得分。持久化此值可保证题目内容后续变化不影响历史得分。注意由于points_earned与points_possible已是加权后的值成绩聚合时不会再次应用题目权重。resetBoolean指示此行得分应重置当前最高分。student_itemStudentItem指向submissions_studentitem表的外键。submissionSubmission指向submissions_submission表的外键。理解要点与courseware_studentmodule中保存的是raw_earned/raw_possible原始分不同submissions_score直接保存加权后的weighted_earned/weighted_possible。这意味着两类表在成绩聚合时的处理方式不同小节聚合Subsection Grade 中的earned_all/possible_all等字段直接对courseware_studentmodule的原始分应用权重后求和而对submissions_score则直接累加其加权值不再二次加权。这一点对于理解不同题型成绩计算的差异至关重要。五、Include in Data Package 列的含义三张主表中多张字段表末尾都有Include in DPData Package一列标注为 Y包含或 N不包含。它标识该字段是否纳入 Open edX 面向分析/数据仓库导出的**数据包Data Package**中。从字段分布规律可以看出用户、课程、成绩数值、时间戳等与分析维度强相关的字段course_id、user_id、percent_grade、letter_grade、passed_timestamp、created、modified、earned_all、possible_all等均为Y仅用于调试/内部实现细节的字段course_edited_timestamp、course_version、subtree_edited_timestamp、visible_blocks引用、blocks_json、hashed等均为N。这一设计让数据分析团队可以放心导出标 Y 的字段而不会把面向排障的内部实现细节污染到分析数据集中。六、延伸阅读与调试建议成绩模型完整源码见 lms/djangoapps/grades/models.py其中BlockRecordList、VisibleBlocks、PersistentSubsectionGrade、PersistentCourseGrade、PersistentSubsectionGradeOverride依次定义注释中包含了大量索引设计动机说明模型层单元测试见 lms/djangoapps/grades/tests/test_models.py可参考其对必填字段完整性如grading_policy_hash缺失触发IntegrityError的断言来理解字段约束成绩事件course/subsection grade calculated 事件见 lms/djangoapps/grades/events.py事件集成测试见 lms/djangoapps/grades/tests/integration/test_events.py成绩表结构迁移历史位于 lms/djangoapps/grades/migrations 目录例如 0006_persistent_course_grades.py 即课程成绩表的初始迁移可用于对照实际建表 SQLStudentModule完整定义见 lms/djangoapps/courseware/models.py其历史表StudentModuleHistory位于同一文件后半部分成绩报告依赖该历史表回溯题目得分变化。运维排障速查当学习者成绩未更新时可按层级依次排查——先看grades_persistentcoursegrade的grading_policy_hash是否与当前策略哈希一致不一致说明策略变更后未触发重算再看grades_persistentsubsectiongrade的earned_graded/possible_graded是否反映了最新作答最后检查courseware_studentmodule或submissions_score中的原始得分记录并留意submissions_score的加权字段在聚合时不再二次加权的特殊规则。【免费下载链接】openedx-platformThe Open edX LMS Studio, powering education sites around the world!项目地址: https://gitcode.com/GitHub_Trending/ed/openedx-platform创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考