简介:这是一套基于SpringBoot与Vue.js全栈开发的网上书城实战项目,面向Java后端与前端初学者、毕业设计学生及Web全栈学习者,覆盖用户购书、订单管理、后台商品与权限管控等核心电商场景。资源包共612个文件,含44个Java后端控制器与实体类(如BookController、User、Order等)、48个JS前端逻辑文件、50个CSS样式与141个XML配置文件,辅以156张界面截图和46个PNG图标资源,整体压缩包22.31MB,结构清晰、模块划分明确。已有2604人学习下载,项目完整集成Shiro权限控制、JWT登录鉴权、FastDFS分布式文件存储、Redis缓存及Nginx反向代理,并提供Swagger-UI接口文档与MyBatis数据持久层实现。读者可直接运行前后端分离架构,深入理解电商系统中支付流程模拟、购物车状态管理、RBAC后台权限设计等关键实践细节,具备良好的教学示范性与工程参考价值。
1. 为什么「基于 SpringBoot + Vue 的网上书城项目」不是练手 Demo,而是工程能力的分水岭?
你搭过 SpringBoot 后端、写过 Vue 页面,但真把「用户注册→登录→浏览图书→加入购物车→下单支付→订单管理」这条链路在本地跑通、能稳定部署、经得起并发压测、代码结构不混乱、后续还能加搜索/评论/库存预警——这已经跨过了“会用框架”的门槛,进入了真实业务交付的临界区。网上书城看着简单,实则是 Web 全栈开发的「最小完备系统」:它强制你面对前后端分离的通信契约(API 设计是否合理)、状态同步难题(购物车本地缓存 vs 后端一致性)、文件上传与静态资源托管(封面图怎么存、怎么读)、权限边界(游客/普通用户/管理员三类角色如何隔离)、以及最关键的——打包部署时 Vue 资源如何被 SpringBoot 正确识别并服务。这不是教科书里的 Hello World,而是你简历上「独立完成全栈项目」那句话的硬核注脚。适合刚学完基础语法、正卡在「不知道下一步该练什么」的 Java 或前端初学者;也适合想快速验证自己能否主导一个中小型业务模块的中级工程师。别被“书城”二字骗了——它背后是 SpringBoot 的自动配置原理、Vue 的路由守卫机制、跨域调试技巧、Nginx 反向代理配置、甚至数据库事务隔离级别的实际取舍。
2. 从零初始化:SpringBoot 后端骨架与 Vue 前端工程的精准对齐
2.1 SpringBoot 后端:选型依据与最小依赖清单
网上书城不是玩具项目,后端必须兼顾开发效率与生产可用性。我坚持用SpringBoot 2.7.x(JDK 8 兼容)或 3.1.x(JDK 17+),拒绝盲目追新——SpringBoot 3.x 强制 Jakarta EE 9+,若团队还在用 Tomcat 9 或某些老中间件,升级成本远超收益。核心依赖只保留四类:
spring-boot-starter-web:HTTP 服务基石;spring-boot-starter-data-jpa+mysql-connector-java:ORM 层,比 MyBatis 更快上手,且 JPA 的@Entity映射天然契合图书、用户、订单这类强结构化数据;spring-boot-starter-validation:校验用户注册邮箱格式、密码强度、图书价格范围,避免脏数据入库;spring-boot-starter-thymeleaf(可选):仅用于开发期快速渲染错误页或登录成功跳转页,绝不用于主页面渲染——这是前后端分离的铁律。
提示:不要引入
spring-boot-starter-security早期就加复杂权限控制。先实现「用户能注册登录」,再用@PreAuthorize("hasRole('USER')")逐步加固,否则调试时连登录接口都调不通,你会怀疑人生。
2.2 Vue 前端:Vue CLI 还是 Vite?版本与构建目标怎么定?
Vue 官方已明确 Vite 是未来,但「网上书城」这类中低交互密度的管理型应用,Vue CLI 4.5.x(Vue 2.7)仍是更稳的选择——尤其当你要对接旧版 Element UI(非 Plus)或需要兼容 IE11(部分企业内网仍存在)。若确定用 Vue 3,则必须选 Vite 4.x + Vue Router 4 + Pinia,理由很现实:Vite 的热更新速度比 Vue CLI 快 3 倍以上,改一行 CSS 不用等 5 秒 webpack 编译;Pinia 的 store 模块拆分比 Vuex 更直观,书城的「购物车模块」「用户信息模块」「图书列表模块」天然对应三个 store 文件。
初始化命令如下(以 Vue 3 + Vite 为例):
# 在项目根目录下创建 frontend 目录 mkdir frontend && cd frontend npm create vite@latest book-store -- --template vue cd book-store npm install npm install vue-router@4 pinia@2 axios@1.6关键点在于:axios版本锁死为1.6.x,因为1.7+默认启用fetch适配器,在某些老旧 Node.js 环境(如 Jenkins 构建机)下会报globalThis is not defined;而vue-router@4必须匹配 Vue 3,否则useRouter()报错。
2.3 前后端通信契约:API 设计不是写接口,而是定义协作边界
很多新手把后端写成/api/user/login,前端直接axios.post('/api/user/login'),结果部署后 404。根本问题在于没约定「路径前缀」和「跨域策略」。我的做法是:
- SpringBoot 中统一配置
server.servlet.context-path=/bookstore,所有接口实际路径为http://localhost:8080/bookstore/api/user/login; - Vue 中
axios.defaults.baseURL = '/bookstore'(注意:不是http://localhost:8080/bookstore),这样开发时用vue.config.js代理,生产时由 Nginx 统一转发; - 接口返回体强制统一为
{ code: 200, msg: "success", data: {} },code 用 200 表示业务成功,400 表示参数错误,500 表示服务器异常——前端所有请求都走同一套response.interceptors处理,不再每个组件里写if (res.code === 200)。
这个契约看似简单,却决定了后续 80% 的联调时间。不信?试试把context-path忘了,或者前端 baseURL 写成绝对地址——你将花 3 小时查 nginx 日志,而不是写业务逻辑。
3. 核心功能落地:图书管理、用户体系与购物车的三层穿透式实现
3.1 图书模块:JPA 实体设计与 RESTful API 的严格映射
网上书城的核心是「图书」,但它的字段远不止书名、作者、价格。真实场景中,你需要:
isbn:唯一标识,设为@Column(unique = true, nullable = false),避免重复上架;coverUrl:字符串存相对路径(如/uploads/9787536692930.jpg),绝不存绝对 URL,否则换域名或加 CDN 就得全量更新;stock:库存数,类型用Integer(不是int),因为 JPA 需要 null 表示“暂无库存”,而int默认为 0;status:枚举字段,用@Enumerated(EnumType.STRING)存ON_SALE,OUT_OF_STOCK,DISCONTINUED,比存数字更易维护。
Controller 层必须遵循 REST 规范:
@RestController @RequestMapping("/api/books") public class BookController { @GetMapping public Result<List<Book>> list(@RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "10") int size, @RequestParam(required = false) String keyword) { // keyword 支持模糊搜索书名/作者,用 JPQL 的 %:keyword% 实现 return Result.success(bookService.list(page, size, keyword)); } @PostMapping public Result<Book> add(@RequestBody @Valid Book book) { // @Valid 触发 @NotBlank、@Min(0) 等校验 return Result.success(bookService.save(book)); } }注意:@RequestParam的defaultValue必须显式写出,否则 page=0 会被当成 null 导致分页失效;@RequestBody前必须加@Valid,否则校验注解形同虚设。
3.2 用户体系:密码加密、JWT 生成与路由守卫的闭环
用户注册登录不是 CRUD,而是安全链条。SpringBoot 侧:
- 密码绝不明文存库,用
BCryptPasswordEncoder加密(new BCryptPasswordEncoder(12),强度 12 是当前平衡点); - 登录成功后生成 JWT,payload 至少含
userId,username,role,exp(过期时间设 2 小时),密钥用KeyGenerator.generateKey()动态生成并存入application.yml,绝不用硬编码字符串; - JWT 验证用
OncePerRequestFilter拦截,解析 token 后将Authentication放入SecurityContextHolder,后续@PreAuthorize才生效。
Vue 侧:
- 登录后将 JWT 存
localStorage(不是sessionStorage,否则关浏览器就登出,体验差); - 全局路由守卫
router.beforeEach检查 token 是否存在且未过期(用jwt-decode库解析 exp 字段),过期则清空 token 并跳转登录页; - 所有需鉴权的 API 请求头自动加
Authorization: Bearer <token>,用 axios interceptor 实现:
// utils/request.js axios.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config })这个闭环一旦断掉一环(比如前端没加 header,或后端 filter 没放 Authentication),就会出现「登录成功但进不了首页」的玄学问题。
3.3 购物车:前端内存 + 后端持久化的双写一致性方案
购物车是网上书城最易翻车的模块。纯前端存localStorage?用户换设备就丢;纯后端存 DB?每次加减都要 DB 读写,QPS 上不去。我的折中方案:
- 前端维护一份轻量级 cartItems 数组,只存
bookId,count,price,用于实时显示数量和总价; - 后端提供
/api/cart接口,支持批量增删改查,用户登录后首次访问时,用GET /api/cart拉取最新状态覆盖前端 cart; - 关键操作(如结算、清空)必须以服务端为准:点击「去结算」时,前端提交 cartItems 到
/api/orders,后端校验库存、扣减、生成订单,成功后再清空/api/cart。
这样既保证体验流畅(加减商品不卡顿),又确保数据最终一致(结算时以 DB 库存为准)。曾有同事把购物车全放前端,结果促销时用户疯狂点「+」,最后发现库存超卖——这就是没守住「最终一致性」边界的血泪经验。
4. 部署攻坚:Vue 打包产物如何被 SpringBoot 正确服务?Nginx 配置的三个致命细节
4.1 Vue 打包:public 目录与 static 资源的归属之争
Vue 项目npm run build后生成dist目录,里面是index.html和一堆js/chunk-xxx.js。很多人直接把整个dist复制到 SpringBoot 的src/main/resources/static下,结果访问http://localhost:8080/显示白屏,F12 看 Network 面板全是 404。原因在于:SpringBoot 的static目录只服务静态文件,不处理 HTML 的<script src="/js/app.xxx.js">路径重写。
正确做法是:
- Vue 的
vite.config.ts中配置base: '/bookstore/'(与 SpringBoot 的context-path一致); - 打包后,将
dist目录下的所有文件(包括index.html)复制到 SpringBoot 的src/main/resources/static/bookstore/目录下; - SpringBoot 启动后,访问
http://localhost:8080/bookstore/即可加载index.html,其内部 script 标签路径自动变为/bookstore/js/app.xxx.js,被 SpringBoot 正确识别。
注意:
static/bookstore/是物理路径,/bookstore/是 URL 路径,二者必须严格一致,否则资源 404。
4.2 SpringBoot 静态资源路径:spring.web.resources.static-locations的隐藏陷阱
默认情况下,SpringBoot 从classpath:/static加载静态资源。但如果你把 Vue 打包文件放在src/main/resources/static/bookstore/,访问/bookstore/时 SpringBoot 会尝试找classpath:/static/bookstore/index.html—— 这没问题。但有个坑:当index.html中引用/bookstore/css/style.css时,SpringBoot 会去classpath:/static/bookstore/css/style.css找,但如果 CSS 文件在dist/css/下,而你只复制了dist里的文件到static/bookstore/,路径就对了。
真正致命的是spring.web.resources.static-locations配置。如果误加了:
spring: web: resources: static-locations: classpath:/static/,classpath:/public/SpringBoot 会按顺序扫描这两个路径,一旦public/下有同名文件(比如public/index.html),它就会优先返回public/的,导致你改了static/bookstore/index.html却看不到效果。线上环境务必删掉此配置,用默认值。
4.3 Nginx 反向代理:location 匹配顺序决定生死
本地开发用 SpringBoot 内置 Tomcat 没问题,但上线必须用 Nginx。常见错误配置:
# 错误!/api/ 会匹配 /api/user/login,但 /bookstore/ 也会匹配 /bookstore/api/user/login,导致 API 请求被当成静态资源返回 404 location / { proxy_pass http://localhost:8080; }正确配置必须精确区分:
# 1. 优先匹配 API 接口,全部代理给 SpringBoot location /bookstore/api/ { proxy_pass http://localhost:8080/bookstore/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } # 2. 匹配 Vue 静态资源,直接由 Nginx 服务,不走后端 location /bookstore/ { alias /var/www/bookstore/dist/; # 注意:alias 后面必须带 /,且路径指向 dist 目录本身 try_files $uri $uri/ /bookstore/index.html; # 解决 Vue Router history 模式刷新 404 } # 3. 兜底,防止其他路径泄露 location / { return 404; }关键点:alias和root的区别。alias /var/www/bookstore/dist/表示/bookstore/xxx对应/var/www/bookstore/dist/xxx;而root /var/www/bookstore/dist表示/bookstore/xxx对应/var/www/bookstore/dist/bookstore/xxx,多了一层目录,必 404。
5. 避坑指南:那些让网上书城项目卡在 90% 进度的 5 个真实翻车现场
5.1 现象:Vue 页面空白,Console 报Failed to resolve component: router-view
原因:Vue 3 项目中,main.js里漏写了app.use(router),或者createRouter时history: createWebHistory()的参数没传import.meta.env.BASE_URL(Vite 环境下必须传)。
解决:检查main.js是否有app.use(router);确认router/index.js中createWebHistory(import.meta.env.BASE_URL)是否存在,且BASE_URL在vite.config.ts中设为'/bookstore/'。
5.2 现象:SpringBoot 启动报错Caused by: java.lang.ClassNotFoundException: javax.xml.bind.JAXBContext
原因:SpringBoot 2.3+ 默认移除了 JAXB,但某些老版本 MySQL 驱动(如mysql-connector-java:5.1.49)依赖 JAXB。
解决:升级 MySQL 驱动到8.0.33,或在pom.xml中显式添加 JAXB 依赖:
<dependency> <groupId>javax.xml.bind</groupId> <artifactId>jaxb-api</artifactId> <version>2.3.1</version> </dependency>5.3 现象:购物车数量加减正常,但页面刷新后归零
原因:前端localStorage存的是字符串,JSON.parse(localStorage.getItem('cart'))后没做空值判断,null被当成[],导致每次刷新都新建空数组。
解决:加健壮性判断:
const cart = JSON.parse(localStorage.getItem('cart') || '[]') if (!Array.isArray(cart)) { localStorage.setItem('cart', JSON.stringify([])) return [] } return cart5.4 现象:图片上传后,SpringBoot 返回200,但前端收不到coverUrl字段
原因:后端 Controller 方法返回Result<String>,但String被 Jackson 序列化成"http://..."(带引号),前端res.data拿到的是字符串而非对象。
解决:统一返回Result<Map<String, Object>>,或定义UploadResult类:
public class UploadResult { private String coverUrl; private String fileName; // getter/setter }5.5 现象:Nginx 部署后,Vue 页面能打开,但点击「我的订单」路由跳转失败,地址栏变成/bookstore/#/orders
原因:Vue Router 用了hash模式(默认),但你在vite.config.ts中设了base: '/bookstore/',导致 hash 路由与 base 冲突。
解决:强制用history模式,并确保 Nginx 配置了try_files $uri $uri/ /bookstore/index.html;(见 4.3 节),同时router/index.js中:
const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), // 必须传 BASE_URL routes: [...] })6. 进阶验证:用 Postman + Chrome DevTools + Actuator 三件套,把「能跑」变成「敢上线」
6.1 接口契约验证:Postman Collection 自动化回归测试
光靠手动点页面测接口,漏测率极高。我习惯用 Postman 建一个BookStore-API-TestCollection,包含:
User Login:POST/bookstore/api/user/login,Body 传{ "username": "test", "password": "123456" },Tests 里写:pm.test("Status code is 200", function () { pm.response.to.have.status(200); }); pm.test("Response has token", function () { var jsonData = pm.response.json(); pm.expect(jsonData.data.token).to.exist; });Add to Cart:POST/bookstore/api/cart,Headers 加Authorization: Bearer {{token}},用{{token}}变量复用登录接口的返回值;Place Order:POST/bookstore/api/orders,Body 传购物车 ID,Tests 验证jsonData.code === 200 && jsonData.data.orderNo.startsWith("ORD")。
每天开发前运行一次 Collection,5 分钟内知道昨天改的代码有没有破坏已有功能。这比写单元测试更快落地,尤其适合毕业设计或外包项目赶工期。
6.2 前端性能审计:Chrome DevTools 的 Network 与 Lighthouse 双视角
网上书城不是炫技项目,但加载慢会被用户秒关。打开 Chrome DevTools → Network 标签:
- 过滤
JS,看chunk-vendors.js是否过大(> 500KB)?如果是,用vite-plugin-compression开启 gzip:// vite.config.ts import { defineConfig } from 'vite' import compress from 'vite-plugin-compression' export default defineConfig({ plugins: [compress({ algorithm: 'gzip' })], }) - 过滤
Img,看封面图是否未压缩?用sharp脚本批量压缩public/uploads/下的图片,尺寸裁剪到 300x400px,质量 75%; - 运行 Lighthouse Audit,重点关注「First Contentful Paint < 1.5s」和「Total Blocking Time < 200ms」,不达标就开
vite-plugin-legacy生成兼容 ES5 的包。
6.3 后端健康监控:Actuator 端点暴露与 Prometheus 集成
SpringBoot Actuator 是生产环境的眼睛。在pom.xml加:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>application.yml中:
management: endpoints: web: exposure: include: health,info,metrics,prometheus,loggers endpoint: health: show-details: when_authorized启动后访问http://localhost:8080/bookstore/actuator/health,返回{"status":"UP"}表示服务存活;/actuator/metrics/jvm.memory.used查 JVM 内存;/actuator/loggers/com.example.bookstore动态调日志级别。如果公司用 Prometheus,加micrometer-registry-prometheus依赖,/actuator/prometheus就能被拉取指标。
我习惯在项目 README.md 里写一句:「部署后请 curl -X GET http://your-server:8080/bookstore/actuator/health,返回 UP 即可对外提供服务」。这句话省去运维同事 2 小时排查时间。
最后说个私藏习惯:每次git commit前,我会npm run build一次,把dist目录内容 diff 一下,确认没有意外的文件变动(比如误提交了 node_modules)。这招帮我避开了三次因dist里混入开发期.map文件导致线上 JS 报错的事故。希望帮到你。
本文还有配套的精品资源,点击获取