
简介基于Spring Boot构建的网上购物商城系统RAR资源包适合正在学习Spring Boot框架或准备开发电商项目的开发者。内容围绕一个完整商城系统的设计展开涵盖用户注册登录、商品展示搜索、购物车管理、订单处理、第三方支付接入、权限安全控制与日志记录等核心模块并给出SpringMVC、Spring Data JPA/MyBatis、Redis缓存、Thymeleaf/Vue.js前端渲染、Docker容器化部署、OAuth2第三方登录及Spring Security安全认证等关键技术选型。资源以RAR压缩包形式提供包体约64.09MB文件总数与具体类型明细暂未提供目前已有118人学习浏览。通过学习该资源读者可以理解从数据库模型设计、RESTful API编写、DAO层实现到前端页面构建、第三方服务集成、测试优化的完整开发流程同时也能梳理促销活动、积分兑换、优惠券等系统的后续扩展方向用于课程设计、毕业设计或技术预研时尤为合适。1. SpringBoot 网上商城不是“后端页面”工程而是自动装配的模块化系统把这份《SpringBoot 网上购物商城系统详解》拆完一遍我的结论是它最有价值的不是 CRUD 代码而是把 SpringBoot 的自动装配、分层架构、缓存与事务组合成了电商闭环。Sentinel 和 Seata 这类重型组件先不引入单靠 SpringBoot 3.x、Spring Security、Redis、MyBatis-Plus 就能撑起用户、商品、购物车、订单四个核心域。商城场景对事务边界、缓存一致性、防超卖的要求远高于普通管理后台而这些恰好是 SpringBoot 开发中最容易被忽略的地方。文章面向两类人想拿 SpringBoot 做毕业设计或面试项目的初中级开发者以及准备把单体商城拆成微服务但想先摸清边界的高级工程师。前者重在跑通流程后者重在看懂 SpringBoot 在真实业务里的取舍。2. 先看骨架自动装配原理、分层结构与商品订单数据模型2.1 自动装配到底装配了什么自己写一个 starter 就全懂了网上商城项目用到的spring-boot-starter-web、spring-boot-starter-data-redis、mybatis-plus-boot-starter本质上都是自动装配器。SpringBoot 启动时AutoConfigurationImportSelector会扫描META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件把里面的配置类逐个加载再按条件注解决定哪些 Bean 真正生效。网上搜springboot自动装配原理的人很多但真正动手写 starter 的少。我这里用一个生产级短信客户端配置类演示AutoConfiguration ConditionalOnClass(name org.springframework.data.redis.core.StringRedisTemplate) EnableConfigurationProperties(SmsProperties.class) public class SmsAutoConfiguration { Bean ConditionalOnMissingBean public SmsClient smsClient(SmsProperties properties) { return new SmsClient(properties.getAccessKey(), properties.getSecretKey()); } }AutoConfiguration标记这是自动配置类ConditionalOnClass表示类路径存在指定类才装载避免强制引入 Redis 依赖EnableConfigurationProperties把application.yml中以mall.sms开头的配置绑定到SmsPropertiesConditionalOnMissingBean允许使用方手动覆盖SmsClient。配置类写好后再建META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件内容只需一行类全路径。明白这个机制后商城项目里大量关于某个依赖为什么不生效的排查都能落地到条件注解上比如ConditionalOnProperty控制支付渠道切换。2.2 商城分层的包结构与模块边界网上购物商城系统的源码工程一般不会把所有代码堆在一个包下日常开发中常见的划分是按业务域拆包而不是按技术层拆包。用户域处理注册登录与收货地址商品域维护 SPU 和 SKU交易域包含购物车、订单、支付回调再单独分出common包存放统一返回体、异常处理器和工具类。控制器层只做参数校验和结果包装业务逻辑下沉到 Service数据访问交给 Mapper。订单这种核心域拆成order包后内部再按 controller、service、mapper、entity、dto 分子包多人协作时冲突面最小。我一般会在每个域下放一个domain子包保存领域事件和状态枚举后续要拆微服务时直接把整个包平移出去即可SpringBoot 的单体模块化优势就在这里体现。2.3 为订单和商品设计表结构库存字段必须有乐观锁商城系统里商品表和订单表的设计直接影响并发正确性。商品建议拆成 SPU 与 SKU 两级SPU 是抽象商品如iPhone 15 Pro MaxSKU 是具体规格如黑色 256G。价格和库存挂在 SKU 上。订单拆主单和订单明细两张表主单记录总金额、状态、收货信息明细记录每个 SKU 的下单价、数量、快照名称。库存表必须加version字段用于乐观锁控制CREATE TABLE product_sku ( id bigint(20) NOT NULL AUTO_INCREMENT, spu_id bigint(20) NOT NULL COMMENT 关联SPU, sku_name varchar(128) NOT NULL COMMENT 规格名称, price decimal(10,2) NOT NULL COMMENT 销售价, stock int(11) NOT NULL DEFAULT 0 COMMENT 可售库存, version int(11) NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, status tinyint(4) NOT NULL DEFAULT 1 COMMENT 上下架状态 1上架 0下架, PRIMARY KEY (id), KEY idx_spu_id (spu_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT商品SKU库存表;version字段配合UPDATE product_sku SET stock stock - #{num}, version version 1 WHERE id #{skuId} AND stock #{num}使用命中行数为 0 就视为库存不足完全避免超卖。订单表默认用bigint自增主键但对外返回的订单号建议用雪花算法生成的长整型避免暴露每日订单量。订单状态字段用tinyint表示在代码里用枚举类映射不要散落魔法值。2.4 MyBatis-Plus 与 JPA 的选型差异这个商城项目的数据访问层我推荐 MyBatis-Plus而不是 Spring Data JPA。商城查询场景里商品列表要根据分类、品牌、价格区间、销量排序动态拼接条件JPA 的 Specification 写起来冗长且 SQL 难优化MyBatis-Plus 的LambdaQueryWrapper配合Page分页插件几行代码就能完成多条件查询。核心代码习惯如下LambdaQueryWrapperProductSku wrapper new LambdaQueryWrapper(); wrapper.eq(ProductSku::getStatus, 1) .eq(StringUtils.hasText(categoryId), ProductSku::getSpuId, categoryId) .orderByDesc(ProductSku::getSalesCount); PageProductSku page skuMapper.selectPage(new Page(pageNum, pageSize), wrapper);eq条件里的StringUtils.hasText(categoryId)是常见的动态条件写法空参数直接跳过该条件Page第一个参数是页码从 1 开始第二个是每页条数。MyBatis-Plus 分页需要配置PaginationInnerInterceptor不配置时 selectPage 只返回全量数据这是springboot mybatis-plus 分页不生效问题的最常见原因。JPA 适合实体关系固定、查询模式单一的管理系统商城这种查询维度多的场景MyBatis 系明显更顺手。3. 核心业务写起来JWT 鉴权、购物车缓存与下单事务3.1 Spring Security JWT 无状态鉴权过滤器商城系统登录态适合用 JWT 而非 Session前后端分离后静态资源与接口可能不在同一域名Session 的跨域处理成本高。Spring Security 配置里关掉表单登录注入自定义过滤器解析请求头中的 Token。核心过滤器继承OncePerRequestFilter保证一次请求只执行一次Component public class JwtAuthenticationFilter extends OncePerRequestFilter { Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String token request.getHeader(Authorization); if (StringUtils.hasText(token) token.startsWith(Bearer )) { Claims claims JwtUtil.parseToken(token.substring(7)); if (claims ! null) { Long userId Long.valueOf(claims.getSubject()); ListGrantedAuthority authorities AuthorityUtils .commaSeparatedStringToAuthorityList(claims.get(roles, String.class)); UsernamePasswordAuthenticationToken authentication new UsernamePasswordAuthenticationToken(userId, null, authorities); SecurityContextHolder.getContext().setAuthentication(authentication); } } chain.doFilter(request, response); } }OncePerRequestFilter保证过滤器在 Servlet 容器中只调用一次避免内部转发时重复执行SecurityContextHolder.getContext().setAuthentication()把认证信息放入当前线程上下文后续PreAuthorize(hasRole(USER))和 Controller 里通过SecurityContextHolder获取用户信息都有依赖。Token 里只放 userId 和角色不放敏感信息密钥通过mall.jwt.secret配置生产环境必须用环境变量注入而不是写死在 yml 里。Security 配置类里将/user/**、/order/**设为需要认证/product/**、/auth/**设为 permitAll支付回调接口要跳过鉴权但单独做签名校验。3.2 商品查询缓存与缓存穿透处理商品详情是商城读取压力最大的接口必须走缓存。缓存 key 设计直接决定命中率我这边习惯用mall:sku:{skuId}存商品 JSON过期时间 15 分钟。查询逻辑要做两层保护缓存未命中查库后回填数据库也不存在时写入一个 60 秒过期的空值标记防止恶意使用不存在的 SKU ID 反复打穿缓存。空值缓存是springboot 使用 redis 缓存穿透问题最简单的兜底方案public ProductVO getProduct(Long skuId) { String key mall:sku: skuId; Object cached redisTemplate.opsForValue().get(key); if (cached ! null) { return (ProductVO) cached; } if (Boolean.TRUE.equals(redisTemplate.hasKey(key :empty))) { return null; } ProductVO product skuMapper.selectDetail(skuId); if (product null) { redisTemplate.opsForValue().set(key :empty, 1, 60, TimeUnit.SECONDS); } else { redisTemplate.opsForValue().set(key, product, 15, TimeUnit.MINUTES); } return product; }先查缓存再查空值标记最后查库回填三段式逻辑能挡住大多数穿透流量。商品列表页的热搜榜、新品榜用 ZSet 存储并按销量排序这类数据允许短暂不一致缓存时间可以放宽到 30 分钟。商品搜索不要用关系型数据库LIKE %keyword%数据量过万后全表扫描明显变慢要么上 Elasticsearch要么至少用 MySQL 全文索引过渡。3.3 购物车的 Redis Hash 结构设计购物车模块用 Redis 存更合适用户未登录时也能操作登录后合并。数据结构用 Hashfield 存 SKU IDvalue 存数量key 为mall:cart:{userId}。Controller 层不直接操作 Redis而是通过CartService封装public void addToCart(Long userId, Long skuId, Integer num) { String key mall:cart: userId; BoundHashOperationsString, Object, Object ops redisTemplate.boundHashOps(key); Object exists ops.get(skuId.toString()); int newNum (exists null ? 0 : Integer.parseInt(exists.toString())) num; ops.put(skuId.toString(), String.valueOf(newNum)); redisTemplate.expire(key, 7, TimeUnit.DAYS); }boundHashOps返回绑定 key 的操作对象后续增删改查不用重复拼接 keyexpire设置 7 天过期过期后用户需要重新加购这是 session 型购物车和持久化购物车的折中。结算时选出购物车里的 SKU 集合联表查出最新价格和库存状态服务端必须重新计算金额绝对不能信任前端传过来的单价。合并购物车就是遍历匿名购物车 key 里的 field逐个累加到用户 key 的 field 上注意用 Hash 的increment方法保证原子性。3.4 下单事务的边界乐观锁扣库存与 Redis 幂等键订单创建涉及订单表插入、订单明细插入、库存扣减、购物车清除四个操作必须放在同一个事务里。但事务里不能包含 RPC 调用和 Redis 操作比如支付接口调用和购物车清除就剔除出事务。下单扣库存的核心代码Transactional(rollbackFor Exception.class) public OrderResult submitOrder(OrderSubmitDTO dto) { String idempotentKey mall:order:idem: dto.getRequestId(); Boolean first redisTemplate.opsForValue() .setIfAbsent(idempotentKey, 1, 5, TimeUnit.MINUTES); if (!Boolean.TRUE.equals(first)) { throw new BizException(订单已提交请勿重复操作); } int rows skuMapper.deductStock(dto.getSkuId(), dto.getQuantity()); if (rows 0) { throw new BizException(库存不足或已售罄); } OrderEntity order buildOrder(dto); orderMapper.insert(order); orderItemMapper.insertBatch(order.getItems()); redisTemplate.delete(mall:cart: dto.getUserId()); return OrderResult.of(order); }setIfAbsent利用 Redis 的 SETNX 语义实现幂等控制requestId由前端生成同一次提交重复点击时第二次请求直接抛业务异常。deductStock走乐观锁更新返回受影响行数为 0 说明库存已被其他请求占满。Transactional的rollbackFor Exception.class必须显式声明Spring 默认只回滚 RuntimeException 和 Error自定义的业务异常若是 checked exception 会导致事务不生效。并发高的秒杀场景下单和扣库存可以分离扣库存成功后发送 RabbitMQ 消息异步创建订单主链路响应更快。4. 高并发改造分布式锁、缓存双删与异步化的演进4.1 单体下 synchronized 到分布式锁的演进商城早期并发不大时扣库存直接用synchronized (skuId.intern())能扛住压力但多实例部署后锁就失效了必须换分布式锁。Redis 分布式锁最小实现是用setIfAbsent加expirepublic boolean tryLock(String lockKey, String requestId, long expireSeconds) { return redisTemplate.opsForValue() .setIfAbsent(lockKey, requestId, expireSeconds, TimeUnit.SECONDS); } public void unlock(String lockKey, String requestId) { String value redisTemplate.opsForValue().get(lockKey).toString(); if (requestId.equals(value)) { redisTemplate.delete(lockKey); } }requestId用 UUID 标识持有者释放锁前比较是否为当前线程持有防止误删别的线程的锁。网上搜springboot 分布式锁能找到大量实现但真实生产还要考虑锁续期问题推荐直接引入 Redisson 的RLock它内置 Watchdog 自动续期机制。事务里加锁的顺序必须一致先锁 SKU再锁用户维度避免多个请求交叉持有锁造成死锁。分布式锁只保护库存扣减这一临界区不要把整个下单流程都锁在锁里。4.2 缓存一致性延迟双删的取舍缓存更新策略在商城场景下先更新数据库再删除缓存是基本盘。最脏的时序是请求 A 读缓存未命中查出旧值请求 B 更新数据库并删缓存请求 A 再回填旧值此时缓存里是脏数据。延迟双删能显著降低概率public void updateStockAndEvictCache(Long skuId, Integer newStock) { String key mall:sku: skuId; redisTemplate.delete(key); skuMapper.updateStock(skuId, newStock); cacheEvictExecutor.schedule(() - redisTemplate.delete(key), 500, TimeUnit.MILLISECONDS); }先删一次缓存再更新数据库最后延迟 500 毫秒删除一次。第二次删除由线程池执行不阻塞主线程500 毫秒的窗口覆盖了读请求查出旧值到回填缓存的时间间隙。这种方式不保证绝对一致但商城商品详情对一致性容忍度较高能接受秒级延迟。如果业务要求强一致正确做法是订阅 MySQL binlog 变更再删缓存中间用 Canal 转发变更事件。缓存和数据库之间没有完美方案只有不同业务场景下的匹配程度。4.3 本地队列削峰与消息队列的选择积分兑换、秒杀这类突发流量直接打到数据库容易把连接池打满。SpringBoot 项目里最轻量的削峰方式是本地阻塞队列加多线程消费Component public class SeckillOrderHandler { private final LinkedBlockingQueueSeckillRequest queue new LinkedBlockingQueue(5000); EventListener(ApplicationReadyEvent.class) public void startConsumers() { for (int i 0; i 4; i) { Thread consumer new Thread(() - { while (true) { try { SeckillRequest request queue.take(); doCreateOrder(request); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }, seckill-consumer- i); consumer.start(); } } }LinkedBlockingQueue容量 5000超过容量的请求直接拒绝并提示活动太火爆EventListener(ApplicationReadyEvent.class)保证应用完全启动后再拉起消费者线程避免依赖未注入。本地队列的缺点是消息不持久化、节点重启丢数据且多实例部署时每个节点各消费各的库存总量需要提前按节点分摊。业务量再上去就得引入 RocketMQ 或 RabbitMQSpringBoot 整合 RocketMQ 只需要配置 producer 和 consumer 的注解即可思路一致前端请求只写消息后端服务异步消费削峰的本质是瞬时流量转队列落到数据库的 QPS 恒定。4.4 多环境配置与模块化拆分商城项目一定要拆分application-dev.yml、application-prod.yml通过spring.profiles.active切换环境。生产环境关闭 Swagger 文档开启 Actuator 但只暴露 health 和 info 端点线上出现 Swagger 未授权访问漏洞多是因为springdoc.api-docs.enabled没有按环境关闭。依赖注入用构造器方式而不用Autowired字段注入方便单元测试也更容易排查循环依赖这是 SpringBoot 官方推荐并逐渐普及的写法。定时任务方面Scheduled注解适合订单超时取消这类简单场景加fixedDelay控制执行间隔定时任务方法内部要捕获异常否则一次异常会中断后续所有调度。5. 上线前最后一遍SpringBoot 打包参数与生产故障清单5.1 容器化部署的 Dockerfile 与 JVM 参数用 Docker 部署 SpringBoot 商城时多阶段构建能显著减小镜像体积。第一阶段用 maven 镜像编译第二阶段用 JRE 镜像运行FROM maven:3.9-eclipse-temurin-17 AS builder WORKDIR /build COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn clean package -DskipTests -Pprod FROM eclipse-temurin:17-jre WORKDIR /app ARG JAR_FILE/build/target/mall-server.jar COPY --frombuilder ${JAR_FILE} ./app.jar ENV JAVA_OPTS-Xms512m -Xmx1024m -XX:UseG1GC ENV TZAsia/Shanghai EXPOSE 8080 ENTRYPOINT [sh, -c, java $JAVA_OPTS -jar ./app.jar]dependency:go-offline提前拉取依赖后续源码变动时充分利用 Docker 的层缓存避免每次重新下载整个仓库依赖。JVM 参数重点设置-Xms和-Xmx为相同值避免堆动态伸缩带来的性能抖动使用 G1 收集器适合大堆场景。部署时通过docker run -e SPRING_PROFILES_ACTIVEprod注入环境数据库密码和 Redis 密码不要写死在镜像里。5.2 生产环境高频问题排查清单与处置建议现象根因处置手段项目无法启动提示端口被占用本地多个服务冲突检查server.port或lsof -i:8080找到占用进程IDEA 创建 SpringBoot 项目超时网络无法访问初始化服务地址改用阿里云初始化地址或本地start.spring.io镜像SpringBoot 版本太高依赖不兼容新版本升级了底层依赖如 Servlet 或 Spring 6锁版本号到 2.7.x 或 3.2.x确认 JDK 对应 17 以上请求响应慢大量超时数据库连接池配置过低hikari.maximum-pool-size调大结合spring.datasource.hikari.connection-timeout线上发生堆内存溢出缓存数据膨胀或大对象未释放用jmap -dump:formatb,fileheap.bin pid抓包分析API 未授权访问Swagger 暴露生产环境未关闭文档端点springdoc.api-docs.enabledfalseActuator 只留 healthJAR 包能启动不代表配置正确上线验证时我最先执行的命令是按接口分层验证健康状态。基础层看数据库连通和 Redis 连通业务层看登录接口和商品列表接口的响应时间流程层跑通一个真实下单流程并检查订单表和库存表数据。诊断运行中问题用jstack pid查看线程状态如果大量线程阻塞在http-nio-8080-exec-*且状态为 WAITING通常是线程池配置过小或下游依赖阻塞如果堆内存使用率持续高位且 GC 频繁优先怀疑缓存 key 没设置过期时间。JAR 包内的BOOT-INF/classes下可以找到所有配置文件生产环境排查时先确认application-prod.yml是否真的被加载用java -jar app.jar --debug启动能看到自动装配报告条件注解未生效时报告里会标注匹配失败原因。本文还有配套的精品资源点击获取