ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Spring Boot + Vue 前后端分离作品展示平台实战指南

Spring Boot + Vue 前后端分离作品展示平台实战指南 最近在梳理前后端分离项目的落地流程时围绕一个轻量级个人作品展示平台做了完整实现项目代号 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. 环境准备与版本说明在动手之前先确认本机的开发环境。本文示例以常见环境为准如果你的版本不同重点理解实现思路即可不用追求版本完全一致。工具版本建议用途JDK8 及以上推荐 11运行 Spring Boot 项目Maven3.6 及以上管理后端依赖与构建MySQL5.7 或 8.0存储作品数据Node.js16 及以上运行前端项目npm8 及以上安装前端依赖IDEIDEA 或 VS Code开发调试后端示例基于 Spring Boot 2.7.x它是稳定且资料较多的版本前端使用 Vue 3 Vite 5 Element Plus 2.x。如果你使用 Spring Boot 3.x需要注意javax包名变为jakartaMySQL 驱动坐标也会变化这是常见的坑点。数据库连接信息请根据你的实际环境修改尤其是用户名和密码。项目结构如下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 更新时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT作品表;使用 utf8mb4 而不是 utf8是为了兼容 emoji 表情和一些特殊字符。作品描述使用TEXT类型可以存储较长内容。排序字段sort用DEFAULT 0保证新插入数据的排序可控。浏览量初始化后从 0 开始后续在查询详情时自增。3.2 后端接口设计接口设计遵循 REST 风格统一以/api/works开头。主要接口如下方法路径功能说明GET/api/works查询作品列表支持 category 参数筛选GET/api/works/{id}查询作品详情浏览量加 1POST/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 version1.0 encodingUTF-8? project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd modelVersion4.0.0/modelVersion parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent groupIdcom.wjkan666/groupId artifactIdwjkan666-backend/artifactId version1.0.0/version namewjkan666-backend/name description作品展示平台后端/description properties java.version11/java.version /properties dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-jpa/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies build plugins plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId /plugin /plugins /build /project如果使用 Spring Boot 3.x需要把父版本改为 3.x并把javax.persistence换成jakarta.persistenceMySQL 依赖坐标换成com.mysql:mysql-connector-jJava 版本建议用 17。这里使用 2.7.18 是为了演示方便也便于不熟悉新版特性的读者对照资料。接下来是数据库和 JPA 配置文件路径为backend/src/main/resources/application.ymlserver: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/wjkan666?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/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.javapackage 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实体属性名使用驼峰createTimeJPA 默认会把创建时间映射到create_time列但为了明确可以用Column(name create_time)显式指定。数据访问层只需要继承 JPA 提供的接口文件路径为backend/src/main/java/com/wjkan666/repository/WorkRepository.javapackage com.wjkan666.repository; import com.wjkan666.entity.Work; import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; public interface WorkRepository extends JpaRepositoryWork, Long { ListWork findAllByOrderBySortAscCreateTimeDesc(); ListWork findByCategoryOrderBySortAsc(String category); }这两个查询方法分别对应“查询全部按排序值升序、创建时间降序排列”和“按分类查询”。Spring Data JPA 会根据方法名自动生成 SQL简单场景下不需要写 JPQL 或原生 SQL。4.3 初始数据初始化为了让项目一启动就有内容可看可以加一个初始化器。使用CommandLineRunner在应用启动完成后检查数据量如果为空就插入几条示例作品。文件路径为backend/src/main/java/com/wjkan666/config/DataInitializer.javapackage 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?textSpringBoot); 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?textVue3); 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?textDeploy); 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 与 ControllerService 层负责业务逻辑。文件路径为backend/src/main/java/com/wjkan666/service/WorkService.javapackage 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 ListWork 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.javapackage 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 ListWork list(RequestParam(required false) String category) { return workService.list(category); } GetMapping(/{id}) public Work detail(PathVariable Long id) { return workService.detail(id); } PostMapping public ResponseEntityWork create(Valid RequestBody Work work) { return ResponseEntity.ok(workService.save(work)); } PutMapping(/{id}) public ResponseEntityWork update(PathVariable Long id, Valid RequestBody Work work) { return ResponseEntity.ok(workService.update(id, work)); } DeleteMapping(/{id}) public ResponseEntityVoid delete(PathVariable Long id) { workService.delete(id); return ResponseEntity.noContent().build(); } }Valid会触发实体中NotBlank等校验规则如果请求体里缺少标题接口会返回 400 错误。新增和修改接口在本地演示时没有权限控制但在生产环境必须加上身份认证否则任何人都能调用这些接口操作数据库。启动类在backend/src/main/java/com/wjkan666/WorkApplication.javapackage 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 vitelatest 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.jsimport { 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.jsimport 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.vuetemplate div classpage header classheader h1wjkan666 作品集合/h1 p一款轻量级个人作品展示平台/p div classfilters el-button v-foritem in categories :keyitem :typecurrentCategory item ? primary : default clickchangeCategory(item) {{ item }} /el-button /div /header el-row :gutter16 classwork-list el-col :xs24 :sm12 :md8 v-forwork in works :keywork.id el-card classwork-card shadowhover img :srcwork.coverUrl classcover :altwork.title / h3{{ work.title }}/h3 p classdesc{{ work.description }}/p div classtags el-tag v-fortag in splitTags(work.tags) :keytag sizesmall typeinfo {{ tag }} /el-tag /div div classmeta span浏览 {{ work.viewCount }}/span span点赞 {{ work.likeCount }}/span /div el-button typeprimary sizesmall clickopenDetail(work) 查看详情 /el-button /el-card /el-col /el-row el-dialog v-modeldialogVisible title作品详情 template v-ifcurrentWork h2{{ currentWork.title }}/h2 p{{ currentWork.description }}/p p分类{{ currentWork.category }}/p p标签{{ currentWork.tags }}/p p浏览量{{ currentWork.viewCount }}/p el-link v-ifcurrentWork.projectUrl :hrefcurrentWork.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 Plusimport { 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 未指定字符集创建数据库时使用 utf8mb4URL 添加useUnicodetruecharacterEncodingutf8Lombok 相关报错IDE 未启用注解处理IDEA 安装 Lombok 插件检查Enable annotation processing端口被占用8080 或 5173 被其他进程占用使用netstat -ano查看占用修改配置端口或结束占用进程JPA 自动建表未生效ddl-auto配置不正确或连接库无权限检查 application.yml确认spring.jpa.hibernate.ddl-autoupdate后端报错时优先看控制台最后几行异常日志前端报错时打开浏览器开发者工具的 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 部署建议前后端分离项目的部署通常有两种方式。第一种是把前端构建后的静态文件交给 NginxNginx 再反向代理/api请求到后端服务。第二种是直接把前端静态文件拷贝到 Spring Boot 的static目录打成 Fat Jar 一起运行适合小规模项目。如果使用 Nginxnginx.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 部署、文件上传时都会有清晰的方向。如果你想继续完善这个作品建议优先做三件事给写操作接口补充认证权限为列表接口加分页把封面图改为本地上传或对象存储。最后提醒一句无论项目规模多小写操作接口都要先考虑鉴权和数据校验这个习惯比任何复杂框架都重要。动手把示例代码在本地跑一遍再尝试改一改功能你会发现前后端开发的很多知识都会在这个小项目里串起来。
返回列表