简介这份SpringBoot天猫商城整站项目适合有一定Java基础、想系统学习前后端分离电商系统的开发者。项目采用SpringBoot作为后端前端整合Bootstrap、Vue.js与jQuery页面由Thymeleaf渲染数据层使用JPA搭配Redis缓存并集成Shiro做登录授权与访问控制同时接入Elasticsearch实现商品检索技术栈覆盖面广能帮助读者理解企业级电商项目的常用解决方案。压缩包共2000个文件以1823张jpg商品与页面素材为主另有64个html页面模板、61个java源码、js/css前端脚本及SQL初始化脚本、项目配置文件等整体大小约287.38MB目录结构完整便于按模块研读。目前已有530人学习下载。通过学习这份资源可以掌握前后端分离接口设计的常见模式参考Redis缓存、Shiro权限和Elasticsearch搜索在SpringBoot中的整合方式还能直接复用整套商城页面与数据脚本适合用于课程设计或毕业设计二次开发。1. tmall_springboot 是什么这套商城技术栈能解决哪些痛点tmall_springboot 是一个很有代表性的电商商城项目名。第一次接手时你大概率会被这套技术组合吓到后端用 SpringBoot JPA权限用 Shiro会话用 Redis搜索用 Elasticsearch前端同时站着 Vue.js、Bootstrap 和 jQuery。听起来像全家桶其实只要先把边界划清它就是一套能快速跑通商品管理、购物车、订单、后台权限的中型商城脚手架。我更喜欢把它当成一份电商前后端分离的标准答案来读。真正会让你翻车的不是那些增删改查而是 Shiro 的 session 怎么跟 Redis 串起来、Elasticsearch 里的商品数据怎么和 MySQL 保持一致、JPA 的懒加载怎么不把接口搞挂。这几个点平时看不到源码属于黑匣子不看透就只能靠一层层试错。这套方案适合中小团队做商城二次开发也适合想快速搭一个电商原型来验证业务的人。如果你已经知道 SpringBoot 的基本用法但没跑过带搜索和权限的完整商城这篇正好能给你一条可复现的落地路径。2. 把全家桶拆开谁管页面、谁管搜索、谁管登录2.1 先认组件商城系统里这套技术栈的分工技术组件在商城里的职责我通常放的位置SpringBoot后端主框架提供 Web 接口和 Bean 容器后端工程根JPA用户、商品、订单、购物车等关系表映射repository / service 层Redis登录会话、热点商品缓存、接口限流独立 Redis 服务Shiro登录认证、后台权限拦截、角色菜单控制后端 filter 链Elasticsearch商品搜索、关键词高亮、筛选排序独立 ES 集群Vue.js商城前台的动态页面与交互前端工程 srcBootstrap jQuery后台管理页面的栅格、表格和 AJAX后端 static 包一份前两年我看到这种全家桶项目第一反应是觉得过度设计。跑通之后才发现这里每个组件都有明确位置JPA 管关系型数据Redis 管热数据和登录态Elasticsearch 管搜索Shiro 管认证授权Vue.js 和 Bootstrap 管前后台界面。关键的“前后端分离”并不等于把 Vue 工程建出来就完事后端接口要能同时喂给两个前端一个是单页应用一个是后台模板页面。常见做法是给 Vue 工程单独开一个端口开发后台模板继续挂在 SpringBoot 的 static 目录下这样后台开发不用额外起 Node 服务。2.2 最小可运行的 SpringBoot 项目骨架依赖清单与商品接口我一般会把 pom.xml 收敛到几样东西。注意这里的 ES 客户端用的是 rest-high-level-client虽然官方早就建议新项目用新客户端但很多电商项目骨架还在用 7.17 这一版代码量最大且资料最多。parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.x/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-jpa/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdorg.apache.shiro/groupId artifactIdshiro-spring-boot-starter/artifactId version1.9.x/version /dependency dependency groupIdorg.elasticsearch.client/groupId artifactIdelasticsearch-rest-high-level-client/artifactId version7.17.x/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency /dependencies这段只有依赖骨架版本里的 x 要按你本地的 Spring Boot 基线去对应。2.7.x 只是建议基线如果选 Spring Boot 3.xshiro-spring-boot-starter 和 ES 客户端的适配版本都不同不要硬抄。Redis 依赖是个容易出问题的地方直接用 starter 会帮你配好连接工厂不要自己再手写一个竞态的 RedisConfig。再往下是最简单的商品接口能验证 JPA 和 JSON 序列化是否通。Entity Table(name product) public class Product { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; private String title; private BigDecimal price; private Integer status; // getter / setter 由 IDE 生成这里省略 } public interface ProductRepository extends JpaRepositoryProduct, Long { PageProduct findByStatus(Integer status, Pageable pageable); Query(select p from Product p where p.title like concat(%, :keyword, %)) PageProduct searchByTitle(Param(keyword) String keyword, Pageable pageable); } RestController RequestMapping(/api/product) public class ProductController { private final ProductRepository productRepository; public ProductController(ProductRepository productRepository) { this.productRepository productRepository; } GetMapping public PageProduct list(RequestParam(defaultValue 1) int page) { return productRepository.findByStatus(1, PageRequest.of(page - 1, 10)); } }这里的 status 字段做上下架过滤前端分页传 page 从 1 开始JPA 内部页码从 0 开始所以查询前先减 1。findByStatus 这种命名方法会按字段自动拼 SQL简单场景不用写 Query。不过这只是验证骨架实际商城接口不会直接返回 JPA 实体最后一章我会专门说这个坑。2.3 Shiro Redis 的登录态设计session 不丢、重启不掉线Shiro 原生的会话保存在本地内存里商城服务一重启所有用户都掉线。更麻烦的是前后端分离后前端拿到的 session id 容易在跨域或浏览器策略下丢失用户一刷新就要重新登录。这个项目把 Shiro 的 SessionDAO 换成 Redis就是为了解决重启和集群下的登录态问题。Configuration public class ShiroRedisSessionConfig { Bean public RedisTemplateString, Object redisTemplate(RedisConnectionFactory factory) { RedisTemplateString, Object redisTemplate new RedisTemplate(); redisTemplate.setConnectionFactory(factory); redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setHashKeySerializer(new StringRedisSerializer()); redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); redisTemplate.afterPropertiesSet(); return redisTemplate; } Bean public RedisSessionDAO redisSessionDAO(RedisTemplateString, Object redisTemplate) { RedisSessionDAO sessionDAO new RedisSessionDAO(); sessionDAO.setRedisManager(redisManager(redisTemplate)); return sessionDAO; } Bean public DefaultWebSessionManager sessionManager(RedisSessionDAO redisSessionDAO) { DefaultWebSessionManager sessionManager new DefaultWebSessionManager(); sessionManager.setSessionDAO(redisSessionDAO); sessionManager.setGlobalSessionTimeout(30 * 60 * 1000L); // 禁止 URL 重写带 jsessionid否则前端每个请求都会被污染 sessionManager.setSessionIdUrlRewritingEnabled(false); return sessionManager; } }这段关键是思路不是完整能直接跑的生产代码。生产环境一般用现成的 shiro-redis 插件把 RedisManager 注入 Shiro自己从零实现 SessionDAO 的序列化和过期策略容易踩坑。需要关注的参数有三个globalSessionTimeout 默认 30 分钟sessionIdUrlRewritingEnabled 必须关掉Redis 的 key 建议加业务前缀比如 tmall:shiro:session:避免和其他业务混在一起。提示如果你发现登录接口正常但刷新后 session 丢失先看 Redis 里有没有 session key再看响应头有没有 Set-Cookie。两个问题通常占九成。3. Elasticsearch 商品搜索让 SQL 里的 like 不再背锅3.1 商品索引结构先设计 mapping 再写代码商品搜索如果直接用 MySQL 的 like 查询数据量到万级就开始明显变慢而且没法做高亮和相关性排序。Elasticsearch 在商城里的定位是搜索与展示不存完整订单关系。第一步不是写 Java 代码而是先定 mapping。{ mappings: { properties: { spuId: { type: long }, title: { type: text, analyzer: ik_max_word, search_analyzer: ik_smart }, categoryId: { type: long }, price: { type: double }, status: { type: integer } } } }title 字段先用中文分词器切分比如“羽绒服男”会被切成更细的 token能让搜索命中更多长尾词。ik_max_word 用于索引时的最大切分ik_smart 用于搜索时更精准的切分这是中文电商搜索里最常见的组合。ES 7 之后去掉了 type 概念索引下直接跟 properties如果你现在维护的还是 ES 6 老索引需要把 /_doc 这种路径都改掉。3.2 批量导入从 MySQL 到 ES 的第一份数据写索引只完成了一半数据怎么从 MySQL 过来才是真正要解决的问题。第一次全量导入时我建议用分页循环而不是一次性查全表否则大商品表会把内存打满。Component public class ProductIndexService { private final ProductRepository productRepository; private final RestHighLevelClient restHighLevelClient; public ProductIndexService(ProductRepository productRepository, RestHighLevelClient restHighLevelClient) { this.productRepository productRepository; this.restHighLevelClient restHighLevelClient; } public void rebuild() throws IOException { int page 0; int size 500; while (true) { PageProduct products productRepository.findAll(PageRequest.of(page, size)); if (products.isEmpty()) { break; } BulkRequest bulkRequest new BulkRequest(); for (Product product : products) { if (product.getStatus() null || product.getStatus() ! 1) { continue; } MapString, Object source new HashMap(); source.put(spuId, product.getId()); source.put(title, product.getTitle()); source.put(price, product.getPrice()); source.put(status, product.getStatus()); bulkRequest.add(new IndexRequest(product_spu) .id(String.valueOf(product.getId())) .source(source, XContentType.JSON)); } if (bulkRequest.numberOfActions() 0) { restHighLevelClient.bulk(bulkRequest, RequestOptions.DEFAULT); } page; } } }这段逻辑里有两个容易忽略的地方。第一分页必须用 PageRequest 每次都新查不要复用同一个 Page 对象否则 JPA 会按上一次的位置继续查结果出现重复数据。第二BulkRequest 是一次性把一批请求打给 ES大批量时 numbersOfActions 要控制常见做法是每 1000 条或 10000 条提交一次不然网关或 ES 会自动拒绝请求。全量导入脚本只适合上线前跑平时不能人工触发太频繁。3.3 搜索接口与高亮关键词高亮不是前端的事ES 的高亮是在查询时返回标签片段前端拿到带标记的颜色高亮。很多人以为高亮是前端用字符串替换做实际上在 ES 里设置高亮字段更高效。下面是一个带价格排序和状态过滤的搜索请求。public SearchResponse search(String keyword, int page, int size) throws IOException { SearchSourceBuilder source new SearchSourceBuilder(); BoolQueryBuilder bool QueryBuilders.boolQuery(); bool.filter(QueryBuilders.termQuery(status, 1)); bool.must(QueryBuilders.matchQuery(title, keyword)); source.query(bool); source.sort(price, SortOrder.ASC); source.from((page - 1) * size); source.size(size); HighlightBuilder highlight new HighlightBuilder(); highlight.field(title); highlight.preTags(b class\keyword\); highlight.postTags(/b); source.highlighter(highlight); SearchRequest request new SearchRequest(product_spu); request.source(source); return restHighLevelClient.search(request, RequestOptions.DEFAULT); }这里的 boolQuery 是关键filter 里的 status 不走 scoring性能比 must 更好title 用 matchQuery 走分词匹配。排序可以直接写在 ES 里价格这种字段不要等到 Java 内存里再排数据量大后一次性能差距就出来了。高亮片段默认每个字段返回一段可以在 HighlightBuilder 里调 fragmentSize 和 numOfFragments避免详情太长。3.4 增量同步别在 Controller 里写双写全量导入解决了历史数据增量数据才是日常搜索不落后的关键。常见做法有三个定时任务查最近修改时间应用发消息后消费还有监听 MySQL binlog 同步。第一版商城用定时任务最简单每隔一分钟查一次 updateTime 大于上次时间的商品再增量写入 ES能覆盖绝大多数中小电商场景。不要在生产代码里写好商品后顺手再写 ES事务不好控制MySQL 回滚了 ES 可能已经写入后面对数会非常痛苦。4. 商城集成避坑记录5 个能查到的具体现象与解法这套技术栈单独用都不难放在一起时容易互相打架。下面 5 条是我在模拟项目里真实遇到过、也在反复排查的问题每一条都按现象、原因、解决的顺序写。4.1 前后端联调时 session 凭空消失现象Vue 前端登录成功跳转后刷新页面又回到登录页后端日志里看不到任何异常。原因Shiro 默认开着 URL 重写某些请求里带着 jsessionid前端没有正确处理 Cookie 域。更常见的是前后端端口不一致跨域请求没带 credentialsSet-Cookie 被浏览器拦掉。解决后端打开跨域配置时设置 withCredentials前端 axios 里也要带 withCredentials。Shiro 那边把 sessionIdUrlRewritingEnabled 关掉只允许前端从 Set-Cookie 拿 sessionId。还要确认 Cookie 的 SameSite 策略跨站点场景下要设成 Lax 或 None同时把 HttpOnly 短周期打开。4.2 后台管理页面样式全丢图片加载不出来现象登录后台后页面纯文本Bootstrap 的 CSS 和 jQuery 的 JS 全部 404。原因Shiro 的过滤器把所有路径都拦了静态资源访问被当成未登录请求重定向到登录页浏览器拿到的是 HTML 而不是 CSS 文件。解决在 Shiro 配置里显式放行静态目录例如 /static/, /css/, /js/, /img/。同时检查路径前缀SpringBoot 的 static 资源默认是 /static 还是根路径不同版本略有差异放行时不要写错。filterChainDefinitionMap.put(/static/**, anon); filterChainDefinitionMap.put(/css/**, anon); filterChainDefinitionMap.put(/js/**, anon); filterChainDefinitionMap.put(/api/login, anon); filterChainDefinitionMap.put(/api/register, anon); filterChainDefinitionMap.put(/**, authc);这个方法网上到处都是但很多人会漏掉 favicon.ico浏览器控制台一片红不影响功能却影响排查。4.3 JPA 懒加载在 JSON 序列化时抛 LazyInitializationException现象查询订单详情接口报 500异常信息带有 LazyInitializationException。原因订单实体里关联了用户和商品默认 fetch 是 LAZY事务结束、连接关闭后Jackson 碰到未加载的关联对象就会炸。解决有三种方式。最省事的是在 application.yml 里配置 hibernate 懒加载解决关闭会话但不建议一开到底。次选是把接口返回值换成 VO只拷贝需要的字段。最后是配置 jackson-datatype-hibernate让序列化时忽略未加载的空代理。实际项目里我一般强制走 VO前面代码能跑但后面维护成本最低。这个坑我在最后一章还会展开。4.4 商品详情页缓存穿透Redis 被打穿现象某个低价爆款商品一上线Redis 缓存不断被清空MySQL 压力飙升接口响应从几十毫秒变成几秒。原因查询逻辑是先查 Redis没有再去 MySQL。高并发下缓存没有预热或者商品下架导致缓存失效同一时间大量请求全部打进了数据库。解决热点商品缓存加上 null 缓存五分钟防穿透更新商品上下架时主动删除缓存再重建还有一个验收习惯压测前先确认详情页是否有预热脚本不能用一把梭直接让用户把热点打出来。4.5 Elasticsearch 和 MySQL 数量对不上现象后台商品总数与搜索总条数不一致差出几十条。原因全量同步跑完后又有商品上下架增量同步没有触发或者触发时只根据商品更新时间但更新发生在凌晨且定时任务漏跑。解决把数据核对写进一个独立接口每天一次对比总数超过阈值就发消息。不要人工去看 Kibana跑一次批量对账脚本比肉眼可靠得多。5. 从零跑起来环境参数、配置文件和启动顺序5.1 软件环境参数表数据库、Redis、Elasticsearch 的最低配置组件版本建议最少部署配置注意事项JDK1.8 或 111 核 2GSpring Boot 2.7 用 JDK8 兼容性最好MySQL5.7 或 8.01 核 2G字符集 utf8mb4时区设 Asia/ShanghaiRedis5.0 以上1 核 1G关闭持久化可加快开发生产必须开 AOFElasticsearch7.17 系列2 核 4G单机启动前要改http.host为0.0.0.0这套配置只够本地开发商城并发上到几百就建议拆 Redis 和 ES 到独立机器。ES 很吃内存启动时如果堆内存不够先调 jvm.options不要一上来给 1G 就强行跑否则等下就 OOM。5.2 配置 application.yml数据源、JPA、Redis、ES 地址spring: datasource: url: jdbc:mysql://localhost:3306/tmall?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: root jpa: hibernate: ddl-auto: update show-sql: true properties: hibernate: format_sql: true redis: host: localhost port: 6379 database: 0 timeout: 3000ms server: port: 8080 servlet: context-path: / elasticsearch: host: localhost:9200商家后台通常用ddl-auto开发期尽快建表生产环境不要用 update否则线上表结构会被自动改这是电商项目的大忌。JPA 的 show-sql 开发期开着日志一多也会影响性能联调完就关上。Redis 的 database 建议单独分一个库出来别和日志、验证码混在一起出事好排查。5.3 启动顺序和前端联调启动顺序有讲究不是先工程后中间件。正确顺序是 MySQL、Redis、Elasticsearch 依次起来再启动 SpringBoot最后启动 Vue 前端。ES 启动比较慢等它日志打出 started 再跑后端否则后端的 RestHighLevelClient 初始化会连接失败。前端 Vue 开发环境代理到一个端口避免跨域。vue.config.js 里最简单写法module.exports { devServer: { port: 3000, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } }代理生效后前端请求 /api/product 会转发到 8080。你可能会碰到 changeOrigin 没开导致 session 丢失登录接口能通但后续请求 401问题就出在这里。后台管理页面如果走 Bootstrap jQuery直接放在后端 static 目录编译产物不用额外起服务这也是这个项目保留两套前端的原因。6. 进阶技巧JPA 实体返回前端前先过一道 VO 层最后一招是我在这个项目里最想强调的一个习惯Controller 永远不要直接返回 JPA 实体。JPA 实体的 lazy 关联、代理对象、双向引用每一样都能在你改接口时跳出来咬一口。与其到处加JsonIgnore不如在 service 层直接把实体转成 VO只暴露前端需要的字段。先加一个序列化模块兜底避免某些遗漏的实体被 Jackson 碰到dependency groupIdcom.fasterxml.jackson.datatype/groupId artifactIdjackson-datatype-hibernate5/artifactId /dependency然后定义一个简单的 VOpublic class OrderVO { private Long orderId; private String orderNo; private BigDecimal totalAmount; private ListOrderItemVO items; // getters / setters 省略 }Service 层从 JPA 查出订单后手动拷贝字段。代码看起来多一点但接口的字段名、层级、返回结构都变得可控。最直接的好处是前端再不拿不到多余的长字段也不会一刷新就碰到懒加载异常。数据库表结构再怎么改前端接口稳定多久你都能兜住。每次看到有人为了省十几行代码直接 return product我都会想劝一句这种写法在 Demo 里没问题但商城项目是要一直加需求、一直改页面的实体一旦直接暴露后面每个字段变更都是一次破坏性升级。我现在遇到电商项目先确认接口返回是不是 VO不是的话宁可多花半天也要改完再继续。这个习惯救了我很多次希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?