资讯详情

智学文枢:学生与习题的状态中枢

📅 2026/10/23 6:30:50 | 华诺云谱 👁 阅读
智学文枢:学生与习题的状态中枢
student_store / question_store学生与习题的状态中枢一句话摘要学生是档案主体、习题是分析对象StudentStore与QuestionStore各自管好内存缓存 CRUD Signal 广播一个专注档案与分科配置一个连图片清理、组合题、知识点替换这些细节都包办了是整个 AI 工作台的数据源头。一、引言智学文枢的业务模型有一个清晰的主从结构学生Student是档案主体习题Question挂靠在学生名下。几乎每个页面都在做同一件事——“选一个学生看他的习题对某道题做分析/讲解/变式/报告”。所以第一性问题来了这么多页面共享的学生和习题数据该由谁来说了算如果把读库逻辑散落在十几个页面里任何一个页面新增学生后其他页面都得被手动通知漏一个就出现幽灵数据更糟的是习题还涉及图片文件、组合题父子关系、知识点标签这些连带资产删除时漏清理一项数据目录里就会堆满孤儿文件。答案是收敛StudentStore和QuestionStore作为两个全局单例成为学生与习题的唯一数据入口。所有页面只做三件事——调用 Store 的方法、订阅 Store 的信号、读取 Store 的缓存。二、核心原理2.1 职责边界Store数据主体连带资产典型页面StudentStore学生 分科配置SubjectConfig头像图片文件学生档案页、看板、学生弹窗QuestionStore习题 知识点QuestionKnowledge习题图片文件、组合题子题习题管理页、AI 分析页等 8 个页面2.2 两条筛选策略SQL 过滤 vs 内存过滤QuestionStore.filter_questions展示了两种过滤策略的混用有 SQL 捷径按学科调repo.get_by_subject(student_id, subject)让 SQLite 干活无 SQL 捷径错题标记、学科错题组合先repo.get_by_student全量拉取再在内存里用列表推导过滤。取舍逻辑很简单题目表数据量在桌面应用场景下通常几百到几千行内存过滤毫秒级完成换来的是 Repository 接口更精简、不用为每个组合条件写 SQL。2.3 组合题一道父题 N 道子题组合题题干 多子问是习题模型里最复杂的结构。create_group_question一次写入父题和全部子题子题共用题干图片、持有parent_question_id删除父题时缓存过滤会连同parent_question_id question_id的子题一起清掉。这是连带资产处理的第一个实例。1:N1:N1:N1:NN:1N:11:11:N学生习题父题题干图片子题1子题2子题3知识点标签习题图片文件分科配置三、代码实现3.1 删除学生连头像图片一起清理delete_student是 Store 处理连带资产的示范——先删数据库记录再清缓存、清头像文件、广播defdelete_student(self,student_id:str)-bool:删除学生含头像图片清理与删除广播 仅删除数据库记录并广播由界面侧随后调用 load_students() 触发全局刷新。 repoStudentRepository()try:studentrepo.get_by_id(student_id)successrepo.delete(student_id)ifsuccess:self._cleanup_avatar(student)self._remove_from_cache(student_id)self.student_deleted.emit(student_id)self.data_changed.emit()returnsuccessfinally:repo.close()源码位置ui/stores/student_store.py L107-L122注意 docstring 里的约定Store 只删库并广播界面侧随后主动调load_students()做全局刷新。这是信号驱动架构里的一个默契——Store 负责数据权威页面负责刷新编排。3.2 外部删除同步notify_deleted数据管理页可以直接操作数据库删除学生此时 Store 不能假装不知道。notify_deleted是外部入口defnotify_deleted(self,student_id:str):外部入口如数据管理页删除学生后同步缓存并统一广播删除事件。 数据已由外部删除此处不再操作数据库与图片。 self._remove_from_cache(student_id)self.student_deleted.emit(student_id)self.data_changed.emit()源码位置ui/stores/student_store.py L124-L130它的存在说明Store 的唯一入口是设计目标但现实中总有绕过它的路径批量导入、数据管理页。notify_deleted提供了一条事后同步的后门——不动库只同步内存缓存并广播把数据已经变了的通知职责补回来。3.3 习题筛选SQL 内存混合deffilter_questions(self,student_id:str,subject:strNone,is_mistake:intNone):按条件筛选习题self._current_filters[student_id]student_id self._current_filters[subject]subject self._current_filters[is_mistake]is_mistake repoQuestionRepository()try:ifsubjectandis_mistakeisnotNone:# 按学科错题筛选all_qrepo.get_by_student(student_id)self._questions[qforqinall_qifq.subjectsubjectandq.is_mistakeis_mistake]elifsubject:self._questionsrepo.get_by_subject(student_id,subject)elifis_mistakeisnotNone:all_qrepo.get_by_student(student_id)self._questions[qforqinall_qifq.is_mistakeis_mistake]else:self._questionsrepo.get_by_student(student_id)self.questions_loaded.emit(self._questions)finally:repo.close()源码位置ui/stores/question_store.py L45-L70四个分支对应四种筛选组合逻辑直接、无 SQL 拼接注入风险_current_filters记录当前筛选状态便于后续增删后保持筛选结果一致。3.4 知识点整体替换靠 ORM 级联知识点不写删旧加新的循环而是直接重赋值集合靠 ORM 的cascadedelete-orphan自动完成替换defsave_knowledge_points(self,question_id:str,knowledge_points:List[str]):整体替换某习题的知识点标签先删旧再写新。 知识点存于 QuestionKnowledge 关联表利用 Question.knowledge_points 的 cascadedelete-orphan 关系重赋值即实现替换空串/None 忽略。 valid[(kor).strip()forkin(knowledge_pointsor[])if(kor).strip()]repoQuestionRepository()try:qrepo.session.get(Question,question_id)ifnotq:returnq.knowledge_points[QuestionKnowledge(question_idquestion_id,knowledge_pointk,confidence0.8,is_primary1,)forkinvalid]repo.session.commit()# 空列表也要提交以删除旧知识点finally:repo.close()源码位置ui/stores/question_store.py L289-L312注释里特别强调空列表也要提交——否则清空全部知识点的操作会被 ORM 当成没改而跳过旧标签残留。QuestionRepositoryQuestionStore习题管理页用户QuestionRepositoryQuestionStore习题管理页用户勾选学科错题 筛选filter_questions(sid, subject, 1)get_by_student(sid)全部习题内存过滤 subject is_mistakeemit questions_loaded(list)刷新表格行四、设计权衡 / 踩坑记录4.1 坑删除习题忘删图片 → 孤儿文件习题图片存在数据目录数据库字段只是路径。若删除只清行不删文件日积月累目录全是无用图片。delete_question的解法是先删文件再删行ifquestion.question_image_path:delete_image(question.question_image_path)successrepo.delete(question_id)源码位置ui/stores/question_store.py L235-L237与头像清理_cleanup_avatar同一思路连带资产与主记录同生命周期。4.2 权衡create 后同步插入缓存头部create_question里self._questions.insert(0, question)新题排在最前符合最新录入在最上面的产品直觉也避免了一次全量重查。代价是当列表处于筛选状态时新插入的题可能不符合当前筛选却仍然显示——但题目流水的场景中录入即处理这个瑕疵被产品流程天然掩盖了。4.3 权衡update 走原地替换而非全量刷新update_question更新后只替换内存中对应下标元素fori,qinenumerate(self._questions):ifq.idquestion_id:self._questions[i]questionbreak源码位置ui/stores/question_store.py L218-L221比全量重查省一次 IO但必须保证内存对象与库中对象一致——如果别处如阶段推进也改了同一条记录这里替换的可能是旧版本。所以refresh_question_status特意重查再替换两个方法各司其职普通编辑用原地替换阶段推进用重查刷新。五、小结StudentStore管档案 分科配置QuestionStore管习题 知识点二者都是内存缓存 Repository 代理 Signal 广播的标准 Store删除操作必须处理连带资产头像文件、习题图片、组合题子题避免孤儿文件与幽灵数据筛选策略按需选择 SQL 过滤或内存过滤数据量小的桌面应用完全够用知识点替换依赖 ORM 级联注意空列表也要提交的细节notify_deleted证明即使有外部绕行路径Store 也要提供事后同步入口把通知职责补全。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑