Spring Boot + Vue 前后端联调踩坑记录

张开发
2026/4/15 23:33:27 15 分钟阅读

分享文章

Spring Boot + Vue 前后端联调踩坑记录
最近在搞那个 AI 内容创作平台 的前后端联调踩了一堆坑从 SSE 时序到参数类型再到响应数据结构一个接一个。本文记录一下大概有4个比较典型的。SSE 连接时序竞争Emitter 不存在SSE Emitter 不存在, taskIdaef5eec7307b4532815a239c45cad95b GET http://localhost:5173/api/article/progress/xxx 500 (Internal Server Error)后端用Async跑 AI 生成文章的异步任务但频繁报 “SSE Emitter 不存在” 错误。原因分析这是一个典型的时序竞争问题。流程如下1. POST /article/create → 返回 taskId同时 Async 启动异步任务 2. 异步线程立即开始执行 AI 生成尝试通过 sseEmitterManager.send() 推送消息 3. 前端拿到 taskId 后才建立 SSE 连接 GET /article/progress/{taskId}如果步骤 2 比步骤 3 先发生emitterMap中还没有该taskId对应的 Emitter就会报错。解决方案在启动异步任务之前先把 SSE Emitter 预创建好放进 emitterMap 里确保异步任务启动时Emitter 已经存在了。ArticleController.java调整一下顺序PostMapping(/create)publicBaseResponseStringcreateArticle(...){StringtaskIdarticleService.createArticleTask(request.getTopic(),loginUser);// 关键先预创建 SSE Emitter放进 map 里sseEmitterManager.createEmitter(taskId);// 之后再启动异步任务此时 Emitter 已经存在可以安全发送articleAsyncService.executeArticleGeneration(taskId,request.getTopic());returnResultUtils.success(taskId);}然后在SseEmitterManager.java的createEmitter方法加个幂等判断防止重复创建publicSseEmittercreateEmitter(StringtaskId){// 如果已经存在直接返回已有的支持预创建场景SseEmitterexistingEmitteremitterMap.get(taskId);if(existingEmitter!null){returnexistingEmitter;}// ... 原来的创建新 Emitter 的逻辑}修复后时序变为1. 创建任务 预创建 Emitter放入 emitterMap 2. 启动异步任务此时 Emitter 已存在可以安全发送 3. 前端建立 SSE 连接返回已预创建的 Emitter文章详情页参数传递类型错误BusinessException: 文章不存在 at ArticleController.getArticle(ArticleController.java:106)文章是创建成功了但是点击“查看文章”前端页面却显示“文章不存在”。原因分析前端 API 函数getArticle期望接收一个对象{ taskId: string }但是我在页面里直接传了个字符串// articleController.ts — 函数签名明确要对象exportasyncfunctiongetArticle(params:API.getArticleParams){const{taskId:param0,...queryParams}paramsreturnrequest(/article/${param0},{...})}// ArticleDetailPage.vue — 我直接传了字符串constresawaitgetArticle(taskId)// 传入字符串对字符串做解构后param0为undefined最终请求变成了/article/undefined。解决方案老老实实传对象就行constresawaitgetArticle({taskId})// 传 { taskId: xxx }我的文章列表显示 “No data”文章生成完成了数据库里也有数据但查看【我的文章】列表页却显示 “No data”。原因分析Axios 的完整响应结构是这样的res AxiosResponse 对象 res.data { code: 200, data: { records: [...], totalRow: 10 }, message: ok }也就是说实际的业务数据在res.data.data里但我在前端直接取了res.data.records// 错误写法 — res.data 是 { code, data, message }根本没有 records 属性dataSource.valueres.data.records||[]pagination.value.totalres.data.totalRow||0当然是空的。解决方案老老实实多写一层.datadataSource.valueres.data.data.records||[]pagination.value.totalres.data.data.totalRow||0删除文章请求体格式错误MismatchedInputException: Cannot construct instance of DeleteRequest (no int/Int-argument constructor/factory method to deserialize from Number value (5))点击删除文章后端直接报 Jackson 反序列化错误。原因后端接口用的是RequestBody DeleteRequest期望接收一个 JSON 对象但我在前端直接传了个数字// 错误写法 — 请求体是数字 5awaitdeleteArticleApi(record.id)Jackson 收到数字5当然没法反序列化成DeleteRequest对象。解决方案老老实实传对象就行// 正确写法 — 请求体是 { id: 5 }awaitdeleteArticleApi({id:record.id})

更多文章