前言

在前几周完成客户端基础框架、登录注册、题库展示、顺序练习、专项练习与模拟考试等核心模块之后,本周我开始集中推进 CarLearn 项目中一个非常关键但也非常容易被低估的部分——错题复习模块。如果说刷题功能解决的是“用户能不能练”,那么错题复习解决的就是“用户能不能真正学会”。因此,这一周我的工作重点不再只是单纯补一个新页面,而是围绕“错题应该如何被记录、如何被管理、如何在多场景下统一汇总、以及如何与用户体系真正绑定”展开了一次比较完整的模块重构。

相比之前那种更偏向页面和流程打通的开发阶段,这一周的工作更偏向结构设计和数据闭环建设。我逐渐意识到,一个长期项目里的错题系统绝不应该只是“错了以后存在本地,然后列出来看看”这么简单,而是应该具备更清晰的业务边界:顺序练习、专项练习、模拟考试三种来源的错题要统一进入同一个错题库;错题数据应该与用户形成真实关联,而不是附着在题目本身;前端本地缓存、后端持久化和后续统计分析之间也应形成明确分工。

本周还有一个非常突出的特点,就是 AI 在项目开发中的参与深度再次提高。这一次 AI 不只是帮助我解释某一段报错,或者补几行界面代码,而是贯穿了需求分析、数据库设计、模块拆分、蓝图结构整理、接口联调和问题排查的全过程。特别是在“错题到底应该放在哪个数据库”“Room、MySQL、MongoDB 各自承担什么职责”“现有代码应不应该继续耦合在 questions 表上”这些问题上,AI 给我的帮助已经不再局限于“答案”,而更像是在持续陪我梳理一个长期项目真正应有的架构思路。


本周工作关键词

Android Studio Kotlin Room Retrofit MySQL MongoDB Flask Blueprint 错题复习 独立错题库 AI辅助开发


一、本周工作概览

本周我主要围绕错题复习模块完成了以下几个方面的工作:

  • 明确“错题复习”不再只是页面,而是独立业务模块

  • 将错题数据从题库数据中拆分出来,设计独立错题库结构

  • 完成 Android 端本地独立错题表 wrong_questions 的构建

  • 让顺序练习、专项练习、模拟考试三类答错题统一写入错题库

  • 完成错题复习页的筛选、排序、移除、从某题开始继续练习等交互

  • 将后端错题正式从 Mongo 过渡为 MySQL 中的 user_wrong_questions

  • 解决 Flask-Migrate 迁移过程中的模块依赖与蓝图导入问题

  • 优化做题页解析区布局,支持长内容下滑查看

  • 继续以 AI 协作方式推进架构决策、代码落地与排错

这一周的核心,不是“我又新做了一个功能页”,而是我开始把错题系统真正做成一个长期可扩展的业务模块


二、重新理解错题复习:它不是附属功能,而是学习闭环核心

一开始我对错题复习的理解比较简单,觉得只要能把做错的题列出来,再支持用户重新作答,这个功能就算基本完成了。但随着项目功能越来越多,我逐渐意识到,如果错题系统只是一个零散附属页面,那么整个项目的学习闭环其实是不完整的。

因为用户真正高频接触错题的场景,根本不止一种。顺序练习会产生错题,专项练习会产生错题,模拟考试交卷后同样会产生一批需要复盘的题。如果这些错题分别散落在不同模块里,或者只依赖局部状态进行记录,那么后续无论是想做统一错题本、薄弱点分析,还是做 AI 个性化讲题,都会越来越困难。

因此,这一周我先做的第一件事,不是立刻写界面,而是先重新定义“错题复习”的业务边界。我把它理解为一个真正独立的学习模块,它应该满足三件事:第一,任何来源的错题都要进入同一个库;第二,这个库必须和用户关联;第三,它既要支持当前的错题展示与重做,也要为后续统计分析和个性化推荐留出空间。

也正是在这个阶段,AI 的帮助非常明显。很多时候我最初会倾向于继续沿用旧结构,例如直接在题库表里加字段、或者先“让它跑起来再说”。但在不断与 AI 讨论之后,我逐渐被提醒要从长期项目视角思考问题:一个演示型原型可以接受结构耦合,但一个真正要持续迭代的项目,一开始就要尽量把数据边界划清楚。这样的提醒对我很重要,因为它让我没有停留在“能用”的层面,而开始认真考虑“能否长期维护”。


三、数据库结构重构:从题目附带错题信息,到独立错题库

这一周最核心的技术变化,就是把错题从“题目字段上的附加状态”重构为“独立数据实体”。

在较早的实现中,错题信息本质上是挂在题目上的,例如通过 wrongCountlastWrongTime 等字段描述某道题是否做错过。这种做法的优点是实现快,但缺点也很明显:题目本体和用户行为混在一起,天然不适合多账号、同步和统计。

因此,我这周做出的一个关键决定,就是把错题正式拆成独立错题表。也就是说,在数据库层面形成了这样一种职责划分:

  • MongoDB 继续保存题库 questions,因为它更适合承载题干、选项、图片、标签、解析等文档型题目数据

  • MySQL 保存 users 和新的 user_wrong_questions,因为这部分属于与用户强关联的业务数据

  • Android 本地通过 Room 维护 wrong_questions 缓存表,用来提升错题页加载速度并支持离线体验

这种结构变化对我来说是本周最大的成长点之一。以前我更多是站在“页面开发者”的角度思考功能,但这次我开始真正站在“业务数据设计者”的角度思考:什么是题目本身,什么是学习行为,什么该存在本地,什么该在后端持久化。

后端新增的错题模型最终设计为:

class UserWrongQuestion(db.Model):
    __tablename__ = "user_wrong_questions"
    __table_args__ = (
        db.UniqueConstraint("user_id", "question_id", name="uq_user_question_wrong"),
    )

    id = db.Column(db.Integer, primary_key=True)
    user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False, index=True)
    question_id = db.Column(db.Integer, nullable=False, index=True)
    subject = db.Column(db.String(20), nullable=False)
    wrong_count = db.Column(db.Integer, nullable=False, default=1)
    created_at = db.Column(db.DateTime, nullable=False)
    updated_at = db.Column(db.DateTime, nullable=False)
    last_wrong_time = db.Column(db.DateTime, nullable=False)

这段代码看起来不长,但它代表的意义很大。因为从这一刻开始,错题终于不再是“题目上的一个附加标记”,而是变成了一个真正意义上的用户学习记录实体。

AI 在这一部分的帮助,主要体现在两个方面。第一,它帮助我快速比较了 “继续挂在 questions 表上” 和 “拆成 user_wrong_questions 独立表” 两种方案的利弊,尤其是在多账号、统计分析和后端同步上的差异。第二,它帮助我更快地理清了 MySQL、MongoDB、Room 三者的职责边界,让我不至于在设计中把所有数据都堆在一个地方。


四、本地独立错题库落地:Room 表结构与错题页逻辑重构

在客户端一侧,我同样没有继续沿用旧方案,而是配套新增了本地独立错题表 wrong_questions,作为错题页和错题复习的主要本地数据来源。

这意味着错题页不再直接依赖旧的题库表状态,而是通过独立错题实体进行组织。这样一来,本地展示和后端同步都能拥有更清晰的逻辑边界:前者负责保证用户立即能看到错题,后者负责保证错题数据可长期保存、可跨端同步。

例如,本地错题写入逻辑最终集中到了 WrongQuestionRepository 中:

suspend fun upsertWrongQuestion(questionId: Int, subject: String) {
    val old = wrongQuestionDao.getWrongQuestionById(questionId)
    val now = System.currentTimeMillis()

    if (old == null) {
        wrongQuestionDao.insertWrongQuestion(
            WrongQuestionEntity(
                questionId = questionId,
                subject = subject,
                wrongCount = 1,
                lastWrongTime = now,
                updatedAt = now
            )
        )
    } else {
        wrongQuestionDao.updateWrongQuestion(
            old.copy(
                wrongCount = old.wrongCount + 1,
                lastWrongTime = now,
                updatedAt = now
            )
        )
    }
}

围绕这一层,我也进一步重构了错题复习页面,让它具备了更像一个真实产品模块的能力。例如,错题页已经不只是“展示几道题”,而是支持:

  • 高频错题 / 最近错题 排序切换

  • 科目一 / 科目四 筛选

  • 点击错题卡片,从该题开始继续练习

  • 单题移出错题本

  • 错题复习答对后自动移除

  • 解析显示与题目图片继续保留

这一部分最让我有成就感的,不是“页面看起来更完整了”,而是它终于从一个临时列表页,成长为一个逻辑上闭环的错题中心。

AI 在这里的作用非常具体。比如在我一开始遇到“筛选看起来没反应”“不同文件中的按钮 id 不统一”“错题页图片目录选错科目”这类问题时,AI 不只是告诉我“哪一行报错”,而是不断提醒我从整套链路去看问题:UI 事件、ViewModel 状态、Room 查询、题目映射、参数传递是不是同一版本。正是这种“帮我理一整套链路”的方式,让我在很多细碎 Bug 上省下了大量反复试错时间。


五、三类练习统一归集:顺序、专项、模拟考试全部写入错题库

本周另一个很关键的推进,是实现了三类来源统一写入错题库

一开始,错题更像是顺序练习里的附属产物;但随着专项练习和模拟考试上线,如果继续各自单独处理,就会导致错题来源分裂。因此,我有意识地把三条链路逐步打通:

  • 顺序练习中答错时,调用独立错题仓库写入

  • 专项练习中答错时,也走同样的写入逻辑

  • 模拟考试则在交卷后遍历错题,把错误作答统一写入独立错题库

例如,在模拟考试交卷逻辑中,我增加了统一错题写入:

questions.forEachIndexed { index, question ->
    val userAnswer = answers[index].orEmpty()
    if (userAnswer.isNotBlank() && userAnswer != question.answer) {
        wrongQuestionRepository.upsertWrongQuestion(
            questionId = question.id,
            subject = question.category
        )
    }
}

这一步的意义在于,从此以后,“错题”不再属于某个具体页面,而是成为了贯穿整个学习路径的统一数据结果。用户在哪种模式下做错并不重要,重要的是这些错题最终都会沉淀到同一个地方,形成可复用的学习资产。


六、后端错题接口正式落地:从测试性 Mongo 写入转向 MySQL 持久化

在后端部分,这一周我也完成了一个非常关键的转向:把错题同步从最初的 MongoDB 测试性接口,过渡到了 MySQL 正式表 user_wrong_questions

我一开始之所以做了 Mongo 版接口,主要是为了尽快验证“后端能不能接收错题同步请求”。但从长期项目的角度看,错题记录本质上是与用户强绑定的业务数据,而用户体系已经在关系型数据库中,因此继续把错题放在 Mongo 里并不合适。最终,我决定让后端正式采用 MySQL 存储错题。

为此,我专门拆出了独立的 wrong_questions blueprint,并设计了如下写入接口:

@wrong_questions_bp.route("/upsert", methods=["POST"])
def upsert_wrong_question():
    try:
        data = request.get_json() or {}

        user_id = data.get("user_id")
        question_id = data.get("question_id")
        subject = data.get("subject")

        record = UserWrongQuestion.query.filter_by(
            user_id=user_id,
            question_id=question_id
        ).first()

        now = datetime.utcnow()

        if record:
            record.wrong_count += 1
            record.subject = subject
            record.updated_at = now
            record.last_wrong_time = now
        else:
            record = UserWrongQuestion(
                user_id=user_id,
                question_id=question_id,
                subject=subject,
                wrong_count=1,
                created_at=now,
                updated_at=now,
                last_wrong_time=now
            )
            db.session.add(record)

        db.session.commit()

        return jsonify({
            "code": 200,
            "msg": "错题记录成功"
        })
    except Exception as e:
        db.session.rollback()
        return jsonify({
            "code": 500,
            "msg": str(e)
        }), 500

这部分开发过程其实并不轻松。因为在尝试执行 flask db migrate 时,我连续遇到了多个和 KG、RAG、视觉检索模块依赖有关的问题,比如应用初始化时会额外加载并不属于本次数据库迁移目标的服务模块,从而引出 neo4jjiebaPILnumpy 等一系列依赖报错。这个过程让我真正体会到,一个项目进入中后期后,数据库迁移不再是“写个模型就行”,它和整个应用的启动路径、蓝图注册顺序、依赖组织方式都有关系。

最终,在不断排查和调整后,我成功确认 MySQL 中已经真正创建出了 user_wrong_questions 表。这标志着后端物理错题库终于落地,不再只是概念设计。

这一阶段,AI 的帮助主要体现在:帮我快速定位到底是循环导入问题、模块初始化问题还是数据库模型问题;帮助我分析“为什么迁移命令实际上被别的蓝图拖住了”;同时不断提醒我,从长期维护角度看,错题接口应该拆成独立 blueprint,而不应继续塞在大路由或 questions 模块里。这样的提示让我避免了为了图快继续堆在旧结构上,从而减少了后面更大的返工风险。


七、解析展示与界面优化:从“能显示”到“能用舒服”

除了数据层,这一周我也顺手把做题页的解析展示做了一轮体验优化。

之前在做题页里,用户答错后虽然能看到正确答案,但解析展示还不够完整,尤其是长文本情况下容易被底部按钮区压住,导致用户无法顺畅查看完整内容。因此,我在这一周做了两件事:

第一,把答题后的反馈改成不仅显示“正确答案”,还显示“回答正确/回答错误 + 题目解析”,让用户的复盘信息更加完整。
第二,把做题页内容区域重构为可滚动容器,让解析区能够下滑查看,不再被底部固定按钮遮挡。

这一步虽然看起来不像数据库重构那么“宏大”,但它其实很重要。因为一个学习类产品如果在用户最需要看解析的时候,界面却让人看不全,那整个错题复习的价值就会被削弱。也正因为如此,我越来越意识到:真正好的功能,不只是逻辑对,更要体验顺。


八、本周遇到的问题与思考

这一周我遇到的问题非常集中地暴露了“长期项目”和“原型项目”的差别。

首先是数据设计的问题。最初那种把错题信息放在题库表上的方式,在早期看起来很方便,但一旦项目开始考虑用户、同步、统计和扩展,它就会迅速暴露出结构性不足。这个问题让我更加理解,为什么说“正确的数据边界”比“暂时少写几行代码”更重要。

其次是后端迁移和模块依赖的问题。以前我对 Flask-Migrate 的理解还比较表面,总觉得定义模型、跑个迁移命令就结束了。但这次真实踩下来之后,我才意识到:只要应用启动路径中还包含很多与本次目标无关的模块,数据库迁移就可能被各种依赖链牵着走。这个过程虽然很折腾,但也让我对项目初始化流程、蓝图拆分以及依赖管理有了更深的认识。

还有一个很深的体会是,AI 在工程开发中并不是“你问一句,它答一句”那么简单。真正高效的方式,是让 AI 先理解你的项目边界、代码结构和长期目标,然后让它参与架构与实现之间的多轮修正。尤其是在这次错题模块开发中,如果没有 AI 持续提醒我从长期项目角度思考数据库和模块设计,我很可能会继续沿用旧结构,短期看似省事,长期却要付出更大代价。


九、本周收获

本周最大的收获,不是某一页 UI 完成了,而是我第一次比较完整地经历了一次“业务模块重构”。

以前我更习惯于沿着页面走:需要什么功能,就把那个页面补出来。但这周我开始真正学会从业务数据、用户行为、后端结构和客户端缓存之间的关系去重新设计一个模块。错题复习这件事,也让我第一次清晰地看见了一个长期项目里“本地缓存 + 后端持久化 + 多场景归集 + 用户关联”应该如何逐步落地。

同时,本周也让我更加确认:AI 在项目中的最大价值,并不是简单节省打字时间,而是在于帮助我更快地建立结构、发现耦合、比较方案、拆解问题,并在不断修改中保持整体方向不偏。这种协作方式,已经开始真正改变我开发项目的思路。


十、下周计划

下周我准备在本周错题模块完成重构的基础上,继续向“学习闭环”和“个性化分析”方向推进。重点包括:

  • 继续验证每个用户对应的错题能否完整写入后端数据库

  • 完善错题页与后端同步之间的数据一致性

  • 进一步优化错题筛选、排序和移除逻辑

  • 基于错题数据尝试做更细粒度的学习分析与统计

  • 继续推进 AI 智能讲题和个性化学习建议能力的落地

我希望下一个阶段,CarLearn 不只是“有一个错题本”,而是能真正利用错题数据去帮助用户发现薄弱点、形成针对性复习策略。


总结

总体来看,本周的核心工作,是围绕 CarLearn 的错题复习模块进行了一次从页面到数据层的完整重构。我不仅完成了客户端错题页交互和本地独立错题库建设,还推动了顺序练习、专项练习和模拟考试三类错题的统一归集,并最终在后端 MySQL 中落地了 user_wrong_questions 这张正式错题表。

更重要的是,这一周让我真正理解了一个长期项目里的错题系统应该如何被设计:它不应只是一个页面,也不应只是题库字段上的附加状态,而应该是一个与用户强关联、可本地缓存、可后端同步、可长期扩展的独立业务模块。

而在这一整个过程中,AI 的帮助不仅体现在代码层面,更体现在思路层面。它帮助我从“先做出来”逐步走向“按长期项目方式做正确”,这也是本周对我来说最有价值的成长之一。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐