拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Spring Boot + Vue 全栈实战:构建卡牌收集与图鉴管理系统

《用 Spring Boot Vue 打造一个“魔卡少女樱”风格卡牌收集系统》先把话放到前面如果你是一名后端开发或者正在学习全栈项目本文会给你一套非常完整的卡牌收集系统实战方案。如果你本身就是《魔卡少女樱》的粉丝那就更合适了。小樱收集库洛牌的过程本质上就是一个“卡牌图鉴管理”的过程卡片有编号、有属性、有状态、有形象玩家需要一张一张收集、切换、收藏。这种业务模型放到软件工程里就是典型的“实体 状态流转 列表检索”场景。本文将基于 Spring Boot MyBatis-Plus Vue 3 Element Plus MySQL从零开始实现一个最小可运行的卡牌收集系统。你可以把诸如“火牌”“水牌”“风牌”这些经典卡牌录入数据库在前端以图鉴网格的形式展示支持按关键词搜索、按属性筛选、收集状态切换、收藏切换。整套代码我都会贴出来并逐段解释核心逻辑确保你能复制到本地直接运行。读完本文你不仅能得到一个带前后端交互的完整项目还能掌握 Spring Boot 项目分层、MyBatis-Plus 条件查询、Vue 3 组合式 API、接口联调等关键技能。为了不让内容停留在“玩具项目”层面我还会在最后补充常见报错排查清单和工程化建议。这套思路可以直接移植到真正的卡牌游戏、盲盒图鉴、成就系统等业务中学习性价比非常高。1. 背景与核心概念1.1 卡牌收集系统是什么卡牌收集系统是一种以“卡牌”为核心资源的业务系统。每一张卡牌通常包含名称、编号、品质、属性、描述、图片等基础信息。用户可以对卡牌执行“查看详情”“切换收集状态”“加为收藏”“按属性筛选”等操作。在《魔卡少女樱》中库洛牌拥有各自的魔法属性比如火、水、风、土、光、影小樱需要和每一张牌建立“契约”也就是完成收集。这个流程映射到系统中就是“卡牌实体”的“状态字段”发生变化。从开发角度看这类系统的共性非常强。无论是游戏里的角色卡池还是电商平台的商品图鉴背后都离不开「增删改查 状态标记 列表筛选」这组基础能力。掌握好这套通用设计以后遇到类似的“图鉴类”需求你都可以快速套用。1.2 它解决什么问题卡牌收集系统最核心的价值是把散落在页面上的“静态卡片”变为“可交互、可追踪、可筛选”的动态数据。如果没有这样一个系统运营人员手动维护卡牌信息会非常痛苦玩家也无法知道自己到底收集了多少张缺了哪几张哪些是自己特别喜欢的。通过数据库表和接口我们可以精确记录每张卡牌的收集状态、收藏状态并通过前端把状态实时展示给用户。对于开发者来说这类系统的学习意义还在于它用到了很多高频技术点。例如数据库表设计中的枚举字段设计MyBatis-Plus 的动态条件查询Spring Boot 的分层架构前端列表渲染和状态点击事件以及前后端分离项目中的跨域或代理配置。这些知识点在真实项目中几乎每天都会遇到所以这个“卡牌图鉴”项目非常适合作为全栈入门练手项目。2. 环境准备与版本说明本教程的示例项目采用前后端分离架构后端提供 RESTful API前端通过 Axios 调用接口。为了不让环境问题阻碍学习下面列出我推荐的软件环境和版本范围。你不需要完全照搬只要保证主版本兼容即可。软件环境参考版本说明JDK1.8 或 11Spring Boot 2.7 系列兼容 JDK 8Maven3.6用于管理后端依赖MySQL5.7 或 8.0数据库引擎建议使用 InnoDBNode.js16前端构建工具运行环境Vue CLI / ViteVite 5创建 Vue 3 项目IDEIntelliJ IDEA / VS Code分别开发后端与前端这里需要说明的是Java 端的 Spring Boot 版本更新很快Spring Boot 3.x 和 2.x 在依赖坐标、配置方式上都有一些差异。本文为了稳妥起见后端示例以 Spring Boot 2.7.18 为例这是目前使用范围很广的稳定版本。如果你的项目准备用 Spring Boot 3.x那么对应的 MyBatis-Plus 需要换成mybatis-plus-spring-boot3-starter整体思路不变。前端部分我们使用 Vue 3 的script setup语法组件库采用 Element Plus。数据库建议先手动创建一个库示例中数据库名为card_db。如果你本地已经占用了这个库名可以改成你喜欢的名字但需要同步修改后端application.yml中的连接地址。3. 核心设计数据模型与接口定义动手写代码之前先把业务模型和接口边界梳理清楚。这是整个项目最值得认真思考的部分因为后面所有代码都是在为这套设计服务。3.1 卡牌实体设计一张“卡牌”在数据库里应该有哪些字段我们不妨从《魔卡少女樱》中的库洛牌特征出发。每张牌都有名字、英文编号、魔法属性、描述、形象图。为了支撑收集玩法还必须加上“是否已收集”和“是否已收藏”这两个状态字段。最终设计出如下核心字段字段名字段含义类型说明id主键BIGINT自增name卡牌名称VARCHAR例如“火牌”code卡牌编号VARCHAR唯一例如“FIRE”attribute属性VARCHAR火、水、风、土等description描述VARCHAR卡牌能力说明image_url图片地址VARCHAR前端展示用collected_status收集状态TINYINT0未收集1已收集favorite_status收藏状态TINYINT0未收藏1已收藏create_time创建时间DATETIME自动填充update_time更新时间DATETIME自动更新这里把collected_status和favorite_status拆成两个字段好处是收集与收藏是两种不同维度互不干扰。你也可以扩展更多状态比如“已锁定”“已兑换”思路是一样的。注意状态字段建议使用数字枚举而不是字符串枚举因为数字在数据库中占空间更小查询效率也更高后端代码里只要做好映射即可。3.2 数据库表结构直接给出建表 SQL。复制到 MySQL 中执行即可注意card_db字符集统一使用utf8mb4这样中文不会出现乱码。CREATE DATABASE IF NOT EXISTS card_db DEFAULT CHARACTER SET utf8mb4; USE card_db; DROP TABLE IF EXISTS card; CREATE TABLE card ( id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT 主键, name VARCHAR(64) NOT NULL COMMENT 卡牌名称, code VARCHAR(32) NOT NULL UNIQUE COMMENT 卡牌编号, attribute VARCHAR(32) NOT NULL COMMENT 属性火/水/风/土等, description VARCHAR(512) DEFAULT COMMENT 卡牌描述, image_url VARCHAR(255) DEFAULT COMMENT 卡牌图片地址, collected_status TINYINT NOT NULL DEFAULT 0 COMMENT 收集状态0未收集1已收集, favorite_status TINYINT NOT NULL DEFAULT 0 COMMENT 收藏状态0未收藏1已收藏, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT卡牌表; INSERT INTO card (name, code, attribute, description, image_url, collected_status, favorite_status) VALUES (火牌, FIRE, 火, 拥有操控火焰力量的卡牌。, /images/fire.png, 1, 1), (水牌, WATER, 水, 能够召唤水流与浪潮的卡牌。, /images/water.png, 0, 0), (风牌, WIND, 风, 操控强风与气流的卡牌。, /images/wind.png, 1, 0), (土牌, EARTH, 土, 控制大地与岩石的卡牌。, /images/earth.png, 0, 0), (光牌, LIGHT, 光, 代表光明与净化的卡牌。, /images/light.png, 1, 1), (影牌, SHADOW, 影, 操控暗影与隐藏的卡牌。, /images/shadow.png, 0, 0);初始化数据中我把状态设成了混合值方便你测试筛选和切换效果。图片地址在示例中使用了/images/fire.png这类相对路径如果你没有准备图片资源前端可以使用占位图或者把image_url改成网络图片链接。3.3 API 接口约定前后端分离项目一定要在开发前把接口约定好否则联调阶段很容易互相甩锅。本项目的接口设计如下请求方式接口路径功能说明GET/api/cards分页查询卡牌列表支持关键字、属性、收集状态筛选GET/api/cards/attributes获取所有属性列表GET/api/cards/{id}获取卡牌详情PUT/api/cards/{id}更新卡牌信息PUT/api/cards/{id}/collected切换收集状态PUT/api/cards/{id}/favorite切换收藏状态这里没有提供删除接口因为对于一个“图鉴”场景来说卡牌通常不会真正被删除即使下架也建议用状态字段标记。这个设计思路在真实项目中非常重要直接物理删除数据往往意味着不可恢复。4. 完整实战案例基于 Spring Boot Vue 实现卡牌收集系统现在正式进入代码实现阶段。我会按照“后端项目搭建 - 后端核心代码 - 前端项目搭建 - 前端页面开发 - 运行验证”的顺序展开保证每一步都能落地。4.1 创建后端项目结构后端项目使用 Maven 构建目录结构如下这同时也是 Spring Boot 的标准分包方式。card-collector ├── pom.xml └── src/main/java/com/example/cardcollector ├── CardCollectorApplication.java ├── config/MybatisPlusConfig.java ├── controller/CardController.java ├── entity/Card.java ├── mapper/CardMapper.java └── service/CardService.java先创建pom.xml引入 Web、MyBatis-Plus、MySQL、Lombok 依赖。Lombok 可以帮我们省略 Getter/Setter 这些模板代码让实体类更清爽。?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.example/groupId artifactIdcard-collector/artifactId version0.0.1-SNAPSHOT/version namecard-collector/name dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies /project然后是启动类和配置文件。启动类没有任何特殊逻辑就是一个标准的 Spring Boot 入口。配置文件application.yml中一定要写上数据库连接和 MyBatis-Plus 的配置。package com.example.cardcollector; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; SpringBootApplication public class CardCollectorApplication { public static void main(String[] args) { SpringApplication.run(CardCollectorApplication.class, args); } }server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/card_db?useUnicodetruecharacterEncodingutf-8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: configuration: map-underscore-to-camel-case: truemap-underscore-to-camel-case的作用是将数据库字段collected_status自动映射到 Java 属性collectedStatus。没有这行配置的话你查询出来的对象状态字段可能会是 null这是新手很容易踩的坑。由于需要分页查询还要配置 MyBatis-Plus 的分页插件拦截器。这个配置类会在后文代码中出现你只需要知道没有它selectPage方法无法正确处理分页。4.2 编写实体、Mapper、Service 和 Controller实体类Card.java对应数据库的card表字段与表结构保持一致。使用TableName指定表名使用TableId指定主键策略。package com.example.cardcollector.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import java.time.LocalDateTime; Data TableName(card) public class Card { TableId(type IdType.AUTO) private Long id; private String name; private String code; private String attribute; private String description; private String imageUrl; private Integer collectedStatus; private Integer favoriteStatus; private LocalDateTime createTime; private LocalDateTime updateTime; }Mapper 层继承 MyBatis-Plus 的BaseMapper不需要写任何 SQL 就能获得单表 CRUD 能力。这是 MyBatis-Plus 相比传统 MyBatis 最大的优势。package com.example.cardcollector.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.example.cardcollector.entity.Card; import org.apache.ibatis.annotations.Mapper; Mapper public interface CardMapper extends BaseMapperCard { }Service 层是本项目逻辑最集中的地方。我们实现了条件查询、详情查询、状态切换等功能。在条件查询方法中使用LambdaQueryWrapper来动态拼接 SQL 查询条件这样能避免字符串拼 SQL 带来的注入风险代码也更优雅。package com.example.cardcollector.service; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.example.cardcollector.entity.Card; import com.example.cardcollector.mapper.CardMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.time.LocalDateTime; import java.util.List; import java.util.stream.Collectors; Service public class CardService { Autowired private CardMapper cardMapper; public IPageCard listCards(long current, long size, String keyword, String attribute, Integer collectedStatus) { LambdaQueryWrapperCard wrapper new LambdaQueryWrapper(); wrapper.like(keyword ! null !keyword.isEmpty(), Card::getName, keyword) .eq(attribute ! null !attribute.isEmpty(), Card::getAttribute, attribute) .eq(collectedStatus ! null, Card::getCollectedStatus, collectedStatus) .orderByAsc(Card::getId); return cardMapper.selectPage(new Page(current, size), wrapper); } public Card getCard(Long id) { return cardMapper.selectById(id); } public boolean updateCard(Card card) { card.setUpdateTime(LocalDateTime.now()); return cardMapper.updateById(card) 0; } public boolean toggleCollected(Long id) { Card card getCard(id); if (card null) { return false; } int newStatus card.getCollectedStatus() ! null card.getCollectedStatus() 1 ? 0 : 1; card.setCollectedStatus(newStatus); card.setUpdateTime(LocalDateTime.now()); return cardMapper.updateById(card) 0; } public boolean toggleFavorite(Long id) { Card card getCard(id); if (card null) { return false; } int newStatus card.getFavoriteStatus() ! null card.getFavoriteStatus() 1 ? 0 : 1; card.setFavoriteStatus(newStatus); card.setUpdateTime(LocalDateTime.now()); return cardMapper.updateById(card) 0; } public ListString listAttributes() { ListCard cards cardMapper.selectList(null); return cards.stream() .map(Card::getAttribute) .distinct() .collect(Collectors.toList()); } }这里要重点解释一下toggleCollected和toggleFavorite的逻辑根据当前状态做“取反”如果原来是 0 就变成 1原来是 1 就变成 0。这样做的好处是前端不需要传递目标状态只需要告诉后端“我点击了收集按钮”后端根据数据库当前值自动切换。实际项目中你也可以设计成传入目标状态取决于你的业务约定。Controller 层负责暴露 HTTP 接口包含查询、切换状态和更新操作。package com.example.cardcollector.controller; import com.baomidou.mybatisplus.core.metadata.IPage; import com.example.cardcollector.entity.Card; import com.example.cardcollector.service.CardService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.util.List; RestController RequestMapping(/api/cards) public class CardController { Autowired private CardService cardService; GetMapping public IPageCard list(RequestParam(defaultValue 1) long current, RequestParam(defaultValue 10) long size, RequestParam(required false) String keyword, RequestParam(required false) String attribute, RequestParam(required false) Integer collectedStatus) { return cardService.listCards(current, size, keyword, attribute, collectedStatus); } GetMapping(/attributes) public ListString attributes() { return cardService.listAttributes(); } GetMapping(/{id}) public Card detail(PathVariable Long id) { return cardService.getCard(id); } PutMapping(/{id}) public boolean update(PathVariable Long id, RequestBody Card card) { card.setId(id); return cardService.updateCard(card); } PutMapping(/{id}/collected) public boolean toggleCollected(PathVariable Long id) { return cardService.toggleCollected(id); } PutMapping(/{id}/favorite) public boolean toggleFavorite(PathVariable Long id) { return cardService.toggleFavorite(id); } }最后是 MyBatis-Plus 分页配置类。没有这个类分页查询会直接返回全部数据或者报错这是 MyBatis-Plus 3.5.x 版本的一个重要行为。package com.example.cardcollector.config; import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }到这里后端核心代码就写完了。此时你可以先启动一下应用通过浏览器访问http://localhost:8080/api/cards来验证接口是否正常。如果一切顺利你会看到一页 JSON 数据这代表后端已经成功连上了数据库。4.3 创建前端项目并配置代理前端我们使用 Vite Vue 3 构建组件库用 Element Plus。首先创建项目目录也可以使用npm create vitelatest card-collector-web自动生成然后手动补充下面几个核心文件。package.json中需要的依赖如下Vite 和 Vue 的版本仅供参考实际使用建议用最新稳定版。{ name: card-collector-web, version: 0.0.1, scripts: { dev: vite, build: vite build }, dependencies: { vue: ^3.4.15, element-plus: ^2.5.0, axios: ^1.6.0 }, devDependencies: { vitejs/plugin-vue: ^5.0.0, vite: ^5.0.0 } }Vite 开发服务器默认端口是 5173后端接口在 8080存在跨域问题。最简单的方式是在vite.config.js中配置代理把/api前缀的请求转发到后端地址这样前端代码里请求路径可以保持简洁也不需要后端启用跨域配置。配置文件如下import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], server: { port: 3000, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })入口文件main.js负责挂载 Vue 应用和 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)4.4 编写卡牌图鉴页面页面是用户直接看到的部分也是“魔卡少女樱”风格最容易体现的地方。我们用一个App.vue文件承载整个图鉴页面包括搜索框、属性筛选下拉框和卡牌网格。这里使用的都是 Vue 3 Composition API 的写法也就是script setup语法比 Options API 更简洁也更适合新项目。template div classcontainer h1魔卡少女樱 · 卡牌收集图鉴/h1 div classfilter el-input v-modelkeyword placeholder搜索卡牌名称 clearable stylewidth: 220px inputfetchCards / el-select v-modelattribute placeholder选择属性 clearable stylewidth: 140px changefetchCards el-option v-foritem in attributeList :keyitem :labelitem :valueitem / /el-select /div el-row :gutter20 el-col :span8 v-forcard in cards :keycard.id el-card classcard-item :class{ collected: card.collectedStatus 1 } img :srccard.imageUrl classcard-img / h3{{ card.name }}{{ card.code }}/h3 p属性{{ card.attribute }}/p p{{ card.description }}/p div classactions el-button :typecard.collectedStatus 1 ? success : primary sizesmall clicktoggleCollected(card) {{ card.collectedStatus 1 ? 已收集 : 未收集 }} /el-button el-button :typecard.favoriteStatus 1 ? warning : default sizesmall clicktoggleFavorite(card) {{ card.favoriteStatus 1 ? 已收藏 : 收藏 }} /el-button /div /el-card /el-col /el-row /div /template script setup import { ref, onMounted } from vue import axios from axios import { ElMessage } from element-plus const cards ref([]) const keyword ref() const attribute ref() const attributeList ref([]) const fetchCards async () { const { data } await axios.get(/api/cards, { params: { current: 1, size: 30, keyword: keyword.value, attribute: attribute.value } }) cards.value data.records } const fetchAttributes async () { const { data } await axios.get(/api/cards/attributes) attributeList.value data } const toggleCollected async (card) { await axios.put(/api/cards/${card.id}/collected) ElMessage.success(card.collectedStatus 0 ? 收集成功 : 已移出收集) fetchCards() } const toggleFavorite async (card) { await axios.put(/api/cards/${card.id}/favorite) ElMessage.success(card.favoriteStatus 0 ? 收藏成功 : 已取消收藏) fetchCards() } onMounted(() { fetchCards() fetchAttributes() }) /script style scoped .container { max-width: 1200px; margin: 0 auto; padding: 20px; } .filter { margin-bottom: 20px; display: flex; gap: 12px; } .card-item { margin-bottom: 20px; text-align: center; } .card-item.collected { border: 2px solid #67c23a; } .card-img { width: 150px; height: 210px; object-fit: cover; border-radius: 8px; } .actions { margin-top: 10px; } /style上面模板中el-input的input事件会在用户输入时触发但是需要注意如果不做防抖每敲一个字母都会调用一次接口。项目规模小无所谓真实项目中建议给搜索框加一个 300ms 的防抖函数避免频繁请求后端。这是一个很重要的前端性能细节。fetchCards方法请求后端列表接口并读取返回体中的records字段因为 MyBatis-Plus 分页对象默认使用records作为当前页数据列表。如果你忘记了这个字段名前端拿到的数据是 undefined页面就会空白。4.5 运行与验证后端运行步骤确保 MySQL 已启动并执行建表 SQL。修改application.yml中的数据库用户名和密码。在 IDEA 中运行CardCollectorApplication或在项目根目录执行mvn spring-boot:run。浏览器访问http://localhost:8080/api/cards如果能返回 JSON 数据说明后端正常。前端运行步骤在card-collector-web目录下执行npm install安装依赖。执行npm run dev启动 Vite 开发服务器。浏览器访问http://localhost:3000。打开页面后你会看到卡牌以网格形式排列。点击“未收集”按钮按钮会变成绿色的“已收集”点击“收藏”按钮按钮会变成黄色的“已收藏”。下拉框可以选择属性搜索框可以按名称模糊搜索。这一切操作都会实时刷新页面数据。如果你在图片位置看到破图不用担心那只是因为示例数据中的图片路径不存在。你可以在public/images下面放几张图片或者直接修改数据库中的image_url为任意图片链接。5. 常见问题与排查思路任何项目都不可能一次启动成功尤其是第一次做前后端分离开发时总会出现各种奇怪的问题。下面我把最常见的几个坑整理成表格并给出排查思路。问题现象常见原因解决思路后端启动时报数据库连接失败MySQL 未启动、用户名密码错误、库名写错检查application.yml连接配置确认数据库存在请求/api/cards返回 404后端没有启动或前端代理没生效先直接访问后端接口再检查 Vite 代理配置返回的 JSON 中collectedStatus为 null数据表字段是collected_status未开启驼峰映射检查map-underscore-to-camel-case: true分页查询返回全部数据或报错没有配置 MyBatis-Plus 分页插件添加MybatisPlusConfig配置类前端页面无数据但接口正常前端读取数据字段名错误MyBatis-Plus 分页数据在records中修改状态后页面不刷新fetchCards()没调用或接口报错打开浏览器控制台看请求状态检查后端日志端口被占用8080 或 3000 被其他程序占用修改配置文件中的端口或终止占用进程除了表格里的内容再单独提醒一个前端问题Element Plus 组件是异步加载的如果页面首次打开时控制台报“ElementPlus is not defined”多半是在main.js中忘记执行app.use(ElementPlus)。这是新手最常犯的错误之一。另一个常见问题是Vue 3 中vue-router使用方式与 Vue 2 完全不同本文虽然没有使用路由但如果你后续要扩展多页面图鉴需要特别注意版本差异。6. 最佳实践与工程建议项目能跑通只是第一步真正常被面试官或者技术负责人追问的是“你如何保证代码质量和可维护性”。这一节分享几个我在实际开发中比较重视的工程化细节。6.1 数据库状态字段不要裸用collected_status和favorite_status在业务里是枚举值但代码中直接写数字 0 和 1 可读性很差。建议在 Java 中定义一个枚举类把状态含义和数字对应关系集中管理。例如CollectedStatusEnum中包含 NOT_COLLECTED(0, 未收集) 和 COLLECTED(1, 已收集)。前端同样不要写死数字可以定义一个常量对象例如const COLLECTED_STATUS { COLLECTED: 1, NOT_COLLECTED: 0 }。这样后续如果状态数值变化只需要修改一处。6.2 接口参数校验不能省目前updateCard接口可以接收任意字段如果前端不小心传了空字符串数据库里就会多一条脏数据。实际开发中建议在实体上增加校验注解比如NotBlank(message 卡牌名称不能为空)然后在 Controller 方法参数上加Valid。数据库字段长度的校验也需要放在服务端完成不能只依赖前端表单校验。6.3 统一返回结果结构我的示例接口直接返回了boolean、IPageCard等不同类型数据这种方式在快速原型阶段很方便但在大型项目中不推荐。更好的做法是定义一个统一的响应类ApiResultT包含code、message、data三个字段。成功时返回200和业务数据失败时返回错误码和提示信息。这样前端可以统一拦截异常和提示维护成本会低很多。6.4 日志与异常处理要配套不要直接让框架把异常堆栈抛给前端。建议在 Controller 层或者全局位置通过RestControllerAdvice统一处理异常将异常信息转换为友好提示。例如当数据库查询失败时前端应该看到“服务暂时不可用”而不是 500 页面和一段英文堆栈。同时在 Service 层记录必要日志特别是状态切换这类写操作建议记录操作人、操作时间、卡牌 ID方便问题回溯。6.5 图片资源单独存储示例中图片地址放的是本地相对路径这只适合本地演示。真实项目中图片通常会上传到对象存储或独立文件服务器数据库只保存访问 URL。这样做的好处是应用服务器不保存本地文件便于扩容和迁移。你只需要把image_url字段理解为一个字符串 URL替换成 CDN 地址即可。6.6 状态切换接口要保证幂等“切换”操作连续点击两次会回到原状态这通常没有问题。但如果你设计的是“设置成已收集”接口就必须考虑重复调用不会产生副作用。在真实项目里收藏和收集动作往往需要记录时间甚至还要记录来源渠道因此不要在切换方法内部直接取反而是可以采用“目标状态”模式将状态修改放到一个事务里执行。同时接口应做幂等设计防止用户重复提交导致数据不一致。7. 总结与学习路线到这里完整的“卡牌收集系统”已经全部落地。我们完成了数据库设计、后端接口开发、前端页面搭建和联调验证整个过程覆盖了 Spring Boot、MyBatis-Plus、Vue 3、Element Plus、Axios 和 MySQL 这些主流技术栈。你现在可以继续做三件事第一把项目代码结构重新梳理一遍尝试加入卡牌分类表让图鉴更丰富第二给系统增加登录和用户体系让每个用户收藏的卡牌独立存储第三把接口返回结构改成统一的ApiResultT并做好全局异常处理和日志记录。学习这类全栈项目最忌讳的是“只复制代码不思考”。建议你关闭这篇教程尝试凭记忆把项目从零写一遍。第一次不成功很正常遇到报错就是最好的学习机会。卡牌收集系统的核心在于状态管理和列表查询这两个能力在今后几乎任何业务系统中都会用到值得多花时间打磨。如果你有更好的扩展想法比如给魔卡少女樱的每张卡牌加入音效、BGM 或者抽卡动画也可以在评论区分享出来。动手写一个属于你自己的卡牌图鉴比看一百遍教程都有用。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门