最近在梳理前后端分离项目的落地流程时,围绕一个轻量级个人作品展示平台做了完整实现,项目代号 wjkan666。这类项目非常适合作为 Spring Boot + Vue 的入门综合练习,既能覆盖常见的 CRUD、数据表设计、跨域联调,又能很快看到一个可直接演示的页面效果。本文会从这个作品平台的需求拆分开始,逐步讲解数据库设计、后端接口开发、前端页面联调、本地运行与常见排错,整套流程可以迁移到博客、作品集、企业内部展示系统等场景。如果你是刚学完 Spring Boot 和 Vue 基础、想找一个完整项目练手,或需要在内部快速搭建一个内容展示站点,这篇文章都值得跟着操作一遍。
1. 背景与核心概念
1.1 作品展示平台的定位
在个人作品分享、团队项目展示等场景中,最常见的方式是直接发一个图文链接。但图文链接存在几个明显问题:作品数量多了之后难以管理,不同分类没有筛选入口,后续想要统计访问量,也需要手工处理。用一个小型 Web 项目把作品集中管理起来,既方便维护,也能作为开发者综合技能的练习项目。
wjkan666 平台要做的不是复杂的内容管理系统,而是“轻量展示 + 基础管理”。前台以卡片形式展示作品,用户点击可以查看详情;后台提供新增、编辑、删除等常规操作。对于个人开发者来说,这种规模的项目可以帮助自己理解前后端交互的完整链路:前端发出请求、后端接收请求、操作数据库、返回 JSON、前端渲染页面。对于团队或企业场景,这个项目也可以作为内部作品库、案例展示站点的原型,后续接上登录认证、文件上传、内容审核就能直接使用。
1.2 为什么选择 Spring Boot + Vue
后端选择 Spring Boot,主要是看中它的快速集成能力和生态成熟度。Spring Boot 通过自动配置大大减少了 XML 和模板代码,配合 Spring Data JPA 可以简化数据库访问;starter 机制让引入 Web、数据库、校验、日志等能力都变成依赖声明,非常适合快速搭建 REST API。前端使用 Vue 3 + Vite + Element Plus,是因为 Vue 3 的组合式 API 让组件逻辑更清晰,Vite 启动速度快,Element Plus 提供现成的布局、卡片、按钮、弹窗等组件,可以省去大量样式工作。
这个选型并不是唯一答案。如果你更熟悉 Python,可以把后端替换为 FastAPI;如果不需要复杂交互,直接用 Thymeleaf 服务端渲染也可以。但 Spring Boot + Vue 是当前企业开发中比较常见的前后端分离组合,掌握这一套流程,后续再学习微服务、分布式部署时会有更好的基础。
1.3 功能范围
wjkan666 平台规划以下功能:
- 作品列表展示:首页按照排序字段展示作品卡片,包含标题、封面、分类、标签、简介。
- 分类筛选:根据作品分类进行列表过滤。
- 作品详情:查看完整描述、项目链接、浏览量和点赞量。
- 作品管理:支持新增、编辑、删除作品。
- 数据初始化:项目首次启动时自动写入示例作品数据,方便演示和二次开发。
考虑到演示项目的学习属性,这里暂不引入登录注册和后台管理界面,管理接口直接暴露在本地演示环境。但在后面的最佳实践部分,我会专门强调生产环境必须补充认证与权限校验,这一点很重要。
2. 环境准备与版本说明
在动手之前,先确认本机的开发环境。本文示例以常见环境为准,如果你的版本不同,重点理解实现思路即可,不用追求版本完全一致。
| 工具 | 版本建议 | 用途 |
|---|---|---|
| JDK | 8 及以上,推荐 11 | 运行 Spring Boot 项目 |
| Maven | 3.6 及以上 | 管理后端依赖与构建 |
| MySQL | 5.7 或 8.0 | 存储作品数据 |
| Node.js | 16 及以上 | 运行前端项目 |
| npm | 8 及以上 | 安装前端依赖 |
| IDE | IDEA 或 VS Code | 开发调试 |
后端示例基于 Spring Boot 2.7.x,它是稳定且资料较多的版本;前端使用 Vue 3 + Vite 5 + Element Plus 2.x。如果你使用 Spring Boot 3.x,需要注意javax包名变为jakarta,MySQL 驱动坐标也会变化,这是常见的坑点。数据库连接信息请根据你的实际环境修改,尤其是用户名和密码。项目结构如下:
wjkan666-project ├── backend # 后端工程 │ ├── pom.xml │ └── src │ ├── main │ │ ├── java │ │ │ └── com/wjkan666 │ │ │ ├── WorkApplication.java │ │ │ ├── controller │ │ │ ├── entity │ │ │ ├── repository │ │ │ ├── service │ │ │ └── config │ │ └── resources │ │ └── application.yml │ └── test └── frontend # 前端工程 ├── package.json ├── vite.config.js └── src ├── main.js ├── api │ └── work.js └── App.vue这个目录结构并不复杂,前端暂时只有一个主要页面,后端按分层把实体、数据访问、业务逻辑和接口分开,方便维护和扩展。
3. 数据库与接口设计
3.1 表结构设计
作品表主要包含基础信息、展示信息和统计信息。基础字段是标题和描述;展示字段包括封面图、分类、标签、项目链接和排序;统计字段包括浏览量和点赞量。为了避免每次手动创建库表,这里使用 JPA 的ddl-auto: update,第一次启动时自动建表。如果你更习惯 SQL 建表,也可以手动执行下面的建表语句:
CREATE DATABASE IF NOT EXISTS wjkan666 DEFAULT CHARACTER SET utf8mb4; USE wjkan666; CREATE TABLE IF NOT EXISTS work ( id BIGINT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(100) NOT NULL COMMENT '作品标题', description TEXT COMMENT '作品描述', cover_url VARCHAR(255) COMMENT '封面图地址', category VARCHAR(50) COMMENT '作品分类', tags VARCHAR(255) COMMENT '标签,多个用逗号分隔', project_url VARCHAR(255) COMMENT '项目链接', sort INT DEFAULT 0 COMMENT '排序值,越小越靠前', view_count BIGINT DEFAULT 0 COMMENT '浏览量', like_count BIGINT DEFAULT 0 COMMENT '点赞量', create_time DATETIME COMMENT '创建时间', update_time DATETIME COMMENT '更新时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='作品表';使用 utf8mb4 而不是 utf8,是为了兼容 emoji 表情和一些特殊字符。作品描述使用TEXT类型,可以存储较长内容。排序字段sort用DEFAULT 0,保证新插入数据的排序可控。浏览量初始化后从 0 开始,后续在查询详情时自增。
3.2 后端接口设计
接口设计遵循 REST 风格,统一以/api/works开头。主要接口如下:
| 方法 | 路径 | 功能 | 说明 |
|---|---|---|---|
| GET | /api/works | 查询作品列表 | 支持 category 参数筛选 |
| GET | /api/works/{id} | 查询作品详情 | 浏览量加 1 |
| POST | /api/works | 新增作品 | 演示环境未做鉴权 |
| PUT | /api/works/{id} | 修改作品 | 演示环境未做鉴权 |
| DELETE | /api/works/{id} | 删除作品 | 演示环境未做鉴权 |
列表接口返回数组,每条数据包含所有基础字段。详情接口返回单个对象,同时更新浏览量。新增、修改、删除接口在真实环境中必须配合管理员权限,否则任何人可以直接操作数据,这是后面要重点强调的安全风险。
4. 后端开发实战
4.1 创建 Spring Boot 项目并配置依赖
后端模块可以使用 IDEA 的 Spring Initializr 创建,也可以手动创建 Maven 工程。关键依赖包括 Web、JPA、MySQL、Validation、Lombok。下面是backend/pom.xml的核心配置:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <relativePath/> </parent> <groupId>com.wjkan666</groupId> <artifactId>wjkan666-backend</artifactId> <version>1.0.0</version> <name>wjkan666-backend</name> <description>作品展示平台后端</description> <properties> <java.version>11</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>如果使用 Spring Boot 3.x,需要把父版本改为 3.x,并把javax.persistence换成jakarta.persistence,MySQL 依赖坐标换成com.mysql:mysql-connector-j,Java 版本建议用 17。这里使用 2.7.18 是为了演示方便,也便于不熟悉新版特性的读者对照资料。
接下来是数据库和 JPA 配置,文件路径为backend/src/main/resources/application.yml:
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/wjkan666?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: ddl-auto: update show-sql: true open-in-view: false logging: level: com.wjkan666: debugddl-auto设置为update后,Hibernate 会在启动时检查实体类和表结构,自动创建或更新表。生产环境更推荐使用数据库迁移工具如 Flyway,并显式控制表结构变更,避免自动化更新带来的风险。open-in-view: false可以避免 Session 在视图层长时间持有,减少数据库连接占用问题。
4.2 实体类与数据访问层
实体类对应数据库表。使用 Lombok 的@Data注解来生成 getter、setter 和 toString,减少样板代码。需要说明的是,Lombok 需要 IDE 安装插件,IDEA 自带支持,Eclipse 可能需要额外配置。实体文件路径为backend/src/main/java/com/wjkan666/entity/Work.java:
package com.wjkan666.entity; import lombok.Data; import javax.persistence.*; import javax.validation.constraints.NotBlank; import java.time.LocalDateTime; @Data @Entity @Table(name = "work") public class Work { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @NotBlank(message = "作品标题不能为空") @Column(nullable = false, length = 100) private String title; @Column(columnDefinition = "TEXT") private String description; private String coverUrl; private String category; private String tags; private String projectUrl; private Integer sort; private Long viewCount; private Long likeCount; @Column(name = "create_time", updatable = false) private LocalDateTime createTime; @Column(name = "update_time") private LocalDateTime updateTime; @PrePersist public void prePersist() { if (viewCount == null) { viewCount = 0L; } if (likeCount == null) { likeCount = 0L; } if (sort == null) { sort = 0; } LocalDateTime now = LocalDateTime.now(); this.createTime = now; this.updateTime = now; } @PreUpdate public void preUpdate() { this.updateTime = LocalDateTime.now(); } }@PrePersist和@PreUpdate是 JPA 的生命周期回调方法,在插入和更新之前自动补充时间、初始统计值。这里有一个新手容易忽略的点:数据库字段名使用蛇形create_time,实体属性名使用驼峰createTime,JPA 默认会把创建时间映射到create_time列,但为了明确,可以用@Column(name = "create_time")显式指定。
数据访问层只需要继承 JPA 提供的接口,文件路径为backend/src/main/java/com/wjkan666/repository/WorkRepository.java:
package com.wjkan666.repository; import com.wjkan666.entity.Work; import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; public interface WorkRepository extends JpaRepository<Work, Long> { List<Work> findAllByOrderBySortAscCreateTimeDesc(); List<Work> findByCategoryOrderBySortAsc(String category); }这两个查询方法分别对应“查询全部,按排序值升序、创建时间降序排列”和“按分类查询”。Spring Data JPA 会根据方法名自动生成 SQL,简单场景下不需要写 JPQL 或原生 SQL。
4.3 初始数据初始化
为了让项目一启动就有内容可看,可以加一个初始化器。使用CommandLineRunner在应用启动完成后检查数据量,如果为空就插入几条示例作品。文件路径为backend/src/main/java/com/wjkan666/config/DataInitializer.java:
package com.wjkan666.config; import com.wjkan666.entity.Work; import com.wjkan666.repository.WorkRepository; import org.springframework.boot.CommandLineRunner; import org.springframework.stereotype.Component; @Component public class DataInitializer implements CommandLineRunner { private final WorkRepository workRepository; public DataInitializer(WorkRepository workRepository) { this.workRepository = workRepository; } @Override public void run(String... args) { if (workRepository.count() > 0) { return; } Work work1 = new Work(); work1.setTitle("Spring Boot 接口实战"); work1.setDescription("一个演示 Spring Boot REST API 开发的小项目,包含参数校验、异常处理和接口文档。"); work1.setCoverUrl("https://via.placeholder.com/300x200?text=SpringBoot"); work1.setCategory("后端"); work1.setTags("Java, Spring Boot, REST"); work1.setProjectUrl("https://github.com/wjkan666/demo"); work1.setSort(1); workRepository.save(work1); Work work2 = new Work(); work2.setTitle("Vue 3 作品展示页"); work2.setDescription("基于 Vue 3 和 Element Plus 实现的响应式作品展示页面。"); work2.setCoverUrl("https://via.placeholder.com/300x200?text=Vue3"); work2.setCategory("前端"); work2.setTags("Vue, Vite, Element Plus"); work2.setProjectUrl("https://github.com/wjkan666/vue-demo"); work2.setSort(2); workRepository.save(work2); Work work3 = new Work(); work3.setTitle("自动化部署脚本"); work3.setDescription("一键发布 Spring Boot 应用的 Shell 脚本工具。"); work3.setCoverUrl("https://via.placeholder.com/300x200?text=Deploy"); work3.setCategory("运维"); work3.setTags("Shell, Linux, Nginx"); work3.setProjectUrl("https://github.com/wjkan666/deploy-demo"); work3.setSort(3); workRepository.save(work3); } }这里使用构造器注入,比字段注入更容易测试和追踪依赖。workRepository.count()用于判断是否已经初始化过,避免每次启动都重复插入数据。
4.4 Service 与 Controller
Service 层负责业务逻辑。文件路径为backend/src/main/java/com/wjkan666/service/WorkService.java:
package com.wjkan666.service; import com.wjkan666.entity.Work; import com.wjkan666.repository.WorkRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.persistence.EntityNotFoundException; import java.util.List; @Service public class WorkService { private final WorkRepository workRepository; public WorkService(WorkRepository workRepository) { this.workRepository = workRepository; } public List<Work> list(String category) { if (category == null || category.trim().isEmpty()) { return workRepository.findAllByOrderBySortAscCreateTimeDesc(); } return workRepository.findByCategoryOrderBySortAsc(category); } @Transactional public Work detail(Long id) { Work work = workRepository.findById(id) .orElseThrow(() -> new EntityNotFoundException("作品不存在:" + id)); work.setViewCount(work.getViewCount() + 1); return work; } public Work save(Work work) { return workRepository.save(work); } @Transactional public Work update(Long id, Work work) { Work exist = workRepository.findById(id) .orElseThrow(() -> new EntityNotFoundException("作品不存在:" + id)); exist.setTitle(work.getTitle()); exist.setDescription(work.getDescription()); exist.setCoverUrl(work.getCoverUrl()); exist.setCategory(work.getCategory()); exist.setTags(work.getTags()); exist.setProjectUrl(work.getProjectUrl()); exist.setSort(work.getSort()); return workRepository.save(exist); } public void delete(Long id) { workRepository.deleteById(id); } }detail方法加了@Transactional,保证浏览量更新在同一事务中提交。update方法先查出已有记录,再把允许修改的字段拷贝进去,避免创建重复记录或丢失创建时间。
Controller 层把 HTTP 请求映射到 Service,文件路径为backend/src/main/java/com/wjkan666/controller/WorkController.java:
package com.wjkan666.controller; import com.wjkan666.entity.Work; import com.wjkan666.service.WorkService; import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import javax.validation.Valid; import java.util.List; @Validated @RestController @RequestMapping("/api/works") public class WorkController { private final WorkService workService; public WorkController(WorkService workService) { this.workService = workService; } @GetMapping public List<Work> list(@RequestParam(required = false) String category) { return workService.list(category); } @GetMapping("/{id}") public Work detail(@PathVariable Long id) { return workService.detail(id); } @PostMapping public ResponseEntity<Work> create(@Valid @RequestBody Work work) { return ResponseEntity.ok(workService.save(work)); } @PutMapping("/{id}") public ResponseEntity<Work> update(@PathVariable Long id, @Valid @RequestBody Work work) { return ResponseEntity.ok(workService.update(id, work)); } @DeleteMapping("/{id}") public ResponseEntity<Void> delete(@PathVariable Long id) { workService.delete(id); return ResponseEntity.noContent().build(); } }@Valid会触发实体中@NotBlank等校验规则,如果请求体里缺少标题,接口会返回 400 错误。新增和修改接口在本地演示时没有权限控制,但在生产环境必须加上身份认证,否则任何人都能调用这些接口操作数据库。
启动类在backend/src/main/java/com/wjkan666/WorkApplication.java:
package com.wjkan666; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class WorkApplication { public static void main(String[] args) { SpringApplication.run(WorkApplication.class, args); } }后端启动后,可以用 Postman 或浏览器访问http://localhost:8080/api/works,应返回 JSON 数组。
5. 前端开发实战
5.1 初始化 Vue 项目和依赖
前端使用 Vite 创建项目。命令如下:
npm create vite@latest frontend -- --template vue模板创建完成后,进入目录安装额外依赖:
cd frontend npm install npm install element-plus axioselement-plus提供 UI 组件,axios用于发送 HTTP 请求。如果下载速度较慢,可以把 npm 镜像切换到国内源。项目中的package.json会自动记录依赖版本,不需要手动修改版本号。
5.2 配置接口代理
前端开发服务器默认端口是 5173,后端是 8080,两者端口不同,直接请求会跨域。最简单的方案是在 Vite 配置代理,把/api前缀的请求转发到后端。文件路径为frontend/vite.config.js:
import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } })配置之后,前端页面上请求/api/works时,Vite 会把请求转发到http://localhost:8080/api/works,浏览器不会感知到跨域过程。后端即使不加@CrossOrigin,也能正常联调。
5.3 封装 API 请求
为了方便复用,将接口调用封装到一个模块中。文件路径为frontend/src/api/work.js:
import axios from 'axios' const request = axios.create({ baseURL: '/api', timeout: 10000 }) export function getWorks(category) { return request.get('/works', { params: category ? { category } : {} }) } export function getWorkDetail(id) { return request.get(`/works/${id}`) } export function createWork(data) { return request.post('/works', data) } export function updateWork(id, data) { return request.put(`/works/${id}`, data) } export function deleteWork(id) { return request.delete(`/works/${id}`) }这里统一使用/api作为 baseURL,配合 Vite 代理即可访问后端。如果后期后端地址变化,只需要修改代理配置,不需要改每个页面组件。
5.4 页面实现
主页面使用 Element Plus 的el-row、el-col、el-card、el-tag等组件展示作品卡片。文件路径为frontend/src/App.vue:
<template> <div class="page"> <header class="header"> <h1>wjkan666 作品集合</h1> <p>一款轻量级个人作品展示平台</p> <div class="filters"> <el-button v-for="item in categories" :key="item" :type="currentCategory === item ? 'primary' : 'default'" @click="changeCategory(item)" > {{ item }} </el-button> </div> </header> <el-row :gutter="16" class="work-list"> <el-col :xs="24" :sm="12" :md="8" v-for="work in works" :key="work.id"> <el-card class="work-card" shadow="hover"> <img :src="work.coverUrl" class="cover" :alt="work.title" /> <h3>{{ work.title }}</h3> <p class="desc">{{ work.description }}</p> <div class="tags"> <el-tag v-for="tag in splitTags(work.tags)" :key="tag" size="small" type="info" > {{ tag }} </el-tag> </div> <div class="meta"> <span>浏览 {{ work.viewCount }}</span> <span>点赞 {{ work.likeCount }}</span> </div> <el-button type="primary" size="small" @click="openDetail(work)"> 查看详情 </el-button> </el-card> </el-col> </el-row> <el-dialog v-model="dialogVisible" title="作品详情"> <template v-if="currentWork"> <h2>{{ currentWork.title }}</h2> <p>{{ currentWork.description }}</p> <p>分类:{{ currentWork.category }}</p> <p>标签:{{ currentWork.tags }}</p> <p>浏览量:{{ currentWork.viewCount }}</p> <el-link v-if="currentWork.projectUrl" :href="currentWork.projectUrl" target="_blank"> 打开项目链接 </el-link> </template> </el-dialog> </div> </template> <script setup> import { onMounted, ref } from 'vue' import { getWorks } from './api/work' const works = ref([]) const categories = ['全部', '后端', '前端', '运维'] const currentCategory = ref('全部') const dialogVisible = ref(false) const currentWork = ref(null) function splitTags(tags) { if (!tags) return [] return tags.split(',').map((tag) => tag.trim()) } async function loadWorks() { try { const category = currentCategory.value === '全部' ? '' : currentCategory.value const res = await getWorks(category) works.value = res.data } catch (error) { console.error('加载作品失败', error) } } function changeCategory(category) { currentCategory.value = category loadWorks() } function openDetail(work) { currentWork.value = work dialogVisible.value = true } onMounted(loadWorks) </script> <style scoped> .page { max-width: 1200px; margin: 0 auto; padding: 20px; } .header { text-align: center; margin-bottom: 24px; } .filters { margin: 16px 0; } .work-list { margin-top: 12px; } .work-card { margin-bottom: 16px; } .cover { width: 100%; height: 180px; object-fit: cover; border-radius: 6px; } .desc { color: #666; font-size: 14px; min-height: 60px; } .tags { margin: 8px 0; } .meta { display: flex; justify-content: space-between; color: #999; font-size: 13px; margin-bottom: 12px; } </style>页面中点击“查看详情”会把当前作品对象放到弹窗中,项目没有单独设置详情路由,适合这种轻量展示场景。如果作品数量很多,建议改成详情页或分页展示,避免一次加载全部数据。
main.js也需要注册 Element Plus:
import { createApp } from 'vue' import ElementPlus from 'element-plus' import 'element-plus/dist/index.css' import App from './App.vue' const app = createApp(App) app.use(ElementPlus) app.mount('#app')5.5 运行与验证
启动后端:
cd backend mvn spring-boot:run后端启动后,控制台会输出 Spring Boot 的启动日志,随后监听 8080 端口。再启动前端:
cd frontend npm run dev浏览器访问http://localhost:5173,正常情况下会看到示例作品的卡片列表。点击分类按钮,列表会刷新;点击“查看详情”,弹窗中会展示作品完整信息。到这里,一个前后端分离的作品展示平台就完整跑通了。
6. 常见问题与排查思路
根据实际调试经验,这类前后端分离项目最常见的问题集中在环境配置、跨域、数据库连接和依赖版本上。下面整理成一个表格,方便快速对照排查。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 后端启动报数据库连接失败 | MySQL 未启动、账号密码错误、库名不存在 | 检查 MySQL 服务状态,确认 URL、用户名、密码;手动创建 wjkan666 数据库 |
| 前端请求接口 404 | 代理没有生效,或后端接口路径不一致 | 检查 Vite proxy 配置,确认/api是否转发;访问后端地址确认接口存在 |
| 前端请求接口 500 | 数据库表结构问题或 JPA 属性映射错误 | 查看后端控制台日志,确认实体类和表结构映射 |
| 控制台显示跨域报错 | 后端没有加允许跨域配置,且没有使用代理 | 优先使用 Vite 代理,或为 Controller 添加@CrossOrigin |
| 中文乱码 | 数据库字符集不是 utf8mb4、连接 URL 未指定字符集 | 创建数据库时使用 utf8mb4,URL 添加useUnicode=true&characterEncoding=utf8 |
| Lombok 相关报错 | IDE 未启用注解处理 | IDEA 安装 Lombok 插件,检查Enable annotation processing |
| 端口被占用 | 8080 或 5173 被其他进程占用 | 使用netstat -ano查看占用,修改配置端口或结束占用进程 |
| JPA 自动建表未生效 | ddl-auto配置不正确或连接库无权限 | 检查 application.yml,确认spring.jpa.hibernate.ddl-auto=update |
后端报错时,优先看控制台最后几行异常日志;前端报错时,打开浏览器开发者工具的 Network 面板,确认请求的状态码和响应内容。这种层层排查的方式能快速缩小问题范围,比无目的地修改配置更有效。
7. 最佳实践与工程建议
7.1 配置管理
示例中的数据库密码直接写在application.yml中,这种写法只适合本地开发。生产环境应该使用环境变量、JVM 参数或配置中心来注入敏感信息。例如:
spring: datasource: password: ${DB_PASSWORD:123456}这样默认值保留用于本地,服务器上通过环境变量DB_PASSWORD注入真实密码。同时,不要把真实数据库连接信息提交到 Git 仓库,可以使用.gitignore忽略application-local.yml,本地配置单独维护。
7.2 安全边界
当前项目最大的安全缺口是写操作接口没有任何鉴权。任何人知道接口路径后都可以新增、修改或删除数据。即使是一个小项目,也应该在生产环境前补上认证授权。最简单的方案是引入 Spring Security,用 JWT 或 Session 登录保护/api/works的 POST、PUT、DELETE 方法。如果暂时不引入安全框架,至少也要在网关或反向代理层面限制写接口的访问来源。
数据校验同样重要。除了标题不能为空,还应该对描述长度、封面 URL 格式、分类长度做校验。前端校验只负责用户体验,后端校验才是安全底线。示例中通过@Valid已经解决了标题校验,但其他字段的规则可以根据实际需要补充。
7.3 性能与可维护性
如果作品量增长,列表接口直接返回全量数据会越来越慢。建议引入分页参数,例如page和size,配合Pageable来实现。封面图不要直接引用外部临时链接,应该由单独的文件服务或对象存储管理。对于访问量、点赞量这类高频更新字段,可以考虑使用 Redis 缓存,再异步同步到数据库,避免每次请求都直接更新数据库行。
日志方面,建议把接口访问日志和业务操作日志分开记录。写操作需要记录操作人、时间和修改内容,方便后期审计。异常处理也不能只靠默认报错,可以定义统一的@RestControllerAdvice,把业务异常转换为友好提示,同时记录完整异常堆栈。这里不展开贴全部代码,但这是工程化必做的收尾工作。
7.4 部署建议
前后端分离项目的部署通常有两种方式。第一种是把前端构建后的静态文件交给 Nginx,Nginx 再反向代理/api请求到后端服务。第二种是直接把前端静态文件拷贝到 Spring Boot 的static目录,打成 Fat Jar 一起运行,适合小规模项目。
如果使用 Nginx,nginx.conf中核心配置如下:
server { listen 80; server_name example.com; location / { root /var/www/wjkan666-frontend; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }try_files是关键,它保证 Vue Router 使用 history 模式时刷新页面不会 404。部署前不要忘记执行npm run build生成dist目录,并对照服务器路径调整root和proxy_pass。
8. 总结与学习路线
到这里,wjkan666 作品展示平台的核心功能已经完整实现:后端提供作品 CRUD 接口,前端通过 Vue 3 展示作品列表并提供分类筛选和详情弹窗,项目可以直接运行,也可以作为二次开发的基础代码。
这个项目的难点不在于单个技术,而在于把 Spring Boot、MySQL、Vue 3 串起来,理解请求如何在浏览器、前端代理、后端 Controller、Service、Repository 之间流转。只要把这套链路跑通,后面学习 Spring Security、Redis、Docker 部署、文件上传时,都会有清晰的方向。如果你想继续完善这个作品,建议优先做三件事:给写操作接口补充认证权限,为列表接口加分页,把封面图改为本地上传或对象存储。
最后提醒一句:无论项目规模多小,写操作接口都要先考虑鉴权和数据校验,这个习惯比任何复杂框架都重要。动手把示例代码在本地跑一遍,再尝试改一改功能,你会发现前后端开发的很多知识都会在这个小项目里串起来。