前后端联调实战:从接口对接到边界问题修复的完整记录
项目背景
本项目是一个基于 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.request 的 enableChunked: 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 }。前端需要处理:
- 参数映射 :前端输入框显示"手机号",实际发送使用
phone字段 - 手机号验证 :正则
/^1[3-9]\d{9}$/验证格式 - 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,大二→2,...)
- 实时验证:输入框失焦时触发验证,显示红色错误提示
- 注册成功后跳转: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 |
获取个人信息 | 字段映射:userId → id,username → name |
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/nextPOST /api/exercises/targetedGET /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中的loadChatHistory、loadChatSessions、sendMultimodalMessage
修复方案: 改为 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 接口对接最佳实践
- 严格对照后端文档:每个接口的路径、参数类型、响应格式都要与文档一致
- 统一响应拦截器 :在
request.js中统一处理响应格式映射,store 层只需关注业务逻辑 - 降级方案:关键功能在接口失败时提供本地模拟数据,保证用户体验
- 错误处理 :所有接口调用都添加 try-catch,错误信息通过
uni.showToast提示用户
7.2 状态管理设计
使用 Pinia 进行状态管理,按模块拆分 store:
| Store | 职责 |
|---|---|
user.js |
用户信息、登录状态、头像上传 |
chat.js |
AI 对话消息、流式响应、会话历史 |
plan.js |
学习计划、任务列表、统计计算 |
exercise.js |
练习会话、答题记录、计时器 |
notification.js |
通知列表、未读数量、设置 |
7.3 经验教训
- 不要假设参数类型 :后端文档说
difficulty是字符串,就不要自作聪明映射为整数 - 注意响应层级 :
request.js返回的是{ code: 200, data },store 中访问业务数据需要res.data - 统一响应检查 :使用
res.ok === 1而非res.code === 200,保持与后端Result格式一致 - 文件上传字段名 :不同接口可能使用不同的字段名(
file、imgFile、image),需严格对照文档
八、项目技术栈
| 技术 | 版本 | 用途 |
|---|---|---|
| uni-app | 最新 | 跨端开发框架 |
| Vue 3 | 3.x | 前端框架 |
| Pinia | 2.x | 状态管理 |
| Spring Boot | 2.x | 后端框架 |
| SSE | - | 流式响应 |
| Session | - | 用户认证 |
九、后续计划
- 等待后端补充高优先级接口,完成剩余功能联调
- 优化 SSE 在 App 端的兼容性测试
- 完善错题拍照上传功能
- 实现学情报告页面的数据可视化
- 生产环境 BASE_URL 配置和跨域处理