山东大学软件学院项目实训-创新实训-计科智伴(五)——个人博客(从接口对接到边界问题修复的完整记录)

前后端联调实战:从接口对接到边界问题修复的完整记录


项目背景

本项目是一个基于 uni-app 的智能学习平台前端,后端采用 Spring Boot 框架,使用 Session + UserHolder 的认证方式。前端需要对接后端提供的 RESTful API 和 SSE 流式接口,实现用户认证、智能问答、学习计划、错题本等核心功能。


一、基础设施搭建:请求层封装

1.1 响应格式适配

后端统一使用 Result 格式返回数据:

json 复制代码
{
  "ok": 1,
  "msg": null,
  "data": {}
}

其中 ok: 1 表示成功,ok: 0 表示失败。前端原有的响应格式为 { code: 200, message, data },需要在响应拦截器中进行映射:

javascript 复制代码
// utils/request.js
success: (res) => {
  if (res.statusCode === 200) {
    const data = res.data
    
    // 适配后端 Result 格式
    if (data.ok !== undefined) {
      if (data.ok === 1) {
        resolve({ code: 200, message: 'success', data: data.data })
      } else {
        uni.showToast({ title: data.msg || '请求失败', icon: 'none' })
        reject(new Error(data.msg))
      }
      return
    }
    
    // 兼容前端原有格式
    if (data.code === 200) {
      resolve(data)
    }
  }
}

技术要点:

  • 响应拦截器同时兼容后端 Result 格式和前端原有格式,保证平滑过渡
  • 业务错误统一通过 uni.showToast 提示用户
  • 401 状态码自动跳转登录页并清除本地 Token

1.2 Session 认证支持

后端使用 Session 认证而非 JWT,前端需要配置 withCredentials: true 确保请求自动携带 Cookie:

javascript 复制代码
uni.request({
  url: BASE_URL + options.url,
  method: options.method || 'GET',
  data: options.data || {},
  header: {
    'Content-Type': 'application/json',
    'Authorization': token ? `Bearer ${token}` : '',
    'X-Platform': platform
  },
  withCredentials: true,  // 关键配置
  // ...
})

1.3 SSE 流式请求封装

智能问答模块需要接收后端 SSE(Server-Sent Events)流式响应。使用 uni.requestenableChunked: true 实现:

javascript 复制代码
export function sseRequest(url, data, callbacks) {
  let aborted = false
  
  uni.request({
    url: BASE_URL + url,
    method: 'POST',
    data: data,
    enableChunked: true,  // 启用分块传输
    header: {
      'Content-Type': 'application/json',
      'Accept': 'text/event-stream'  // 声明接受 SSE
    },
    success: (res) => {
      if (aborted) return
      
      const text = res.data
      const lines = text.split('\n')
      
      for (const line of lines) {
        if (line.startsWith('data: ')) {
          const content = line.substring(6)
          
          if (content === '[DONE]') {
            callbacks.onComplete?.()
            return
          }
          
          callbacks.onMessage?.(content)
        }
      }
    }
  })
  
  return {
    abort: () => { aborted = true }
  }
}

技术要点:

  • enableChunked: true 启用 HTTP 分块传输,支持流式接收
  • SSE 格式解析:按行分割,提取 data: 开头的消息
  • [DONE] 标记表示流结束
  • 返回 abort 方法用于取消请求(用户点击"停止生成"按钮时调用)

二、用户认证模块联调

2.1 登录接口对接

后端登录接口为 POST /users/login,请求体为 { phone, password }。前端需要处理:

  1. 参数映射 :前端输入框显示"手机号",实际发送使用 phone 字段
  2. 手机号验证 :正则 /^1[3-9]\d{9}$/ 验证格式
  3. Session 存储:后端创建 Session,前端通过 Cookie 自动维护
javascript 复制代码
// api/index.js
export function login(data) {
  const params = {
    phone: data.username,  // 前端 username 字段映射为 phone
    password: data.password
  }
  return post('/users/login', params)
}

2.2 注册功能实现

注册页面包含用户名、手机号、密码、年级、专业等字段。关键处理:

  1. 年级映射:前端中文选项映射为数字(大一→1,大二→2,...)
  2. 实时验证:输入框失焦时触发验证,显示红色错误提示
  3. 注册成功后跳转:1.5 秒后自动返回登录页

2.3 记住密码功能

使用本地存储实现记住密码,密码采用 Base64 + 字符位移的轻量级加密:

javascript 复制代码
function encryptPassword(password) {
  // 字符位移 + Base64 编码
  const shifted = password.split('').map(c => 
    String.fromCharCode(c.charCodeAt(0) + 3)
  ).join('')
  return btoa(shifted)
}

function decryptPassword(encrypted) {
  const shifted = atob(encrypted)
  return shifted.split('').map(c => 
    String.fromCharCode(c.charCodeAt(0) - 3)
  ).join('')
}

三、智能问答模块联调

3.1 文本流式问答

对接后端 POST /api/ai/chat 接口,使用 SSE 接收流式响应。在 Pinia store 中实现:

javascript 复制代码
// store/chat.js
async sendChatMessage(message, subject = '') {
  if (!this.conversationId) {
    this.conversationId = 'sess_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9)
  }
  
  const requestData = {
    prompt: message,
    sessionId: this.conversationId,
    subject: subject,
    useRag: true
  }
  
  const { abort } = apiSendChat(requestData, {
    onMessage: (content) => {
      this.updateStreamingContent(content)  // 逐步更新流式内容
    },
    onComplete: () => {
      if (this.streamingContent) {
        this.addAssistantMessage(this.streamingContent)
      }
      this.stopStreaming()
    },
    onError: (error) => {
      this.addAssistantMessage('抱歉,我暂时无法回答这个问题')
      this.stopStreaming()
    }
  })
  
  this.currentAbort = abort
}

交互变化:

  • AI 生成期间输入框禁用,显示"停止生成"按钮
  • 点击停止按钮调用 abort() 中断 SSE 请求

3.2 多模态问答(看图答题)

对接后端 POST /api/ai/chat/multimodal 接口,支持图片上传:

javascript 复制代码
async sendMultimodalMessage(imagePath, prompt = '') {
  const response = await apiChatMultimodal(imagePath, prompt)
  const data = response.data
  
  // 展示识别结果
  this.addUserMessage(prompt || '图片内容:' + data.recognizedText, data.imageUrl)
  // 展示 AI 回答
  this.addAssistantMessage(data.answer)
  // 置信度低时提示
  if (data.confidence < 0.7) {
    this.addAssistantMessage('⚠️ 图片识别置信度较低...')
  }
}

3.3 对话历史加载

对接后端 GET /ai/history/{type}/{chatId} 接口,将后端消息格式转换为前端格式:

javascript 复制代码
async loadChatHistory(conversationId = '') {
  const res = await apiGetHistory(targetId, 'chat')
  if (res.ok === 1 && res.data && Array.isArray(res.data)) {
    this.messages = res.data.map(msg => ({
      id: msg.id || Date.now().toString(),
      role: msg.role || (msg.userId ? 'user' : 'assistant'),
      content: msg.content || msg.message || '',
      time: msg.timestamp ? this.formatTimestamp(msg.timestamp) : this.getCurrentTime(),
      image: msg.imageUrl || null
    }))
  }
}

四、三阶段接口接入计划

4.1 第一阶段:高优先级接口(5 个)

接口 说明 关键处理
GET /users/profile 获取个人信息 字段映射:userIdidusernamename
GET /users/learning-profile 获取学情画像 传入 userId 参数
POST /api/mistakes 添加错题 请求体:{ qId, userAnswer }
POST /study-plans 创建学习计划 AI 自动生成,创建后自动获取任务列表
GET /api/onboarding/questions 获取引导问题 支持按步骤动态加载,失败时使用模拟数据降级

4.2 第二阶段:中优先级接口(6 个)

接口 说明 关键处理
POST /users/avatar 上传头像 multipart/form-data,字段名 file
POST /users/profile 创建用户画像 提交问卷时调用,存储到本地
PUT /users/profile/{userId} 更新用户画像 路径参数 + 请求体
POST /api/exercises/next 获取下一批练习 难度参数为字符串 medium
GET /api/exercises/targeted 获取专项练习 GET 方式,参数:kpId, difficulty, count
GET /ai/history/{type} 获取会话列表 返回会话 ID 数组

4.3 第三阶段:低优先级接口(4 个)

接口 说明 关键处理
GET /users/profile/{userId} 获取用户画像 路径参数 userId
POST /api/user/materials 上传学习资料 multipart/form-data,额外参数 userId, materialType
POST /fileUpload 文件上传 字段名 imgFile(非默认 file
GET /fileDownload/{filename} 文件下载 返回完整 URL,直接用于下载

五、边界问题排查与修复

在接口联调过程中,发现了 7 个边界问题,这些问题在代码审查阶段被逐一修复。

5.1 难度参数类型错误(3 处)

问题描述: 前端将难度字符串 easy/medium/hard 映射为整数 1/2/3,但后端文档明确要求 difficulty 字段为字符串类型

影响接口:

  • POST /api/exercises/next
  • POST /api/exercises/targeted
  • GET /api/exercises/targeted

后端文档示例:

json 复制代码
{
  "userId": 1,
  "knowledgePoint": "二叉树",
  "difficulty": "medium",  // 字符串类型!
  "count": 5
}

修复方案: 移除整数映射,直接使用字符串。

javascript 复制代码
// 修复前
difficulty: data.difficulty ? { easy: 1, medium: 2, hard: 3 }[data.difficulty] : undefined

// 修复后
difficulty: data.difficulty || 'medium'

5.2 响应格式检查错误(3 处)

问题描述: 前端使用 res.code === 200 检查响应,但 request.js 响应拦截器已将后端 { ok: 1, data } 映射为 { code: 200, data },在 store 中应直接使用 res.data 访问业务数据。

影响文件:

  • store/chat.js 中的 loadChatHistoryloadChatSessionssendMultimodalMessage

修复方案: 改为 res.ok === 1 检查(或直接依赖拦截器的错误处理)。

javascript 复制代码
// 修复前
if (res.code === 200 && res.data && Array.isArray(res.data)) {

// 修复后
if (res.ok === 1 && res.data && Array.isArray(res.data)) {

5.3 数据访问层级错误(1 处)

问题描述: submitSingleAnswerWithFeedback 方法直接使用 result.correct,但 request.js 返回的是 { code: 200, data: { correct, ... } },应该访问 res.data.correct

修复方案: 添加 const result = res.data 提取实际业务数据。

javascript 复制代码
// 修复前
const result = await submitSingleAnswer(exerciseId, answer, userId)
this.exerciseResults[this.currentIndex] = {
  correct: result.correct === true || result.correct === 1,
  // ...
}

// 修复后
const res = await submitSingleAnswer(exerciseId, answer, userId)
const result = res.data  // 提取业务数据
this.exerciseResults[this.currentIndex] = {
  correct: result.correct === true || result.correct === 1,
  // ...
}

六、前端待补充接口需求文档

在联调过程中,整理了 28 个 前端需要但后端尚未实现的接口,生成《前端待补充接口需求文档》提供给后端团队。

6.1 高优先级接口(12 个)

模块 接口 说明
任务 PUT /tasks/{taskId}/status 更新任务状态(支持 0/1/2)
练习 GET /api/exercises/subjects 获取科目列表
练习 GET /api/exercises/subjects/{subjectId}/topics 获取知识点列表
练习 POST /api/exercises/{sessionId}/submit 批量提交答案(考试模式)
引导 GET /api/courses 获取课程列表
引导 POST /api/profile/generate AI 生成学情画像
错题 PUT /api/mistakes/{mistakeId}/status 标记错题状态
报告 GET /api/report 获取学情报告
用户 GET /api/user/info 获取用户信息
用户 PUT /api/user/info 更新用户信息
通知 GET /api/notifications 获取通知列表
通知 GET /api/notifications/unread-count 获取未读数量

6.2 中优先级接口(10 个)

包括获取推荐问题、清空对话历史、获取练习记录、上传错题(拍照识别)、获取错题统计、打卡签到、获取学习统计、获取能力成长趋势、通用文件上传、全部标记已读。

6.3 低优先级接口(6 个)

包括取消 AI 生成、通知设置(获取/保存)、获取成就列表、获取等级信息、导入用户画像。


七、技术总结

7.1 接口对接最佳实践

  1. 严格对照后端文档:每个接口的路径、参数类型、响应格式都要与文档一致
  2. 统一响应拦截器 :在 request.js 中统一处理响应格式映射,store 层只需关注业务逻辑
  3. 降级方案:关键功能在接口失败时提供本地模拟数据,保证用户体验
  4. 错误处理 :所有接口调用都添加 try-catch,错误信息通过 uni.showToast 提示用户

7.2 状态管理设计

使用 Pinia 进行状态管理,按模块拆分 store:

Store 职责
user.js 用户信息、登录状态、头像上传
chat.js AI 对话消息、流式响应、会话历史
plan.js 学习计划、任务列表、统计计算
exercise.js 练习会话、答题记录、计时器
notification.js 通知列表、未读数量、设置

7.3 经验教训

  1. 不要假设参数类型 :后端文档说 difficulty 是字符串,就不要自作聪明映射为整数
  2. 注意响应层级request.js 返回的是 { code: 200, data },store 中访问业务数据需要 res.data
  3. 统一响应检查 :使用 res.ok === 1 而非 res.code === 200,保持与后端 Result 格式一致
  4. 文件上传字段名 :不同接口可能使用不同的字段名(fileimgFileimage),需严格对照文档

八、项目技术栈

技术 版本 用途
uni-app 最新 跨端开发框架
Vue 3 3.x 前端框架
Pinia 2.x 状态管理
Spring Boot 2.x 后端框架
SSE - 流式响应
Session - 用户认证

九、后续计划

  1. 等待后端补充高优先级接口,完成剩余功能联调
  2. 优化 SSE 在 App 端的兼容性测试
  3. 完善错题拍照上传功能
  4. 实现学情报告页面的数据可视化
  5. 生产环境 BASE_URL 配置和跨域处理

相关推荐
Oll Correct1 小时前
计算机二级WPS Office第十四套WPS演示
笔记·计算机二级wps
AugustRed1 小时前
Flyway 数据库版本迁移 零基础完整学习文档
数据库·学习
我的xiaodoujiao1 小时前
API 接口自动化测试详细图文教程学习系列23--结合Pytest框架使用4-前后置处理
python·学习·测试工具·pytest
USC-XiangLuXun1 小时前
局部科技小创新是有意义的
科技·学习·生活
哇嘎呀2 小时前
OSPF笔记
网络·笔记
Upsy-Daisy2 小时前
IOTA 学习笔记(三):IOTA 的技术演进路线
笔记·学习
有个人神神叨叨2 小时前
Agent Memory 学习笔记-1.0
笔记·学习
一只肥瘫瘫2 小时前
STM32 程序升级学习笔记:Bootloader、IAP 与串口升级流程
笔记·stm32·学习
qq_571099352 小时前
学习周报四十七
学习