研途灵伴——联调我修了七个 Bug
写在前面这周我们组的研途灵伴项目进入联调阶段。功能基本都搭完了但拼到一起之后问题一个接一个地冒接口 500、页面白屏、按钮看不清字、骨架屏永远不消失。一共修了七个 Bug另外有两个问题排查后发现涉及架构层面的决策暂时没修先记下来等团队讨论。这篇文章把每个问题的排查过程和修法都记一遍算是给自己留个底。一、两个 500——重构之后调用方忘改了最先冒出来的是两个 500都在学习会话模块。调POST /api/v1/study-session/start的时候后端直接报 500错误信息是CareService.on_study_start() got an unexpected keyword argument now。结束会话那个接口也一样on_mood_recorded()报了同样的错。我去看了一下调用方的代码study_session.py里写了self.care_service.on_study_start(user_id, nowstart_time, auto_commitFalse)。再去看care.py里on_study_start的签名——只有一个user_id参数now和auto_commit根本不接受。应该是之前某次重构改了CareService的方法签名但调用方没跟着改。两个地方各删一行多余的参数问题就解决了。这种 bug 不难查但很典型重构改了接口调用方漏改了。Python 不像 TypeScript 有编译期类型检查少一个参数运行时才报错联调的时候才发现。二、饮食页面白屏——数组和对象没对齐点击饮食记录按钮页面直接白屏。打开控制台一看TypeError: Cannot read properties of undefined (reading find)。顺藤摸瓜查下去后端/api/v1/meal/menu返回的data字段直接是一个数组。但前端getMealMenus的返回类型写的是{items: MealMenu[]}父组件拿到返回值后执行menus.items得到的是undefined传给子组件后.find()就崩了。改法很简单在getMealMenus里把裸数组包一层return { items: unwrap(response.data) }让实际返回值和类型声明对上。这个 bug 暴露了一个联调中很常见的问题后端觉得返回数组没问题前端觉得返回对象更合理两边各改各的类型系统又拦不住运行时的结构不匹配。如果后端的接口文档或者类型定义足够严格这种问题在开发阶段就能发现。三、错题本标签重复——同一份数据存了两份错题详情页里这道题目如何解决区域的知识点标签出现了重复。比如自然语言处理出现了两次。查了一下后端的数据流创建错题时knowledge_points存进了WrongQuestion模型的 JSON 字段同时相同的值又作为WrongQuestionTagtag_typeknowledge存进了 tag 表。详情 API 返回时两个字段都带着这些值前端两组都渲染自然就重复了。修法在前端渲染tags的时候加一行过滤跳过tag_value已经存在于knowledge_points中的条目。这个不算严格的 bug更像是数据冗余导致的展示问题。后端存了两份一样的数据前端得自己判断该信哪一份。四、小测再练没有图片——数据在模块间传递时丢了从错题本点小测再练进入答题页题目只有文字没有图片。但原始的错题记录里是有图的。问题出在数据传递链路上错题来源的WrongQuestion有images字段但小测走的是QuestionItem模型这个模型没有images。后端构建小测题目 payload 的时候只取了QuestionItem的字段图片就这样丢了。改法涉及后端三个地方_get_questions_from_wrong_review改为返回三元组(QuestionItem, origin, images)把图片一起带出来_build_start_question_payload新增images参数写入响应schemaQuizStartQuestionResponse新增images字段前端也跟着改了QuizStartQuestion类型加上imagesQuiz 页面渲染题目时用Image组件展示。这个问题属于典型的数据在模块间传递时丢失。每个模块只关心自己的模型定义没人负责把图片从错题一路带到小测。这种问题在单独开发各自模块时不会发现联调时才暴露。五、聊天按钮看不清字——旧 API 在新版本上的坑Tutor 回复消息底部有一排动作按钮“加入错题本”小测再练之类的。绿色文字配深色气泡背景肉眼几乎看不清。查了一下ActionButtons.tsx用的是 Ant Design 的typeprimaryghost{true}。ghost 模式下按钮是透明背景文字颜色继承 primary 色teal在深色背景上对比度不够。一开始想走 CSS 覆盖的路子加了.ant-btn-primary.ant-btn-ghost的样式规则。结果没用——Ant Design 5 用的是 CSS-in-Js优先级比外部 CSS 高样式根本覆盖不上去。最后换成了 Ant Design 5 的新 propscolorprimaryvariantsolid。文字变白色背景变成 teal 实心对比度一下就够了。已完成状态的按钮用variantoutlined保持灰色风格。这件事让我对 Ant Design 5 的 API 体系有了更清楚的认识。type/ghost是旧写法color/variant是新写法两者不能混用。如果项目一开始就统一用新 API这类问题根本不会出现。六、情绪页面骨架屏永远不消失——这个最折腾这个问题排查时间最长也是我觉得最有意思的一个。打开情绪记录页面左侧的今日心情打卡表单正常显示但右侧最近 7 天趋势卡片和下方历史记录卡片始终是骨架屏——灰色条状占位符内容永远加载不出来。我一开始以为是某个 API 接口挂了但单独调三个接口都没问题。后来发现是三层问题叠在一起才产生的第一层Promise.all的失败传播MoodPage用Promise.all并行调了三个 API。Promise.all的语义是全部成功——只要有一个 reject整个 Promise 就 reject。虽然外层有 try-catch-finallyfinally里写了setLoading(false)但在快速重渲染的场景下存在竞态条件。第二层全局 store 触发的竞态情绪页面监听了全局 store 里的moodRefreshSequence。当其他模块比如聊天、学习会话调用emitRefreshTargets([mood, ...])时这个 sequence 会递增触发情绪页面重新加载数据。每次重新加载开头就setLoading(true)如果上一次还没加载完新的setLoading(true)会覆盖掉finally里的setLoading(false)loading 就永远卡在 true。第三层组件间共享 loading 状态MoodTrend和MoodHistory都通过loading{loading}接收同一个状态。一旦 loading 卡住所有 Card 同时卡在骨架屏。修法Promise.all改成Promise.allSettled每个 API 独立处理成功和失败一个挂了不影响其他移除MoodTrend和MoodHistory的loading属性组件内部自己处理空状态显示还没有情绪记录之类的提示清掉了不再使用的trendLoading状态变量改完之后即使某个 API 超时或者报错其他数据照常展示骨架屏不会再卡死。七、另外两件事除了上面七个 Bug这轮还做了两个小改动Vite 预加载给vite.config.ts加了build.warmup.clientFiles把主要页面组件加进预加载列表。改动不大但能减少首次打开页面时的白屏时间。未修复 Bug 沉淀有两个问题排查后发现涉及架构层面的决策暂时没修记录到了未修复的bug/目录下“错题本 correct_answer 在聊天与答疑链路中始终为空”——聊天和截图答疑来源的错题没有标准答案需要确认标准答案的业务定义“情绪打卡提交因 CareService 调用 LLM 超时而卡死”——on_mood_recorded触发的 care 服务会调用 LLM API没有超时设置导致整个请求挂起这两个问题不是修不了是修之前需要团队先统一口径。八、几点感受联调不比开发轻松。每个模块单独看都没问题拼到一起之后各种边界问题就冒出来了。500、白屏、骨架屏卡死这些都不是代码写错了而是拼起来之后才有的病。竞态条件是最难查的 Bug。情绪页面那个骨架屏问题不是逻辑错了而是多个异步操作在特定时序下产生了不可预期的行为。时序相关的 bug 很难用单元测试覆盖因为执行顺序是不确定的。最后是靠理清楚数据流和状态更新的时序才定位到的。Ant Design 升级要注意 API 迁移。ghost 按钮的问题本质是旧 API 在新版本上表现不如预期。如果项目一开始就用color/variant写法这类问题根本不会出现。以后用新框架的时候得先看看有没有 API 迁移指南。数据在模块间传递时容易丢东西。小测图片缺失的问题每个模块只关心自己的模型没人负责把数据一路带下去。这类问题在项目初期不容易发现联调时才暴露。如果能在模型设计阶段就考虑好跨模块的数据流后面会省很多事。九、还差什么情绪打卡提交卡死的问题需要团队讨论后决定修法核心是 care 服务的 LLM 调用需要加超时错题本 correct_answer 为空的问题需要确认标准答案到底由谁提供这轮主要是修 Bug没有新增功能模块最后这轮联调修下来最大的收获不是修掉了几个具体 bug而是对系统拼装这件事有了更具体的体感。单个模块开发的时候边界是清晰的输入输出是可控的。但多个模块拼到一起之后时序、数据结构、状态管理之间的配合就变得复杂了。情绪页面的骨架屏问题尤其典型——Promise.all的失败传播、全局 store 触发的重渲染、组件间共享 loading 状态三层问题叠在一起单独看每一层都不算 bug合在一起就是用户体验灾难。修这种问题没有捷径只能一层一层拆开看找到真正的根因。打补丁只会让下一次排查更难。