Skip to content

Spring Boot + Vue 全栈项目:待办应用从0到1

某天需要一个练手项目,把前后端分离的流程跑通。选了最经典的待办应用,后端 Spring Boot + MySQL + Redis(做列表缓存),前端 Vue 3。前后花了两个小时,用 AI 辅助写了大部分代码。

技术栈

  • 后端:Spring Boot 2.7.0,Spring Data JPA,MySQL 8.0,Redis(Jedis)
  • 前端:Vue 3,Axios,Vite
  • 部署:后端 jar 包用 nohup 跑在 ECS,前端静态文件放 Nginx,配置跨域

后端代码

application.properties

properties
spring.datasource.url=jdbc:mysql://localhost:3306/todo_db?useSSL=false&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=123456
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true

# Redis 配置(本地默认)
spring.redis.host=localhost
spring.redis.port=6379

Todo 实体类

java
@Entity
@Table(name = "todos")
public class Todo {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String title;
    private boolean completed;
    // 构造器、getter/setter 省略
}

TodoRepository

java
@Repository
public interface TodoRepository extends JpaRepository<Todo, Long> {
}

TodoService(含 Redis 缓存逻辑)

java
@Service
public class TodoService {
    @Autowired
    private TodoRepository todoRepository;
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    private static final String TODOS_CACHE_KEY = "all_todos";

    public List<Todo> getAllTodos() {
        // 先从 Redis 取
        List<Todo> cached = (List<Todo>) redisTemplate.opsForValue().get(TODOS_CACHE_KEY);
        if (cached != null) {
            return cached;
        }
        List<Todo> todos = todoRepository.findAll();
        redisTemplate.opsForValue().set(TODOS_CACHE_KEY, todos, 10, TimeUnit.MINUTES);
        return todos;
    }

    public Todo addTodo(String title) {
        Todo todo = new Todo();
        todo.setTitle(title);
        todo.setCompleted(false);
        Todo saved = todoRepository.save(todo);
        // 添加后让缓存失效,下次查询重新加载
        redisTemplate.delete(TODOS_CACHE_KEY);
        return saved;
    }

    public void deleteTodo(Long id) {
        todoRepository.deleteById(id);
        redisTemplate.delete(TODOS_CACHE_KEY);
    }
}

TodoController

java
@RestController
@CrossOrigin(origins = "http://localhost:5173")  // 前端开发端口
@RequestMapping("/api/todos")
public class TodoController {
    @Autowired
    private TodoService todoService;

    @GetMapping
    public List<Todo> getAll() {
        return todoService.getAllTodos();
    }

    @PostMapping
    public Todo add(@RequestParam String title) {
        return todoService.addTodo(title);
    }

    @DeleteMapping("/{id}")
    public void delete(@PathVariable Long id) {
        todoService.deleteTodo(id);
    }
}

前端代码

安装依赖

bash
npm create vite@latest todo-frontend -- --template vue
cd todo-frontend
npm install axios

main.js

javascript
import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#app')

App.vue

vue
<template>
  <div>
    <h1>待办事项</h1>
    <input v-model="newTitle" @keyup.enter="addTodo" placeholder="输入任务" />
    <button @click="addTodo">添加</button>
    <ul>
      <li v-for="todo in todos" :key="todo.id">
        {{ todo.title }}
        <button @click="deleteTodo(todo.id)">删除</button>
      </li>
    </ul>
  </div>
</template>

<script>
import axios from 'axios'

export default {
  data() {
    return {
      todos: [],
      newTitle: ''
    }
  },
  mounted() {
    this.fetchTodos()
  },
  methods: {
    async fetchTodos() {
      const res = await axios.get('http://localhost:8080/api/todos')
      this.todos = res.data
    },
    async addTodo() {
      if (!this.newTitle.trim()) return
      await axios.post(`http://localhost:8080/api/todos?title=${this.newTitle}`)
      this.newTitle = ''
      this.fetchTodos()
    },
    async deleteTodo(id) {
      await axios.delete(`http://localhost:8080/api/todos/${id}`)
      this.fetchTodos()
    }
  }
}
</script>

配置跨域(后端已加@CrossOrigin,前端无需额外处理)

如果后端部署到云服务器,前端需要替换 localhost:8080 为实际后端 IP。

遇到的坑及解决

1. Redis 缓存一致性问题
一开始只在查询时写入缓存,添加/删除时没有删除缓存,导致前端看到的数据不一致。解决方案:在 addTododeleteTodo 中删除 all_todos 这个 key。

2. 跨域请求失败
前后端分离,前端端口 5173,后端 8080,浏览器拦截。加 @CrossOrigin(origins = "http://localhost:5173") 解决。生产环境换成具体域名。

3. 前端 axios 请求参数格式
POST 请求传 ?title=xxx 是 URL 编码方式。也可以改用 @RequestBody,但简化起见就用 @RequestParam

4. Spring Data JPA 懒加载问题
没有涉及关联关系,所以没有遇到。

部署要点

  • 后端打包:mvn clean package,生成 todo-0.0.1-SNAPSHOT.jar
  • 上传到 ECS,运行:nohup java -jar todo-0.0.1-SNAPSHOT.jar > log.log 2>&1 &
  • 前端打包:npm run build,得到 dist 文件夹
  • 将 dist 内容放到 Nginx 的 /var/www/html,配置 Nginx 反向代理 /api/ 到后端地址,避免跨域。

总结

这个项目麻雀虽小五脏俱全:前后端分离、数据库、缓存、跨域、打包部署。花了两小时用 AI 辅助生成大部分代码,主要精力花在调试缓存一致性和跨域上。面试时可以直接演示这个 demo,并能说清楚每个技术点的作用。

如果需要提升,可以加入 JWT 鉴权、用户隔离、Redis 持久化配置等,但目前版本足够展示全栈基础。