
说句实在话我第一次用Cursor搭全栈项目的时候心里是既兴奋又怀疑的。兴奋的是以前初始化一个JavaVue前后端分离项目光是配Maven依赖、写跨域配置、整理工程目录这些重复劳动就能耗掉大半天怀疑的是AI生成出来的代码到底能不能直接跑通还是说只能当个花架子看看。带着这种纠结我用一个周末完整走了一遍流程——从两个空目录开始用Cursor辅助把后端Spring Boot接口、前端Vue页面、MySQL数据库、前后端联调全部跑通最后还顺手部署到了一台测试服务器上。这篇文章就是那份完整的过程记录。我会把每个环节的操作路径、让Cursor干活时的提问方式、以及最后我对AI辅助开发这件事的边界反思都写清楚。如果你对Java和Vue有一点基础但还没完整做过前后端分离项目或者正在犹豫要不要把AI工具纳入日常开发流程这篇内容应该能给你一份比较实在的参考。1. 技术栈定夺Java和Vue的组合是怎么选出来的1.1 需求反推这个项目到底要做什么动手之前我先把业务场景收窄了做一个内部待办管理平台包含用户登录、待办任务的增删改查、任务完成状态切换和置顶操作。功能不多但足够覆盖前后端分离项目里最常见的几个环节——认证授权、业务接口、页面交互、数据持久化。选技术栈的时候我对照过几个组合最后圈定JavaVue的理由其实很简单。维度Java Spring BootNode.js ExpressPython Django生态成熟度高企业级案例多中高偏中小型应用中适合快速原型招人/交接成本低后端开发存量最大中中偏高类型体系强类型编译期兜底弱类型运行时暴露问题动态类型靠规范约束部署运维jar包单机即跑简单依赖Node运行时依赖Python环境和依赖管理我不是说其他两个组合不好而是站在做一个能长期演进、团队里随便一个后端都能接手的角度Spring Boot确实是最没有惊喜但最稳的选项。Vue这边同理Vue 3的Composition API上手比React的Hooks心智负担小一些配合Vite的冷启动速度开发体验在同类框架里属于第一梯队。前后端分离这件事本身也不用多说——前端只管渲染和交互后端只提供数据和业务规则两边可以独立开发、独立部署、独立扩容。小项目里这样做可能显得有点重但一旦涉及多人协作或者后续要拆移动端这套结构的收益就会非常明显。1.2 Cursor在开发流程中的真实定位技术栈定了接下来的问题是Cursor到底在这场开发里扮演什么角色我的结论可能和很多人想的让AI自己写我负责验收不太一样——它的定位更像是结对编程助手负责把重复的编码劳动压缩掉而不是替你拿主意。具体来说Cursor在这个项目里帮我做了三件事第一基于当前代码库的上下文生成有结构的多文件代码而不是只做单文件补全第二针对报错信息进行多轮对话排查省去了大量搜索时间第三快速生成样板代码比如实体类、DTO、CRUD接口、前端表单组件这些写了不涨经验但没写又不能跑的东西。但它有一个很明显的边界它不了解你的业务规则也没法替你评估一个需求到底应不应该这么做。所以我在整个过程中坚持一个策略——人做主AI做执行。架构决策、字段约束、安全策略这些我自己定重复性编码交给它。分工一旦明确后面每一步都能跑得比较顺。2. 前后端骨架搭建从空目录到能跑通的Demo2.1 后端Spring Boot工程初始化我习惯先从后端开始因为前端页面要联调必须依赖接口先把后端骨架立起来后面前端开发才有锚点。项目目录建好之后我直接让Cursor生成了初始的Maven工程文件核心依赖配置如下dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.5/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies这里有个坑我必须单独提一句。我用的是Spring Boot 3.x对应Java 17MyBatis-Plus 3.5.x版本之后才完整支持Spring Boot 3的starter命名如果你还在用老的mybatis-plus-boot-starter版本启动时会直接因为类路径冲突报错。早年遇到这种问题可能要查半天现在直接把报错信息贴给Cursor它很快就能定位到是版本不匹配顺手把依赖版本改掉就好了。接下来是数据源配置。我这边开发环境用的是本地MySQL配置文件里把端口固定在8081避免和前端Vite的默认端口冲突server: port: 8081 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/todo_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456有个细节值得注意连接串里characterEncodingutf8和serverTimezoneAsia/Shanghai这两个参数一定要写前者保证中文数据不会乱码后者避免时间字段和本地时区对不上。很多新手在这里踩了坑后面接口返回的时间总是差8小时其实根子就在配置上。2.2 前端ViteVue3环境搭建后端工程能启动之后我再初始化前端。用npm create vitelatest创建一个Vue项目这里我选择的是JavaScript版本而不是TypeScript不是TS不好而是我希望整个Demo的注意力集中在前后端交互逻辑上不想让类型定义干扰主线的观看体验。初始化完成后装上Vue Router、Pinia、Axios和Element Plusnpm create vitelatest todo-web -- --template vue cd todo-web npm install npm install vue-router pinia axios element-plus装完依赖我先让Cursor生成了一套前端目录结构约定好以后所有页面组件放src/views公共组件放src/componentsAPI请求统一放src/api状态管理放src/store。这一步看起来不起眼但对工程后续的维护帮助非常大——项目一旦长起来最怕的就是组件和接口调用散落得到处都是。2.3 统一返回体与工程目录规范前后端分离项目里有一个容易被忽略的细节就是接口返回结构不统一。有些接口直接返回对象有些接口只返回一个布尔值前端处理起来就会非常痛苦——每一次响应都要判断一次数据结构错误处理更是各写各的。我在项目一开始就让Cursor生成了一个全局返回体Result后面所有接口都返回这个结构Data public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(success); result.setData(data); return result; } public static T ResultT error(String message) { ResultT result new Result(); result.setCode(500); result.setMessage(message); return result; } }配合全局异常处理器把业务异常也统一成这个格式返回。这样前端只需要在Axios拦截器里处理一次code后续所有页面的错误提示逻辑都能复用这是一笔非常划算的事前投资。3. 后端业务落地的全过程建表、分层、接口自测3.1 让Cursor基于需求生成数据表后端骨架跑通后我开始设计业务表。这一步我没有自己手写SQL而是把需求描述给Cursor设计一个待办管理系统的数据库包含用户表和任务表。任务需要状态字段待办/已完成、优先级高/中/低、截止时间、置顶标记。用户和任务是一对多关系。请生成建表SQL包含必要的索引。它生成的结果基本可用但这个环节我特别想提醒一句AI生成的SQL只是起点不是终点。它可能会漏掉一些你认为理所当然的约束比如删除时的级联策略、状态字段的默认值、创建时间是否自动填充。我最后调整成了这样CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(255) NOT NULL, nickname VARCHAR(50), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_username (username) ); CREATE TABLE task ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, title VARCHAR(200) NOT NULL, description TEXT, status TINYINT DEFAULT 0 COMMENT 0-待办 1-已完成, priority TINYINT DEFAULT 1 COMMENT 1-低 2-中 3-高, is_top TINYINT DEFAULT 0 COMMENT 0-普通 1-置顶, due_time DATETIME, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_user_status (user_id, status) );提示让AI生成SQL时一定要追加一句请补充索引和默认值设计。否则生成出来的表往往只有字段定义运行起来性能和数据一致性都会打折扣。3.2 Service层业务逻辑与事务处理表建好之后我用MyBatis-Plus在Cursor里生成了对应的实体类和Mapper接口。实体类这块AI基本不会出错因为字段和数据库列能一一对应属于典型的机械劳动。但到了Service层我就要多留一个心眼了。以创建任务为例需求是校验用户存在、设置默认状态、如果任务数量超过阈值需要提示用户。Cursor生成的代码逻辑基本正确但我检查时额外关注了事务注解是否加对位置。AI经常会在需要事务边界的方法上漏掉Transactional或者把它加到私有方法上——而Spring的事务代理对私有方法是不生效的这是新手容易踩到而且特别隐蔽的问题。Service Transactional(rollbackFor Exception.class) public class TaskServiceImpl implements TaskService { Autowired private TaskMapper taskMapper; Autowired private UserMapper userMapper; Override public Task createTask(TaskCreateDTO dto) { User user userMapper.selectById(dto.getUserId()); if (user null) { throw new BusinessException(用户不存在); } Task task new Task(); task.setUserId(dto.getUserId()); task.setTitle(dto.getTitle()); task.setDescription(dto.getDescription()); task.setPriority(dto.getPriority()); task.setStatus(0); task.setIsTop(0); taskMapper.insert(task); return task; } }这里我让Cursor帮我写了一个自定义异常类和全局异常处理器这样业务校验失败时不是返回一个裸的500而是返回统一的Result错误结构前端可以直接读取message字段弹提示。3.3 Controller接口设计与Postman验证Controller层的接口设计我用下面这张表定下来避免前后端在接口路径上产生理解偏差方法路径说明POST/api/auth/login登录返回token和用户信息GET/api/tasks分页查询当前用户的任务列表POST/api/tasks创建任务PUT/api/tasks/{id}更新任务内容或状态DELETE/api/tasks/{id}删除任务POST/api/tasks/{id}/top置顶/取消置顶接口路径确定后我直接按这个规格让Cursor生成Controller代码。它在生成时基本能保持RESTful风格但我会特别检查两点一是分页参数命名是否和前端约定一致我用current和pageSize二是接口返回是否都包了一层Result。这两点不一致联调阶段就会被反复来回吊打。接口写完我用Postman做了一遍冒烟测试重点验证登录接口能正常返回token、任务创建后能在数据库里查到记录、分页查询能正确返回总数和列表。这三个核心链路通了后端骨架就算站住了。4. 前端页面与接口联调从假数据到真实数据的切换4.1 页面路由与组件拆分后端接口稳定之后前端开发才真正有了锚点。我规划了三个主要页面登录页、任务列表页、任务编辑弹窗。路由配置如下import { createRouter, createWebHistory } from vue-router; const routes [ { path: /login, component: () import(../views/Login.vue) }, { path: /tasks, component: () import(../views/TaskList.vue) }, { path: /, redirect: /tasks } ]; const router createRouter({ history: createWebHistory(), routes }); router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (to.path ! /login !token) { next(/login); } else { next(); } });任务列表页我拆成了三个子组件任务表格、状态筛选器、编辑弹窗。组件拆分的标准很简单——如果一段交互逻辑能在另一个页面里复用就把它拎出来。比如状态筛选器任务列表页用得到后续如果加一个已归档任务页面也能用。这个判断方式比死记组件粒度要适中这种空话实用得多。4.2 Axios封装与Vite代理配置前端所有请求我都收口到一个封装的Axios实例里。这样登录拦截、错误提示、token注入都只有一处不用在每个页面里重复写以后维护起来也只需要改一个文件import axios from axios; import { ElMessage } from element-plus; const service axios.create({ baseURL: /api, timeout: 10000 }); service.interceptors.request.use(config { const token localStorage.getItem(token); if (token) { config.headers.Authorization Bearer ${token}; } return config; }); service.interceptors.response.use( response { const res response.data; if (res.code ! 200) { ElMessage.error(res.message || 请求失败); return Promise.reject(new Error(res.message)); } return res.data; }, error { if (error.response error.response.status 401) { localStorage.removeItem(token); window.location.href /login; } ElMessage.error(error.message || 网络异常); return Promise.reject(error); } );开发环境的跨域问题我用Vite代理解决配置在vite.config.js里export default defineConfig({ server: { port: 5173, proxy: { /api: { target: http://localhost:8081, changeOrigin: true } } } });这个配置的意思是前端发送的/api开头请求在开发阶段由Vite转发到后端的8081端口从而绕开浏览器的跨域限制。前端代码里写的接口地址不用带服务器域名保持了相对路径的干净。4.3 联调阶段真实踩到的三个坑联调是整个项目里最容易让人心态崩掉的阶段也是最值得写出来的部分。我这次实际遇到三个问题每一个都能单独写一篇避坑贴。第一个是Long类型主键精度丢失。MyBatis-Plus默认使用雪花算法生成任务ID这个ID是一个19位的Long类型。但JavaScript的Number类型最大安全整数只有2^53-1比这个大的数字会被四舍五入导致前端拿到的任务ID和数据库里实际的值不一样。我一开始发现点击编辑按钮时总是弹任务不存在排查半天才发现前端传过去的ID已经变了。解决办法是在后端把Long类型的ID序列化成字符串JsonSerialize(using ToStringSerializer.class) private Long id;第二个是时间格式不统一。后端返回的时间默认是个时间戳数字前端显示成1700000000000这种形态显然不能直接给用户看。我在返回体里统一指定了格式前端再用工具格式化一次两层保险JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8) private LocalDateTime createTime;第三个是跨域配置的双保险。开发环境有Vite代理顶住但部署之后前端静态资源和后端接口不在同一个服务浏览器还是会拦。我在后端配了一个全局CORS配置同时Nginx那边也会做反向代理两个方案互相兜底后面会详细说。5. 打包部署让项目在服务器上真正跑起来5.1 后端Maven打包与发布联调通过之后项目进入部署阶段。后端的打包很简单本质上就是一条Maven命令mvn clean package -DskipTests执行完之后target目录下会生成一个可执行的jar包。但我这里要特别提一个和生产环境相关的问题代码里默认配置的是本地数据库地址直接拿这个jar包部署到服务器肯定连不上数据库。我的做法是拆分配置文件开发默认用application.yml生产环境通过Spring Boot的Profile机制加载application-prod.yml启动时手动指定激活哪个Profile# application-prod.yml spring: datasource: url: jdbc:mysql://your-db-host:3306/todo_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: prod_user password: yourpassword server: port: 8081启动命令带上Profile参数java -jar todo-server.jar --spring.profiles.activeprod5.2 前端构建产物与Nginx反向代理前端打包同样简单npm run build构建完成后会生成一个dist目录里面是纯静态文件。我把dist目录上传到服务器的/opt/todo-web/下然后配置Nginx把前端页面和后端接口串起来server { listen 80; server_name your-domain-or-ip; location / { root /opt/todo-web/dist; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8081; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }这里有个细节必须说明try_files $uri $uri/ /index.html;这一行不是可有可无的。Vue Router如果使用history模式刷新一个非首页路径时Nginx会先尝试找对应的真实文件找不到就回退到index.html交给前端路由接管渲染。如果漏掉这行刷新任务列表页会直接404而且是那种怎么排查都想不到的问题。部署完之后我在浏览器里完整跑了一遍主流程登录、创建任务、编辑任务、完成任务、置顶、刷新页面、退出登录。前后端数据一致时间格式正常错误提示也能正确弹出。到这一步这个从零开始的项目才算真正画上了句号。6. 用Cursor提效以后我反而更看重的那些边界6.1 哪些环节千万别让AI替你拿主意项目跑通了但作为实际使用者我必须说点反直觉的经验。Cursor确实把大量重复工作压缩了但它是一个顺着你的话往下说的工具——如果你的需求本身就是错的它会帮你生成一份逻辑严谨、风格优雅的错误代码而且看起来比你自己写的还合理。至少在这几个环节我不会让它替我做决定。一是数据库表结构里的索引策略和约束规则这涉及数据一致性和查询性能需要基于真实的访问模式来定AI不了解你的业务流量。二是安全相关逻辑比如密码加密方案、token过期策略、越权校验这些必须自己掌握原理否则出了问题连排查方向都没有。三是架构层面的取舍比如要不要引入缓存、要不要拆服务这类决策应该由成本和收益推导而不是让AI从模板里挑一个最常见的答案。我这么说不是否定AI的价值而是把它的定位摆正它可以是非常高效的执行者但在关键决策上它需要的是一个能扫清经验盲区的人类主管。6.2 Cursor提升效率的实用操作习惯最后分享几个我用Cursor时沉淀下来的操作习惯全是实际开发中摸着石头过河总结的。第一在项目根目录放一份规则说明文件把自己团队的代码规范、接口返回格式、命名约定写进去。这样Cursor在生成代码时会自动遵循这些约束风格会稳定很多不用每次对话都重新交代一遍。第二对话时尽量带上具体文件引用别泛泛地说改一下登录接口。给它指到具体的Controller文件、具体的异常类它完成任务的准确率会高一个档次。第三生成完代码之后别急着验收先让它解释一下这段代码的执行逻辑。我遇到过一个情况它生成的定时清理任务方法名和注释都完全正确但定时表达式写的是每天凌晨一点执行需求其实是要每天执行一次。解释逻辑这个动作能快速暴露它是不是真的理解了需求还是只是拼了一堆看起来差不多的代码。第四报错信息直接整个粘给它。以前遇到一个奇怪的Java泛型编译错误搜索引擎翻了半天没结果后来把完整报错栈贴给Cursor它一眼就指出了类型擦除导致的桥接方法问题。这个效率差距是决定性的。第五关键代码写完立刻自测不要攒到最后一次性联调。我用这个项目的经验说话每次只改一个接口、跑一次测试出问题的时候定位范围极小攒到最后再联调三个Bug互相纠缠排查成本是指数级上升的。这套流程整个走下来我最直观的感受是Cursor把大量重复的编码、配置、排错时间压缩掉了但压缩出来的时间必须花在更有价值的事情上——想清楚业务规则、做真正的人工代码审查、把测试用例补全。如果你正准备开始第一个前后端分离项目我建议把它当成一个很懂代码但不了解你业务的同事而不是许愿机。