从零搭一个全栈小项目,我踩了这些坑

TechEvangelist
2025-12-25 01:34
阅读 1135

上周五晚上十一点半,办公室只剩我和隔壁组的运维小哥还在对线上告警。咖啡已经凉了三杯,但我的脑子里还在回放那个该死的前端请求超时错误——明明后端接口返回正常,为什么前端就是拿不到数据?这事儿让我彻底意识到:作为一个写了三年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 传递,就得小心 credentialsorigin 的配合。线上部署时还得让运维在 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 打交道的人,我对延迟极其敏感。立刻动手优化:

  1. 分页:前端加虚拟滚动,后端用 PageHelper 分页
  2. 字段裁剪:不需要的字段(比如创建时间、更新人)一律不查
  3. 缓存热点数据:用户信息用 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

最热最新
暂无评论
TechEvangelistLv.1
0
影响力
0
文章
0
粉丝