ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Spring Boot + Vue.js 全栈实战:手作发圈盲盒商城系统开发指南

2026/8/6 2:38:16 拓冰建站 浏览量
Spring Boot + Vue.js 全栈实战:手作发圈盲盒商城系统开发指南 最近在整理个人项目时发现很多开发者对于如何将一个小型、有趣的创意项目比如一个“手作发圈盲盒”的电商概念快速、低成本地实现成一个可演示的Web应用很感兴趣。这类项目麻雀虽小五脏俱全非常适合用来练习前后端分离、API设计、支付模拟等全栈技能。本文将手把手带你从零开始用最主流的技术栈Spring Boot Vue.js构建一个模拟的“手作发圈盲盒”商城系统涵盖商品展示、盲盒抽取、模拟下单全流程。无论你是想学习全栈开发还是为毕业设计寻找灵感这篇文章都能提供一套完整、可复现的代码方案。1. 项目背景与核心概念在深入代码之前我们有必要厘清这个项目要解决的核心问题以及涉及的关键概念。1.1 什么是“手作发圈盲盒”这是一个结合了手工制作手作、饰品发圈和流行玩法盲盒的电商概念。用户支付一个固定价格如59.9元随机获得一个或多个未知款式的手工发圈。其魅力在于“未知的惊喜感”类似于潮玩盲盒但成本和技术门槛更低适合作为个人手作创业或练手项目。1.2 技术项目目标我们的目标不是搭建一个真正的电商平台而是构建一个高度仿真的技术演示系统。通过这个项目我们将实践以下技术点后端服务提供RESTful API管理商品盲盒、处理“购买”逻辑。前端界面展示精美的商品页面实现交互式的“开盲盒”动画效果。模拟流程完整走通“浏览 - 选择 - 模拟支付 - 获取结果”的用户链路。技术栈整合Spring Boot (后端) Vue.js (前端) 的经典组合学习前后端如何通信。1.3 核心业务流程商品展示前端页面展示盲盒的封面、价格、描述。用户“购买”用户点击购买按钮前端调用后端API。后端“抽奖”逻辑后端接收到请求后根据预设的概率例如普通款70%隐藏款20%稀有款10%随机选择一个发圈款式。返回结果后端将抽中的发圈信息图片、名称、描述返回给前端。前端展示结果前端以一种具有“悬念感”的方式如翻转动画、慢慢揭示展示用户抽中的具体发圈。接下来我们将从环境搭建开始一步步实现这个系统。2. 环境准备与版本说明一个清晰的环境是项目成功的基石。请确保你的开发环境包含以下组件。2.1 后端环境 (Spring Boot)JDK: 版本 8 或 11 (推荐11长期支持版本)。本文示例使用 JDK 11。构建工具: Apache Maven 3.6 或 Gradle 6.x。本文使用 Maven 进行依赖管理。IDE: IntelliJ IDEA (推荐)、Eclipse 或 VS Code。数据库 (可选): 本项目为简化演示数据存储在内存中。实际项目中可使用 MySQL、PostgreSQL等。如需持久化请提前安装。Spring Boot 版本: 2.7.x (一个稳定且资料丰富的版本)。2.2 前端环境 (Vue.js)Node.js: 版本 14.x 或 16.x。这是运行 Vue 和 npm 的基础。包管理器: npm 或 yarn。本文使用 npm。Vue 版本: Vue 3 (组合式API更现代) 或 Vue 2 (选项式API更稳定)。本文示例使用 Vue 3。前端构建工具: Vite (推荐速度极快) 或 Vue CLI。本文使用 Vite 创建项目。UI 库 (可选): 为了快速搭建美观界面我们可以引入 Element Plus (Vue 3) 或 Vant 等组件库。本文示例会使用 Element Plus。2.3 项目结构概览我们将创建两个独立的项目文件夹hairband-blindbox-demo/ ├── backend/ # Spring Boot 后端项目 └── frontend/ # Vue 3 前端项目两个项目分别启动通过 HTTP 接口进行通信。这是一种标准的前后端分离架构。3. 后端开发Spring Boot API 实现后端是整个应用的大脑负责业务逻辑和数据处理。我们首先来搭建后端服务。3.1 创建 Spring Boot 项目使用 Spring Initializr 或 IDE 的创建向导生成一个基础项目。Project: Maven ProjectLanguage: JavaSpring Boot: 2.7.xGroup: com.exampleArtifact: blindbox-backendDependencies: 选择Spring Web(用于提供 REST API)。生成后用 IDE 导入项目。3.2 定义数据模型 (Model)首先我们定义“盲盒商品”和“抽奖结果”的实体类。// 文件路径src/main/java/com/example/blindboxbackend/model/BlindBox.java package com.example.blindboxbackend.model; /** * 盲盒商品实体 */ public class BlindBox { private Long id; private String name; // 盲盒系列名称如“春日花园系列” private String coverImage; // 封面图片URL private String description; // 描述 private Double price; // 价格59.9 // 构造方法、Getter和Setter省略实际开发请使用Lombok或手动生成 public BlindBox(Long id, String name, String coverImage, String description, Double price) { this.id id; this.name name; this.coverImage coverImage; this.description description; this.price price; } // ... getters and setters }// 文件路径src/main/java/com/example/blindboxbackend/model/HairBand.java package com.example.blindboxbackend.model; /** * 具体的发圈款式实体 */ public class HairBand { private Long id; private String name; // 款式名称如“星空紫”、“奶油黄” private String image; // 款式图片URL private String style; // 款式类型COMMON(普通), HIDDEN(隐藏), RARE(稀有) private String description; public HairBand(Long id, String name, String image, String style, String description) { this.id id; this.name name; this.image image; this.style style; this.description description; } // ... getters and setters }// 文件路径src/main/java/com/example/blindboxbackend/model/DrawResult.java package com.example.blindboxbackend.model; /** * 抽奖结果返回对象 */ public class DrawResult { private boolean success; private String message; private HairBand hairBand; // 抽中的发圈 private BlindBox blindBox; // 所属的盲盒 public DrawResult(boolean success, String message, HairBand hairBand, BlindBox blindBox) { this.success success; this.message message; this.hairBand hairBand; this.blindBox blindBox; } // ... getters and setters }3.3 实现服务层 (Service)服务层包含核心的“抽奖”业务逻辑。我们模拟一个简单的概率系统。// 文件路径src/main/java/com/example/blindboxbackend/service/BlindBoxService.java package com.example.blindboxbackend.service; import com.example.blindboxbackend.model.*; import org.springframework.stereotype.Service; import javax.annotation.PostConstruct; import java.util.*; Service public class BlindBoxService { // 模拟数据库存储 private MapLong, BlindBox blindBoxMap new HashMap(); private ListHairBand allHairBands new ArrayList(); /** * 初始化数据模拟从数据库加载 */ PostConstruct public void initData() { // 初始化一个盲盒 BlindBox box new BlindBox(1L, 夏日海洋系列手作发圈盲盒, https://example.com/images/box-cover.jpg, 内含6款不同海洋生物主题手作发圈随机获得1个隐藏款等你发现, 59.9); blindBoxMap.put(box.getId(), box); // 初始化该盲盒下的所有可能发圈款式 allHairBands.add(new HairBand(101L, 小丑鱼橙, https://example.com/images/fish-orange.jpg, COMMON, 活力满满的橙色系)); allHairBands.add(new HairBand(102L, 海豚蓝, https://example.com/images/dolphin-blue.jpg, COMMON, 清爽的淡蓝色系)); allHairBands.add(new HairBand(103L, 水母紫, https://example.com/images/jellyfish-purple.jpg, COMMON, 梦幻的渐变色)); allHairBands.add(new HairBand(104L, 贝壳粉, https://example.com/images/shell-pink.jpg, COMMON, 温柔的少女粉)); allHairBands.add(new HairBand(105L, 海星黄, https://example.com/images/starfish-yellow.jpg, COMMON, 明亮的柠檬黄)); allHairBands.add(new HairBand(106L, 珊瑚红, https://example.com/images/coral-red.jpg, COMMON, 热情的正红色)); // 隐藏款 allHairBands.add(new HairBand(107L, 深海珍珠白, https://example.com/images/pearl-white.jpg, HIDDEN, 泛着珠光的稀有白色款)); // 稀有款 allHairBands.add(new HairBand(108L, 鲸落星空, https://example.com/images/whale-galaxy.jpg, RARE, 深蓝底色上点缀星光纪念款)); } /** * 获取盲盒列表 */ public ListBlindBox getAllBlindBoxes() { return new ArrayList(blindBoxMap.values()); } /** * 根据ID获取盲盒详情 */ public BlindBox getBlindBoxById(Long id) { return blindBoxMap.get(id); } /** * 核心抽奖方法 * param blindBoxId 盲盒ID * return 抽奖结果 */ public DrawResult drawHairBand(Long blindBoxId) { BlindBox blindBox getBlindBoxById(blindBoxId); if (blindBox null) { return new DrawResult(false, 盲盒不存在, null, null); } // 简单的概率模拟普通款70%隐藏款20%稀有款10% double random Math.random(); HairBand drawnBand null; if (random 0.7) { // 抽中普通款 drawnBand getRandomHairBandByStyle(COMMON); } else if (random 0.9) { // 抽中隐藏款 (0.7 random 0.9) drawnBand getRandomHairBandByStyle(HIDDEN); } else { // 抽中稀有款 (random 0.9) drawnBand getRandomHairBandByStyle(RARE); } if (drawnBand null) { // 如果对应类型的款式列表为空则随机返回一个 drawnBand allHairBands.get(new Random().nextInt(allHairBands.size())); } return new DrawResult(true, 恭喜你抽中了【 drawnBand.getStyle() 】款, drawnBand, blindBox); } /** * 从指定类型的发圈中随机选取一个 */ private HairBand getRandomHairBandByStyle(String style) { ListHairBand styleList allHairBands.stream() .filter(h - style.equals(h.getStyle())) .toList(); if (styleList.isEmpty()) { return null; } return styleList.get(new Random().nextInt(styleList.size())); } }3.4 创建控制器 (Controller)控制器负责接收HTTP请求调用服务并返回JSON响应。// 文件路径src/main/java/com/example/blindboxbackend/controller/BlindBoxController.java package com.example.blindboxbackend.controller; import com.example.blindboxbackend.model.BlindBox; import com.example.blindboxbackend.model.DrawResult; import com.example.blindboxbackend.service.BlindBoxService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.util.List; RestController RequestMapping(/api/blindbox) CrossOrigin(origins *) // 允许前端跨域访问生产环境应指定具体域名 public class BlindBoxController { Autowired private BlindBoxService blindBoxService; /** * 获取所有盲盒列表 */ GetMapping(/list) public ListBlindBox getBlindBoxList() { return blindBoxService.getAllBlindBoxes(); } /** * 获取指定盲盒详情 */ GetMapping(/detail/{id}) public BlindBox getBlindBoxDetail(PathVariable Long id) { return blindBoxService.getBlindBoxById(id); } /** * 进行抽奖 */ PostMapping(/draw/{id}) public DrawResult draw(PathVariable Long id) { // 在实际项目中这里需要验证用户身份、库存、支付状态等。 // 此处为演示直接调用抽奖逻辑。 return blindBoxService.drawHairBand(id); } }3.5 配置文件与启动为了前端能顺利访问我们需要配置应用端口并处理跨域上面Controller已用CrossOrigin简单处理也可通过配置类。# 文件路径src/main/resources/application.properties server.port8080 spring.application.nameblindbox-backend现在启动BlindBoxBackendApplication主类后端API服务就在http://localhost:8080运行起来了。你可以用 Postman 或浏览器测试一下接口GET http://localhost:8080/api/blindbox/listPOST http://localhost:8080/api/blindbox/draw/14. 前端开发Vue.js 界面与交互后端API就绪后我们来构建用户直接交互的前端界面。4.1 创建 Vue 3 项目打开终端进入frontend目录使用 Vite 快速创建项目。npm create vuelatest blindbox-frontend # 按照提示选择Vue Router (No), Pinia (No), ESLint (Yes)... cd blindbox-frontend npm install安装 Element Plus 和 Axios (用于HTTP请求)npm install element-plus axios npm install element-plus/icons-vue # 图标库4.2 项目结构与配置主要修改src/App.vue和创建页面组件。 在src/components下创建BlindBoxShop.vue作为主页面。 在main.js中引入 Element Plus。// 文件路径src/main.js import { createApp } from vue import App from ./App.vue import ElementPlus from element-plus import element-plus/dist/index.css import * as ElementPlusIconsVue from element-plus/icons-vue const app createApp(App) // 注册所有图标 for (const [key, component] of Object.entries(ElementPlusIconsVue)) { app.component(key, component) } app.use(ElementPlus) app.mount(#app)4.3 实现主页面组件 (BlindBoxShop.vue)这个组件将展示盲盒商品并处理购买和开奖逻辑。!-- 文件路径src/components/BlindBoxShop.vue -- template div classblindbox-shop h1 styletext-align: center; margin: 30px 0; color: #409EFF; 手作发圈盲盒商城 /h1 !-- 盲盒商品展示 -- div classbox-list el-card v-forbox in blindBoxList :keybox.id classbox-card shadowhover img :srcbox.coverImage classbox-cover alt盲盒封面 / div stylepadding: 14px; h3{{ box.name }}/h3 p classdescription{{ box.description }}/p div classprice-section span classprice¥{{ box.price }}/span el-button typeprimary :iconShoppingCart clickhandleBuy(box.id) :loadingloading[box.id] 立即购买模拟 /el-button /div /div /el-card /div !-- 抽奖结果弹窗 -- el-dialog v-modelresultDialogVisible title开奖结果 width500px center div v-ifdrawResult.success classresult-content el-result iconsuccess title恭喜你 template #extra p你抽中了 strong{{ drawResult.hairBand.style }}/strong 款式/p el-image :srcdrawResult.hairBand.image fitcover classresult-image / h3{{ drawResult.hairBand.name }}/h3 p{{ drawResult.hairBand.description }}/p p classhint此仅为技术演示无实物发货/p /template /el-result /div div v-else el-result iconerror :titledrawResult.message / /div template #footer span classdialog-footer el-button clickresultDialogVisible false关闭/el-button el-button typeprimary clickresultDialogVisible false 再抽一次 /el-button /span /template /el-dialog /div /template script setup import { ref, onMounted } from vue import { ShoppingCart } from element-plus/icons-vue import axios from axios import { ElMessage } from element-plus // 后端API基础地址 const API_BASE http://localhost:8080/api/blindbox // 响应式数据 const blindBoxList ref([]) const resultDialogVisible ref(false) const drawResult ref({ success: false, message: , hairBand: null, blindBox: null }) const loading ref({}) // 用于记录每个按钮的加载状态 // 生命周期钩子组件挂载时获取盲盒列表 onMounted(() { fetchBlindBoxList() }) // 方法获取盲盒列表 const fetchBlindBoxList async () { try { const response await axios.get(${API_BASE}/list) blindBoxList.value response.data } catch (error) { console.error(获取盲盒列表失败:, error) ElMessage.error(网络异常请稍后重试) } } // 方法处理购买抽奖逻辑 const handleBuy async (blindBoxId) { // 设置该按钮为加载状态 loading.value[blindBoxId] true try { // 调用后端抽奖API const response await axios.post(${API_BASE}/draw/${blindBoxId}) drawResult.value response.data // 显示结果弹窗 resultDialogVisible.value true } catch (error) { console.error(抽奖失败:, error) ElMessage.error(抽奖过程出现错误请重试) } finally { // 无论成功失败都取消加载状态 loading.value[blindBoxId] false } } /script style scoped .blindbox-shop { max-width: 1200px; margin: 0 auto; padding: 20px; } .box-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 30px; } .box-card { transition: transform 0.3s; } .box-card:hover { transform: translateY(-5px); } .box-cover { width: 100%; height: 200px; object-fit: cover; border-radius: 4px 4px 0 0; } .description { color: #666; font-size: 14px; line-height: 1.5; margin: 10px 0; height: 60px; overflow: hidden; text-overflow: ellipsis; } .price-section { display: flex; justify-content: space-between; align-items: center; margin-top: 15px; } .price { font-size: 24px; font-weight: bold; color: #f56c6c; } .result-image { width: 200px; height: 200px; border-radius: 10px; margin: 15px auto; display: block; border: 2px dashed #409EFF; } .hint { font-size: 12px; color: #999; text-align: center; margin-top: 10px; } /style4.4 修改 App.vue将主页面组件引入到根组件中。!-- 文件路径src/App.vue -- template div idapp BlindBoxShop / /div /template script setup import BlindBoxShop from ./components/BlindBoxShop.vue /script style #app { font-family: Avenir, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; color: #2c3e50; min-height: 100vh; background-color: #f8f9fa; } /style4.5 运行前端项目在frontend目录下运行npm run devVite 会启动开发服务器通常在http://localhost:5173。现在打开浏览器访问这个地址你应该能看到一个美观的盲盒商城页面。点击“立即购买”按钮会向后端发送请求并弹窗显示随机抽中的发圈结果。5. 核心功能扩展与优化基础功能完成后我们可以考虑增加一些更贴近真实场景的功能。5.1 添加“我的背包”功能让用户能看到自己抽中的所有发圈。这需要后端增加用户管理和数据持久化。后端创建User实体和UserDrawRecord实体关联用户ID、抽中的发圈ID和时间。draw接口需要接收用户身份可从Token或Session获取并将记录存入数据库。前端增加一个“我的背包”页面调用新的API如GET /api/user/my-hairbands获取并展示该用户的抽奖记录列表。5.2 引入状态管理 (Pinia)当应用状态如用户信息、背包列表需要在多个组件间共享时使用 Pinia 比组件间传值或事件总线更优雅。安装 Pinianpm install pinia创建userStore和inventoryStore来集中管理用户状态和背包数据。5.3 增强抽奖动画目前的抽奖结果展示比较直接。可以增加一个“开盒”动画来提升体验。使用 CSSkeyframes或 JavaScript 动画库如anime.js制作一个盲盒摇晃、打开、光芒四射的动画序列。在点击“购买”后先播放动画期间禁用按钮动画结束后再显示结果弹窗。5.4 概率配置化将概率配置70%20%10%从硬编码改为可配置例如存储在数据库或配置文件中如application.yml。这样运营人员可以动态调整概率而无需修改代码。# application.yml blindbox: probability: common: 0.7 hidden: 0.2 rare: 0.1在Service中通过Value注解或ConfigurationProperties注入这些值。6. 常见问题与排查思路在开发和运行过程中你可能会遇到以下问题问题现象可能原因排查与解决思路前端访问后端API报跨域错误 (CORS)后端未正确配置跨域1. 检查后端Controller是否添加了CrossOrigin注解。2. 或创建全局的CORS配置类。前端页面空白控制台报错1. 组件引入路径错误。2. 依赖未正确安装。3. Vue/Element Plus版本不兼容。1. 检查浏览器控制台(Console)的具体错误信息。2. 运行npm install确保依赖完整。3. 核对package.json中版本号。点击购买按钮无反应后端无日志1. 前端请求地址错误。2. 后端服务未启动。3. 网络问题。1. 打开浏览器开发者工具的“网络(Network)”选项卡查看请求是否发出、地址是否正确。2. 确认后端Spring Boot应用是否在localhost:8080成功启动。3. 尝试用Postman直接调用后端API看是否正常。后端启动失败端口被占用8080端口已被其他程序如另一个Tomcat使用。1. 在application.properties中修改server.port8081。2. 或关闭占用端口的进程。抽奖结果总是同一款后端随机数逻辑或数据初始化有问题。1. 检查BlindBoxService.initData()方法是否被正确调用PostConstruct。2. 调试Math.random()的取值范围和概率判断逻辑。Element Plus 组件样式丢失没有正确引入CSS文件。确保在main.js中导入了import element-plus/dist/index.css。7. 项目部署与生产环境考量虽然这是一个演示项目但了解如何将其部署到线上以及真实项目中需要考虑的问题对技能提升很有帮助。7.1 前端构建与部署构建静态文件在frontend目录运行npm run build。这会在dist文件夹生成优化后的静态文件HTML, JS, CSS。部署到Web服务器将dist文件夹内的所有文件上传到任何静态文件托管服务如 Nginx、Apache、对象存储阿里云OSS、腾讯云COS或 Vercel/Netlify 等平台。7.2 后端打包与部署打包JAR在backend目录运行mvn clean packageMaven或使用IDE的打包功能。会在target目录生成一个可执行的*.jar文件。服务器运行将JAR文件上传到服务器需安装对应版本JDK使用命令java -jar your-app.jar启动。推荐使用nohup或 systemd 服务来保持进程常驻。7.3 生产环境重要考量数据库务必使用真实的数据库如MySQL替换内存存储并设计合理的表结构。配置分离将数据库连接、Redis地址、文件上传路径等配置移到application-prod.yml通过启动参数--spring.profiles.activeprod激活。安全性API防护为POST /api/blindbox/draw/{id}等敏感接口添加身份认证如JWT。跨域生产环境应将CrossOrigin(origins *)改为具体的域名如origins https://your-frontend-domain.com。输入校验对接口参数进行校验如使用Valid。日志配置日志框架如Logback将日志输出到文件便于排查问题。监控与健康检查Spring Boot Actuator 可以提供应用健康状态、指标等端点方便监控。通过这个从零到一的“手作发圈盲盒”全栈项目实战我们不仅实现了一个有趣的应用概念更串联起了Spring Boot后端API开发、Vue.js前端组件化开发、前后端数据交互、基础动画效果等核心技能点。你可以在此基础上继续深化比如加入用户系统、真实的支付集成微信支付/支付宝沙箱、库存管理、更复杂的概率算法等让它成为一个真正的个人全栈作品。