资讯详情

资讯详情

SpringBoot+Vue+ES电商后台全链路实战指南

简介这是一套完整的淘宝天猫风格电商商城实战项目面向Java后端与全栈开发者聚焦高并发场景下的SpringBoot企业级应用开发。项目采用前后端分离架构后端基于SpringBootJPAShiroRedisElasticsearch构建涵盖用户认证、商品搜索、缓存优化与权限控制等核心模块前端整合Vue.js、Bootstrap与jQuery页面渲染使用Thymeleaf3兼顾开发效率与工程规范。资源包共2000个文件含61个Java业务逻辑与配置类、64个HTML模板页、1823张商品及界面截图JPG/PNG/GIF辅以JS/CSS/SQL/properties等配套文件整体达287.38MB结构清晰、模块完整可直接导入IDE运行调试。目前已有532人学习下载读者可获得可商用的电商系统源码、标准化目录结构、Elasticsearch商品检索集成方案、Shiro细粒度权限配置示例及Redis缓存实战策略是进阶SpringBoot全栈开发的优质参考范例。1. 一个能跑通的电商后台从登录页到商品搜索SpringBoot Vue Elasticsearch 全链路可调试你刚 clone 下来一个叫tmall_springboot的仓库README.md里写着“淘宝天猫风格电商商城”但点开目录全是src/main/java/com/example/...和一堆vue/src/views/没文档、没数据库脚本、application.yml里 Redis 和 ES 配置全空着——这种项目90% 的人卡在「启动报错」那一步然后默默删掉。但其实它不是玩具 Demo它真有用户中心、商品管理、订单流程、全文搜索、权限控制Shiro、缓存加速Redis和高亮检索Elasticsearch而且前后端完全分离Vue 走 axios 调 SpringBoot REST APIJPA 做数据层Bootstrap jQuery 混合支撑老式管理页。它适合两类人一是想快速搭一个功能完整、技术栈主流、能真实改能上线的 SpringBoot 电商骨架的中级 Java 开发二是正在学 Shiro 权限模型、Elasticsearch 商品搜索、Redis 缓存穿透防护的实战者。别被标题里的“淘宝天猫”吓住——它不模拟秒杀或分布式事务但把电商核心模块的接口定义、状态流转、缓存策略、搜索分词逻辑都落到了代码里且每层都留了调试入口。我上周用它给某高校实验室搭教学平台从拉代码到跑通搜索页只花了 3 小时关键就在这套结构后端是标准 SpringBoot 分层Controller → Service → Repository前端 Vue 是路由级模块拆分连登录态校验都同时支持 Shiro Session 和 JWT Token 切换。下面带你一节一节拆明白怎么让它真正动起来。2. 环境准备与服务依赖MySQL Redis Elasticsearch 三件套的最小可行配置这个项目不是“开箱即用”而是“开箱即配”——它的设计哲学是让开发者亲手把每个中间件接上从而理解数据流向。你不需要部署整套云环境本地 Docker 或直接安装均可但必须明确版本边界MySQL 5.7JPA 默认方言适配、Redis 6.2Shiro Session 存储需 RESP3 支持、Elasticsearch 7.17注意不是 8.xspring-boot-starter-data-elasticsearch2.7.x 仅兼容 7.x。下面按真实踩坑顺序说明。2.1 MySQL 初始化建库、建表、填基础数据项目没带schema.sql但src/main/resources/data.sql里藏了初始化语句。别急着执行——先确认你的 MySQL 用户有CREATE DATABASE权限。推荐新建专用库CREATE DATABASE tmall CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;提示utf8mb4是硬性要求商品标题含 emoji 或生僻字时utf8会报错Incorrect string value。然后执行data.sql路径src/main/resources/data.sql。它会创建 8 张表user,product,category,order,order_item,product_image,review,shiro_role_permission。重点看user表初始化数据INSERT INTO user (id, username, password, email, phone, status, create_time) VALUES (1, admin, $2a$10$QqGzZvXwYyKpLmNnOoPpQqRrSsTtUuVvWwXxYyZzAaBbCcDdEeFfGgHh, admintmall.com, 13800138000, 1, NOW());密码是 BCrypt 加密后的adminShiro 会自动校验。如果你要改密码用在线 BCrypt 工具生成新 hash别手写。2.2 Redis 配置不只是缓存更是 Shiro Session 的载体application.yml中 Redis 配置段必须补全spring: redis: host: 127.0.0.1 port: 6379 database: 0 password: # 若有密码请填写 timeout: 2000 lettuce: pool: max-active: 8 max-idle: 8 min-idle: 0 max-wait: -1关键点在于Shiro 的RedisCacheManager和RedisSessionDAO都依赖此配置。项目中com.example.tmall.config.shiro.ShiroConfig.java定义了 Session 持久化Bean public SessionDAO sessionDAO() { RedisSessionDAO sessionDAO new RedisSessionDAO(); sessionDAO.setRedisManager(redisManager()); // 此处绑定上面的 RedisManager sessionDAO.setExpire(1800); // Session 过期时间30分钟 return sessionDAO; }如果你跳过这步登录后刷新页面就 401因为 Shiro 找不到 Session 存储位置。实测发现Redis 未启动时SpringBoot 启动不报错但首次登录会卡在Subject.login(token)日志里只有RedisConnectionException: Unable to connect—— 这是典型静默失败务必检查 Redis 进程。2.3 Elasticsearch 配置7.17 版本的索引模板与分词器预设Elasticsearch 不是可选组件商品搜索/api/product/search强依赖它。application.yml中配置spring: elasticsearch: uris: http://127.0.0.1:9200 username: elastic password: changeme # Docker 默认密码若改过请同步但光配地址不够。项目在src/main/java/com/example/tmall/config/es/ElasticsearchConfig.java中定义了索引初始化逻辑PostConstruct public void initIndex() { try { if (!restHighLevelClient.indices().exists( new GetIndexRequest(product_index), RequestOptions.DEFAULT)) { CreateIndexRequest request new CreateIndexRequest(product_index); request.mapping(MappingBuilder.build()); // 关键调用自定义映射 restHighLevelClient.indices().create(request, RequestOptions.DEFAULT); } } catch (IOException e) { log.error(Failed to init ES index, e); } }MappingBuilder.build()返回的是一个 JSON 字符串定义了product_index的字段类型{ mappings: { properties: { id: { type: long }, name: { type: text, analyzer: ik_max_word, search_analyzer: ik_smart }, description: { type: text, analyzer: ik_max_word }, price: { type: double }, categoryId: { type: long } } } }注意ik_max_word和ik_smart是中文分词插件必须手动安装 elasticsearch-analysis-ik 插件7.17 对应版本。Docker 启动命令示例docker run -d \ --name es-tmall \ -p 9200:9200 -p 9300:9300 \ -e discovery.typesingle-node \ -e ES_JAVA_OPTS-Xms512m -Xmx512m \ -v /path/to/elasticsearch/plugins:/usr/share/elasticsearch/plugins \ docker.elastic.co/elasticsearch/elasticsearch:7.17.0然后进入容器安装 IK./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.17.0/elasticsearch-analysis-ik-7.17.0.zip不装 IK搜索“手机”会匹配不到“苹果手机”因为默认 standard 分词器把中文当单字切。3. 后端启动与核心模块拆解从 Shiro 登录鉴权到 Elasticsearch 商品搜索启动前请确保pom.xml中依赖版本对齐。该项目使用 Spring Boot 2.7.18非 3.x对应 Spring Framework 5.3.x因此spring-boot-starter-web、spring-boot-starter-data-jpa、spring-boot-starter-data-elasticsearch全部锁定在2.7.18。若你本地 Maven 仓库有冲突强制指定properties spring-boot.version2.7.18/spring-boot.version /properties dependencyManagement dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-dependencies/artifactId version${spring-boot.version}/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement3.1 Shiro 权限控制链从 LoginController 到 Realm 数据加载登录入口在com.example.tmall.controller.LoginController.javaPostMapping(/login) public Result login(RequestBody UserLoginDTO dto) { UsernamePasswordToken token new UsernamePasswordToken(dto.getUsername(), dto.getPassword()); Subject subject SecurityUtils.getSubject(); try { subject.login(token); // 关键触发 Realm 认证 User user (User) subject.getPrincipal(); String jwt jwtUtil.generateToken(user.getId(), user.getUsername()); return Result.success(Map.of(token, jwt, user, user)); } catch (AuthenticationException e) { return Result.fail(用户名或密码错误); } }subject.login(token)会调用com.example.tmall.realm.CustomRealm.java的doGetAuthenticationInfo方法Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { UsernamePasswordToken upToken (UsernamePasswordToken) token; String username upToken.getUsername(); User user userService.findByUsername(username); // JPA 查询 if (user null) { throw new UnknownAccountException(用户名不存在); } if (user.getStatus() ! 1) { throw new LockedAccountException(账户已被禁用); } // 构造 SimpleAuthenticationInfo凭证、加密盐、realm 名 SimpleAuthenticationInfo info new SimpleAuthenticationInfo( user, user.getPassword(), ByteSource.Util.bytes(user.getCredentialsSalt()), getName() ); return info; }这里暴露了两个关键设计点密码加盐存储user.getCredentialsSalt()是数据库中独立字段值为 UUID 字符串BCrypt 加密时参与运算状态拦截user.getStatus()控制启用/禁用比单纯删用户更安全。权限注解用在 Controller 层例如商品删除DeleteMapping(/{id}) RequiresPermissions(product:delete) // Shiro 注解 public Result delete(PathVariable Long id) { productService.delete(id); return Result.success(); }权限数据来自shiro_role_permission表通过com.example.tmall.service.impl.ShiroServiceImpl.java的findPermissionsByUserId方法动态加载实现 RBAC 模型。3.2 商品搜索模块ElasticsearchTemplate 高亮 分页实战搜索接口ProductSearchController.search()是全链路最值得细读的部分PostMapping(/search) public Result search(RequestBody ProductSearchDTO dto) { // 1. 构建 NativeSearchQueryBuilder NativeSearchQueryBuilder builder new NativeSearchQueryBuilder(); builder.withQuery(boolQuery() .must(matchQuery(name, dto.getKeyword()).operator(Operator.AND)) .filter(rangeQuery(price).gte(dto.getMinPrice()).lte(dto.getMaxPrice())) ); // 2. 添加高亮 builder.withHighlightFields( new HighlightBuilder.Field(name).preTags(em).postTags(/em), new HighlightBuilder.Field(description).preTags(em).postTags(/em) ); // 3. 分页 builder.withPageable(PageRequest.of(dto.getPageNum() - 1, dto.getPageSize())); SearchHitsProduct hits elasticsearchTemplate.search(builder.build(), Product.class); ListProduct products hits.getSearchHits().stream() .map(hit - { Product p hit.getContent(); // 4. 注入高亮结果 if (hit.getHighlightFields().containsKey(name)) { p.setName(hit.getHighlightFields().get(name).getHighlights().get(0).getFragment()); } return p; }) .collect(Collectors.toList()); return Result.success(new PageResult(products, hits.getTotalHits(), dto.getPageSize())); }这段代码教了四件事boolQuery().must().filter()实现关键词价格区间组合查询HighlightBuilder控制em包裹高亮词前端 Vue 直接v-html渲染PageRequest.of(pageNum-1, size)是 Spring Data Elasticsearch 的偏移习惯hit.getContent()是原始文档hit.getHighlightFields()是高亮片段必须手动合并。如果你搜索无结果先检查product_index是否有数据用 Kibana Dev Tools 执行GET /product_index/_count返回{count:0}说明索引为空——此时需调用/api/product/syncToEs接口同步 MySQL 商品数据该接口在ProductSyncController中内部用ElasticsearchOperations.bulkIndex()批量写入。3.3 Redis 缓存策略穿透、雪崩、击穿的三层防护落地缓存逻辑集中在com.example.tmall.service.impl.ProductServiceImpl.java。以getById(Long id)为例Override public Product getById(Long id) { String key product: id; // 1. 先查 Redis Product product redisTemplate.opsForValue().get(key); if (product ! null) { return product; } // 2. 缓存穿透防护布隆过滤器此处简化为空值缓存 String nullKey null:product: id; if (redisTemplate.hasKey(nullKey)) { return null; } // 3. 查 DB product productRepository.findById(id).orElse(null); if (product null) { // 写空值过期 2 分钟防穿透 redisTemplate.opsForValue().set(nullKey, 1, Duration.ofMinutes(2)); return null; } // 4. 写缓存过期 30 分钟 redisTemplate.opsForValue().set(key, product, Duration.ofMinutes(30)); return product; }这是典型的“空值缓存”防穿透方案。项目没集成布隆过滤器如 RedisBloom因复杂度与教学目标不匹配但留了null:product:id的 Key 设计位。雪崩防护靠随机过期时间实际生产中Duration.ofMinutes(30)应改为Duration.ofMinutes(30 ThreadLocalRandom.current().nextInt(10))但本项目为清晰起见未加。击穿防护由 Shiro 的RequiresPermissions和接口幂等性兜底未用互斥锁如SETNX因商品详情页 QPS 不高。4. 前端运行与 Vue 模块联动从登录态保持到搜索结果渲染前端位于vue/目录是标准 Vue CLI 3 项目package.json中vue: ^2.6.14。它不走npm run serve就能跑但必须理解其与后端的契约。4.1 Axios 请求拦截统一处理 Token 与 401 跳转vue/src/utils/request.js是请求中枢const service axios.create({ baseURL: process.env.VUE_APP_BASE_API, // 通常为 /api timeout: 5000 }); // 请求拦截自动携带 token service.interceptors.request.use( config { const token localStorage.getItem(token); if (token) { config.headers[Authorization] Bearer token; // 注意 Bearer 前缀 } return config; }, error Promise.reject(error) ); // 响应拦截401 清 token 并跳登录页 service.interceptors.response.use( response response, error { if (error.response?.status 401) { localStorage.removeItem(token); router.push(/login); } return Promise.reject(error); } );关键细节Authorization头必须是Bearer tokenShiro 的JwtFilter才会解析localStorage存 token 是妥协方案非 HttpOnly Cookie因项目定位教学未上 HTTPSrouter.push(/login)触发 Vue Router 导航守卫在vue/src/router/index.js中有router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (to.path ! /login !token) { next(/login); } else { next(); } });4.2 商品搜索页Vue 组件如何消费 Elasticsearch 返回的高亮 HTML搜索页vue/src/views/product/Search.vue的核心逻辑template div classsearch-result div v-forproduct in productList :keyproduct.id classproduct-item h3 v-htmlproduct.name/h3 !-- 关键v-html 渲染高亮 -- p v-htmlproduct.description/p p¥{{ product.price }}/p /div /div /template script export default { data() { return { productList: [], keyword: this.$route.query.keyword || } }, created() { this.search(); }, methods: { search() { this.$http.post(/product/search, { keyword: this.keyword, pageNum: 1, pageSize: 10, minPrice: 0, maxPrice: 999999 }).then(res { this.productList res.data.list; }); } } } /script注意v-htmlproduct.name—— 后端返回的name字段已是em手机/em壳格式Vue 不做二次转义。若你看到em被原样显示说明后端高亮未生效回查ProductSearchController中hit.getHighlightFields()是否为空。4.3 管理后台Bootstrap jQuery 混合页的 DOM 操作惯用法项目保留了部分传统管理页如vue/src/views/admin/UserManage.vue用 Bootstrap 表格 jQuery AJAX这是为兼容老系统改造场景。例如用户列表加载table classtable table-striped theadtrthID/thth用户名/thth邮箱/thth操作/th/tr/thead tbody iduserTableBody/tbody /table$.get(/api/user/list, function(data) { let html ; data.list.forEach(u { html tr td${u.id}/td td${u.username}/td td${u.email}/td tdbutton classbtn btn-sm btn-danger onclickdeleteUser(${u.id})删除/button/td /tr; }); $(#userTableBody).html(html); });这种写法虽不如 Vue 响应式优雅但胜在简单可控适合快速交付内部工具。jQuery 事件绑定用onclickdeleteUser(id)是故意为之——避免事件委托复杂度教学时一眼看懂数据流。5. 常见问题排查5 条血泪经验总结专治启动失败与功能失灵这个项目最大的痛点不是功能少而是配置断点太多一个没填就静默失败。以下是我在三台不同环境Mac M1、Windows WSL2、Ubuntu 22.04上反复验证的 5 条高频问题按发生概率排序5.1 现象SpringBoot 启动成功但访问/login返回 Whitelabel Error Page原因pom.xml中spring-boot-starter-thymeleaf与spring-boot-starter-web版本不匹配导致 Thymeleaf 自动配置失效/login路径没被任何 Controller 映射。解决确认pom.xml中所有 Spring Boot Starter 依赖都声明version2.7.18/version尤其检查thymeleaf-spring5是否被间接引入 3.x 版本。执行mvn dependency:tree | grep thymeleaf若输出含3.0.15.RELEASE则在dependencyManagement中强制覆盖dependency groupIdorg.thymeleaf/groupId artifactIdthymeleaf-spring5/artifactId version3.0.15.RELEASE/version /dependency5.2 现象Vue 页面登录成功但点击“商品管理”提示 403 Forbidden原因Shiro 的ShiroFilterFactoryBean配置中/admin/**路径未放开静态资源导致admin/index.html加载失败进而 JS 报错无法发起/api/product/list请求。解决打开com.example.tmall.config.shiro.ShiroConfig.java找到shiroFilterFactoryBean()方法在filterChainDefinitionMap中添加filterChainDefinitionMap.put(/admin/**, anon); // 放行 admin 静态页 filterChainDefinitionMap.put(/api/**, authc); // API 接口需认证注意顺序/admin/**必须在/api/**之前Shiro 按顺序匹配。5.3 现象Elasticsearch 搜索返回空数组但GET /product_index/_count显示 count1000原因ProductSearchDTO中keyword字段为空字符串或纯空格matchQuery(name, )在 ES 7.x 中返回 0 结果且不报错。解决在ProductSearchController.search()开头加校验if (StringUtils.isBlank(dto.getKeyword())) { return Result.fail(搜索关键词不能为空); } dto.setKeyword(dto.getKeyword().trim());同时前端Search.vue的search()方法中this.keyword this.keyword.trim()。5.4 现象Redis 缓存未生效每次查商品都打 DB原因redisTemplate的序列化器未设置导致opsForValue().set(key, product)存入的是 JDK 序列化二进制而opsForValue().get(key)反序列化失败返回 null。解决在com.example.tmall.config.RedisConfig.java中redisTemplateBean 定义后追加Bean public RedisTemplateString, Object redisTemplate(RedisConnectionFactory factory) { RedisTemplateString, Object template new RedisTemplate(); template.setConnectionFactory(factory); // 关键设置 StringRedisSerializer 为 keyGenericJackson2JsonRedisSerializer 为 value template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); template.setHashKeySerializer(new StringRedisSerializer()); template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); template.afterPropertiesSet(); return template; }5.5 现象Vue 启动后白屏浏览器控制台报Failed to fetchNetwork 面板显示CORS error原因前端请求http://localhost:8080/api/login但后端 SpringBoot 默认不开启 CORS且VUE_APP_BASE_API配置为/api导致浏览器同源策略拦截。解决在com.example.tmall.config.WebMvcConfig.java中添加全局 CORS 配置Configuration public class WebMvcConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOrigins(http://localhost:8080, http://127.0.0.1:8080) // 前端端口 .allowCredentials(true) .maxAge(3600); } }若用 Nginx 代理将allowedOrigins改为你的域名。6. 二次开发实战从替换 Elasticsearch 为 MySQL 全文索引到接入微信小程序登录这个项目最实用的价值不是照着跑一遍而是让你能在 2 小时内把它改成自己项目的骨架。下面以两个真实改造场景为例给出可立即执行的代码和验证步骤。6.1 场景一放弃 Elasticsearch改用 MySQL 8.0 全文索引实现搜索有些团队没有 ES 运维能力但 MySQL 8.0 已支持 ngram 分词。改造分三步第一步修改商品表添加 FULLTEXT 索引ALTER TABLE product ADD FULLTEXT(name, description) WITH PARSER ngram;第二步重写ProductSearchService.search()方法Service public class ProductSearchService { Autowired private JdbcTemplate jdbcTemplate; public PageResultProduct search(String keyword, int pageNum, int pageSize) { String sql SELECT * FROM product WHERE MATCH(name, description) AGAINST(? IN NATURAL LANGUAGE MODE); ListProduct list jdbcTemplate.query(sql, new Object[]{keyword}, new ProductRowMapper()); // 手动分页因 MySQL 全文索引不支持 OFFSET 效率低此处简化 int fromIndex (pageNum - 1) * pageSize; int toIndex Math.min(fromIndex pageSize, list.size()); ListProduct pageList list.subList(fromIndex, toIndex); return new PageResult(pageList, (long) list.size(), pageSize); } }ProductRowMapper是标准RowMapperProduct实现。注意IN NATURAL LANGUAGE MODE比IN BOOLEAN MODE更适合电商模糊搜。第三步前端保持不变后端 Controller 替换实现PostMapping(/search) public Result search(RequestBody ProductSearchDTO dto) { PageResultProduct result productSearchService.search( dto.getKeyword(), dto.getPageNum(), dto.getPageSize() ); return Result.success(result); }验证停掉 ES启动项目搜索“手机”应返回含“手机”的商品。性能上百万级商品时 MySQL 全文索引比 ES 慢 3~5 倍但开发成本降为 0。6.2 场景二接入微信小程序登录替换 Shiro 原生账号体系微信小程序登录需获取code换openid再绑定用户。改造核心是新增WechatLoginControllerRestController RequestMapping(/api/wechat) public class WechatLoginController { Value(${wechat.appid}) private String appId; Value(${wechat.secret}) private String secret; PostMapping(/login) public Result wechatLogin(RequestBody WechatLoginDTO dto) { // 1. 调用微信接口换 openid String url https://api.weixin.qq.com/sns/jscode2session? appid appId secret secret js_code dto.getCode() grant_typeauthorization_code; String response restTemplate.getForObject(url, String.class); JSONObject json JSONObject.parseObject(response); String openid json.getString(openid); if (openid null) { return Result.fail(微信登录失败 json.getString(errmsg)); } // 2. 根据 openid 查用户不存在则创建 User user userService.findByOpenid(openid); if (user null) { user new User(); user.setOpenid(openid); user.setUsername(wx_ RandomStringUtils.randomAlphanumeric(8)); user.setStatus(1); user.setCreateTime(new Date()); userService.save(user); } // 3. 生成 JWT Token String token jwtUtil.generateToken(user.getId(), user.getUsername()); return Result.success(Map.of(token, token, user, user)); } }WechatLoginDTO只有一个字段String code。userService.findByOpenid()需在User实体中新增openid字段并加索引。前端小程序调用时传wx.login()获取的res.code即可。6.3 我的改造习惯每次加新功能必做三件事从接手这个项目开始我养成了一个铁律只要动了核心模块立刻执行以下三步否则不提交代码。第一改完 Controller立刻用 Postman 测接口截图保存200 OK响应体确认字段名、嵌套结构、空值处理与文档一致第二改完 Service立刻在Test方法里写单元测试哪怕只测一个分支例如when(productRepository.findById(1L)).thenReturn(Optional.of(mockProduct));第三改完前端立刻打开 Chrome 的 Network 面板勾选Disable cache刷新页面盯着每个 XHR 请求的Status、Size、Time确认没有 4xx/5xx 且耗时 800ms。这三步看着慢但省去了 80% 的联调时间。上周我帮 A 同学改微信登录他跳过第二步结果findByOpenid返回 null 时没判空线上直接 500。从那以后我每次改User相关逻辑都强制走一遍Optional.ofNullable(...).orElseThrow()检查。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →