ARTICLE DETAIL

资讯详情

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

AI生成代码后,如何打通Spring Boot与Vue3的前后端联调链路

AI生成代码后,如何打通Spring Boot与Vue3的前后端联调链路 先说我做完这个项目最大的感受AI 确实把编码门槛拉低了但并没有把联调门槛拉低。你可以让 AI 几分钟内生成一整套 Spring Boot 后端和 Vue 页面但真正让数据从数据库流到表格里、再从表单流回数据库还得靠人把接口契约、异常处理、跨域策略、字段序列化这些散落的细节串起来。这篇文章我不会只给你贴代码而是还原一条完整路线从把需求翻译成接口草案开始到 Maven 搭出 Spring Boot 工程再按 RESTful 规范把接口写扎实最后用 Vue3 页面把链路打通并解决联调中的典型问题。适合正在做课程设计、团队交接或者第一个全栈项目的开发者尤其是想让 AI 帮忙但又被“代码能生成、系统跑不通”折磨过的人。1. 让 AI 写前后端之前先逼自己把接口草案写清楚1.1 AI 编程时代的核心矛盾产出速度变快出错密度也变高我在最初尝试用 AI 辅助做前后端项目时习惯是直接说“帮我写一个任务管理系统”然后看 AI 输出一长串 Controller、Service、Mapper 和页面代码。代码确实漂亮但一跑起来就出问题前端以为返回的是{ data: [...] }后端返回的却是{ code: 0, data: { records: [...] } }前端提交的字段叫createTime后端实体里写的是createdAt。这种问题本质不是 AI 笨而是因为需求在“人脑”里不够具体时AI 只能在语言模型概率空间里猜一个最像样的实现。所以我的建议是在你输入第一句 AI 提示词之前先逼自己把接口契约写成文字。这件事的时间成本通常只要 20 分钟却能把后续联调时间压缩到原来的三分之一以下。1.2 把一句模糊需求拆成资源、字段和操作三张清单假设我们要做一个任务待办管理功能。大多数人的第一反应是用户能新增任务、能看列表、能改状态。这个粒度对 AI 来说太粗了。我会先按“资源视图”拆开。第一步是定义资源。这个系统里最核心的资源就是任务 Task它拥有唯一标识、标题、描述、状态、创建时间和更新时间。第二步是定义状态集合。任务状态不要用字符串随便存建议先约定枚举TODO待处理、IN_PROGRESS进行中、DONE已完成。这一步如果漏了前端下拉框、后端枚举、数据库字段就各说各话。第三步是写下所有操作并给每个操作配上 HTTP 方法和路径。这块直接采用 RESTful 风格会非常清晰操作说明HTTP 方法路径请求体要点响应数据新增任务POST/api/v1/taskstitle、description创建后的完整任务对象分页查询GET/api/v1/tasks?page0size10statusTODO无分页结果含记录列表和总数查询详情GET/api/v1/tasks/{id}无单个任务对象全量更新PUT/api/v1/tasks/{id}title、description、status更新后的任务对象删除任务DELETE/api/v1/tasks/{id}无无内容204 状态码这张表就是“人机契约”的核心。我通常把它直接贴给 AI再让 AI 生成后端代码和接口文档。从实践经验看AI 在明确的 URL、方法和字段约束下输出质量和可控度会有非常明显的提升。这里的道理很简单你给 AI 的上下文不是“空泛需求”而是接近最终代码结构的规格说明书。2. 后端地基用 Maven 构建 Spring Boot 工程并统一响应结构2.1 工程初始化的两条路我推荐你走哪条Spring Boot 工程初始化通常有两条路。一条是去 Spring Initializr 网站勾选依赖再下载压缩包另一条是直接用 IDEA 的 Spring Initializr 创建。两种方式本质一样都是生成一个 Maven 工程目录。我个人的建议是无论哪种方式最终都要能看懂 pom.xml因为 AI 生成的代码也依赖这份依赖清单。以 Spring Boot 3.x 为例最终 pom.xml 中至少需要这几组依赖spring-boot-starter-web提供 MVC 和 Tomcat 内嵌容器、spring-boot-starter-data-jpa做数据库持久化、spring-boot-starter-validation做参数校验、h2本地无外部依赖调试用、mysql-connector-j生产切 MySQL 用。如果你希望接口文档自动生成可以再加 springdoc-openapi-starter-webmvc-ui。实测下来 springdoc 的版本要和 Spring Boot 版本匹配否则启动阶段会扫包报错。2.2 统一响应体为什么每写一个接口都要返回同样结构的 JSON项目里许多人写接口时非常随意成功时直接返回对象失败时返回一个 Map 或自定义错误页。短时间看没什么问题但一旦接口数量超过 10 个前端就要在每个请求里做各种类型判断。正确的做法是从第一个接口开始就定义统一的 API 响应结构。我会让 AI 先生成这样一个泛型类public class ApiResponseT { private int code; private String message; private T data; public static T ApiResponseT success(T data) { ApiResponseT response new ApiResponse(); response.setCode(0); response.setMessage(success); response.setData(data); return response; } public static T ApiResponseT error(int code, String message) { ApiResponseT response new ApiResponse(); response.setCode(code); response.setMessage(message); return response; } // getter / setter 省略 }注意这里的 code 并不是 HTTP 状态码而是业务状态码。我习惯把业务成功固定为 0HTTP 状态码继续保持它本身的语义200 代表请求成功400 代表参数有误401 代表未认证500 代表服务端异常。前端拿到响应后优先判断code 0再取data这个模式在团队协作中非常好用。2.3 数据库配置本地 H2 跑通逻辑生产 MySQL 一键切换为了让项目在没有安装 MySQL 的机器上也能立刻跑起来我建议本地开发阶段使用 H2 内存数据库同时保留 MySQL 的 profile 配置。这样 AI 生成的代码、脚本和页面不会因为数据库连不上而中断。# application.yml spring: datasource: url: jdbc:h2:mem:taskdb driver-class-name: org.h2.Driver username: sa password: jpa: hibernate: ddl-auto: update show-sql: true如果切到 MySQL只需要增加一个 application-mysql.ymlspring: datasource: url: jdbc:mysql://localhost:3306/task_manager?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai driver-class-name: com.mysql.cj.jdbc.Driver username: root password: yourpassword jpa: hibernate: ddl-auto: update启动命令变成mvn spring-boot:run -Dspring-boot.run.profilesmysql即可。这里有个容易踩的坑Spring Boot 2.7 之后 MySQL 驱动坐标名称变化很大如果引入的是mysql:mysql-connector-java旧坐标在 Spring Boot 3.x 里很可能出现驱动类找不到的情况。统一使用新坐标com.mysql:mysql-connector-j就能避开。3. 让 AI 按 RESTful 规范生成资源接口再人工卡住细节3.1 从 Controller 到 DTOAI 生成的代码哪些能直接用拿到接口草案后我通常会让 AI 按“Controller → Service → Repository → Entity”的顺序逐层生成。不要一次让它生成全部代码更好的办法是让它先按实体和字段约束生成 EntityEntity Table(name task) public class Task { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; private String title; Column(length 1000) private String description; Enumerated(EnumType.STRING) private TaskStatus status; private LocalDateTime createdAt; private LocalDateTime updatedAt; PrePersist void onCreate() { this.createdAt LocalDateTime.now(); this.updatedAt this.createdAt; } PreUpdate void onUpdate() { this.updatedAt LocalDateTime.now(); } }Entity 直接暴露给前端是很多教程爱做的偷懒事但实际联调你会发现它相当危险。Entity 里一旦包含审计字段、关联对象或内部逻辑JSON 响应就会多出很多前端用不到也不该看到的字段。更合理的做法是引入 DTO让 Controller 层只返回视图对象。虽然多写一层看起来麻烦但它换来的是接口结构的稳定性。Entity 改了不影响前端前端加了展示字段也不影响数据库表。3.2 参数校验是 AI 最常忽略的地方必须显式要求我发现 AI 默认生成的接口能正常处理“理想输入”但几乎不做参数校验。比如 POST 接口要求 title 必填AI 生成的代码通常只在 Service 里判断空字符串但缺少 Spring Validation 的注解。这对联调影响很大前端哪天漏传了字段后端返回的就不是规范 JSON而是默认的 400 错误页前端拿到后完全没法解析。解决办法是在 DTO 上直接加校验注解并让 Controller 使用Validpublic class TaskCreateRequest { NotBlank(message 任务标题不能为空) Size(max 50, message 任务标题长度不能超过50) private String title; Size(max 1000, message 任务描述长度不能超过1000) private String description; }Controller 中这样接收PostMapping public ApiResponseTaskVO create(Valid RequestBody TaskCreateRequest request) { return ApiResponse.success(taskService.create(request)); }随后配置一个全局异常处理器把MethodArgumentNotValidException统一转成符合 ApiResponse 结构的错误体。这样前端无论传错什么参数都能从 response 里解析出明确的中文提示这是联调体验提升最明显的一步。3.3 接口写完后先用工具自测不急着写前端接口层代码完成后第一件事不是去写前端页面而是打开火狐或 Chrome 的 REST 客户端或者用 Postman 把每个接口轮一遍。这里我最常做的是先创建一条任务然后立刻分页查询核对返回字段的命名是否和接口草案完全一致。如果此时发现字段拼写不一致成本极低等前端页面写好了再发现就需要两边同时改。一个很实用的细节是把接口返回的 JSON 直接复制到前端项目的类型定义文件里作为 TypeScript interface 或 JSDoc 注释。这一步能强制后端字段命名和前端引用完全对齐让 AI 后续生成页面代码时也不容易引用错字段。4. 把接口与数据库真正打通避开“假接口”和“隐蔽坑”4.1 Long 型 ID 的序列化陷阱前端数字精度丢失这是一个非常典型、但 AI 很少会主动告诉你的问题。Java 后端的Long类型最大可以到 19 位数字而 JavaScript 的 Number 类型安全整数范围只有2^53 - 1也就是大约 9007199254740991。一旦主键 ID 超过这个范围前端拿到的 ID 就会不精确。如果后续你要用这个 ID 做详情查看、编辑或删除请求就会打到错误的资源上。解决办法有两个层级。第一层是全局配置 Jackson让所有 Long 类型在序列化为 JSON 时转为字符串Configuration public class JacksonConfig { Bean public Jackson2ObjectMapperBuilderCustomizer longToStringCustomizer() { return builder - { builder.serializerByType(Long.class, ToStringSerializer.instance); builder.serializerByType(Long.TYPE, ToStringSerializer.instance); }; } }第二层是在前端用 string 类型接收 ID。这个坑在真实项目中非常高频我强烈建议在你的项目里优先处理。4.2 Repository 分页查询默认页码别搞错Spring Data JPA 的Pageable默认页码从 0 开始而很多前端分页组件页码从 1 开始。AI 生成的 Controller 可能直接接收page参数再丢给 Repository导致前端传第 1 页后端返回的是第 2 页内容。更隐蔽的是当查询条件里带状态筛选时写法稍有不对就会出现 N1 查询或者内存分页。正确的做法是直接在 Repository 层声明带条件的分页查询public interface TaskRepository extends JpaRepositoryTask, Long { PageTask findByStatus(TaskStatus status, Pageable pageable); }Service 层把前端的页码做一次转换public PageTaskVO list(int page, int size, TaskStatus status) { Pageable pageable PageRequest.of(page - 1, size, Sort.by(createdAt).descending()); PageTask taskPage (status null) ? taskRepository.findAll(pageable) : taskRepository.findByStatus(status, pageable); return taskPage.map(taskMapper::toVO); }我建议在前后端约定里写死前端页码从 1 开始后端接口接收的 page 也是从 1 开始后端内部再减 1。这样比“前端传 0 表示第一页”要直观得多。4.3 事务所、软删除与时间字段AI 不会替你考虑的设计决策AI 生成的基本 CRUD 往往不会处理逻辑删除也不会考虑“列表接口默认只查未删除数据”。一旦你的业务需要保留操作记录物理删除就很危险。所以我的习惯是在新增表的时候直接加一个deleted字段所有查询默认带deleted false条件。在 Repository 中可以用Query或Where注解来收敛不要让每个 Service 方法都手写这个条件。另外创建时间和更新时间建议完全交给数据库或 JPA 的PrePersist、PreUpdate处理而不是由前端传进来。如果前端提交的 DTO 里包含createTime这样的字段最好的做法是直接从 DTO 中移除避免被恶意覆盖。这样接口契约也会更干净。5. 前端接管用 Vue3 页面把请求真正发到后端5.1 初始化 Vite 工程与代理配置前端侧我建议使用 Vite 创建项目命令是npm create vitelatest task-frontend -- --template vue。然后安装 axios。开发模式下前端页面跑在 5173 端口后端接口跑在 8080 端口直接跨域请求会被浏览器拦截。我不推荐在 Spring Boot 里配CrossOrigin解决开发问题因为生产环境大概率会用 Nginx 反代跨域策略根本不需要后端代码参与。更干净的方式是在 Vite 里配置代理// vite.config.js export default defineConfig({ server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } });这样前端请求/api/v1/tasks时Vite 开发服务器会把请求转发到http://localhost:8080/api/v1/tasks浏览器层面不产生跨域后端也无需额外配置。5.2 axios 拦截器把统一响应体转换成前端友好数据为了避免每个页面都写一遍错误处理我会在 axios 实例中统一封装响应拦截器。当后端返回的code ! 0时直接弹出错误提示当 HTTP 状态码是 401 或 500 时统一走全局错误提示逻辑。这样页面组件里只需关心成功时的数据。import axios from axios; const request axios.create({ baseURL: /api, timeout: 10000 }); request.interceptors.response.use( (response) { const res response.data; if (res.code ! 0) { alert(res.message || 请求失败); return Promise.reject(new Error(res.message)); } return res.data; }, (error) { if (error.response) { if (error.response.status 400) { alert(参数有误请检查输入); } else if (error.response.status 500) { alert(服务器内部错误); } } return Promise.reject(error); } );注意这里的 return 值是res.data也就是说调用request.get(...)时组件拿到的直接就是业务数据不再需要反复做response.data.data这种双层拆包。5.3 一个列表页与一个新增表单打通完整链路在核心链路里一个列表页需要三件事初始化时加载数据表格渲染展示操作列响应删除或状态切换。用 Vue Composition API 写出来的最小实现大概长这样script setup import { ref, onMounted } from vue; import request from ./api; const taskList ref([]); const total ref(0); const page ref(1); const pageSize ref(10); async function loadTasks() { const data await request.get(/v1/tasks, { params: { page: page.value, size: pageSize.value } }); taskList.value data.records; total.value data.total; } onMounted(loadTasks); /script新增任务的提交逻辑则通过表单收集 title 和 description再调用request.post(/v1/tasks, form)。后端接收后返回创建好的任务对象前端重新拉一次列表即可。到这里“页面 → 代理 → Controller → Service → Repository → 数据库”这条完整链路就已经通了。我在实测时最喜欢做的验证是提交完新任务后不刷新页面直接在 DevTools 的 Network 面板里看新增请求的响应体确认数据库返回的主键 ID 是字符串createdAt 是 ISO 时间格式。前端页面显示的时间如果需要本地化再在展示时做一次dayjs格式化。6. 联调实战把「前端说没数据、后端说有数据」这类矛盾一次排清6.1 问题边界三层定位法即使接口文档、类型定义都做好了联调阶段依然可能出问题。最常见的场景是前端说“请求成功了但表格是空的”后端说“我直接调接口有数据”。这类矛盾的本质是两边看到的数据环境或代码分支不一致。我把联调问题分成三层第一层是接口层检查请求是否真的到达后端响应状态码是什么第二层是数据层检查后端返回的数据结构是否和前端解析逻辑一致第三层是渲染层检查前端拿到数据后是否用对了字段。排查时按这个顺序走很少绕远路。现象优先怀疑方向快速验证手段Network 里请求显示 404路由或代理路径不对直接复制完整 URL在浏览器地址栏访问Network 里请求显示 500后端代码异常看后端控制台堆栈通常能直接定位请求 200 但表格无数据数据解析路径不对在响应拦截器里 console.log 原始数据数据量不对后端分页页码语义不一致分别请求 page0 和 page1比对两份结果字段显示 undefined前后端命名不一致展开 JSON 树逐个字段对照类型定义6.2 让接口文档自动化而不是人肉维护联调中最怕所谓“口头契约”也就是前端问后端“列表返回啥”后端口头说“records”前端照着写了但第二天后端把字段改成list页面就静默白屏。你很难在编译阶段发现因为 JS 里读取不存在的字段只是返回 undefined。我强烈建议在 Spring Boot 项目里集成 springdoc-openapi接口一启动就能在/swagger-ui.html看到所有接口的定义和示例。AI 生成的接口只要带上 OpenAPI 注解文档就会自动更新。前端可以直接从页面复制 JSON 示例作为 mock 数据来源双方对字段定义的偏差会在联调开始前就暴露。6.3 最容易翻车的三类联调问题与修复清单第一类是请求体格式不对。前端axios.post默认会发送 JSON但有时候组件库表单封装会误把数据拼成 form-urlencoded 格式后端用RequestBody接收时就会报HttpMessageNotReadableException响应直接变成 400。遇到这类问题先在 Network 里看请求头Content-Type是不是application/json再看请求体是不是合法 JSON 字符串。第二类是日期时间格式不一致。Spring Boot 默认序列化LocalDateTime会带T分隔符比如2025-01-01T12:00:00前端如果直接展示就会很难看。我建议在 application.yml 中把全局格式配置清楚spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8这里有个坑需要注意date-format对LocalDateTime的作用并不总是有效因为 Java 8 时间类型走的是JavaTimeModule。稳妥的做法是加一个JsonFormat(pattern yyyy-MM-dd HH:mm:ss)或者让前端直接用 dayjs 做格式化。实战中我通常选择前端格式化后端保持 ISO 标准格式避免所有接口都被人为改造。第三类是分页返回结构不统一。JPA 的Page对象序列化后包含content、totalElements、totalPages、number等字段但如果你在 Service 里手动封装成 VO 或 Map前端看到的字段就是另一套。这里建议接口草案里直接规定分页结构records放列表数据total放总数current放当前页码。后端用一个统一的PageResultT类来组装所有分页接口保持一致前端写一套分页组件就能通吃所有列表。7. AI 没有替你做的部分安全、监控与部署节奏7.1 AI 能生成 CRUD但生成不了权限模型如果项目需要登录鉴权AI 生成的代码往往会在每个接口上贴个PreAuthorize(hasRole(ADMIN))但它不会帮你设计用户表、角色表、路由表和 token 刷新机制。我的经验是权限模型必须在动手写代码前用文档定义清楚然后再让 AI 基于这个模型生成代码。否则后补权限会非常痛苦可能要改动所有前端路由和后端切面。7.2 给接口加监控与健康检查用 Micrometer 和 Actuator 说话联调阶段如果前端报告请求很慢你很难判断是网络问题、接口性能问题还是数据库查询慢。我建议在 Spring Boot 项目中开启 Actuator并接入 Micrometer 指标。Spring Boot 3.x 项目加依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-actuator/artifactId /dependency dependency groupIdio.micrometer/groupId artifactIdmicrometer-registry-prometheus/artifactId /dependency然后访问/actuator/health可以看到服务健康状态访问/actuator/metrics可以看到 JVM 内存、线程和 HTTP 请求耗时等指标。很多人在联调时忽略这层价值其实它非常有用。前端说“保存任务很慢”你可以直接通过指标看接口 P99 耗时而不是靠双方反复试。7.3 前后端分离部署本地调通后如何放到服务器项目开发本地跑通后部署方式直接影响后续联调环境是否稳定。我比较推荐的方案是把后端打成 Docker 镜像前端npm run build后把dist目录交给 Nginx 托管然后再反向代理/api到后端容器。后端 Dockerfile 可以非常简洁FROM eclipse-temurin:17-jre WORKDIR /app COPY target/task-backend-0.0.1-SNAPSHOT.jar app.jar EXPOSE 8080 ENTRYPOINT [java, -jar, app.jar, --spring.profiles.activemysql]前端的 Nginx 配置只需要保证两件事location /指向静态文件location /api/把请求转发到后端服务并处理好proxy_set_header。这种结构下前端页面和后端服务在浏览器看来是同源的CORS 问题彻底消失部署后的联调环境和本地开发环境保持高度一致。这也是我在试过多种部署方式后最终稳定沿用的一条路。最后再分享一点个人体验这套流程我前后完整跑过五次以后最大的体会是AI 真正提升效率的部分是“按契约翻译成代码”而真正决定项目质量的部分仍然是“人如何定义契约”。你在接口草案、统一响应、字段校验上多花的每一分钟都会在联调时加倍还给你。也许下次做新功能时你可以试试把一个模糊需求先拆成资源、字段和操作三张清单再让 AI 进入工作流。这样产出的代码不仅能用而且像一个团队长期维护过的代码而不是一个一次性原型。
返回列表