当前位置: 首页 > news >正文

计科智伴开发日志(七)|学情画报从零到 776 行、学情报告接口重构与 AI 建议落地

系列目录:计科智伴——基于 Spring Boot + uni-app 的 AI 个性化学习平台开发实录

上一篇把错题诊断 Agent、AI 学习计划生成、前端 SSE 打字机都做完了。本期核心任务两件:建出"学情画报"页面,让用户直观看到学情分析结果;改造 ReportController,让报告接口真正支持有意义的周期对比。两个 commit,改了约 20 个文件,新增有效代码约 1,400 行。


一、问题背景:数据有了,但用户看不见

前五周做的是"把数据采集和 AI 分析能力建起来",但采集完之后那些数据存在哪、用户怎么看到,一直没做。具体来说有三个缺口:

UserProfile 里的knowledgeMastery存了每个知识点的掌握度(0~1 的浮点数),weakPoints存了薄弱知识点列表——这两个字段是诊断 Agent 回写进去的,但没有页面展示它们。/api/report接口虽然存在,但只返回本周答题数、正确数、学习时长三个字段,没有上期对比,没有知识点维度,没有 AI 分析,前端报告页渲染出来内容基本是空的。另外"重新生成学习计划"的入口也缺失,用户画像更新后只能靠问卷重来。

本周先解决"可见性"问题——数据展示出来,再谈"正确性"问题。


二、学情画报页面(profile-report.vue)

2.1 三区块结构

页面从上到下分三个区块。

画像摘要卡片:展示学习目标(备战考试/准备面试/考研复习/系统提升)、目标分数(仅备战考试时有效)、在学课程列表、日均学习时长(从 timePref 解析)。数据来源是GET /users/profile,一次拿完所有字段。

知识掌握热力图:从GET /api/home/heatmap[{knowledgeName, score, category}],用 uCharts 渲染柱状图,颜色按三档映射——mastered(≥ 0.8)绿色、learning(0.5~0.8)蓝色、weak(< 0.5)红色。首次进入无数据时展示空状态卡,不渲染空坐标系。

AI 学情诊断报告:点击"生成 AI 分析"按钮触发同步接口,等待期间展示骨架屏,结果回来后展示约 300 字的分析文案。

2.2 timePref 解析的边界处理

后端 UserProfile 存的是"08:00-09:00,20:00-21:00"这样的字符串,前端摘要卡要展示"日均可学习 X 分钟"。原本想让后端直接返回分钟数,但 timePref 是核心字段,改格式会影响其他逻辑(比如逾期任务顺延时读 timePref 计算 maxPerDay),所以前端自己解析:

function parseStudyMinutes(timePref) { if (!timePref) return 0 return timePref.split(',').reduce((total, seg) => { const [start, end] = seg.trim().split('-') if (!start || !end) return total const [sh, sm] = start.split(':').map(Number) const [eh, em] = end.split(':').map(Number) return total + (eh * 60 + em) - (sh * 60 + sm) }, 0) }

边界情况包括逗号前后有空格、只有一个时段、时段格式不完整。if (!start || !end) return total做空值守卫,格式异常时静默跳过而不是整个函数报错。

2.3 空数据的设计决策

首次进入的用户 heatmapData 是空数组,uCharts 渲染空数据会出一个只有坐标轴没有柱子的空图,看起来像 bug。改成v-if="heatmapData.length === 0"时展示空状态文案,v-else时才渲染图表组件。这个"宁可不显示也不显示空图"的原则,后来在知识点掌握度详情页(kp-mastery.vue)里也复用了。


三、学情报告接口重构(ReportController)

3.1 从"三个数字"到"完整报告"

原来的/api/report返回内容大概就是totalQuestionscorrectCountstudyMinutes三个字段。改造后的接口(仍然是GET /api/report?period=week|month|semester)返回五块内容:overview(本期概览)、weeklyComparison(上期对比)、subjectPerformance(按课程错题分布)、knowledgeMastery(知识掌握度)、aiSuggestions(规则建议)。前两块是本次重点。

3.2 weeklyComparison:时间窗口的边界细节

这是本次改动里最容易写错的一处。核心逻辑:本期是[now - days, now],上期是[now - 2*days, now - days],两个窗口严格相邻、不重叠:

LocalDateTime since = LocalDateTime.now().minusDays(days); // 本期起点 // 上期:终点 = 本期起点,起点 = 本期起点再往前 days 天 LocalDateTime prevEnd = since; LocalDateTime prevStart = prevEnd.minusDays(days); List<UserQuestionRecord> prevQrs = userQuestionRecordMapper.selectList( new LambdaQueryWrapper<UserQuestionRecord>() .eq(UserQuestionRecord::getUserId, userId) .ge(UserQuestionRecord::getSubmitTime, prevStart) .lt(UserQuestionRecord::getSubmitTime, prevEnd)); // 严格小于

第一版把prevEnd写成了LocalDateTime.now(),上期和本期完全重叠,对比数据永远是 0 变化。调试时在返回值里临时加了debugPrevStartdebugPrevEnd字段才发现窗口对不上。

3.3 subjectPerformance:三级关联聚合

错题到课程需要三级跳:WrongQuestion → Question(通过 qId)→ KnowledgePoint(通过 kpId)→ Course(通过 courseId)。项目里基本没写 XML Mapper 全用 LambdaQueryWrapper,三表 join 要写原生 SQL。考虑到一个用户在一个周期内通常不超过 100 条错题,选择在 Java 里循环聚合:

Map<Long, long[]> courseStats = new LinkedHashMap<>(); // courseId → [total, resolved] Map<Long, String> courseNameCache = new HashMap<>(); for (WrongQuestion wq : wrongList) { if (wq.getQId() == null) continue; Question q = questionMapper.selectById(wq.getQId()); if (q == null || q.getKpId() == null) continue; KnowledgePoint kp = knowledgePointMapper.selectById(q.getKpId()); if (kp == null || kp.getCourseId() == null) continue; Long cid = kp.getCourseId(); courseStats.computeIfAbsent(cid, k -> new long[]{0, 0}); courseStats.get(cid)[0]++; if (Boolean.TRUE.equals(wq.getIsResolved())) courseStats.get(cid)[1]++; courseNameCache.computeIfAbsent(cid, k -> { Course c = courseService.getById(k); return c != null ? c.getCourseName() : "课程" + k; }); }

N+1 问题确实存在,但单期错题量小,代价可接受,后期量上来再批量优化。本期无错题时用UserProfile.currentCourses展示"0 错题"空行,让前端有东西可渲染。


四、同步 AI 建议生成

4.1 接口设计

新增POST /api/report/ai-suggest,前端在学情画报页点击按钮时调用,同步等待大模型返回。提示词注入本期答题数与正确率、用户学习目标与目标分数、最近 3 个薄弱知识点名称,让模型产出 200~300 字的分析。

4.2 降级兜底

模型超时或调用失败时不能一直 loading,改成规则降级:

try { suggestion = chatClient.prompt().user(prompt).call().content(); } catch (Exception e) { log.warn("AI 建议生成失败,降级为规则建议: {}", e.getMessage()); suggestion = buildRuleSuggestion(totalQuestions, accuracy, unresolvedMistakes); } private String buildRuleSuggestion(int total, double acc, long mistakes) { if (total == 0) return "本期还没有答题记录,先从一个知识点开始练习吧。"; if (acc < 0.6) return "正确率偏低,建议先回到课本巩固基础概念,不要急于追求题量。"; if (mistakes > 5) return "错题本里还有 " + mistakes + " 道未消化,建议优先把错题过一遍。"; if (acc > 0.9) return "正确率很高,可以尝试更难的题目或扩展到新知识点。"; return "学习情况稳定,保持节奏,持续巩固薄弱知识点。"; }

覆盖"没做题/正确率低/错题多/正确率高/正常"五种情况,确保按钮点下去永远有反馈。


五、踩坑复盘

坑一:AnswerDTO 字段名反序列化失败。前端发的是questionId,DTO 里字段名叫qId,Jackson 默认按字段名匹配导致永远是 null。选择直接改字段名为questionId并全局替换,而不是加@JsonProperty注解打补丁。

坑二:weeklyComparison 时间窗口重叠。上面 3.2 节已详述,根因是prevEnd用了now而不是since

坑三:UserProfile 快照导致 PUT 后 GET 返回旧值。UserHolder 里存的是登录时的 ThreadLocal 快照对象,数据库改了但内存里的对象没变。修复为 GET 接口按 userId 重查数据库,UserHolder 只取 userId 用于鉴权:

@GetMapping("/profile") public Result getProfile() { Long userId = UserHolder.getUser().getUserId(); UserProfileDTO profile = userProfileService.getUserProfile(userId); return Result.ok(profile); }

六、本期小结

指标数据
profile-report.vue 行数776 行
ReportController 新增/改写方法4 个
weeklyComparison 覆盖周期week / month / semester(7/30/90 天)
AI 建议降级规则5 条
踩坑修复3 处

下一步重点:knowledgeGrowth 目前始终返回空数组,需要按日期×知识点做真实聚合;AI 建议的同步接口耗时约 3 秒,后续评估改异步+轮询减少等待感。

http://www.jsqmd.com/news/1016676/

相关文章:

  • 开封市黄金回收门店推荐 五家靠谱店铺TOP排行榜及联系方式地址电话+白银回收+铂金回收+彩金回收当场结算 - 大熊猫898989
  • 承德市黄金回收门店推荐 五家靠谱店铺TOP排行榜及联系方式地址电话+白银回收+铂金回收+彩金回收当场结算 - 大熊猫898989
  • SEGE抽屉防潮舱:把日用品安放在干爽秩序里
  • 2026年私立普高怎么联系,靠谱的招生渠道与费用盘点 - 工业品牌热点
  • MCP2515配置避坑指南:从SPI时序到中断处理,那些手册里没细说的实战经验
  • 手把手教你用TiggerRamDisk绕过iPhone/iPad激活锁(支持iOS16.3,Win7/Win10/Mac教程)
  • 避坑指南:汇川PLC Easy320串口通信报错48?详解RcvSize设置与数据转发完整流程
  • 贵港市黄金回收门店推荐 五家靠谱店铺TOP排行榜及联系方式地址电话+白银回收+铂金回收+彩金回收当场结算 - 大熊猫898989
  • Pandas内存优化实战:6个立即生效的数据类型降级技巧
  • 2026年6月北京除甲醛公司深度评测:技术革新与安心之选 - 品牌推荐
  • 2026年非开挖顶管施工工程队性价比排行,聊聊广州深圳本地施工队怎么选 - 工业品牌热点
  • 昆明市黄金回收门店推荐 五家靠谱店铺TOP排行榜及联系方式地址电话+白银回收+铂金回收+彩金回收当场结算 - 大熊猫898989
  • ORCAD原理图实战:搞定网表报错与元器件属性错乱的5个真实案例
  • 别再只盯着DO-178C了:聊聊机载软件工具鉴定中,那些容易被忽略的‘操作需求’怎么写(附避坑指南)
  • Spyder里报错‘No module named gurobipy‘?别慌,手把手教你搞定Python环境与Gurobi的配置
  • 池州市黄金回收门店推荐 五家靠谱店铺TOP排行榜及联系方式地址电话+白银回收+铂金回收+彩金回收当场结算 - 大熊猫898989
  • DANCE:深度学习模型不确定性量化的双重自适应方法
  • 2026年婚姻家庭律师怎么收费,离婚分割律师价格对比解析 - 工业品牌热点
  • 来宾市黄金回收门店推荐 五家靠谱店铺TOP排行榜及联系方式地址电话+白银回收+铂金回收+彩金回收当场结算 - 大熊猫898989
  • 贵阳市黄金回收门店推荐 五家靠谱店铺TOP排行榜及联系方式地址电话+白银回收+铂金回收+彩金回收当场结算 - 大熊猫898989
  • 赤峰市黄金回收门店推荐 五家靠谱店铺TOP排行榜及联系方式地址电话+白银回收+铂金回收+彩金回收当场结算 - 大熊猫898989
  • PyTorch GPU初始化门限:从torch.cuda.is_available到CUDA上下文激活
  • Vue 3 入门教程
  • Spyder里报错‘No module named gurobipy‘?别慌,手把手教你搞定Python环境与IDE的兼容问题
  • 2026年知识产权数据风控金融领域服务商深度观察:谁在提供可靠的专利估值与另类数据? - 优质品牌商家
  • PSoC 5LP新手避坑指南:搞定LED亮度调节与LCD显示的那些‘坑’
  • 手机信号差?别急着换手机,先看看这个藏在主板上的“信号放大器”
  • VCS仿真中UVM编译报错Top 10:从‘gnu/stubs-32.h’到‘Null object access’的保姆级排查手册
  • 2026年心居搬家是否有售后服务,分析服务费用多少钱 - 工业品牌热点
  • 2026年6月北京除甲醛公司深度评测:从技术到服务,谁是真正的“源头治理”实力派? - 品牌推荐