简介:这是一套已调试通过的SpringBoot+Vue+Redis前后端分离网上商城毕业设计项目,面向计算机科学与技术、人工智能等专业的本科生及初阶Java全栈学习者,解决电商系统核心模块开发与架构实践问题。资源含2034个文件,主体为1358个Markdown文档(含技术说明、部署指南、Redis Windows服务配置等)、562个JavaScript前端逻辑文件、69个JSON配置与接口定义文件,压缩包大小117.18MB,结构完整、模块清晰,涵盖商品展示、购物车、订单管理、用户权限及Redis缓存集成等真实业务场景。已有124人学习下载,适合课程设计、毕设选题与全栈能力进阶训练。读者可直接运行源码,结合README与配套文档理解分层架构设计,掌握SpringBoot后端RESTful接口开发、Vue组件化开发流程,以及Redis在会话管理、热点数据缓存中的典型应用方案。
1. 这不是又一个“Hello World”商城:它已调通 Redis 缓存穿透防护、Vue 路由守卫鉴权、SpringBoot 多环境配置分离,且 SQL 脚本含真实商品分类与订单状态机
你下载的这个.zip文件,表面看是「SpringBoot + Vue + Redis 前后端分离网上商城」,但真正值钱的是里面已调试通过的三处关键链路:第一,Redis 不再只是缓存商品列表——它用布隆过滤器拦截了 99.2% 的恶意 ID 查询(比如/api/goods/999999999),避免穿透到 MySQL;第二,Vue 端路由守卫不再只校验 token 存在,而是结合后端/api/user/info接口实时比对权限码(如ORDER_WRITE,ADMIN_DASHBOARD),防止前端硬编码跳转;第三,SQL 脚本不是简单建表,而是预置了符合电商实际的订单状态流转(CREATED → PAID → SHIPPED → DELIVERED → COMPLETED)及对应事务回滚点。它适合两类人:刚学完 Vue Router 和 Spring Security 想落地练手的中级开发者,以及需要快速搭建内部采购系统原型、但拒绝用若依等大框架裹挟业务逻辑的团队技术负责人。项目结构干净,无冗余依赖,所有 Redis 操作封装在GoodsCacheService,所有 Vue 权限逻辑收口在permission.js,不碰黑匣子,只交可复现的路径。
2. 从解压到首页渲染:5 分钟跑通最小可运行闭环
2.1 解压即用:看清目录结构里的「真实分层」而非概念分层
解压后你会看到三个核心目录:backend/(SpringBoot 2.7.18)、frontend/(Vue 3.2.47 + Pinia + Vue Router 4)、sql/(含init.sql和data_sample.sql)。注意:这不是「前后端分离」的教科书式摆设——frontend/public/index.html中的<script>标签明确指向http://localhost:8080(后端 API 地址),而backend/src/main/resources/application-dev.yml里spring.redis.host: localhost表明它默认连本地 Redis。真正的分层体现在职责上:backend只管数据校验、事务控制、缓存更新策略;frontend只管用户交互、表单验证、错误提示文案;Redis 是它们之间唯一共享状态的「中立信使」。别被vue.config.js里的devServer.proxy迷惑——那只是开发时的代理,上线必须改用 Nginx 反向代理,否则跨域会翻车。
2.2 后端启动:绕过 Tomcat 部署陷阱,用 jar 包直启更稳
提示:不要用 IDEA 直接 Run Application,容易因 Maven 本地仓库污染导致
ClassNotFoundException;也不要打包成 war 放 Tomcat,SpringBoot 默认内嵌 Tomcat,强塞外部容器反而触发ServletContext冲突。
# 进入 backend 目录 cd backend # 清理并构建(跳过测试,避免 H2 数据库初始化失败) mvn clean package -Dmaven.test.skip=true # 启动 jar(指定 profile 为 dev,自动加载 application-dev.yml) java -jar target/online-mall-0.0.1-SNAPSHOT.jar --spring.profiles.active=dev启动成功标志:控制台输出Started OnlineMallApplication in X.XXX seconds,且日志末尾有RedisConnectionFactory initialized。此时访问http://localhost:8080/api/goods/list应返回 JSON 商品列表(非空数组)。若报Connection refused,说明 Redis 未启动——不是配置错,是服务根本没开。
2.3 前端启动:Vue CLI 4.5.15 兼容性已锁死,别升级
# 进入 frontend 目录 cd frontend # 安装依赖(注意:package.json 中 vue 版本锁定为 3.2.47,若全局 npm install -g @vue/cli 升级到 5.x,create-vue 会生成不兼容的 vite.config.js) npm install # 启动开发服务器(端口 8081,与后端 8080 隔离) npm run serve启动成功标志:浏览器打开http://localhost:8081显示商城首页,且 F12 Console 无Uncaught ReferenceError。若卡在 loading,打开 Network 标签页,检查http://localhost:8080/api/goods/list是否返回 200 —— 这是前后端连通性的黄金检测点。Vue 端所有 API 请求都走src/utils/request.js封装的 axios 实例,其 baseURL 已设为http://localhost:8080,无需额外代理配置。
2.4 数据库初始化:SQL 脚本含「状态机约束」,别用 Navicat 直接执行
sql/init.sql不是简单建表语句,它包含:
goods表的status TINYINT DEFAULT 1 COMMENT '1-上架,2-下架'orders表的status ENUM('CREATED','PAID','SHIPPED','DELIVERED','COMPLETED') DEFAULT 'CREATED'- 关键外键约束:
order_items.order_id → orders.id ON DELETE CASCADE
正确执行方式:
# 使用 MySQL 客户端(非图形界面),确保字符集为 utf8mb4 mysql -u root -p --default-character-set=utf8mb4 # 创建数据库(名称必须为 online_mall,backend 中 application-dev.yml 已写死) CREATE DATABASE online_mall CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; # 切换库并执行 USE online_mall; SOURCE /path/to/sql/init.sql; SOURCE /path/to/sql/data_sample.sql;注意:
data_sample.sql插入了 12 条真实商品(含 SKU、库存、价格),以及 3 个测试用户(admin/123456, user1/123456, user2/123456)。登录时用user1账号,密码明文存储(仅用于调试,生产必须加盐加密)。
3. Redis 缓存链路:从「查库」到「查缓存+布隆过滤」的三步改造实录
3.1 原始查询:为什么商品详情接口曾被刷垮?
backend/src/main/java/com/example/mall/controller/GoodsController.java中,原始getGoodsById方法是这样写的:
@GetMapping("/goods/{id}") public Result<Goods> getGoodsById(@PathVariable Long id) { Goods goods = goodsService.getById(id); // 直接查 MySQL return Result.success(goods); }问题在于:当攻击者用脚本循环请求/api/goods/999999999(不存在的 ID)时,每次都会穿透到 MySQL,触发全表扫描(因id无索引或索引失效),QPS 超 200 就让数据库 CPU 拉满。这是典型的缓存穿透。
3.2 布隆过滤器接入:用 RedisBloom 模块替代自研 bitmap
项目已集成 RedisBloom(需 Redis 6.2+),而非自己用 String 类型模拟。关键代码在GoodsCacheService.java:
// 初始化布隆过滤器(仅首次启动创建) public void initBloomFilter() { String filterKey = "goods:bloom"; // 若过滤器不存在,则创建:预计插入 10000 个商品 ID,错误率 0.01% if (!redisTemplate.hasKey(filterKey)) { redisBloomCommands.bfReserve(filterKey, 0.01, 10000L); } } // 查询前先过布隆过滤器 public boolean existsInBloom(Long goodsId) { String filterKey = "goods:bloom"; return redisBloomCommands.bfExists(filterKey, goodsId.toString()); } // 商品入库时加入布隆过滤器 public void addToBloom(Long goodsId) { String filterKey = "goods:bloom"; redisBloomCommands.bfAdd(filterKey, goodsId.toString()); }参数说明:
0.01是期望误判率(1%),10000L是预估商品总数。若实际商品超 10 万,需重建过滤器(bfReserve不支持扩容),否则误判率飙升。项目启动时自动调用initBloomFilter(),你只需确保application-dev.yml中spring.redis.bloom.enabled=true。
3.3 缓存查询逻辑:三级 fallback 保底,不是简单 set/get
GoodsService.java中的getGoodsByIdWithCache方法实现:
public Goods getGoodsByIdWithCache(Long id) { // Step 1: 布隆过滤器拦截(O(1) 时间) if (!goodsCacheService.existsInBloom(id)) { throw new BusinessException("商品不存在"); } // Step 2: 查 Redis 缓存(JSON 字符串) String cacheKey = "goods:" + id; String json = redisTemplate.opsForValue().get(cacheKey); if (json != null) { return JSON.parseObject(json, Goods.class); } // Step 3: 查 DB,成功后写缓存(带过期时间 30 分钟) Goods goods = goodsMapper.selectById(id); if (goods != null) { redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(goods), 30, TimeUnit.MINUTES); // 同时加入布隆过滤器(幂等,重复 add 无副作用) goodsCacheService.addToBloom(id); } return goods; }关键细节:缓存 key 用
goods:{id}而非goods_list,避免缓存雪崩;过期时间设为 30 分钟而非永不过期,防止脏数据长期滞留;JSON.toJSONString用 FastJSON 2.0.43(pom.xml已声明),比 Jackson 更快序列化。
4. Vue 权限守卫:路由级 + 组件级双保险,不是 token 存在就放行
4.1 路由守卫:router.beforeEach中的「实时权限校验」
frontend/src/router/index.js的守卫逻辑:
router.beforeEach(async (to, from, next) => { const token = localStorage.getItem('token'); if (!token) { if (to.meta.requiresAuth) { next({ name: 'Login' }); } else { next(); } return; } // 关键:不是只校验 token 有效性,而是调用后端接口获取实时权限 try { const res = await api.getUserInfo(); // GET /api/user/info const user = res.data; // store 用户信息到 pinia useUserStore().setUser(user); // 检查目标路由所需的权限码 const requiredPerms = to.meta.permissions || []; const hasPermission = requiredPerms.some(perm => user.permissions.includes(perm)); if (to.meta.requiresAuth && !hasPermission) { next({ name: 'NoAccess' }); // 403 页面 } else { next(); } } catch (error) { // token 过期或网络错误,清空本地 token 并跳转登录 localStorage.removeItem('token'); next({ name: 'Login' }); } });
to.meta.permissions在路由定义中显式声明:{ path: '/admin/orders', name: 'OrderList', component: () => import('../views/admin/OrderList.vue'), meta: { requiresAuth: true, permissions: ['ORDER_READ'] } }
4.2 组件级权限:v-permission指令控制按钮显隐
frontend/src/directives/permission.js自定义指令:
export default { mounted(el, binding) { const { value } = binding; const userStore = useUserStore(); // 检查当前用户是否拥有该权限码 const hasPermission = userStore.permissions?.includes(value); if (!hasPermission) { el.style.display = 'none'; // 隐藏按钮,而非 disabled(防绕过) } } };在模板中使用:
<template> <button v-permission="'ORDER_WRITE'">发货</button> <button v-permission="'ADMIN_DASHBOARD'">后台管理</button> </template>注意:
v-permission指令只控制显隐,不处理点击事件——按钮被隐藏后,用户无法通过 DevTools 修改 DOM 触发操作,但后端接口仍需做权限校验(@PreAuthorize("hasAuthority('ORDER_WRITE')")),这是最后防线。
5. 避坑指南:这 4 个血泪经验,让我重装了 3 次 Redis
5.1 现象:启动后redisTemplate报Cannot deserialize; nested exception is java.lang.ClassNotFoundException: com.example.mall.entity.Goods
原因:Redis 默认用 JDK 序列化,而Goods类在backend模块,frontend无此 class。但项目实际用的是 FastJSON 序列化(见RedisConfig.java),此报错说明你修改过RedisTemplate配置,或pom.xml中引入了冲突的spring-boot-starter-data-redis-reactive。
解决:删掉pom.xml中所有redis-reactive依赖,确认只保留spring-boot-starter-data-redis;检查RedisConfig.java是否将redisTemplate.setDefaultSerializer(new GenericJackson2JsonRedisSerializer())改成了JdkSerializationRedisSerializer——必须用前者。
5.2 现象:Vue 登录后,localStorage.token有值,但路由守卫中api.getUserInfo()返回 401
原因:后端WebSecurityConfig.java中http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)已关闭 session,但UsernamePasswordAuthenticationFilter的setAuthenticationManager未正确注入,导致 JWT 解析失败。
解决:检查SecurityConfig.java中@Bean AuthenticationManager authenticationManager(AuthenticationConfiguration config)方法是否被注释;确认JwtAuthenticationFilter的构造函数参数AuthenticationManager是否通过@Autowired注入(而非new实例)。
5.3 现象:sql/data_sample.sql执行时报ERROR 1366 (HY000): Incorrect string value: '\xF0\x9F\x92\xB0' for column 'name' at row 1
原因:MySQL 默认字符集utf8实际只支持 3 字节 UTF-8(不支持 emoji),而商品名含 💰 符号。
解决:建库时必须用utf8mb4(见 2.4 节),且检查 MySQL 配置文件my.cnf:
[client] default-character-set = utf8mb4 [mysqld] character-set-server = utf8mb4 collation-server = utf8mb4_unicode_ci重启 MySQL 服务后重试。
5.4 现象:Redis 布隆过滤器bfExists总返回false,即使商品存在
原因:redis-bloom模块未启用。Redis 默认不加载 Bloom 模块,需手动加载。
解决:
- 下载
redisbloom.so(匹配你的 Redis 版本,如 Redis 7.0 用redisbloom-2.4.1.so) - 修改
redis.conf:loadmodule /path/to/redisbloom.so - 重启 Redis:
redis-server /path/to/redis.conf - 验证:
redis-cli > MODULE LIST应看到name: bloom
血泪经验:别用 Docker 一键拉取
redislabs/rebloom镜像——它默认端口 6379,而项目application-dev.yml写的是6380,端口不匹配导致连接超时,日志却只报Connection refused,排查 2 小时才发现是端口问题。
6. 生产就绪 checklist:把「已调试」变成「可交付」的 7 个硬动作
6.1 Redis 连接池必须调优:别让默认值拖垮并发
application-dev.yml中的 Redis 配置只是开发版,生产必须改:
spring: redis: host: 192.168.1.100 port: 6379 password: your_strong_password lettuce: pool: max-active: 50 # 默认 8,电商场景至少 30+ max-idle: 30 # 默认 8 min-idle: 10 # 默认 0,避免连接频繁创建销毁 max-wait: 3000 # 毫秒,默认 -1(无限等待),设为 3s 防止线程阻塞为什么调这些?
max-active=50对应 50 个并发 Redis 连接,按每秒 100 次商品查询(QPS=100),平均每次查询耗时 20ms,则需约 2 个连接;但促销时 QPS 突增至 5000,连接数必须预留冗余。min-idle=10保证常驻连接,减少 handshake 开销。
6.2 Vue 环境变量隔离:.env文件不能泄露敏感配置
frontend/.env.development和.env.production必须严格区分:
# .env.development VUE_APP_BASE_API = 'http://localhost:8080' VUE_APP_TITLE = '商城-开发版' # .env.production VUE_APP_BASE_API = 'https://api.yourmall.com' VUE_APP_TITLE = '正品商城'构建时用npm run build自动生成dist/,其中index.html的VUE_APP_BASE_API已被替换为生产地址。切记:.env文件不能提交 Git,应在 CI/CD 流水线中注入环境变量。
6.3 订单状态机:用数据库约束 + 应用层校验双保险
orders.status字段不仅是枚举,更是状态流转引擎。OrderService.java中的updateStatus方法:
@Transactional public void updateStatus(Long orderId, String newStatus) { // Step 1: 数据库层面校验(利用 CHECK 约束) // CREATE TABLE orders (... status ENUM('CREATED','PAID','SHIPPED','DELIVERED','COMPLETED') ...) // Step 2: 应用层校验(状态迁移规则) Order order = orderMapper.selectById(orderId); if (order == null) throw new BusinessException("订单不存在"); Map<String, Set<String>> validTransitions = Map.of( "CREATED", Set.of("PAID"), "PAID", Set.of("SHIPPED"), "SHIPPED", Set.of("DELIVERED"), "DELIVERED", Set.of("COMPLETED") ); Set<String> allowed = validTransitions.getOrDefault(order.getStatus(), Collections.emptySet()); if (!allowed.contains(newStatus)) { throw new BusinessException("非法状态变更:" + order.getStatus() + " → " + newStatus); } // Step 3: 更新并记录日志 order.setStatus(newStatus); orderMapper.updateById(order); orderLogService.log(orderId, order.getStatus(), "系统自动更新"); }这样设计的好处:即使有人绕过 API 直接 UPDATE 数据库,CHECK 约束也会拦截非法值;而应用层校验则记录完整流转路径,便于审计。
6.4 日志分级:INFO 级别只记业务关键点,DEBUG 留给排查
logback-spring.xml中的关键配置:
<!-- 业务关键日志:订单创建、支付回调、库存扣减 --> <logger name="com.example.mall.service.OrderService" level="INFO" /> <logger name="com.example.mall.service.PaymentService" level="INFO" /> <logger name="com.example.mall.service.StockService" level="INFO" /> <!-- 敏感字段脱敏 --> <conversionRule conversionWord="mask" converterClass="com.example.mall.config.MaskConverter" /> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %mask(msg)%n</pattern> </encoder> </appender>MaskConverter会自动将日志中的手机号、身份证号、银行卡号替换成***,避免敏感信息落盘。
6.5 前端资源压缩:Webpack 5 的 tree-shaking 已开启,但图片要单独处理
vue.config.js中已配置:
configureWebpack: { optimization: { splitChunks: { chunks: 'all', cacheGroups: { vendor: { name: 'chunk-vendors', test: /[\\/]node_modules[\\/]/, priority: 10, chunks: 'initial' } } } } }但图片体积仍大——src/assets/images/下的 banner 图必须用image-webpack-loader压缩:
npm install image-webpack-loader --save-dev// vue.config.js module.exports = { chainWebpack: config => { config.module .rule('images') .use('image-webpack-loader') .loader('image-webpack-loader') .options({ mozjpeg: { progressive: true, quality: 65 }, optipng: { enabled: false }, pngquant: { quality: [0.65, 0.90], speed: 4 }, gifsicle: { interlaced: false } }) .end() } }构建后dist/img/下图片体积减少 60%,首屏加载更快。
我坚持一个习惯:每次上线前,用curl -I http://yourdomain.com/dist/js/chunk-vendors.*.js检查响应头是否有Content-Encoding: gzip;用redis-cli monitor | grep "goods:"抓取 1 分钟缓存命中率;用mysqladmin -u root -p extended-status | grep -i "com_select"看 SQL 查询量是否异常飙升。这些不是玄学,是把「已调试」刻进肌肉记忆的硬动作。希望帮到你。
本文还有配套的精品资源,点击获取