ARTICLE DETAIL

资讯详情

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

SpringBoot+Vue网上书城实战:从开发到部署全链路避坑指南

SpringBoot+Vue网上书城实战:从开发到部署全链路避坑指南 简介这是一套基于SpringBoot与Vue.js全栈开发的网上书城实战项目面向Java后端与前端初学者、毕业设计开发者及Web全栈学习者覆盖用户前台购书、后台商品管理、订单支付等核心电商功能。资源包共612个文件包含44个Java后端控制器与实体类如BookController、User、Order等、48个JS前端逻辑文件、50个CSS样式文件、141个XML配置文件含MyBatis映射、以及大量图片资源156张JPG、46张PNG和HTML页面模板整体压缩包22.31MB结构清晰前后端分离明确。已有2604人学习下载项目集成Shiro权限控制、JWT登录认证、Swagger-UI接口文档、FastDFS分布式文件存储及Nginx反向代理配套MySQL建库脚本与Redis启动说明开箱即用。读者可直接部署运行深入理解电商系统架构设计、前后端联调流程与主流技术栈协同实践。1. 为什么一个“网上书城”项目成了 SpringBoot Vue 组合的练兵场你不是在做一个电商巨头的替代品而是在验证一套能闭环交付、可快速迭代、前后端职责清晰、新人能上手、老手能压测的工程范式。网上书城这个场景表面看是图书增删改查购物车订单但背后藏着真实业务里最常踩的坑用户登录态跨域怎么稳Vue 路由懒加载和 SpringBoot 静态资源路径怎么不打架图书搜索既要支持关键词模糊匹配又不能把《Java 编程思想》搜成“Java 思想编程”——这得靠分词策略和接口响应时间的平衡支付回调验签失败时日志里只打了一行null你得知道该去哪层拦截器加 traceId更现实的是当产品经理说“明天上线试运营”你得确认打包后的dist/目录真能被spring-boot-starter-web的ResourceHandler正确映射而不是打开首页就看到Cannot GET /。这不是玩具项目。它用最小业务集用户、图书、分类、购物车、订单、评论覆盖了 SpringBoot 的自动配置边界如ConditionalOnClass在集成 Redis 时的触发逻辑、Vue 的构建时与运行时差异process.env.NODE_ENV在vue.config.js和组件中取值时机不同、以及两者粘合处最脆弱的环节API 前缀统一管理、401 跳转逻辑、文件上传的 multipart 处理链路。我带过的 3 届实习生都是从跑通这个项目开始真正理解什么叫“部署即验证”——不是mvn clean package成功而是java -jar bookmall.jar启动后浏览器输入http://localhost:8080真的能加载出首页轮播图且点击“立即购买”后控制台 Network 面板里能看到POST /api/order/create返回200并带orderNo字段。这才是起点。2. 服务端SpringBoot 项目结构与核心模块落地2.1 项目骨架初始化Maven 依赖选型与版本锁定网上书城不需要 Flink 实时计算也不需要 ActiveMQ 异步解耦初期订单量 100 单/天但必须避开spring-boot-starter-web默认嵌入 Tomcat 与 Vue 打包后静态资源冲突的雷区。常见翻车点是用spring-boot-starter-thymeleaf混合渲染结果 Vue 的router-view被 Thymeleaf 模板引擎提前解析成空 div。正确做法是纯前后端分离SpringBoot 只做 API Server。!-- pom.xml 核心依赖 -- dependencies !-- Web 基础排除默认 Tomcat为后续 Nginx 静态托管留路 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId exclusions exclusion groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-tomcat/artifactId /exclusion /exclusions /dependency !-- 替换为 Undertow轻量且对长连接更友好为未来 WebSocket 评论推送预留 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-undertow/artifactId /dependency !-- MyBatis-Plus 3.5.3.1注意不是最新版4.x 对 JDK 17 兼容性有坑而网上书城多数部署在 JDK 8/11 -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency !-- MySQL 驱动8.0.33 是当前生产环境最稳版本避免 useSSLtrue 导致连接失败 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version scoperuntime/scope /dependency !-- Lombok省掉 70% 的 getter/setter但必须配合 IDEA Lombok Plugin否则编译报错 -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency !-- Hutool 工具包处理日期格式化、JSON 转换、文件上传校验等琐碎逻辑比 Apache Commons 更贴合国内开发习惯 -- dependency groupIdcn.hutool/groupId artifactIdhutool-all/artifactId version5.8.22/version /dependency /dependencies提示spring-boot-starter-undertow替代 Tomcat 不是为了性能玄学而是避免server.tomcat.basedir与 Vuepublic/目录路径混淆。Undertow 默认不创建临时工作目录静态资源路径更可控。2.2 数据库设计图书、用户、订单三张核心表的字段取舍网上书城不是 ERP字段宁缺毋滥。比如“图书表”book新手常犯的错是照搬京东字段author_introduction、editor_recommendation、catalog_content全塞进去结果列表页加载慢、搜索响应超时。实际业务中90% 的流量集中在封面图、书名、作者、价格、库存、分类 ID 这 6 个字段。其他字段用TableField(exist false)标记在详情页按需查。-- book 表InnoDButf8mb4_unicode_ci CREATE TABLE book ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键, isbn varchar(13) NOT NULL COMMENT ISBN-13唯一索引, title varchar(100) NOT NULL COMMENT 书名, author varchar(50) NOT NULL COMMENT 作者, price decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT 售价, stock int NOT NULL DEFAULT 0 COMMENT 库存, cover_url varchar(255) DEFAULT NULL COMMENT 封面图 URL相对路径如 /covers/123.jpg, category_id bigint NOT NULL COMMENT 分类 ID, status tinyint NOT NULL DEFAULT 1 COMMENT 状态1-上架0-下架, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_isbn (isbn), KEY idx_category_status (category_id,status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci;关键点isbn设为UNIQUE KEY而非主键因为 ISBN 有校验逻辑最后一位可能是 X且部分古籍无 ISBNcover_url存相对路径由 Nginx 或 SpringBoot 的WebMvcConfigurer统一映射到/covers/**避免前端硬编码域名idx_category_status复合索引支撑“文学类 上架图书”这类高频查询比单列category_id索引快 3 倍以上实测 10 万数据。2.3 API 分层设计Controller → Service → Mapper 的责任切分网上书城的“加入购物车”接口最容易写成大泥球。正确分层是CartController只做三件事——校验 JWT Token、解析RequestBody CartAddDTO、调用cartService.add()、返回Result.success()CartService事务边界在此包含“检查库存是否充足”、“检查用户购物车是否已达上限如 50 件”、“生成或更新 cart_item 记录”CartItemMapper只负责单表 CRUD绝不写 JOIN 查询。购物车列表需要显示图书名称、价格、封面图那是CartService调用bookService.getById(cartItem.getBookId())拆成两次查询用Cacheable缓存图书信息而不是在CartItemMapper里写LEFT JOIN book。// CartService.java Transactional(rollbackFor Exception.class) public void add(Long userId, Long bookId, Integer count) { // 1. 检查库存先查再扣避免超卖 Book book bookService.getById(bookId); if (book null || book.getStock() count) { throw new BusinessException(库存不足); } // 2. 检查购物车容量业务规则单用户最多 50 件 long cartItemCount cartItemMapper.selectCount( new QueryWrapperCartItem().eq(user_id, userId) ); if (cartItemCount 50) { throw new BusinessException(购物车已满请先结算); } // 3. 执行插入或更新乐观锁where stock #{count} and id #{bookId} int effect bookMapper.updateStock(bookId, count); // 自定义 SQL原子扣减 if (effect 0) { throw new BusinessException(库存变动请重试); } // 4. 写入购物车项忽略重复插入异常用 ON DUPLICATE KEY UPDATE CartItem item new CartItem().setUserId(userId).setBookId(bookId).setCount(count); cartItemMapper.insertOrUpdate(item); }参数说明bookMapper.updateStock()是自定义 XML SQL用UPDATE book SET stock stock - #{count} WHERE id #{bookId} AND stock #{count}实现原子扣减比先SELECT再UPDATE少一次 DB 往返且避免并发超卖。3. 前端Vue 3 Composition API 的工程化搭建3.1 Vue CLI 项目初始化与vue.config.js关键配置网上书城不用 Vite初期团队熟悉度低、插件生态不如 CLI 成熟但必须绕过vue-cli-service serve默认端口8080与 SpringBoot 冲突。同时npm run build产出的dist/目录要能被 SpringBoot 的ResourceHandler识别为静态资源根目录。// vue.config.js const path require(path) module.exports { // 关键让开发服务器代理 API 请求到 SpringBoot devServer: { port: 8081, // 避开 8080 proxy: { /api: { target: http://localhost:8080, // SpringBoot 地址 changeOrigin: true, pathRewrite: { ^/api: // 把 /api/book/list → /book/list } } } }, // 关键构建输出路径与 SpringBoot 静态资源路径对齐 outputDir: path.resolve(__dirname, ../backend/src/main/resources/static), // 关键public 目录下的文件如 favicon.ico会直接复制到 static/ 下 assetsDir: static, // 关键关闭 index.html 的 hash否则 SpringBoot 无法定位入口文件 filenameHashing: false, // 关键配置 webpack alias避免 import /components/BookList.vue 写成绝对路径 configureWebpack: { resolve: { alias: { : path.resolve(__dirname, src) } } } }血泪经验outputDir必须指向 SpringBoot 的src/main/resources/static而不是src/main/webappSpringBoot 2.0 已废弃该目录。否则mvn package后bookmall.jar里没有前端资源启动报Whitelabel Error Page。3.2 路由与权限控制基于vue-router的动态路由方案网上书城的权限分三级游客可浏览图书、搜索、登录用户可加购、下单、管理员可管理图书、订单。用router.beforeEach全局守卫 后端返回的userRole字段控制比前端硬编码meta.roles更可靠。// router/index.js import { createRouter, createWebHistory } from vue-router import HomeView from ../views/HomeView.vue import BookDetailView from ../views/BookDetailView.vue import CartView from ../views/CartView.vue import OrderCreateView from ../views/OrderCreateView.vue const routes [ { path: /, name: Home, component: HomeView }, { path: /book/:id, name: BookDetail, component: BookDetailView, props: true }, { path: /cart, name: Cart, component: CartView, meta: { requiresAuth: true } }, { path: /order/create, name: OrderCreate, component: OrderCreateView, meta: { requiresAuth: true } }, { path: /admin/books, name: AdminBooks, component: () import(../views/admin/BookManage.vue), meta: { requiresAuth: true, role: ADMIN } } ] const router createRouter({ history: createWebHistory(), routes }) // 全局路由守卫 router.beforeEach(async (to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next(/login) return } if (to.meta.role) { // 从 localStorage 读取用户角色登录后由后端返回并存储 const userRole localStorage.getItem(userRole) if (userRole ! to.meta.role) { next(/403) return } } next() }) export default router注意userRole不应存在 Vuex/Pinia 中而应存于localStorage。因为页面刷新后 Pinia store 丢失但localStorage持久化避免用户登录后刷新就跳回登录页。3.3 图书搜索实现Vue 中调用后端分词接口的实践热搜词里有hanlp分词在springboot但网上书城不需要复杂 NLP。用 MySQL 的MATCH AGAINST全文索引即可满足 95% 场景。Vue 端只需传关键词后端用BookService.search(keyword)封装逻辑。!-- components/SearchBar.vue -- template div classsearch-bar input v-modelkeyword keyup.enterhandleSearch placeholder搜索图书、作者... classsearch-input / button clickhandleSearch classsearch-btn搜索/button /div /template script setup import { ref } from vue import { useRouter } from vue-router import { useBookStore } from /stores/book const keyword ref() const router useRouter() const bookStore useBookStore() const handleSearch () { if (!keyword.value.trim()) return // 触发全局搜索状态用于面包屑显示 bookStore.setSearchKeyword(keyword.value) // 跳转到搜索结果页携带关键词作为 query 参数 router.push({ path: /search, query: { q: keyword.value } }) } /script后端对应接口// BookController.java GetMapping(/search) public ResultListBook search(RequestParam String q) { // 关键对关键词做基础清洗防止 SQL 注入 String cleanQ q.replaceAll([^\\u4e00-\\u9fa5a-zA-Z0-9\\s], ) ListBook books bookService.searchByKeyword(cleanQ) return Result.success(books) } // BookServiceImpl.java Override public ListBook searchByKeyword(String keyword) { // 使用 MySQL 全文索引需提前建索引ALTER TABLE book ADD FULLTEXT(title, author) return bookMapper.selectList( new QueryWrapperBook() .apply(MATCH(title, author) AGAINST({0} IN NATURAL LANGUAGE MODE), keyword) .eq(status, 1) // 只查上架图书 ) }避坑点MySQL 全文索引对短词如“Java”效果差需配合IN BOOLEAN MODE或WITH QUERY EXPANSION。但网上书城初期直接用NATURAL LANGUAGE MODE 前端关键词高亮用span classhighlightJava/span替换已足够。4. 前后端联调API 通信、跨域、静态资源托管的避坑指南4.1 开发阶段Vue 代理与 SpringBoot CORS 配置的双重保险网上书城开发时Vue 运行在http://localhost:8081SpringBoot 在http://localhost:8080跨域是必然。只配 Vue 代理或只配 SpringBoot CORS 都不保险必须双管齐下。Vue 代理已见 3.1 节解决开发时浏览器请求被拦截问题。SpringBoot CORS补充防止某些特殊请求如带Content-Type: application/json的 POST被浏览器预检OPTIONS拒绝。// config/WebMvcConfig.java Configuration public class WebMvcConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOrigins(http://localhost:8081) // 严格指定不用 *带 credentials 时禁止 .allowCredentials(true) // 允许携带 cookie用于登录态 .maxAge(3600) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS); } }现象 → 原因 → 解决现象登录成功后/api/cart/list接口返回401 Unauthorized但console.log(localStorage.getItem(token))显示 token 存在。原因Vue 请求未设置withCredentials: true导致浏览器不发送 CookieSpringBoot 的JwtAuthenticationFilter拿不到 token。解决在utils/request.js中全局配置axios.defaults.withCredentials true4.2 生产阶段Vue 打包产物如何无缝集成到 SpringBoot Jar这是网上书城项目最常翻车的环节。npm run build生成的dist/目录必须精确落入 SpringBoot 的static/目录且index.html的base路径要与 SpringBoot 的server.servlet.context-path一致。# 正确流程Linux/macOS cd frontend npm run build # 此时 dist/ 目录已生成且 vue.config.js 的 outputDir 指向 backend/src/main/resources/static # 确保 backend 目录下已有 static/ 文件夹若无手动创建 mkdir -p ../backend/src/main/resources/static # 复制 dist 内容到 static注意不是复制 dist 文件夹是复制其内部所有文件 cp -r dist/* ../backend/src/main/resources/static/ # 进入 backend 目录打包 cd ../backend mvn clean package关键检查点target/bookmall.jar!/BOOT-INF/classes/static/下必须有index.html、js/、css/、img/目录index.html中script src/js/app.js的/js/前缀必须与 SpringBoot 的server.servlet.context-path/匹配若设为/mall则 script src 应为/mall/js/app.jsSpringBoot 启动日志中必须出现Mapped URL [/]** to HandlerExecutionChain表明静态资源处理器已注册。现象 → 原因 → 解决现象java -jar bookmall.jar启动后访问http://localhost:8080显示Whitelabel Error Page。原因index.html被 SpringBoot 当作普通资源由ResourceHttpRequestHandler处理但index.html中引用的js/app.js404导致 Vue 应用无法挂载。解决检查backend/src/main/resources/static/目录结构确认js/、css/目录存在且非空用jar -tf bookmall.jar | grep static验证 jar 包内路径。4.3 文件上传Vue Element Plus SpringBoot MultipartFile 的完整链路网上书城后台需上传图书封面。Vue 用el-upload后端用RequestParam MultipartFile file但必须处理好文件大小限制、类型校验、存储路径。!-- views/admin/BookForm.vue -- el-upload classupload-demo action :http-requesthandleUpload :show-file-listfalse :before-uploadbeforeUpload el-button sizesmall typeprimary点击上传/el-button div slottip classel-upload__tip只能上传 jpg/png 文件且不超过 2MB/div /el-upload// handleUpload 方法 const handleUpload (params) { const formData new FormData() formData.append(file, params.file) axios.post(/api/upload/cover, formData, { headers: { Content-Type: multipart/form-data } }).then(res { // 保存返回的 coverUrl 到表单数据 form.coverUrl res.data.url }) } const beforeUpload (file) { const isJPG file.type image/jpeg || file.type image/png const isLt2M file.size / 1024 / 1024 2 if (!isJPG) { ElMessage.error(上传封面只能是 JPG/PNG 格式!) } if (!isLt2M) { ElMessage.error(上传封面大小不能超过 2MB!) } return isJPG isLt2M }// UploadController.java PostMapping(/upload/cover) public ResultString uploadCover(RequestParam MultipartFile file) { // 1. 校验文件类型双重校验前端 后端 String contentType file.getContentType() if (!image/jpeg.equals(contentType) !image/png.equals(contentType)) { throw new BusinessException(封面格式不支持仅支持 JPG/PNG) } // 2. 校验文件大小MultipartFile.getSize() 是字节2MB 2097152 if (file.getSize() 2097152) { throw new BusinessException(封面大小不能超过 2MB) } // 3. 生成唯一文件名避免中文乱码、覆盖 String originalFilename file.getOriginalFilename() String ext FilenameUtils.getExtension(originalFilename) String newFilename IdUtil.fastSimpleUUID() . ext // 4. 保存到服务器指定目录如 /opt/bookmall/covers/ String uploadPath /opt/bookmall/covers/ File uploadDir new File(uploadPath) if (!uploadDir.exists()) { uploadDir.mkdirs() } try { file.transferTo(new File(uploadPath newFilename)) } catch (IOException e) { throw new BusinessException(封面上传失败 e.getMessage()) } // 5. 返回相对路径供前端存入数据库 return Result.success(/covers/ newFilename) }现象 → 原因 → 解决现象上传成功后数据库存入/covers/abc.jpg但页面显示 404。原因SpringBoot 默认只映射classpath:/static/**而/covers/是服务器绝对路径未配置资源处理器。解决在WebMvcConfig.java中添加Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/covers/**) .addResourceLocations(file:/opt/bookmall/covers/); }5. 部署与验证从本地调试到 Linux 服务器的一键脚本5.1 本地一键启动脚本start-dev.sh统一管理前后端网上书城开发时频繁切换目录、开多个终端窗口极易出错。写一个 shell 脚本用tmux或screen启动两个 pane比 IDE 插件更可靠。#!/bin/bash # start-dev.sh echo 启动网上书城开发环境... # 启动 SpringBoot 后端监听 8080 echo ✅ 启动后端... cd backend mvn spring-boot:run -Dspring-boot.run.jvmArguments-Xdebug -Xrunjdwp:transportdt_socket,servery,suspendn,address5005 /dev/null 21 BACKEND_PID$! cd .. # 启动 Vue 前端监听 8081 echo ✅ 启动前端... cd frontend npm run serve /dev/null 21 FRONTEND_PID$! cd .. echo 前端地址http://localhost:8081 echo 后端地址http://localhost:8080 echo 调试端口5005IDEA Attach # 捕获 CtrlC优雅关闭 trap echo 正在关闭服务...; kill $BACKEND_PID $FRONTEND_PID; exit SIGINT # 等待用户中断 wait使用方式chmod x start-dev.sh ./start-dev.sh。脚本会后台启动两个服务并监听CtrlC自动 kill 进程避免端口占用。5.2 生产环境部署Nginx SpringBoot Jar 的最小化配置网上书城上线不推荐用java -jar直接暴露 8080 端口。必须用 Nginx 做反向代理和静态资源托管这是安全基线。# /etc/nginx/conf.d/bookmall.conf upstream bookmall_backend { server 127.0.0.1:8080; } server { listen 80; server_name bookmall.example.com; # 静态资源由 Nginx 直接服务比 SpringBoot 快 3 倍 location / { root /opt/bookmall/dist; try_files $uri $uri/ /index.html; } # API 请求代理到 SpringBoot location /api/ { proxy_pass http://bookmall_backend/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } # 图书封面等上传文件 location /covers/ { alias /opt/bookmall/covers/; expires 1h; } }关键点root /opt/bookmall/distNginx 直接读取 Vue 打包后的dist/目录不经过 SpringBoottry_files $uri $uri/ /index.html解决 Vue Router history 模式下刷新 404 问题location /api/末尾的/必须保留否则proxy_pass会把/api/book/list错误转发为http://127.0.0.1:8080/api/book/list多了一层 apiexpires 1h为封面图设置缓存减少服务器压力。5.3 验证清单上线前必须跑通的 5 个黄金用例不要相信“打包成功就万事大吉”。网上书城上线前必须人工执行以下验证每一条都对应一个真实用户旅程用例操作步骤预期结果验证人1. 首页加载浏览器访问http://bookmall.example.com页面完全渲染轮播图自动播放分类导航栏显示正常Network 面板无 404前端2. 图书搜索在首页搜索框输入“设计模式”回车跳转到搜索页显示至少 3 本相关图书标题中“设计模式”高亮全员3. 加入购物车打开一本图书详情页 → 点击“加入购物车” → 顶部购物车图标数字1 → 点击购物车图标购物车页面列出该图书显示正确价格、数量localStorage中cartItems数组长度为 1后端4. 用户登录点击“登录” → 输入测试账号密码 → 点击登录跳转回首页右上角显示用户名localStorage中token和userRole存在后续 API 请求带Authorizationheader测试5. 订单创建购物车中勾选商品 → 点击“去结算” → 填写收货地址 → 点击“提交订单”页面跳转到订单成功页显示orderNo数据库order表新增一条记录status1待支付运维我的习惯每次上线前我会用一部安卓手机、一部 iPhone、一台 Windows 笔记本、一台 macOS 笔记本分别打开http://bookmall.example.com执行这 5 个用例。不是为了测兼容性而是为了确认 DNS 解析、HTTPS 证书、CDN 缓存、移动端 viewport 设置全部生效。曾经有一次iPhone 上首页白屏查出来是viewportmeta 标签漏写了initial-scale1.0而安卓机默认兼容模式没暴露这个问题。这种细节自动化测试覆盖不了必须人眼确认。希望帮到你。本文还有配套的精品资源点击获取
返回列表