从作业批改系统到学习管理平台:一次完整的全栈开发实践

项目背景

今天完成了一个学习管理平台的数据可视化模块开发。这个项目基于已有的作业批改系统,扩展了教师端的数据分析功能,包括班级学情看板、知识点掌握度热力图、高频错题统计等。

技术栈:

  • 后端:Spring Boot + MyBatis-Plus + PostgreSQL
  • 前端:Vue3 + 原生 CSS
  • AI 网关:OpenClaw(多模型调度)

核心功能实现

1. 数据可视化看板

教师端新增了一个数据看板页面,包含以下模块:

模块 功能描述
核心指标卡片 学生总数、作业总数、平均正确率、需关注学生
成绩分布图 柱状图展示各分数段人数占比
知识点热力图 12个知识点的掌握度可视化(绿/黄/红)
高频错题 TOP10 错误率排序,前3标红高亮
学生学情列表 支持搜索、排序,需关注学生标红
AI教案生成 选择教学目标,生成针对性教案

2. 知识点掌握度分析

这是本次开发的核心难点。我们需要:

  1. 修改 AI 批改 Prompt:让 AI 在批改作业时,同时分析涉及的知识点及掌握程度
  2. 新增知识点表结构knowledge_point(字典表)+ homework_knowledge(关联表)
  3. 动态热力图:根据实际作业内容,动态展示相关知识点(而不是固定列表)

AI 返回的 JSON 格式示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"totalScore": 75,
"knowledgePoints": [
{
"name": "二维数组",
"mastery": 85,
"status": "掌握"
},
{
"name": "循环结构",
"mastery": 60,
"status": "薄弱"
}
]
}

遇到的问题与解决方案

问题1:StackOverflowError - 无限递归

现象:后端抛出 java.lang.StackOverflowError,日志显示 OpenClawServiceImpl.chat() 方法无限调用。

原因:方法重载时,参数类型匹配错误导致自己调用自己。

1
2
3
4
5
// 错误代码
@Override
public String chat(String message, String sessionId, String status) {
return chat(message, sessionId, status); // 死循环!
}

解决:修正参数类型转换逻辑,确保调用的是真正实现方法。

1
2
3
4
5
6
// 正确代码
@Override
public String chat(String message, String sessionId, String status) {
Integer statusCode = status != null ? Integer.parseInt(status) : null;
return chat(message, sessionId, statusCode); // 调用另一个重载
}

问题2:JSON 解析失败 - 格式不统一

现象:AI 返回的内容有时包含 markdown 代码块标记(```json),有时又直接返回 JSON,导致解析失败。

原因:Skill 的 Prompt 要求不够严格,AI 有时会添加额外说明文字。

解决

  1. 强化 Prompt 要求:”极其重要:必须只返回JSON,不要添加任何其他文字!
  2. 后端增加数据清洗逻辑,去除可能的 markdown 标记:
1
2
3
4
5
6
7
8
9
10
11
12
private String cleanRawResponse(String rawResponse) {
String cleaned = rawResponse.trim();
if (cleaned.startsWith("```json")) {
cleaned = cleaned.substring(7).trim();
} else if (cleaned.startsWith("```")) {
cleaned = cleaned.substring(3).trim();
}
if (cleaned.endsWith("```")) {
cleaned = cleaned.substring(0, cleaned.length() - 3).trim();
}
return cleaned;
}

问题3:热力图数据为空

现象:知识点热力图显示为0,数据库查询没有结果。

原因homework_evaluation 表的 class_id 字段没有正确设置,导致 JOIN 查询失败。

解决

  1. 确保 User 实体包含 classId 字段
  2. 保存批改结果时,从用户信息中获取班级ID:
1
2
3
4
User user = userMapper.selectById(userId);
if (user != null && user.getClassId() != null) {
entity.setClassId(user.getClassId());
}

技术亮点

1. 动态知识点分析

传统的学习系统使用预定义的知识点列表,但我们的系统让 AI 根据作业内容动态分析涉及的知识点。这样无论是 Java、C语言还是其他科目,都能自动提取相关知识点。

2. 数据驱动的教案生成

基于学生的薄弱知识点,系统可以自动生成针对性教案。教师只需选择教学目标(巩固基础、突破难点等),AI 就会结合班级数据生成个性化教案。

3. 前后端分离的实时数据

前端使用 Vue3 的 watch 监听班级选择变化,自动加载对应数据。后端提供 RESTful API,支持按班级筛选和排序。

项目结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
后端 (Spring Boot)
├── Entity/
│ ├── ClassInfo.java # 班级信息
│ ├── HomeworkEvaluation.java # 作业评价
│ └── HomeworkKnowledge.java # 知识点掌握情况
├── Controller/
│ ├── DashboardController.java # 数据看板接口
│ └── TeachingPlanController.java # 教案生成接口
├── Service/
│ └── DashboardServiceImpl.java # 数据统计逻辑
└── Mapper/
└── HomeworkKnowledgeMapper.java

前端 (Vue3)
└── views/
└── PageFour.vue # 教师数据看板

Skill (OpenClaw)
└── homework-grader/
└── SKILL.md # 批改作业 Prompt

总结

这次开发让我深刻体会到:

  1. AI 输出的不确定性:即使 Prompt 写得很清楚,AI 仍可能返回各种格式,后端必须做好容错处理。

  2. 数据关联的重要性:一个 class_id 字段的遗漏,会导致整个查询链条断裂。数据库设计时要考虑完整的关联关系。

  3. 渐进式开发的价值:先让基础功能跑通(静态数据),再逐步替换成动态数据,最后优化细节。

  4. 跨层调试的复杂性:问题可能出现在 Skill、后端、前端任何一个环节,需要分层定位。

项目已开源,欢迎交流讨论。