从零搭一个全栈小项目,我踩了这些坑
上周五晚上十一点半,办公室只剩我和隔壁组的运维小哥还在对线上告警。咖啡已经凉了三杯,但我的脑子里还在回放那个该死的前端请求超时错误——明明后端接口返回正常,为什么前端就是拿不到数据?这事儿让我彻底意识到:作为一个写了三年Spring Boot的老油条,是时候重新梳理下前后端协作的细节了。
其实我一直觉得,算法工程师写业务代码有点“大材小用”,但现实很骨感。我们组人手紧张,领导一句“你懂点工程,顺手搞一下吧”,我就被迫重拾Java,搭了个内部工具平台。正好最近在看新机会,也想趁机折腾点新技术,于是就有了这篇实战记录。
起因:一个被产品经理“临时加塞”的需求
事情要从上个月说起。产品老大在周会上突然说:“咱们能不能做个轻量级的数据标注平台?就给算法团队自己用。”听起来很简单,对吧?结果需求文档里藏着一堆魔鬼细节:用户登录、任务分配、实时保存、进度同步……还要求“下周上线”。
我当场就想掀桌子——这不是要做个迷你版Label Studio吗?但转念一想,反正晚上也要调参,不如顺便练练手。于是决定用 Spring Boot + Vue3 搭个最小可用系统(MVP),既能快速交付,又方便后续扩展。
技术选型上,后端稳字当头:Spring Boot 2.7(公司统一版本)、MyBatis-Plus、JWT鉴权;前端则放飞自我:Vue3 + TypeScript + Pinia + Axios。毕竟工作中前端框架锁死在React 16,私下不玩点新的对不起自己。
第一个坑:跨域不是配个注解就完事了
你以为 @CrossOrigin 加上就能万事大吉?Too young.
本地开发时,前端跑在 http://localhost:5173,后端是 http://localhost:8080。第一次联调,控制台直接炸出一行红:
Access to XMLHttpRequest at 'http://localhost:8080/api/login' from origin 'http://localhost:5173' has been blocked by CORS policy
行吧,老问题了。我在主类加了全局配置:
@Configuration
public class CorsConfig {
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(Arrays.asList("http://localhost:5173"));
config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
config.setAllowCredentials(true); // 注意!
config.setAllowedHeaders(Arrays.asList("*"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", config);
return source;
}
}
结果前端还是报错。查了半天才发现:前端Axios请求里带了 withCredentials: true,但后端没允许凭证。加上 setAllowCredentials(true) 后,又因为 allowedOrigins 不能设为 * 而卡住。最终硬编码了开发和测试环境的域名。
教训:跨域配置看似简单,但一旦涉及 Cookie 或 Token 传递,就得小心 credentials 和 origin 的配合。线上部署时还得让运维在 Nginx 层统一处理,别指望后端扛所有锅。
第二个坑:前端传参,后端收不到?
搞定跨域后,登录接口又翻车了。前端明明传了 { username: 'admin', password: '123456' },后端却收到两个 null。
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginDTO dto) {
// dto.username == null !!!
}
我一度怀疑是 Spring Boot 的 Jackson 出了问题。后来打开浏览器 Network 面板一看,好家伙——前端发的是 form-data,而 @RequestBody 只能解析 JSON!
原来是同事写的前端封装函数默认用了 FormData。改成 JSON 格式后搞定:
// 前端 axios 配置
axios.post('/api/login', { username, password }, {
headers: { 'Content-Type': 'application/json' }
})
顺便在后端加了个全局异常处理器,防止以后类似问题直接 500:
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(HttpMessageNotReadableException.class)
public ResponseEntity<String> handleJsonParseError() {
return ResponseEntity.badRequest().body("请求体格式错误,请发送JSON");
}
}
性能优化:别让慢查询拖垮你的深夜加班
系统上线后,标注页面加载越来越慢。查了下日志,发现一个接口平均耗时 1.2s —— 就因为它一次查了 1000 条任务数据,每条还关联了图片 URL、用户信息、标签列表……
作为天天和 GPU 打交道的人,我对延迟极其敏感。立刻动手优化:
- 分页:前端加虚拟滚动,后端用 PageHelper 分页
- 字段裁剪:不需要的字段(比如创建时间、更新人)一律不查
- 缓存热点数据:用户信息用 Caffeine 本地缓存
关键代码:
// Service 层
public Page<TaskVO> getTasks(Long userId, int pageNum, int pageSize) {
PageHelper.startPage(pageNum, pageSize);
List<Task> tasks = taskMapper.selectByUserId(userId);
return PageUtil.toPage(tasks, task -> convertToVO(task));
}
// 缓存用户信息
@Cacheable(value = "users", key = "#id")
public User getUserById(Long id) {
return userMapper.selectById(id);
}
优化后,接口响应从 1200ms 降到 80ms。那一刻,我仿佛看到了自己多出来的两小时睡眠时间。
最终效果:一个能跑的小系统
折腾两周后,这个内部工具终于能用了。虽然界面简陋(UI 是我自己用 Tailwind 瞎搞的),但核心功能齐全:
| 功能模块 | 技术实现 | 备注 |
|---|---|---|
| 用户认证 | JWT + Redis 存 token | 支持自动续期 |
| 任务管理 | MyBatis-Plus 分页 | 支持按状态过滤 |
| 实时保存 | 前端防抖 + 后端幂等 | 避免重复提交 |
| 权限控制 | Spring Security + 自定义注解 | 按角色限制访问 |
最让我得意的是,整个项目结构清晰,新人来了也能快速上手。我把前后端代码都整理成模板,放在公司 GitLab 上,标题就叫 《Spring Boot + Vue3 快速启动模板》 —— 结果第二天就被三个组 fork 了。
写在最后:技术探索的意义
说实话,这种“小打小闹”的项目,在大厂可能连 PRD 都过不了。但对我而言,它是一次宝贵的全栈实践。白天调模型参数,晚上调 HTTP 状态码,这种切换反而让我保持对技术的敏感度。
而且,跳槽面试时,比起空谈“熟悉微服务架构”,拿出一个自己从零搭建、解决过真实问题的项目,说服力强太多了。上周刚面了一家 startup,对方看到我的 GitHub 里有这个项目,直接问:“能讲讲你是怎么处理前后端联调问题的吗?” —— 这不就是送分题?
所以,别小看那些“边角料”需求。它们可能是你突破舒适区的最佳入口。毕竟,炼丹不只是调 learning rate,有时候也包括调通一个 CORS 请求。
对了,如果你也在用 Spring Boot 和现代前端框架,欢迎交流踩坑经验。至于那个周五晚上的超时 Bug?最后发现是前端代理配置漏了个路径……唉,不说了,我去续杯咖啡了。

评论 0