山东大学软件学院项目实训-创新实训-计科智伴(六)——个人博客(后端运行后真实调整)

写在前面

这是第六篇博客。经过前五篇的铺垫,项目的基础架构和核心功能已经基本成型。本篇不再赘述项目背景和基础搭建过程,而是聚焦于后端成功运行后,前后端联调阶段遇到的真实问题与解决方案------从SSE流式通信的跨端适配,到AI服务的反序列化陷阱,再到数据一致性的深度修复。这些内容代表了项目从"能跑"到"好用"的关键跨越。

本文建议在以下位置插入一张应用整体运行截图,例如AI对话页面或学情报告页面的完整运行截图,展示最终效果。图片说明可写为:计科智伴应用核心功能运行效果。


一、App端SSE流式通信的跨端适配

1.1 问题背景

AI对话模块采用SSE(Server-Sent Events)实现流式响应,后端逐字输出AI回复内容。在H5和小程序端,使用uni.requestenableChunked: true可以正常接收流数据,但在App端却出现了"数据憋到最后一次性出来"的现象------用户发送消息后长时间无响应,然后所有内容瞬间显示,完全失去了流式的意义。

1.2 根因分析

经过排查发现,uni-app的App端底层网络请求走的是Android OkHttp或iOS NSURLSession原生库。这些原生网络库对分块传输有自带的缓冲策略,会等待数据积累到一定量才向上层传递,导致enableChunked配置在App端形同虚设。

1.3 解决方案:renderjs + XMLHttpRequest桥接

既然原生网络层无法绕过,那就退回到浏览器层。uni-app的App端视图层实际运行在Webview中,可以直接使用浏览器原生的XMLHttpRequestAPI。通过renderjs机制,在视图层发起XHR请求,利用xhr.onprogress事件实现真正的流式数据接收。

核心思路是创建一个无UI的桥接组件AppSseBridge.vue,它包含两个部分:

逻辑层 :接收父组件传递的请求参数,通过$emit将chunk/done/error事件传回父组件。

renderjs层 :监听prop变化,用XMLHttpRequest发起POST请求,在onprogress回调中实时截取新增内容。

复制代码
// renderjs层核心逻辑
setupSSEConnection(payload, ownerInstance) {
  const xhr = new XMLHttpRequest();
  xhr.open(payload.method, payload.url, true);
  xhr.setRequestHeader('Content-Type', 'application/json');
  
  // 关键:通过onprogress实现流式接收
  xhr.onprogress = (event) => {
    const newText = xhr.responseText.substring(this._cursor);
    this._cursor = xhr.responseText.length;
    if (newText) {
      // 将新增内容传回逻辑层
      ownerInstance.callMethod("onSSEChunk", newText);
    }
  };
  
  xhr.send(JSON.stringify(payload.data));
}

request.js中通过条件编译实现平台分支:

复制代码
复制代码
export function sseRequest(url, data, callbacks) {
  // #ifdef APP-PLUS
  // App端:使用renderjs桥接方案
  return createAppSseBridge(url, data, callbacks)
  // #endif

  // #ifndef APP-PLUS
  // H5/小程序:使用uni.request + enableChunked
  return createUniRequestSse(url, data, callbacks)
  // #endif
}

1.4 SSE数据解析修复

在实现过程中还发现一个细节问题:后端返回的SSE数据包含多种事件类型(如event:session_idevent:delta),如果简单累积所有data:字段,会导致多余内容混入AI回复。

修复方案是跟踪event:类型声明,只处理event:delta对应的数据行,忽略其他事件类型。这确保了AI回复内容的纯净性。


二、Markdown渲染与代码高亮

2.1 需求来源

AI对话返回的内容包含Markdown格式(标题、列表、代码块等),直接以纯文本显示效果很差。特别是代码块,没有语法高亮和格式化,可读性极差。

2.2 实现方案

引入marked作为Markdown解析器,highlight.js作为代码高亮引擎。在markdown.js中配置marked使用highlight.js进行代码块渲染:

复制代码
import { marked } from 'marked'
import hljs from 'highlight.js'

marked.setOptions({
  highlight: function(code, lang) {
    if (lang && hljs.getLanguage(lang)) {
      return hljs.highlight(code, { language: lang }).value
    }
    return hljs.highlightAuto(code).value
  },
  breaks: true,  // 换行符转为<br>
  gfm: true      // 启用GitHub Flavored Markdown
})

在chat.vue中,AI消息使用<rich-text>组件渲染生成的HTML:

复制代码
<view class="message-bubble" v-if="msg.role === 'assistant'">
  <rich-text :nodes="msg.renderedHtml"></rich-text>
</view>

代码高亮采用Catppuccin Mocha配色方案,关键字紫色、字符串绿色、注释灰色、函数蓝色,整体风格统一且舒适。


三、AI错题识别的Jackson反序列化陷阱

3.1 问题现象

用户上传题目图片进行智能错题识别时,后端调用AI接口直接崩溃:

复制代码
UnrecognizedPropertyException: Unrecognized field "text_tokens" 
(class PromptTokensDetails), not marked as ignorable

3.2 根因定位

Spring AI 1.0.0-M6的PromptTokensDetails类只定义了audio_tokenscached_tokens两个字段,但我们使用的sophnet.com OpenAI兼容API返回了额外的text_tokens字段。Jackson默认对未知字段采取严格模式,遇到未定义的字段就抛出异常。

3.3 修复方案

从两个层面解决这个问题:

全局配置 :在application.yml中关闭Jackson的未知字段失败策略:

复制代码
spring:
  jackson:
    deserialization:
      fail-on-unknown-properties: false

代码层面 :为RestClient.BuilderWebClient.Builder配置自定义的ObjectMapper,确保所有HTTP客户端都忽略未知字段:

复制代码
ObjectMapper mapper = Jackson2ObjectMapperBuilder.json()
    .featuresToDisable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
    .build();
converters.add(0, new MappingJackson2HttpMessageConverter(mapper));

这里converters.add(0, ...)将自定义converter放在列表首位,确保优先于Spring Boot默认的converter生效。


四、UI全面优化:从功能到体验

4.1 AI对话页面优化

在功能跑通后,对AI对话页面进行了系统性的视觉升级:

顶部导航栏 :添加毛玻璃效果(backdrop-filter: blur(20rpx))和sticky定位,滚动时固定在顶部,底部边框改为半透明细线,整体更精致。

消息气泡

  • 用户消息采用渐变背景linear-gradient(135deg, #4F6EF7, #8B5CF6),文字白色,视觉层次更清晰

  • AI消息使用白色背景配合柔和阴影和细边框

  • 添加messageIn入场动画,消息出现时从下方淡入

欢迎区域 :机器人头像添加float浮动动画(上下轻微浮动),标题和描述文字添加fadeInUp入场动画,页面加载时更有仪式感。

输入区域:修复bottom定位偏差,优化输入框聚焦态边框颜色,发送按钮添加渐变背景。

复制代码
.msg-user .message-bubble {
  background: linear-gradient(135deg, #4F6EF7 0%, #8B5CF6 100%);
  box-shadow: 0 6rpx 16rpx rgba(79, 110, 247, 0.25);
  color: #FFFFFF;
}

@keyframes fadeInUp {
  from { opacity: 0; transform: translateY(20rpx); }
  to { opacity: 1; transform: translateY(0); }
}

4.2 错题页面优化

错题页面的优化重点在于信息层次和交互反馈:

统计头部 :添加装饰性圆形背景元素(::before伪元素),统计卡片添加点击缩放效果。

筛选栏:改为sticky定位配合毛玻璃效果,滚动时始终可见。筛选标签间距从20rpx缩小到12rpx,更紧凑。

错题卡片

  • 添加cardIn入场动画

  • 题目内容添加独立背景框(浅灰色+圆角+细边框),与卡片主体区分

  • 操作按钮改为水平排列,间距更合理

  • 已掌握卡片透明度从0.6调整为0.65,保持可读性

    .mistake-question-box {
    background: #F9FAFB;
    border-radius: 12rpx;
    padding: 20rpx;
    margin-bottom: 20rpx;
    border: 1rpx solid #F3F4F6;
    }

    @keyframes cardIn {
    from { opacity: 0; transform: translateY(10rpx); }
    to { opacity: 1; transform: translateY(0); }
    }

4.3 移动端图片自适应修复

AI对话页面上传图片后,在移动端屏幕下图片超出消息气泡范围。根因是图片使用width: auto,当原图宽度超过容器时不会自动缩放。

修复方案:将width: auto改为width: 100%,添加box-sizing: border-box,配合父容器的overflow: hidden实现双重保护。

复制代码
.message-image {
  width: 100%;           /* 改为100%,确保不超出容器 */
  max-width: 100%;
  height: auto;
  box-sizing: border-box; /* padding/border不导致溢出 */
}

五、知识前测与AI画像生成

5.1 动态出题机制

Onboarding流程中的知识前测从固定题目改为动态出题。用户选择课程后,系统调用GET /api/onboarding/quiz接口,根据所选课程从题库中随机抽取5道题目。

前端需要处理后端返回的题目格式:options可能是string[]{label, content}[],需要统一转换为前端可用的格式。同时题目上方显示课程名和知识点名标签,让用户清楚当前题目所属范围。

5.2 AI学情建议生成

前测提交后,后端统计正确率,结合用户画像信息(学习目标、每日学习时间、期望分数等),调用AI接口生成约300字的学情分析报告。

提示词设计要点:

  • 提供用户基本信息:学习目标、时间偏好、学习偏好

  • 提供测试结果:各知识点正确率

  • 要求输出:综合分析,指出优势和薄弱领域,给出具体学习路径建议

前端在学情报告页面将AI建议按行拆分,逐行渲染,每条建议独立成段,阅读体验更好。

复制代码
// 将AI建议按行拆分
computed: {
  parsedSuggestions() {
    if (!this.aiSuggestions) return []
    return this.aiSuggestions
      .split('\n')
      .filter(line => line.trim())
      .map(line => line.replace(/^[\d\.\-\*]\s*/, ''))
  }
}

5.3 数据流设计

复制代码
用户选课程 → 调用GET /api/onboarding/quiz获取题目
  → 用户答题 → 前端判断正确性 → 调用POST /api/onboarding/submit-quiz
  → 后端统计正确率 → 构建画像 → AI生成建议 → 入库
  → 前端接收画像数据 → 保存到本地+Pinia store → 跳转首页
  → 进入报告页 → 从store获取aiSuggestions → 渲染展示

六、数据库与题库全面补充

6.1 知识点覆盖

查询发现16门课程没有关联知识点,包括离散数学、操作系统、数据库、算法设计与分析等核心课程。编写SQL脚本为每门课程添加5个核心知识点,共80个新知识点(kp_id 23-102)。

6.2 题库补充

为80个新知识点各补充5道题目,共400道新题目。至此,21门课程全部有关联知识点,102个知识点每个都有5道题目,前测可覆盖所有课程的所有知识点。

6.3 数据库表结构同步

队友导入数据库时出现中文乱码和部分表缺失问题。使用pg_dump重新导出完整数据库(UTF-8编码),包含20张表的完整DDL结构和种子数据,使用--clean --if-exists支持安全覆盖导入。


七、数据一致性修复

7.1 个人主页编辑刷新不生效

问题:用户在个人主页编辑资料后,刷新页面仍显示旧数据。

根因profile-info-edit.vueinitForm()方法只在store中所有字段都为空时才调用fetchUserInfo()。由于Pinia store配置了persist持久化,store中始终有旧数据,导致条件永远不满足。

修复:改为每次进入页面都从后端获取最新数据:

复制代码
// 修改前:只有所有字段为空时才获取
if (!info.name && !info.school && !info.major && !info.grade) {
  await this.userStore.fetchUserInfo()
}

// 修改后:每次都获取最新数据
await this.userStore.fetchUserInfo()

7.2 学情画像编辑后数据不同步

问题:用户在个人主页编辑学情画像后,其他页面看不到更新的数据。

根因:保存成功后只调用了后端接口写入数据库,没有更新Pinia store。

修复 :保存成功后调用profileStore.fetchUserProfile(userId)刷新store数据。同时在store中新增profile对象存储画像基础信息,配置persist持久化,确保数据在各页面间同步。

7.3 聊天历史消息渲染修复

问题:API返回历史消息包含assistant和user两条消息,但前端只显示了user消息,assistant消息内容为空。

根因loadChatHistory映射消息时没有为assistant消息生成renderedHtml字段。chat.vue模板中assistant消息使用<rich-text :nodes="msg.renderedHtml">渲染,renderedHtmlundefined时不显示任何内容。

修复 :在消息映射时添加renderedHtml字段,并调用renderMarkdown(content)生成HTML。同时添加.reverse()将后端倒序(最新在前)转为正序(最新在底部)。

复制代码
this.messages = data.reverse().map((msg, idx) => {
  const role = (msg.role || '').toLowerCase() === 'assistant' ? 'assistant' : 'user'
  const content = msg.content || ''
  return {
    id: msg.msgId,
    role,
    content,
    renderedHtml: role === 'assistant' && content ? renderMarkdown(content) : '',
    time: formatTime(msg.createTime)
  }
})

八、其他关键修复

8.1 新会话记录写入

新会话创建时,streamChat方法中没有调用sessionService将新会话写入数据库,导致listSessions接口查不到任何记录,用户点击历史按钮显示"没有历史会话"。

修复:在新会话创建后立即写入session表,使用try-catch包裹避免影响主流程。

8.2 习题接口适配

后端ExerciseApiController返回的question id是字符串类型,但submitSingleAnswer接口需要Long类型路径参数。同时options格式不匹配(后端string[]vs 前端{label, content}[])。

修复:在前端映射时进行类型转换和格式适配,使用String.fromCharCode(65 + i)自动生成A/B/C/D标签。

8.3 localhost解析问题

Windows hosts文件缺少127.0.0.1 localhost标准配置,部分客户端无法解析localhost。将application.yml中所有服务的localhost改为127.0.0.1(PostgreSQL、Redis、Neo4j、MinIO)。

8.4 Redis客户端恢复

之前因Lettuce在Windows上出现DNS解析问题临时切换为Jedis。现Redis容器稳定运行,恢复默认Lettuce客户端。


总结

本篇记录了后端运行后前后端联调阶段的核心工作。这个阶段的特点是问题驱动------大部分修改都源于实际运行中暴露的bug或体验问题。从SSE流式通信的跨端适配到Jackson反序列化陷阱,从UI全面优化到数据一致性修复,每一步都是对系统稳定性和用户体验的打磨。

下一步将重点关注知识库模块的改造和个性化学习功能的完善,让系统从"能用"真正走向"好用"。

相关推荐
杨先生哦1 小时前
【2026 热端攻防系列 2/12】DOM 型 XSS 深度实战:AI 多态变形免杀 + 全维度防御
前端·人工智能·笔记·安全·web安全·xss
问心无愧05131 小时前
ctf show web入门115
android·前端·笔记
沪漂阿龙1 小时前
Vector Store:FAISS、Chroma、Milvus、Qdrant、ES 怎么选?
人工智能·elasticsearch·架构·milvus·faiss
Suxing91 小时前
C语言基础分享——内存里的“左右手互搏”术:大小端
c语言·开发语言·学习
zhangrelay2 小时前
ROS2 Lyrical 入门+进阶+精通+……
linux·笔记·学习·机器人·课程设计
意图共鸣2 小时前
意图共鸣科技《AI记忆链商业化白皮书3.0》阐述记忆共识构想:让AI记忆像普通话一样实现标准化流转
人工智能
Henry-SAP2 小时前
SAP MRP 增强自定义业务功能解析
人工智能·sap·erp
喵叔哟2 小时前
Week 3 --Day 5:性能优化与监控
人工智能·python·性能优化·langchain
babe小鑫2 小时前
2026年大数据与计算机专业学习数据分析的技术价值
大数据·学习·数据分析