ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SpringCloud+Vue分布式商城实战:从源码跑通到Docker Compose一键部署

SpringCloud+Vue分布式商城实战:从源码跑通到Docker Compose一键部署 简介这份资源是面向计算机相关专业学生与Java学习者的分布式网上商城系统完整项目包基于SpringCloud微服务架构搭配Vue前端实现可直接用于毕业设计、课程设计或期末大作业。系统按管理员与用户两类角色划分权限管理员可操作首页、个人中心、用户管理、商品信息管理、商品分类管理、系统管理与订单管理用户则能维护个人信息并浏览商品。压缩包共792个文件约26.19MB其中115个Java源文件承载后端业务逻辑45个Vue组件与164个js、53个css构建前端交互界面另有sql数据库脚本、yml配置、bat启动脚本及答辩PPT、LW等文档结构完整。项目已通过严格调试可正常启动开发环境为JDK1.8、MySQL5.7、Tomcat7与Maven兼容Eclipse、IDEA等工具。目前已有450人学习适合需要完整分布式商城实战案例的读者参考。1. 从一份商城源码包说起SpringCloudVue 分布式架构到底解决了什么问题你拿到一个基于SpringCloudVue的分布式架构网上商城系统源码数据库.zip第一反应大概率是解压、导入 IDE、改数据库连接、跑起来。但真正动手后会发现单体商城和分布式商城完全不是一回事——服务拆开之后端口、注册中心、网关路由、跨服务调用、前端请求转发每一层都可能让你卡住。这个标题背后对应的是一套典型的 Java 微服务电商落地路径后端用 SpringCloud 把用户、商品、订单、库存、支付拆成独立服务前端用 Vue 做前后端分离的商城界面数据库按业务垂直拆分。它适合想从单体 CRUD 项目过渡到微服务架构的开发者也适合需要一套可运行商城骨架做二次开发的人。核心难点不在写业务代码而在把分布式协作的“基础设施”跑通。2. 环境准备与工程结构先把 SpringCloud 和 Vue 的依赖版本对齐2.1 JDK、Maven、Node 的版本选择与验证分布式项目最容易翻车的地方不是代码而是版本。SpringCloud 的版本必须和 SpringBoot 严格对应Vue 的 Node 版本又会影响依赖安装。常见做法是JDK 用 1.8 或 11Maven 用 3.6Node 用 14 或 16。不要盲目上 JDK 17很多老版本 SpringCloud 组件在 17 上会因为模块化限制直接启动失败。先验证本机环境java -version mvn -v node -v npm -v如果java -version输出不是 1.8 或 11建议用 SDKMAN 或手动切换 JAVA_HOME。Maven 要确认settings.xml里配了国内镜像否则拉 SpringCloud 依赖会非常慢。Node 版本用nvm管理最省心切到 14 或 16 后再装 Vue 依赖。参数说明java -version看的是运行时版本mvn -v会同时显示 Maven 和它使用的 JDK。两者不一致时以mvn -v里的 Java version 为准因为编译用的是 Maven 绑定的 JDK。2.2 多模块工程的目录划分与启动顺序解压源码后典型结构是mall-parent ├── mall-common ├── mall-gateway ├── mall-registry ├── mall-user ├── mall-product ├── mall-order ├── mall-inventory └── mall-webmall-registry是注册中心通常是 Eureka 或 Nacosmall-gateway是网关mall-common放公共实体和工具类其余是业务服务mall-web是 Vue 前端工程。启动顺序必须是注册中心 → 网关 → 业务服务 → 前端。顺序错了服务注册不上网关路由 404。在 IDEA 里导入时先以 Maven 项目导入根目录等依赖下载完。然后检查每个服务的application.yml重点看三处端口、注册中心地址、数据库连接。数据库脚本一般在sql目录下先建库再导入。提示如果注册中心用的是 Nacos除了启动服务端还要在控制台新建命名空间和分组否则服务注册到默认 public 下网关可能找不到。2.3 Vue 前端的依赖安装与接口代理配置进入mall-web目录npm install npm run serve如果npm install卡住或报错先换淘宝镜像npm config set registry https://registry.npmmirror.com然后删除node_modules和package-lock.json重装。Vue 项目里通常有vue.config.js里面配了devServer.proxy把/api代理到网关地址。如果前端请求 404先看这个代理有没有指向正确的网关端口。// vue.config.js module.exports { devServer: { port: 8080, proxy: { /api: { target: http://localhost:9000, // 网关地址 changeOrigin: true, pathRewrite: { ^/api: } } } } }逻辑说明前端所有以/api开头的请求会被转发到网关网关再根据路由规则转发到具体服务。changeOrigin设为 true 是为了避免跨域问题。参数target必须和网关实际端口一致pathRewrite决定是否去掉前缀要和后端 Controller 的映射路径匹配。3. 服务注册、网关路由与跨服务调用分布式协作的三根支柱3.1 注册中心配置服务能不能被找到以 Eureka 为例注册中心服务的application.ymlserver: port: 8761 eureka: instance: hostname: localhost client: register-with-eureka: false fetch-registry: false service-url: defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/业务服务的配置eureka: client: service-url: defaultZone: http://localhost:8761/eureka/ spring: application: name: mall-user参数说明register-with-eureka: false和fetch-registry: false只在注册中心自己身上配表示它不注册自己也不拉取服务列表。业务服务的spring.application.name就是服务名网关和 Feign 调用都靠这个名字找服务。如果服务启动后 Eureka 控制台看不到先检查defaultZone地址有没有写错再检查服务是否真的启动成功。3.2 网关路由统一入口怎么配网关的application.ymlserver: port: 9000 spring: application: name: mall-gateway cloud: gateway: routes: - id: user-service uri: lb://mall-user predicates: - Path/user/** - id: product-service uri: lb://mall-product predicates: - Path/product/**逻辑说明lb://mall-user表示从注册中心负载均衡到mall-user服务。Path/user/**表示所有/user开头的请求转发到用户服务。前端请求/api/user/list经过代理去掉/api后变成/user/list网关匹配到user-service路由转发到用户服务。常见坑路由的id不能重复uri如果写成http://localhost:8081就失去了负载均衡意义predicates的路径要和 Controller 的RequestMapping对得上。3.3 Feign 跨服务调用订单服务怎么查用户和库存订单服务需要调用用户服务和库存服务用 Feign 声明式调用FeignClient(name mall-user) public interface UserClient { GetMapping(/user/{id}) ResultUser getUserById(PathVariable(id) Long id); } FeignClient(name mall-inventory) public interface InventoryClient { PostMapping(/inventory/deduct) ResultBoolean deduct(RequestParam(productId) Long productId, RequestParam(count) Integer count); }逻辑说明FeignClient(name mall-user)里的 name 必须和注册中心里的服务名一致。接口方法签名要和被调用服务的 Controller 完全对应包括路径、参数名、请求方式。PathVariable和RequestParam里的值不能省否则运行时会报参数绑定失败。参数说明Result是公共模块里的统一返回体。如果被调用服务返回的是ResultUserFeign 接口也要用同样的泛型否则反序列化会出问题。跨服务调用超时时间可以在配置里调feign: client: config: default: connectTimeout: 5000 readTimeout: 5000注意Feign 调用默认没有熔断如果库存服务挂了订单服务会一直等。生产环境要配 Hystrix 或 Sentinel源码里如果没带自己加一个降级逻辑。4. 数据库拆分与分布式事务订单和库存怎么保持一致4.1 垂直分库的建表与连接配置分布式商城的数据库通常按业务拆成多个库用户库、商品库、订单库、库存库。每个服务连自己的库不直接跨库 JOIN。建表脚本在sql目录下导入时注意字符集用utf8mb4否则商品名称里的特殊字符会乱码。每个服务的application.yml里配自己的数据源spring: datasource: url: jdbc:mysql://localhost:3306/mall_order?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver参数说明serverTimezone必须配否则 MySQL 8 会报时区错误。characterEncodingutf8要和建库时的字符集一致。如果用的是 MySQL 5.7驱动类可以换成com.mysql.jdbc.Driver。4.2 下单流程里的跨服务数据一致性下单操作涉及订单库和库存库。常见做法是订单服务先创建订单状态为“待扣减”然后调用库存服务扣减库存扣减成功后再把订单状态改成“已确认”。如果扣减失败订单状态改成“已取消”。Transactional public Result createOrder(OrderDTO dto) { Order order new Order(); order.setStatus(PENDING); orderMapper.insert(order); ResultBoolean deductResult inventoryClient.deduct(dto.getProductId(), dto.getCount()); if (deductResult.getCode() 200 deductResult.getData()) { order.setStatus(CONFIRMED); orderMapper.updateById(order); return Result.success(); } else { order.setStatus(CANCELLED); orderMapper.updateById(order); return Result.fail(库存不足); } }逻辑说明Transactional只能保证本地订单库的事务跨服务调用不在同一个事务里。所以用状态机来补偿先落单再扣库存根据结果改状态。如果库存服务超时订单会停在“待扣减”需要定时任务扫这些单子做补偿。参数说明OrderDTO里至少要有productId、count、userId。库存服务的deduct接口要做幂等防止重复扣减。常见做法是用orderId做唯一键或者用 Redis 记录已处理的请求。4.3 分布式定时任务补偿超时订单超时未支付的订单要自动取消并回滚库存。单机定时任务在分布式环境下会重复执行常见方案是用 Redis 分布式锁或者 Quartz 集群模式。Scheduled(cron 0 */5 * * * ?) public void cancelTimeoutOrders() { String lockKey lock:cancel_order; Boolean locked redisTemplate.opsForValue().setIfAbsent(lockKey, 1, 30, TimeUnit.SECONDS); if (Boolean.TRUE.equals(locked)) { try { ListOrder orders orderMapper.selectTimeoutOrders(); for (Order order : orders) { order.setStatus(CANCELLED); orderMapper.updateById(order); inventoryClient.rollback(order.getProductId(), order.getCount()); } } finally { redisTemplate.delete(lockKey); } } }逻辑说明setIfAbsent是 Redis 的 SETNX 操作只有第一个抢到锁的节点能执行任务。30秒是锁的过期时间防止节点宕机后锁不释放。cron表达式表示每 5 分钟执行一次。回滚库存的接口也要做幂等。参数说明锁的过期时间要大于任务执行时间否则任务没跑完锁就过期了另一个节点会重复执行。如果任务量很大可以分页处理每页 100 条。5. 避坑与排查源码跑不起来时先看这几个地方5.1 服务注册不上Eureka 控制台一片空白现象启动业务服务后Eureka 控制台看不到实例网关路由 503。原因defaultZone地址写错或者服务启动时注册中心还没起来或者spring.application.name有下划线导致服务名不合法。解决先确认注册中心自己启动成功浏览器能打开 Eureka 页面。再检查业务服务的eureka.client.service-url.defaultZone是否指向正确的 IP 和端口。服务名用中划线不要用下划线。5.2 网关转发 404前端请求全部失败现象前端请求/api/user/list返回 404但直接访问用户服务端口能通。原因网关路由的Path断言和实际请求路径不匹配或者pathRewrite把前缀去掉了但后端 Controller 没加对应前缀。解决打开网关日志看请求进来后匹配到了哪条路由。如果没匹配到检查Path表达式。如果匹配到了但转发后 404检查pathRewrite和后端映射路径。常见做法是后端 Controller 统一加/user、/product前缀网关不做pathRewrite。5.3 Feign 调用报错Method has too many Body parameters现象启动时报Method has too many Body parameters。原因Feign 接口里用了多个RequestBody或者参数没有加RequestParam、PathVariable注解。解决Feign 接口里GET 请求参数用RequestParam或PathVariablePOST 请求只能有一个RequestBody。如果有多个参数用RequestParam或者封装成一个 DTO。5.4 数据库时区错误The server time zone value is unrecognized现象启动服务时报时区错误或者插入时间差 8 小时。原因MySQL 8 的驱动需要显式指定serverTimezone。解决JDBC URL 里加serverTimezoneAsia/Shanghai。如果用的是 MySQL 5.7升级驱动到 8.x 后也要加。5.5 Vue 前端跨域Access-Control-Allow-Origin现象前端请求网关时报跨域错误。原因前端开发服务器和网关端口不同浏览器同源策略拦截。解决在vue.config.js里配devServer.proxy让前端请求走代理。如果已经配了代理还报跨域检查代理的target是否写成了前端自己的地址。生产环境用 Nginx 反向代理把前端静态资源和网关放在同一个域名下。6. 进阶技巧用 Docker Compose 把整套分布式商城一键拉起来手动启动注册中心、网关、四个业务服务、MySQL、Redis每次都要开一堆终端。我一般会写一个docker-compose.yml把基础设施和业务服务编排在一起。这样换台机器也能快速复现不用再折腾环境。version: 3.8 services: mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: 123456 ports: - 3306:3306 volumes: - ./sql:/docker-entrypoint-initdb.d redis: image: redis:6 ports: - 6379:6379 registry: build: ./mall-registry ports: - 8761:8761 gateway: build: ./mall-gateway ports: - 9000:9000 depends_on: - registry user: build: ./mall-user depends_on: - registry - mysql product: build: ./mall-product depends_on: - registry - mysql order: build: ./mall-order depends_on: - registry - mysql - redis inventory: build: ./mall-inventory depends_on: - registry - mysql逻辑说明depends_on只保证启动顺序不保证服务真正就绪。MySQL 初始化脚本放在docker-entrypoint-initdb.d下容器第一次启动时会自动执行。每个业务服务目录下要有Dockerfile基于openjdk:8-jre或openjdk:11-jre构建。参数说明MYSQL_ROOT_PASSWORD要和业务服务里配的密码一致。如果本机 3306 被占用改成3307:3306同时业务服务的 JDBC URL 也要改成3307。Redis 默认无密码生产环境要加requirepass。验证方法docker-compose up -d后等一分钟访问http://localhost:8761看服务是否注册访问http://localhost:9000/user/list看网关是否转发成功。如果某个服务一直重启用docker-compose logs -f 服务名看日志。我自己的习惯是每次改完配置先docker-compose down再up -d避免旧容器缓存影响。源码包里的 SQL 脚本如果和实体类字段对不上以实体类为准改 SQL别反过来改代码否则后面加功能会越来越乱。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进