简介:一款基于Springboot+Vue的网上电商项目,采用前后端分离架构,前台覆盖商品展示、分类筛选、购物车与订单查询,后台包含商品管理、订单管理、用户管理与信息维护,并集成支付宝沙箱支付环境,适合Java学习者、毕业设计或中小型电商项目二次开发;技术栈涉及Springboot、Maven、Mybatis、Vue、Redis,源码已做基础调试,可较流畅运行。资源包共615个文件,约18.89MB,以Java源码及编译class为多,同时包含Vue组件、JS脚本、XML/yml配置、SQL数据库脚本、页面图片与Webpack配置等,前后端目录模块清晰。从内容预览可看到商品、订单、支付、用户等核心业务Controller以及Shiro权限配置,便于理解接口设计与鉴权流程。项目内置Restful接口分层结构,可帮助掌握Mybatis持久化、Redis缓存、Maven构建等常见企业开发技巧;支付宝沙箱交易代码也可作为支付场景参考模板。已有297人学习下载,适合需要完整前后端分离项目参考、支付对接演示或电商后台管理实践的人群。
1. 网上电商项目:前后端分离+SpringBoot+SSM+MySQL+Redis,这套技术栈到底难在哪
搜索「网上电商项目」的人,通常不是来问一个网站长什么样的,而是手里已经有一个商城需求:商品要展示、购物车要算钱、订单要提交、库存要扣减,后台管理还要管上架和改价。这些模块单独拆开都不难,难的是它们同时跑在一套前后端分离的代码里——前端Vue负责页面交互,后端SpringBoot整合SSM对外提供接口,MySQL存最终数据,Redis在中间扛读多写少的流量。这篇文章把组合背后的选型理由、关键代码、踩坑点和验证手段逐个说清楚,适合正在做毕业设计、接外包或者想拿电商项目当练手的人照着复现。
2. 后端骨架:SpringBoot与SSM协同工作,先让项目能启动
很多新人拿到“Java电商项目”第一反应是我到底用SSM还是SpringBoot。这个标题把两者放在一起,不是重复,而是分工:SpringBoot负责自动装配、内嵌Tomcat和启动入口,SSM里的Spring管理业务Bean、SpringMVC提供接口路由、MyBatis负责SQL读写。你用SpringBoot把一个SSM项目跑起来,这才是这套组合最常见的形态。
2.1 pom.xml里的依赖配比:SpringBoot父工程与MyBatis starter
我把一个后端骨架的pom.xml简化成下面这样,最小可跑:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <relativePath/> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.2</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>这里最关键的是版本选择。我一般会用SpringBoot 2.7.x配合MyBatis starter 2.3.x,这两个组合久经考验,网上搜到的配置和面试题也大多基于这一代。SpringBoot 3.x把javax改成了jakarta,很多老SSM项目里的import javax.servlet会大面积报错,刚上手没必要自己给自己挖坑。mysql-connector-java不用写版本,父工程统一管理。
2.2 启动类与Mapper扫描:@MapperScan还是@Mapper注解
SpringBoot的启动类写法非常固定,真正有讲究的是Mapper怎么被容器发现:
import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication @MapperScan("com.shop.mapper") public class ShopApplication { public static void main(String[] args) { SpringApplication.run(ShopApplication.class, args); } }两种注册方式的选择我按这个习惯来:接口类少的项目直接在接口上写@Mapper,一个接口一个,看得清楚;接口数量超过十个就集中在启动类上写@MapperScan。电商项目商品、购物车、订单、库存、用户、优惠券这些模块加起来少说二十张表,mapper接口数量可想而知,所以用@MapperScan更省事。这里有个细节要注意:如果你用了XML文件,XML里的namespace必须和接口全限定名一致,否则启动不报错,一调用就报Invalid bound statement (not found)。
2.3 application.yml的配置参数:数据源、Redis连接池只调这三个关键点
配置是整个项目最先翻车的地方,我见过太多人在MySQL连接和Redis序列化上卡半天。把最小配置写出来看:
server: port: 8080 servlet: context-path: / spring: datasource: url: jdbc:mysql://127.0.0.1:3306/shop?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver redis: host: 127.0.0.1 port: 6379 password: timeout: 3000ms lettuce: pool: max-active: 16 max-idle: 8 min-idle: 0 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.shop.entity configuration: map-underscore-to-camel-case: true配置里的坑集中在这几个地方。MySQL地址中的serverTimezone=Asia/Shanghai不能省,不写会直接报时间区错误;characterEncoding=utf8不写,中文全变成问号;useSSL=false是本地开发防止握手警告。Redis除了连接地址,最需要关注的是Lettuce连接池参数,max-active=16意味着并发超过16时请求要排队,后续压测和调优都以这个数字为基准。MyBatis的map-underscore-to-camel-case设置成true,数据库的user_name字段就能自动映射成userName,不用每张表都写resultMap。
2.4 为什么不直接换成MyBatis-Plus
被问得最多的一个问题:现在新项目不都用MyBatis-Plus吗,为什么标题还写ssm。我的看法是原版SSM能让你把SQL掌握在自己手里。电商项目与普通管理后台最大的差异在库存扣减和订单统计,这两类SQL有条件判断、行锁和聚合,MyBatis-Plus的Wrapper在复杂SQL上反而绕。用它做增删改查确实快,但新手一旦依赖selectById和save这种内置方法,遇到真正的并发问题时连SQL都写不顺。SSM骨架里自己写XML mapper,不是退步,是给后续优化留余地。
3. 商城核心业务:Session存购物车,Redis扛热数据,数据库保最终一致
电商项目里如果只挑一个业务模块检验代码水平,我选购物车与库存。购物车既要有用户操作的实时性,又要在下单时保证商品状态没过期;库存则在并发下单时面临超卖风险。这两块写好了,整个项目就成立了一大半。
3.1 商品详情页的Redis缓存:读多写少的缓存策略
商品详情是典型的读多写少场景,一次商品上架,成千上万用户看商品详情。把热点商品放进Redis是标准做法:
public ProductVO getProductDetail(Long productId) { String key = "product:detail:" + productId; ProductVO product = redisTemplate.opsForValue().get(key); if (product != null) { return product; } Product productDO = productMapper.selectById(productId); if (productDO == null) { throw new BusinessException("商品不存在"); } product = convertToVO(productDO); redisTemplate.opsForValue().set(key, product, 30, TimeUnit.MINUTES); return product; }这段代码逻辑很简单:先查Redis,命中直接返回;未命中查一次数据库,再写入Redis并设置30分钟过期。这里要解释为什么是30分钟而不是更长:商品详情里的价格、库存、活动标签都不能接受长期不更新,30分钟是折中值。商品管理人员改完价格后,如果接受延迟生效,就不用做缓存删除;想立即生效,就在更新接口里执行redisTemplate.delete(key)。注意,这里别用opsForValue().setIfAbsent做续期,那种写法容易在并发时造成缓存穿透。
3.2 购物车为什么存在Redis而不是MySQL
把购物车存MySQL是最容易踩的坑。购物车数据频繁增删改,用户加一件减一件都要更新数据库,涉及行锁和事务,并发稍高数据库连接就吃紧。常见的方案是购物车用Redis Hash存:
public void addToCart(Long userId, Long productId, Integer count) { String cartKey = "cart:user:" + userId; redisTemplate.opsForHash().put(cartKey, String.valueOf(productId), count.toString()); // 如果购物车有数量上限,统一在这里校验 }opsForHash().put对应Redis的HSET命令,同一个商品ID多次放入就覆盖数量,天然做了合并。查购物车用opsForHash().entries(cartKey)一次取回全部商品,到下单环节再回MySQL查最新价格和库存,防止用户下单时价格已经变了。
这里面值得注意的一点是购物车数据对丢失的容忍度。用户往购物车里加东西,不代表马上要买,Redis如果宕机重启,这部分数据可能丢失。我见过两种处理:一是接受丢失,提示用户重新添加;二是写购物车时异步追加一份到MySQL备份表。前一种适合大多数项目,后一种适合客单价高的商城。你根据自己对数据安全的要求选,别想当然认为存Redis就万事大吉。
3.3 库存扣减与超卖:先用Redis做预扣
超卖是电商项目翻车概率最高的Bug。两个用户同时买同一个SKU的最后一件商品,如果代码先查库存再更新,两个请求都查到库存为1,然后各自执行扣减,库存最终变成负数。错误示范是很多教学项目里的“查出来判断再UPDATE”,正确做法是让扣减自己保证原子性。
我常用的是Redis预扣方案:
public boolean decreaseStockWithRedis(Long productId, Long skuId, Integer count) { String stockKey = "stock:sku:" + skuId; Long rest = redisTemplate.opsForValue().decrement(stockKey, count); if (rest == null || rest < 0) { // 扣超了,把扣掉的数量加回来 redisTemplate.opsForValue().increment(stockKey, count); return false; } int rows = stockMapper.decreaseStock(skuId, count); if (rows == 0) { // 数据库扣减失败,回补Redis redisTemplate.opsForValue().increment(stockKey, count); log.error("库存扣减失败,productId={}, skuId={}", productId, skuId); return false; } return true; }这里decrement是原子的,两个线程同时扣减也不会互相覆盖。但注意这段代码有个窗口期:Redis扣成功了,MySQL还没来得及更新时,另一个读请求看到的还是旧库存。要彻底消除这个窗口,可以把Redis里的库存当作查询时的唯一依据,MySQL只做对账和最终存储,也就是修改库存入口也只走Redis,数据库不再提供扣减能力,只异步落一条流水。这也是很多互联网项目的常见做法。
数据库Mapper的SQL也要配合加上条件判断:
<update id="decreaseStock"> UPDATE sku_stock SET stock = stock - #{count} WHERE sku_id = #{skuId} AND stock >= #{count} </update>条件里的stock >= #{count}是关键兜底,即使缓存放行了一个超卖请求,数据库也会拦住它。这两层都用上,超卖的Bug才真正锁死。
3.4 订单生成的幂等控制:防重复提交不能只靠前端按钮置灰
用户点击“提交订单”后网络卡顿,下意识再点一次,前端按钮置灰挡得住正常人,挡不住重试和脚本。订单接口的幂等有两个层面要处理:一是同一购物车不能生成两笔订单,二是用户发起重复请求时不能重复扣库存。
常见做法是下单前生成一个订单号orderSn,这个字段在数据库里建唯一索引,然后向订单表插入记录。重复请求带来的是同一个orderSn插入两次,第二次插入被MySQL拒掉,异常被捕获后直接返回第一笔订单号,用户界面看到的是同一个结果。这个方案比用Redis锁更保险,因为它的依据是数据库唯一约束,不会因为日志丢失而失效。Redis锁的方案也不是不能用,只是要面对锁过期时间设置成多少秒这类问题,设短了业务没执行完锁就失效,设长了性能下降。我一般把基于数据库唯一索引的幂等作为默认方案。
4. 前端Vue对接:从环境配置到打包进SpringBoot
前后端分离的“分离”感受最深刻的是联调阶段。前端只认接口、后端只认页面数据,两边往一起碰的时候,跨域、Token、路由刷新问题轮着来。Vue端准备好一套能跑的工程模板,能省出大量调试时间。
4.1 Vue环境搭建与镜像配置:node版本不一致让npm install反复失败
Vue项目能不能一次装完依赖,七成取决于Node版本。拿Vue2 + Vue CLI为例,Node 14到Node 16比较稳;Node 18以上装node-sass这种老依赖时经常报错,因为node-sass要下载对应版本的二进制文件,下载源有时候还不稳定。推荐装依赖前先看package.json里的依赖声明,再决定用哪个Node版本。
安装依赖有一个提速技巧,在项目根目录放一份.npmrc文件:
registry=https://registry.npmmirror.com sass_binary_site=https://npmmirror.com/mirrors/node-sass这两行指定了npm镜像源和node-sass的二进制下载源。不配这个,新机器上npm install卡在node-sass下载半天下不动的场景特别多。但记住,这是开发期配置,别提交到生产环境依赖里,部署机上网络环境不一定允许访问这个镜像。
4.2 开发环境跨域:用Vue代理而不是让后端改CORS
前后端分离最典型的现象:浏览器控制台报跨域错误。不少人第一反应是让后端加@CrossOrigin注解,实际上开发环境有更干净的解法,用vue.config.js的代理,所有接口请求都经过Vue开发服务器转发:
module.exports = { devServer: { port: 3000, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, pathRewrite: { '^/api': '' } } } }, productionSourceMap: false }配置的含义是:前端运行在3000端口,所有以/api开头的请求都会被转发到http://localhost:8080,同时把路径前缀去掉。这样后端接口写成/product/list,前端就请求/api/product/list,浏览器看到的只是访问自己的3000端口,根本不产生跨域。
生产环境里这个责任就交给Nginx了,后端不需要让SpringBoot的CORS配置放得很宽。后端CORS粗暴设成allowedOriginPatterns("*")一时爽,等上线后任何第三方域名都能调用接口,安全测试第一轮就过不去。
4.3 axios封装与登录态:token放在localStorage里的边界
前端需要统一处理请求头和错误码,不能每个页面都自己写一遍axios。我习惯在src/utils/request.js里封装:
import axios from 'axios' const service = axios.create({ baseURL: '/api', timeout: 10000 }) service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = 'Bearer ' + token } return config }) service.interceptors.response.use( response => response.data, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token') window.location.href = '/login' } return Promise.reject(error) } ) export default service把token放在localStorage是大多数管理后台的常见做法,因为刷新页面后token不丢。但你要意识到localStorage有XSS风险,页面里任何一处v-html插入用户内容都可能被脚本读走token。如果做的是支付相关的电商系统,建议把token放内存变量里,配合刷新时重新获取的逻辑,安全性会更好。
4.4 前端打包后放进SpringBoot:静态资源路径别只盯着dist目录
项目收尾时要把前端产物放到SpringBoot里统一部署。执行npm run build生成dist目录,然后把dist目录里的文件复制到SpringBoot的src/main/resources/static目录下,启动SpringBoot后浏览器直接访问http://localhost:8080就能看到页面。
这里有一个高频坑:dist目录复制过去后,页面上找不到静态资源,按F12看到请求路径全是/js/app.js,但实际文件在/static/js/。原因是Vue默认publicPath是根路径/,部署到子路径就出问题。构建前改vue.config.js:
module.exports = { publicPath: process.env.NODE_ENV === 'production' ? '/static/' : '/' }把生产环境资源前缀改成/static/,这样生成的app.js路径是/static/js/app.js,正好对上SpringBoot静态资源目录。改完重新build再复制,才不会再出现白屏。这一步也解释了为什么前后端分离项目在开发时用Vue代理、部署时用静态资源映射,两边机制根本不同。
5. 电商项目避坑指南:跨域、超卖、缓存穿透、依赖冲突与前端路由的五个Miss
这个项目里真正让新手熬到后半夜的问题就那么几个,我挑最有代表性的五次踩坑过程,按现象、原因、解决完整梳理。
5.1 跨域请求头报错:authorization带不过去
现象是登录完成后,浏览器报“Request header field authorization is not allowed by Access-Control-Allow-Headers in preflight response”。并不是后端接口写错了,而是浏览器在正式请求前发了一个OPTIONS预检请求,预检时发现响应头里没有允许Authorization这个请求头,所以直接拦截。
原因在于后端CORS配置只设置了允许来源,没设置允许请求头。解决办法在后端加一个CorsFilter配置:
configuration.addAllowedHeader("Authorization"); configuration.addAllowedHeader("Content-Type"); configuration.addAllowedMethod("*");我吃过亏的地方是只解决了Authorization,后面遇到Content-Type: application/json又报一次同样的错。不如一次性把常用请求头都放行,省得来回改。但别贪图方便把addAllowedOrigin("*")到处用,后端指定具体域名更稳妥。
5.2 库存显示有货,下单时却提示已售罄
现象是商品详情页Redis缓存显示库存还剩5件,用户下单却立刻被拒绝,前端以为系统出错了。原因是我在做库存预扣时只更新了Redis里的数字,商品详情查询也直接读Redis,逻辑上确实应该显示5件,但订单系统已经被并发请求抢完了名额,剩下的只是没同步到缓存里的脏数据。
解决办法是库存判断以Redis预扣为准,但详情页展示时要预留一个缓冲阈值。比如真实库存5件,详情页展示阈值3件,超过3件就显示“库存紧张”,低于阈值直接显示“已售罄”。这样用户体验不会特别吃惊,同时给并发扣减留了安全余量。做电商展示库存不是展示真实剩余量,而是展示一个转化率友好的数字,这个认知要尽早建立。
5.3 Redis缓存击穿:热点商品第一次下架时数据库被打满
现象是热门商品详情接口平时响应只要20毫秒,有一次活动结束后数据库CPU直接飙到90%。原因是缓存设置30分钟过期,大量用户在同一时间访问同一个已经过期的Key,Redis全部Miss,所有请求同时打到MySQL询商品详情。
解决办法有两层。第一层在查询逻辑里加锁,同一个商品ID在缓存失效时只让一个线程去查数据库,其他线程等待该线程回填缓存再取数据,也就是“缓存重建锁”,用Redis的setIfAbsent就能实现。第二层是更简单的兜底:给缓存Key设置一个比过期时间稍长的逻辑过期时间,让一个后台任务定期主动刷新热门商品缓存。我一般把这两个手段都用上,防止极端场景下某个大流量商品突然变成冷数据。
5.4 Spring Boot与SSM依赖冲突:Failed to configure a DataSource
现象是项目启动时报“Failed to configure a DataSource: 'url' attribute is not specified”,但你明明在application.yml里配好了连接地址。原因通常是SpringBoot的自动配置先于MyBatis介入,或者你引用了spring-boot-starter-jdbc和mybatis-spring-boot-starter两套数据源管理依赖互相冲突。
解决办法先确认一件事:如果你根本没有配置正确数据源,先核对spring.datasource.url写的格式;如果格式正确还是报错,就要看是不是依赖多余。大多时候这个报错是SpringBoot找不到数据库连接,因为依赖里只有spring-boot-starter-web而没有加任何数据源依赖。SSM项目里把MyBatis starter加上就能解决。如果已经加了,检查是不是连了多个数据源但没有指定@Primary。
5.5 Vue打包后SpringBoot刷新404,路由history模式配置必须对齐
现象是打包放到SpringBoot后,访问首页正常,点菜单跳转也没问题,但按F5刷新时页面变白,SpringBoot日志里报一个404。原因是Vue启用了HTML5 history路由模式,前端路由由Vue Router接管,页面刷新时浏览器向SpringBoot请求了/order/detail这个真实路径,但SpringBoot后端没有“通配到index.html”的规则。
解决办法有两个方向,生产上我优先推荐给前端路由补一层服务端回退,在SpringBoot里注册一个Controller或者配置转发,把所有非静态资源路径转回index.html。但最省事的是把前端路由模式改成hash模式,URL变成/#/order/detail,刷新时不会触发服务端路由,省掉一堆配置。如果你不追求特别干净的URL地址,电商管理后台用hash模式没有实质缺点,这是最直观的后悔药。
6. 压测验证:用JMeter模拟并发下单,看Redis会在哪个水位先倒下
代码写完只是第一步,一个电商项目能不能上线,要靠压测数据说话。没有压测就上线,约等于把一个黑匣子交给运维,线上出问题只能靠猜。最简单有效的验证方式是JMeter跑脚本,统计错误率和吞吐量。
先按这个思路设计压测场景:用100个并发线程,每个线程循环20次,模拟同一个人反复浏览商品详情然后下单。请求里带上登录token,脚本里要用正则表达式提取器从登录响应里拿值。跑之前先确认一个基准数据:数据库里这个SKU的库存要足够压测消耗,比如1000件,不能用50件测并发,否则数据很快耗尽,压测结果完全失真。
压测结束后重点看两份数据:错误率要低于0.1%,吞吐量看项目目标,一个小型商城系统接口如果每秒只能处理200个请求也能用,但要是商品详情这种读接口压测下来每秒只有50,说明Redis或数据库连接池配置有明显问题。我用一个对比法来验证缓存价值:先关闭Redis缓存跑一次同样场景,再打开缓存跑一次,两次结果一对照,Redis值不值得引入一目了然。
我通常还会把JMeter的配置文件保存到项目根目录/test/jmeter/order.jmx,后续每次改订单逻辑都要跑一遍这个脚本。有一次我改了一个库存判断的SQL,自认为没问题,跑完压测发现吞吐量比上一版掉了30%,追查下去才定位到索引没生效。从那以后我养成一个习惯:凡是涉及订单、库存、查询接口的改动,不跑完压测不合并代码。压测脚本就是这套电商项目最重要的测试资产,它比任何代码注释都能更早告诉你什么时候要出事了。希望帮到你。
本文还有配套的精品资源,点击获取