ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

前后端联调接口bug排查:重复提交、跨域、传参和版本缓存一次讲透

前后端联调接口bug排查:重复提交、跨域、传参和版本缓存一次讲透 项目标题里那句史上最细经典的前端后端与接口bug说实话搞前后端联调的人看到这几个字多少都有点条件反射式的头皮发麻。我在团队里前后端都写过从前端页面到后端接口再到数据库表结构踩过的接口bug少说也有几十个史上最细我不敢自封但这篇想把这几年在前后端分离项目里遇到的典型问题尤其是按钮重复提交、跨域、参数匹配、版本缓存这几类高频问题一次讲透。文章会用一个接近真实业务的项目案例做底子把问题现象、排查过程、根因分析、解决方案和为什么这么解决的完整链路都走一遍项目案例里的代码和配置可以直接抄作业适合正在做前后端分离项目实战的人也适合准备前端面试题或后端开发面试时想拿实战案例做谈资的人。网上的接口bug资料大多只讲某个单点问题比如一句前端要防重复点击或后端要配置跨域但实际联调的时候bug往往不是单点原因而是前端传递方式、后端接收逻辑、甚至中间代理层三者之间的配合出了问题。我希望这篇能帮你建立起完整的排查思路出问题先看哪一层每一层最容易埋雷的地方在哪里怎么快速定位而不是靠肉眼猜。下面从项目背景开始一步步拆。1. 项目背景与整体架构拆解1.1 项目场景一个典型的前后端分离实战项目几年前我带过一个积分商城类的项目现在回头看这类业务网络上有大量类似的前后端分离项目实战需求很典型。前端负责页面展示和用户交互后端负责业务逻辑和数据处理两者通过HTTP接口通信。支付、下单、领取优惠券这些操作都涉及金额敏感数据对接口的健壮性要求特别高同时也是接口bug最容易出现的地方。当时遇到的第一个冲击是你以为接口bug只是传参对不对的问题但实际上一旦进入联调阶段bug的形态千奇百怪。用户点击提交按钮之后页面卡住后端数据库里却插入了多条相同订单前端明明配置了请求地址后端却报跨域前端传了一个时间字符串后端直接返回500线上版本更新之后用户手机上的旧页面还访问着已经下线了的接口。这些问题几乎覆盖了接口联调的全部痛点每个问题背后都牵扯到前后端设计理念的差异。更有意思的是这些问题在网络热词里也频繁出现比如前后端对于按钮重复提交校验方法、后端跨域、前端传参、通过版本号的变更让前端强制刷新页面说明这不是个别团队的困惑而是行业级的共性问题。理解这些问题需要先搞清楚这个项目的基本架构。1.2 技术选型背后的考量为什么是Vue 3 Spring Boot这个项目的技术栈是业界用得最多的组合前端Vue 3 Vite TypeScript Element Plus后端Spring Boot 3 MyBatis Plus数据库MySQL部署用Nginx。选这套组合的原因不复杂Vue 3的Composition API让组件逻辑复用更干净Vite开发服务器启动速度快对热更新支持好Spring Boot 3带来了Jakarta EE命名空间和更好的可观测性支持MyBatis Plus则让通用CRUD不用写太多XML。这类选型也经常出现在前端组件库、后端开发实战这类搜索场景里说明这是主流选择。真正决定项目成败的不是框架本身而是前后端之间的接口契约。我们的做法是一开始就用Apifox统一维护接口文档字段名、类型、必填项、错误码全部在文档里写清楚后端按文档实现前端按文档联调。但实际开发中接口文档总是跟不上代码变化于是bug开始冒头。下图是简化后的调用链路读懂了这张图后面每个问题的排查路径就清晰了浏览器页面 - 前端API层 - 开发代理 / Nginx / CDN - 网关/负载 - 后端Controller - Service - Mapper - MySQL问题可能出现在这条链路的任何一个节点而接口bug只是表象。比如按钮重复提交表面是前端交互问题根子上可能是后端缺少幂等设计跨域报错表面是后端问题实际是浏览器安全策略加代理层配置没对齐。下面进入具体案例。2. 经典bug之一按钮重复提交与幂等性兜底2.1 问题现象与排查路径用户的反馈是我明明只点了一次提交怎么创建了两条一样的订单或者积分扣了两次。接到反馈后第一件事不是看代码而是先复现。打开浏览器DevTools切换到Network面板把网络记录清空然后快速连续点击提交按钮你会发现请求列表里出现了两个甚至多个相同的POST请求请求参数完全一样时间戳只差几百毫秒。顺着请求找后端日志能看到同一个业务方法被执行了多次数据库里出现了多条记录。这个现象在后端日志里尤其明显每一条创建订单的日志都对应一个重复请求。需要说明的是这不仅仅是用户手抖的问题。在抽奖、秒杀、支付回调这类场景里客户端网络抖动导致的自动重试、移动端弱网环境下的重复请求、用户习惯性双击都会制造大量重复提交。就算前端做了按钮禁用也无法覆盖所有触发路径。排查时还有一个容易踩的坑只在前端代码里找问题。前端确实要处理但如果后端不做幂等控制问题就永远只能靠用户耐心不够来掩盖。任何一次重复请求落在后端数据库就可能产生脏数据这是业务正确性问题不能只靠前端体验层去兜。2.2 前端侧的拦截方案loading、disabled与防抖前端最常见也最基础的方案是给提交按钮加loading状态和disabled属性。Vue 3里的写法很直接script setup import { ref } from vue import { submitOrder } from /api/order const submitting ref(false) async function handleSubmit() { if (submitting.value) return submitting.value true try { await submitOrder({ productId: 1001, count: 1 }) // 成功后提示并跳转 } catch (e) { // 失败后提示错误信息 } finally { submitting.value false } } /script template el-button typeprimary :loadingsubmitting :disabledsubmitting clickhandleSubmit 提交订单 /el-button /template这段代码的核心在于点击后立刻把submitting置为true并在请求结束前阻止任何二次进入。在实际项目中我见过不少人只加了loading样式没有在函数入口做if (submitting.value) return这种判断结果loading的动画挡不住键盘回车触发的提交重复请求照样发出去。所以样式和逻辑要一起上。此外还可以配合防抖或节流。防抖适合提交后短时间内的连点节流适合高频操作但需要保持一定频率。但在接口请求这种场景里我更推荐用状态锁而不是防抖因为防抖的本质是延迟执行用户连点三次可能三次都被合并成一次请求但第一次请求还没回来第二次就无法发出。对于提交操作来说loading锁是最直观也最不容易出错的。但这里必须提醒前端拦截只是降低重复概率不能保证数据层面绝对安全。原因是多端的用户可能开着两个页面、移动端离线时浏览器自动重发请求、抓包工具重放请求这些都在前端的拦截能力之外。所以真正的防线必须在后端。2.3 后端侧的幂等性设计唯一索引与幂等Key后端防重复提交的核心思想是幂等意思是同一个请求执行一次和执行多次最终结果一致。实现幂等至少有两条路数据库唯一约束以及专门的幂等Token机制。第一次处理重复订单时我们的订单表已经有业务订单号但业务订单号是后端根据时间戳生成的同一毫秒内两个请求生成的可能不同。我在订单表上加了一个请求唯一Key字段request_id并且建立了唯一索引。前端每次创建订单时生成一个UUID作为request_id随请求一起提交后端插入前先查一下这个request_id是否已经存在存在就直接返回已有订单结果不存在才继续插入。// 伪代码幂等控制的单元测试场景经常用的写法 public OrderResult createOrder(CreateOrderRequest req) { // 先查幂等表 IdempotentRecord record idempotentMapper.selectByRequestId(req.getRequestId()); if (record ! null) { // 已处理过直接返回历史结果不再执行下单逻辑 return record.getResult(); } // 执行真正的下单逻辑 OrderResult result doCreateOrder(req); // 记录幂等标识与结果 idempotentMapper.insert(req.getRequestId(), result); return result; }注意这里有个并发隐患两个完全相同的请求并发到达都查不到记录然后同时走插入最终还是会重复。很多人会低估这个场景实际上同一请求在极短时间内并发到达并不罕见尤其是移动端重试和网关重放。彻底的方案是让幂等标识本身具备唯一约束插入时如果主键或唯一索引冲突说明是重复请求直接在异常处理里捕获并返回处理中或已处理。实际项目中我还用过一个更轻量的方案在Redis里以request_id为Key用SETNX命令做占位设置过期时间比如5秒拿到锁的请求才继续执行业务逻辑。这个方案不用建表性能也好但要注意Redis锁的过期时间不能太短否则慢请求的锁提前失效重复请求又会进来。网上关于后端spring boot 3的课程里经常会把这两类幂等方案放到一起讲因为它确实是面试的高频考点。3. 经典bug之二跨域问题拦的不是接口是浏览器3.1 同源策略到底拦的是什么跨域大概是前后端联调中出现频率最高的报错之一。你是不是见过这样的浏览器报错Access to XMLHttpRequest at http://localhost:8080/api/xxx from origin http://localhost:5173 has been blocked by CORS policy。很多前端同事会下意识觉得是后端没放开但实际跨域背后的逻辑远不止加一个请求头那么简单。我用生活化类比来解释浏览器就好比一个社区的保安规定只有本小区居民同源才能自由进出外面的人要进来的话必须出示由业主后端服务器亲笔签名的通行证CORS响应头。浏览器拦截的本质不是请求发不出去而是响应被浏览器扣下了。注意跨域限制只发生在浏览器端用Postman或curl直接请求接口通常不会触发跨域因为Postman没有执行同源策略这也解释了很多后端同事我用Postman测明明没问题你前端一调就报错的困惑。同源的判断标准是协议、域名、端口三者完全一致。本地开发最常见的场景就是前端跑在5173端口后端跑在8080端口端口不同跨域。中间层的代理配置不是为了突破浏览器安全策略而上不了台面恰恰是为了让请求在开发和生产环境下都能以最合规的方式转达。3.2 开发环境为什么推荐代理而不是CORS硬开开发阶段解决跨域有两个思路一是后端在响应里返回允许跨域的CORS头二是前端通过开发服务器代理转发请求。我强烈推荐第二种。原因有两个第一开发环境把后端接口地址配在代理里前端代码里不需要写死任何跨域相关的东西后期迁移环境特别方便第二生产环境下接口和页面大概率通过同一个Nginx域名暴露根本不存在跨域开发阶段强行加一堆CORS配置反而会掩盖真实问题。Vite开发服务器配置代理很简单在vite.config.ts里加server.proxyexport default defineConfig({ server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true, // 如果后端路径也带/api就不用重写 // rewrite: (path) path.replace(/^\/api/, ) } } } })这里有个容易踩坑的点rewrite要不要写。如果前端请求的路径是/api/order/list后端接口也是/api/order/list那就不用rewrite但有些后端接口根本不含/api前缀比如后端Controller映射是/order/list而前端统一用/api前缀那就必须rewrite把/api去掉。调整配置之后记得重启Vite开发服务器代理配置改动不能热更新。开发阶段养成在Network里看请求URL的习惯请求地址是否走代理、是否被代理到正确的target一眼就能看出来。3.3 生产环境下Nginx与Spring Boot的CORS配置生产环境最常见的是页面和接口共用同一个域名Nginx做静态资源服务和接口反向代理这样同源跨域问题自然消失。但也有例外比如要把接口暴露给第三方系统调用或者前端静态资源放在CDN上这时候后端就得主动声明允许跨域。Spring Boot 3时代有一个非常坑的细节很多老资料已经过时。Spring Boot 2.x里可以直接用allowedOrigins()但Spring Boot 3基于Jakarta规范把allowedOrigins()和allowCredentials(true)的组合视为非法启动时会直接报错。正确做法是使用allowedOriginPatterns(*)或者明确列出允许来源Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOriginPatterns(https://your-domain.com) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }配CORS之后有几个问题必须留意。第一前端请求带了自定义Header比如tokenallowedHeaders必须包含对应的Header名或者直接通配第二预检请求OPTIONS是一个单独的请求后端要能正确响应很多鉴权过滤器会直接拦掉OPTIONS导致实际请求永远执行不了第三maxAge设置预检请求的缓存时间设长一点能减少浏览器反复发OPTIONS带来的性能损耗。还有一点一些接口是走Spring Security的可以在SecurityConfig里放行OPTIONS请求否则CORS配置配得再对也会被安全链路拦截。4. 经典bug之三前端传参与后端接收不匹配4.1 类型精度丢失前端传的是数字后端接到的不是同一个数字这是我在项目里踩过最隐蔽的坑。场景发生在订单编号上我们后端数据库主键用了雪花IDSnowflake一个ID长这样1689223875111222333。前端用JavaScript的Number接收这个数字问题瞬间出现因为JavaScript能精确表示的最大整数只有2的53次方减1也就是9007199254740991而雪花ID明显超出这个范围。前端拿到的末尾几位数字直接变成了0哪怕只是把它显示在页面上已经和真实ID不一致了更别提拿着它去请求详情接口后端根本查不到记录。网络热词里前端sdk、前端开发skills经常在讲这种精度问题但直到自己遇到才真正有体感。撞上这个坑之后我用了两种方案双保险。后端统一把Long类型的主键通过在Jackson配置中序列化为字符串返回前端收到的就不再是数字而是一串文本精度就不会丢同时前端代码里所有ID字段的TypeScript类型声明为string而不是number避免有人误用。Configuration public class JacksonConfig { Bean public Jackson2ObjectMapperBuilderCustomizer jacksonCustomizer() { return builder - builder .serializerByType(Long.class, ToStringSerializer.instance) .serializerByType(Long.TYPE, ToStringSerializer.instance); } }这个配置是全局生效的但要注意如果某些接口的Long字段业务上就是普通数字而不需要转字符串全局配置会影响它们。实际项目中我是给ID类字段加了JsonSerialize(using ToStringSerializer.class)注解只针对指定字段生效避免全局改动的副作用。4.2 时间格式问题的连锁反应时间格式的bug属于那种看似简单但杀伤力极大的类型。前端日期选择器输出的是2024-06-01 12:30:00后端实体类的字段类型是LocalDateTime如果不做任何配置Jackson反序列化时直接抛异常接口报500报错日志里的关键信息是Failed to deserialize java.time.LocalDateTime。你可能会觉得奇怪不就是个格式问题吗但Jackson默认的时间格式是ISO标准格式例如2024-06-01T12:30:00中间是个T不是空格。解决方案是在application.yml里配置全局时间格式同时给日期参数加上注解双保险spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8如果后端接收的是请求参数里单个时间字段比如GET接口的startTime还得在Controller参数上配合DateTimeFormat(pattern yyyy-MM-dd HH:mm:ss)因为jackson.date-format对query参数不生效。这个区别容易忽略很多人在JSON请求体里传时间没问题一换成GET参数又报错。时区问题也要注意。服务端默认时区可能是UTC如果生成的时间传给前端展示时不做时区转换用户看到的时间可能相差8小时。线上环境我建议服务器时区和数据库时区都显式设为Asia/Shanghai并在数据库连接串里加上serverTimezoneAsia/Shanghai这能避免很多看起来莫名其妙的时间对不上问题。4.3 字段命名不一致引发的空值陷阱接口联调最烦的bug之一前端传了参数后端对应字段却是null整个流程走到后面直接空指针。这个问题十有八九是字段命名不一致导致的。前端习惯用驼峰命名比如userName后端如果数据库命名规范是下划线user_name而实体类字段映射用的是userNameMyBatis会自动做驼峰映射但如果Controller层接收的DTO和前端参数名对不上比如前端传了user_name后端DTO字段是userName又没有配JsonProperty(user_name)那这个字段就接收不到。我们在项目里做了一件事所有接口的请求和响应模型都统一定义并在关键字段上显式标注JsonProperty。不要指望命名习惯一样因为不同人、不同团队、甚至不同语言生态的默认风格都不一样。更靠谱的做法是接口文档里明确每个字段的JSON名前后端各自按文档走联调前先用Mock数据跑一遍字段映射。真到排查的时候可以在前端Network里看请求Payload和后端日志里打印的入参对象逐字段对比十秒钟就能定位到是哪个字段名对不上。5. 经典bug之四版本发布后前端强制刷新页面5.1 为什么用户看到的永远是旧页面这个问题的触发场景很典型版本升级后端接口做了变更前端构建后的JS文件也更新了但用户浏览器里的还是旧页面。用户不刷新就不会重新加载最新的JS文件于是旧页面掉用了新接口或者新接口返回的数据结构变化后旧页面渲染失败白屏、操作报错、接口404接连出现。根本原因是浏览器对静态资源的缓存策略。构建生成的JS和CSS文件名一般会带上内容hash比如index-b3a8f2.js文件内容变了hash就变理论上浏览器请求index.html时拿到的新文件名就会触发新文件下载。但问题在于index.html本身可能被浏览器缓存了如果Nginx或CDN对index.html也设置了较长的Cache-Control浏览器连页面都不重新拉取自然不知道JS文件名已经变了。排查这个问题时我先看的是Network面板里index.html和JS文件的响应头。如果index.html的Cache-Control是max-age31536000那几乎可以断定问题就出在这index.html是不应该被浏览器强缓存的。正确配置是带hash的静态资源可以长缓存因为文件名本身是版本标识内容变了文件名就变了放心缓存index.html只能协商缓存让浏览器每次请求都问一下服务器这个文件变了没变了就拉新的没变就用旧的。5.2 基于版本号的强制刷新方案修改Nginx配置只是第一道防线它解决的是首次访问新版本时能不能拿到新页面。但很多业务场景里用户已经长时间停留在某个页面上比如后台管理系统的运营人员一整天都开着订单页面不关期间前端发布了好几个版本这时候就算刷新页面用户不主动操作就永远停留在旧的前端代码里。针对这个场景业界流行的做法是版本号变更让前端强制刷新页面具体思路是后端提供一个获取当前前端版本号的接口这个版本号在每次前端发布时更新可以是打包时间、Git提交号或MQ里的版本配置。前端启动时记录当前运行版本之后定时轮询后端接口发现版本号与当前不一致就弹窗提示用户系统已更新请刷新页面或者直接调用location.reload()强制刷新。流程大致是这样前端构建时把版本号写入一个静态文件或环境变量比如package.json里的version字段配合构建脚本生成build_version.js。后端或静态服务器提供一个/version接口返回当前应该生效的版本号。前端设置一个全局变量记录当前版本比如从构建时注入的变量里读取。定期比如每60秒调用一次版本接口对比版本不一致就触发刷新逻辑。为了不打扰用户可以只在检测到变更时弹一个轻提示让用户主动点击刷新而不是直接无感知刷新导致表单数据丢失。5.3 具体实现细节与注意点我的实际实现里前端用的是Vite构建我在.env.production里加了一个变量VITE_BUILD_VERSION20240601.01每次发布时手动修改这个版本号Vite构建时会把它编译进产物。然后在入口文件main.ts里做版本检测let currentVersion import.meta.env.VITE_BUILD_VERSION async function checkVersion() { try { const res await fetch(/version) const data await res.json() if (data.version ! currentVersion) { // 弹提示并刷新 alert(检测到新版本请点击确定刷新页面) location.reload() } } catch (e) { // 接口失败不要影响正常使用静默处理 } } // 每60秒检测一次页面切回前台时也可以触发 setInterval(checkVersion, 60 * 1000) document.addEventListener(visibilitychange, () { if (!document.hidden) checkVersion() })这里有几个非常容易踩坑的点。第一/version接口一定要用Cache-Control: no-cache否则浏览器缓存了版本号前端永远检测不到更新。第二强制刷新不能做得太频繁否则用户在填写长表单时突然被刷新会非常崩溃所以我才用alert让用户确认。第三如果项目用的是history路由location.reload()会在当前路由刷新没问题如果部署在Nginx且没配置history路由的try_files刷新可能404这类问题属于部署配置范畴要在Nginx里把不存在路径都指到index.html。再提一个思路现在主流的前端框架都有基于构建产物hash的自动更新检测方案原理和上面的版本号轮询类似不过更自动化。它的思路是前端定期请求index.html把返回里的JS文件名和自己当前使用的做对比文件名变了就说明有新版本。这个方法完全不需要后端维护版本接口成本更低。但要注意请求index.html时必须加一个随机参数或确保不被缓存否则一样的坑。6. 接口联调的排查技巧与常见问题实录6.1 排查链路先从Network入手再读后端日志经过上面几个案例你会发现接口bug的排查路径高度相似完全可以总结成一套固定的排查链路遇到任何bug都按这个顺序来能省大量时间。第一步浏览器Network面板看请求是否发出。这一步回答的是前端到底发没发请求、发的什么请求。可以看请求的URL、请求方法、Headers、Query String Parameters和Request Payload确认前端传的参数和后端接口契约是否一致。很多问题在这一步就能看出来比如参数名拼错了、字段类型传成字符串了、路径少了个斜杠。第二步看响应状态码和响应体。2xx基本说明请求走到了后端并且业务处理完成但要注意2xx也可能携带业务错误码比如200响应体里code50004xx大多是参数问题或权限问题要注意400很多时候是参数类型不匹配404可能是路径不对或服务没部署这个接口401是没登录403是没权限5xx一般就是后端逻辑异常了需要后端日志辅助排查。第三步看后端日志。好的项目都会给每个请求生成一个traceId从网关传到后端各层前端响应头里也能带回traceId。排查时前端把traceId贴给后端后端在日志里一搜就能看到完整的调用链从Controller入口到Service到SQL哪一步返回异常一目了然。我在项目里给RESTful接口加了统一的响应包装Result code、message、data三层结构前端Axios的拦截器统一处理code这样不用每个接口写一堆错误判断。但也要注意统一包装有时候会让后端异常的真实堆栈信息被吞掉所以调试模式下要把异常栈输出到日志并且把message写到响应体里方便前端直接看到原因。6.2 高频问题速查表下面这张表是我在实际项目和带新人时总结的覆盖了前后端接口联调里出现频率极高的一批问题以及对应的排查方向。问题现象高概率根因快速排查手段点击提交按钮生成多条重复数据前端未做提交锁后端无幂等控制Network看同参数POST是否多次发出查数据库重复记录浏览器报CORS错误开发环境代理未配置或生产CORS配置不允许来源DevTools看请求URL是否走代理响应头是否有Access-Control-Allow-Origin前端传number后端收到精度丢失JS Number精度不足Long转String未生效在响应JSON里直接看ID字段确认是否带引号时间字段反序列化报错Jackson默认ISO格式和前端的空格格式不匹配看后端日志中LocalDateTime解析异常配置全局时间格式字段值全是null前端参数名和后端DTO字段不一致Network与后端入参日志逐字段对比检查JsonProperty老版本页面掉用新接口404index.html被强缓存或history路由未兜底看index.html响应头Cache-Control检查Nginx try_files接口偶发超时数据库慢查询、连接池耗尽、第三方接口阻塞查慢SQL日志看线程池与数据库活跃连接数编码中文乱码请求头Content-Type未带UTF-8或数据库字符集不一致检查请求头、JDBC连接URL的characterEncoding配置这张表也印证了接口bug的一个特点现象看起来五花八门但只要把前端请求构造、后端参数接收、中间代理层转发、后端响应序列化这四层拆开看每个问题都能落到某层去定位。6.3 调试工具与协作习惯工具层面推荐几个组合基本覆盖所有调试场景。前端最核心的是浏览器DevTools的Network面板必要时配合Console看报错接口调试用Apifox或Postman二者都能做环境变量和断言Apifox的Mock功能对前后端并行开发尤其好用后端还没写好接口时前端可以先Mock数据联调页面后端排查接口自动化问题时可以引入Java接口自动化测试框架比如RestAssured配合TestNG或JUnit把接口回归测试纳入CI流水线。还有一个容易忽略的好习惯联调阶段把前后端日志时间戳对齐。后端日志一般会记录每个接口的耗时前端Network面板也有时间线两者一对比能算出网络传输耗时和服务端处理耗时定位性能问题特别有效。比如接口前端等了3秒后端日志显示只执行了200毫秒那问题大概率出在中间网络或代理层如果后端日志就显示执行了2800毫秒那重点就该放在后端SQL或下游调用上别在前端瞎调。工具再多最终还是要落到沟通上。项目里前后端吵得最多的永远是这明明是你那边的问题我的破局方法是遇到问题先把证据贴出来谁提出接口没问题谁就要给出对应的日志或抓包结果用证据说话而不是用职位和嗓门说话。前期把接口联调规范定好至少能砍掉一半的无效争吵。7. 面试笔试背后的同一套逻辑很多人平时刷前端面试题或后端面试题时觉得考点是一块一块零散的知识点比如跨域八股文、幂等八股文。但真正把项目案例复盘一遍你会发现面试官真正想听的往往不是你知道这个知识点而是你在真实项目里遇到问题时的分析路径和决策依据。面试官问讲讲你遇到最深的一个bug你要能像这篇文章一样主动交代三件事问题是什么、怎么排查的、为什么用这个方案而不用另一个。比按钮加了loading就不会重复提交更有说服力的表述是前端做了loading锁之后我发现仍然无法完全避免用户开两个页面重放请求所以后端又加了基于request_id的幂等控制通过数据库唯一索引兜底这才真正解决掉这个bug。同样的逻辑适用于后端接口自动化测试场景。用Java接口自动化测试框架对你的接口做回归不只是为了测出bug更是为了把上面这些坑固化成用例。比如幂等性用例发送两次相同request_id断言只生成一条订单比如精度用例返回的ID字段是字符串断言类型正确比如时间格式用例传空格的yyyy-MM-dd HH:mm:ss也能正常返回。把这些用例跑在CI上后面再有人不小心改了配置测试立刻红起来bug就不会流传到用户手里。热词里还有个AJAX请求和Form表单请求的区别相关问题它也贯穿了上面的多个案例。Form表单提交是浏览器原生的请求方式页面会整体跳转AJAX是异步请求不刷新页面。表单提交里很难拿到精细的loading状态控制后端接收的是form-urlencoded格式而AJAX大多数用JSON格式。接口联调时如果你搞混了Content-Type后端接收就全是null这也是一个高频bug。理解不同请求格式的本质区别很多联调问题都能提前规避。最后再分享点实在的复盘这几个经典接口bug我的一个很深的体会是技术层面的解法其实都清楚真正难的是缺乏一套出问题先按链路排查、不要凭感觉猜的方法论。我现在处理接口问题永远默认执行一套动作打开Network看请求复制curl命令在命令行里重放看后端日志搜traceId排查数据库确认数据状态。一步接一步绝大多数bug十分钟内都有定论。还有一个亲身吃过亏的教训千万不要在代码里打着临时调试的旗号写死环境地址比如前端写死http://localhost:8080或者后端写死某个外网IP。这类硬编码会在环境切换时制造出大量看起来像代码bug但其实只是配置没走公共配置的假象浪费的时间足以让你怀疑人生。环境相关的东西全部走配置前端走环境变量和代理后端走application.yml和Nacos配置中心这个习惯比任何技术方案都省心。最后再分享一个小技巧接口联调期间前后端各自维护一份接口变更日志任何改动不管多小都记录下来字段改名、类型变更、接口下线、响应结构调整全部记录。这个日志平时看起来多余但在生产事故复盘和新人交接时价值极大。很多项目的接口bug最后查来查去不是技术问题而是改动的信息没有同步一个人改了字段另一个不知道这种信息不对齐才是接口bug背后最隐蔽的敌人。重视它你会少踩很多坑。
返回列表