简介:基于微服务架构的小程序商城系统,面向微信电商开发、架构设计与毕业设计人群,覆盖用户中心、商品中心、订单中心、支付中心等核心业务模块,重点解决服务拆分、独立部署、弹性扩展与接口协作等问题。系统同时提供微信小程序端与商家管理平台,串联注册登录、商品上下架、库存管理、购物车、微信支付及订单处理全流程,并将服务治理、运行监控和链路追踪纳入整体设计,便于在高并发场景下维持稳定并快速定位故障。压缩包体积约58.77MB,资源页暂未列出文件数量与类型明细,具体内容以实际下载包为准。已有367人浏览学习,适合已具备后端基础、希望理解微服务在企业级商城项目中落地路径的读者;借助该资源可以学习典型模块的拆分方式、接口交互逻辑、支付与监控集成思路,并迁移到同类电商系统中复用。
1. 从单体到微服务:小程序商城为什么值得拆
小程序商城不是新物种,但你只要经历过一次大促或者一次爆款上架,就会明白单体应用那种“一个 war 包管所有”的做法有多被动。商品、库存、订单、支付、会员、营销全部耦合在一起,任何一个环节出问题,整个链路都跟着抖动;更头痛的是团队协作——订单组改一行代码,商品组就要陪着回归测试。基于微服务的小程序商城系统,解决的核心问题不是“技术时髦”,而是让商城业务具备独立的伸缩能力、独立的故障隔离能力和独立的发布节奏。
这套系统的关键路径其实很清晰:小程序端通过微信登录拿到 openid,后端用网关统一收口请求,拆出来的商品、订单、库存、支付等微服务各自持有数据,服务间用 OpenFeign 或消息队列协作。落地框架目前主流是 Spring Cloud Alibaba,注册配置中心 Nacos、远程调用 OpenFeign、流量防护 Sentinel,再配上 knife4j 聚合各服务的接口文档。本文将按“拆分思路 → 基础架构搭建 → 小程序登录与用户体系 → 订单与库存实战 → 压测与排错”的路径,把完整落地方案讲清楚。
2. 先理清拆分边界:哪些模块该拆,哪些不该拆
2.1 服务划分的三种思路与商城场景的选择
微服务拆分没有标准答案,但思路可以归纳为三种:按业务能力拆、按领域事件拆、按团队组织拆。小程序商城最常用的是按业务能力拆,因为商城领域的业务边界非常清晰:用户、商品、库存、订单、支付、营销,每个模块的变更频率和数据生命周期都不一样。
拿订单和库存举例。订单是交易核心,创建订单后要校验库存、锁定库存、生成快照、对接支付回调,整个链路对一致性和可追踪性要求极高;而库存模块关注的是“还有多少货”,它的热点是秒杀场景下的超卖问题。这两个模块如果放在一起,订单服务的高频写操作会直接影响库存服务的查询性能。拆开之后,库存服务可以单独做缓存预热、单独做数据库读写分离。
以下是小程序商城常见的服务拆分清单:
| 服务名 | 核心职责 | 数据归属 | 依赖关系 |
|---|---|---|---|
| user-service | 微信登录、会员信息、收货地址 | 用户库 | 无 |
| product-service | 商品SPU/SKU、类目、品牌 | 商品库 | 无 |
| stock-service | 库存数量、锁定/释放 | 库存库 | 依赖商品 |
| order-service | 下单、订单状态流转、售后 | 订单库 | 依赖商品、库存、用户 |
| payment-service | 微信支付下单、回调、退款 | 支付库 | 依赖订单 |
| marketing-service | 优惠券、秒杀、拼团活动 | 营销库 | 依赖商品、订单 |
这个表的意义在于:每个服务都有独立的数据库,这是微服务和“伪微服务”的分水岭。很多团队只是把代码拆了几个 Maven 模块,但数据库还共用一个,最终服务没解开,事务问题倒是全来了。
2.2 拆分时的三个红线约束
第一,禁止跨服务直接查数据库。order-service 要展示订单里的商品信息,不能去查商品库,只能通过 product-service 提供的接口获取,或者在下单时把商品快照冗余到订单表里。商城场景里商品价格和名称经常变动,所以订单里冗余快照是必要的。
第二,服务间调用链不能成环。商品服务不能反过来依赖订单服务,否则一旦出现循环依赖,Nacos 里的实例列表会出诡异问题,Feign 调用的超时排查也会变得很困难。
第三,数据一致性要先分级别。下单链路里的“扣库存 + 创建订单”,要用 Seata 分布式事务或本地消息表保证最终一致;而“更新商品销量”这种可以容忍延迟的数据,直接发 MQ 异步处理就够了。后面第 5 章会专门讲这个取舍。
2.3 一个容易踩的坑:把“功能点”当“服务”
新手最常犯的错误是把功能点拆成微服务。比如把“购物车”单独拆成一个 cart-service,把“轮播图”拆成 banner-service。这种拆法的后果是服务粒度太细,服务间通信开销远大于业务逻辑本身,而且每个服务都要单独部署、单独维护配置,运维成本直线上升。
我的经验是:一个服务至少要有一个独立的业务闭环。购物车属于用户交易行为的一部分,可以放在 user-service 或 order-service 中;轮播图只是商品展示的附属物,直接放在 product-service 里。判断标准很简单:如果这个模块的数据库表超过 5 张,且被多个端(小程序、管理后台、开放接口)复用,才值得独立成服务;否则就是过度设计。
3. 用 Nacos + Gateway + OpenFeign 搭起微服务骨架
3.1 工程结构与版本选型
微服务商城的基础骨架,我一般会用 Maven 多模块工程来组织,这样依赖版本统一管理,各服务模块互相隔离。假设项目根目录叫 mall,结构如下:
mall ├── mall-common // 公共工具、统一返回结果、异常处理 ├── mall-gateway // Spring Cloud Gateway 网关服务 ├── mall-user // 用户服务 ├── mall-product // 商品服务 ├── mall-stock // 库存服务 ├── mall-order // 订单服务 └── mall-payment // 支付服务版本选型直接用 Spring Boot 2.7.x 搭配 Spring Cloud Alibaba 2021.0.5.0,这套组合经过大量生产环境验证,稳定性远好于追新版本。微服务整合 knife4j 时要注意,knife4j 的版本必须与 Spring Boot 版本匹配,否则会出现文档页面空白的问题。
先在根 pom.xml 里统一管理依赖版本:
<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>2.7.18</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>2021.0.8</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>2021.0.5.0</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>版本统一管理的价值在于避免依赖冲突,特别是 Spring Cloud 与 Spring Cloud Alibaba 之间如果版本不对齐,Nacos 服务注册会直接报错,而且错误信息很隐晦,往往只显示连接超时。
3.2 Gateway 网关:统一入口与路由配置
网关是小程序请求进入后端的第一道门。所有的请求先到网关,网关做身份校验、限流、路由转发。小程序端只需要配置一个合法的请求域名,指向网关地址即可,不需要关心后端到底有多少个微服务实例——这是微服务对比单体应用在小程序适配上的巨大优势。
网关模块的核心配置如下:
server: port: 8080 spring: application: name: mall-gateway cloud: nacos: discovery: server-addr: localhost:8848 gateway: routes: - id: user-service uri: lb://mall-user predicates: - Path=/api/user/** filters: - StripPrefix=2 - id: product-service uri: lb://mall-product predicates: - Path=/api/product/** filters: - StripPrefix=2 - id: order-service uri: lb://mall-order predicates: - Path=/api/order/** filters: - StripPrefix=2 discovery: locator: enabled: true这里要解释几个关键参数。lb://mall-user中的lb表示负载均衡,网关会从 Nacos 中发现名为mall-user的服务实例列表,再按负载均衡策略分发请求。StripPrefix=2表示去掉 URL 中的前两段路径,也就是小程序端请求/api/user/login,网关转发到用户服务时变成/login。
discovery.locator.enabled=true是开发调试期的便利开关。开启后可以直接通过http://网关地址/服务名/接口路径访问任意服务,方便本地联调;但在生产环境建议关闭,防止服务名被外部直接探测。
3.3 OpenFeign 服务间调用:下单链路的组装
服务拆完之后,服务间调用成了高频操作。OpenFeign 是 Spring Cloud 生态中声明式 HTTP 客户端的事实标准,它的核心价值是让服务间调用像调用本地方法一样简单。
下面以订单服务调用库存服务为例。库存服务提供一个扣减库存的接口:
@RestController @RequestMapping("/stock") public class StockController { @PostMapping("/deduct") public Result<Void> deduct(@RequestBody StockDeductDTO dto) { stockService.deduct(dto.getSkuId(), dto.getQuantity()); return Result.success(); } }订单服务这边声明一个 Feign 客户端:
@FeignClient(name = "mall-stock", fallback = StockFeignFallback.class) public interface StockFeignClient { @PostMapping("/stock/deduct") Result<Void> deduct(@RequestBody StockDeductDTO dto); }@FeignClient(name = "mall-stock")中的name必须与库存服务在 Nacos 中注册的服务名一致。加了fallback参数后,当库存服务不可用或超时时,会走StockFeignFallback这个降级兜底类,避免订单服务被拖死。
这里有一个非常关键的连接超时设置。OpenFeign 默认连接超时是 10 秒,读超时是 60 秒,这个参数在微服务商城场景明显偏长。下单链路需要快速失败,不能让用户长时间等待。建议在配置文件中调整为:
feign: client: config: default: connect-timeout: 3000 read-timeout: 50003.4 knife4j 聚合微服务接口文档的配置细节
服务拆多了以后,接口文档的维护就是灾难。knife4j 基于 Swagger 增强了 UI 和聚合能力,可以在网关层把各个微服务的 OpenAPI 文档聚合成一份。每个微服务模块只需引入依赖:
<dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>knife4j-openapi2-spring-boot-starter</artifactId> <version>4.4.0</version> </dependency>然后在各服务的 application.yml 中启用:
knife4j: enable: true setting: language: zh_cn网关层需要配置 Swagger 资源聚合,把各服务的文档地址暴露到同一个页面上。开发调试时访问http://localhost:8080/doc.html就能看到所有微服务的接口列表,前端同学对接接口的效率会提升很多。
4. 小程序登录与用户体系:从 wx.login 到 openid 的完整链路
4.1 为什么不能直接信任小程序传来的用户信息
小程序商城第一步要解决的就是用户身份识别。微信小程序端调用wx.login()可以得到一个临时凭证code,这个 code 的有效期只有 5 分钟,且只能使用一次。拿着 code 请求微信的jscode2session接口,才能换到openid和session_key。
这里面有一个关键安全点:绝对不能用小程序端传过来的昵称、头像直接创建用户记录。因为小程序的wx.getUserProfile()返回的数据是可以被篡改的,如果不经后端校验而直接入库,就会产生脏数据。后端拿到 openid 后才去数据库查这个用户是否存在,不存在则创建新用户,存在则更新登录时间。
4.2 登录接口的后端实现
用户服务提供一个登录接口,完整代码逻辑如下:
@PostMapping("/login") public Result<LoginVO> login(@RequestBody WxLoginDTO dto) { // 1. 请求微信接口获取 openid String url = "https://api.weixin.qq.com/sns/jscode2session?appid=" + appId + "&secret=" + appSecret + "&js_code=" + dto.getCode() + "&grant_type=authorization_code"; String result = restTemplate.getForObject(url, String.class); JSONObject json = JSON.parseObject(result); String openid = json.getString("openid"); // 2. 根据 openid 查询或创建用户 User user = userMapper.selectByOpenid(openid); if (user == null) { user = new User(); user.setOpenid(openid); user.setCreateTime(new Date()); userMapper.insert(user); } // 3. 生成自定义登录态 token String token = UUID.randomUUID().toString().replace("-", ""); redisTemplate.opsForValue().set("login:token:" + token, String.valueOf(user.getId()), 7, TimeUnit.DAYS); LoginVO vo = new LoginVO(); vo.setToken(token); return Result.success(vo); }这段代码里有几个参数说明值得注意。jscode2session接口需要appid、secret和js_code三个核心参数,其中appsecret绝不能出现在小程序代码中,只能保存在后端服务里。code2session成功后返回的openid是用户的唯一标识,同一个用户在不同小程序下的 openid 是不同的,但在同一小程序下永久不变。
登录后生成的token存放在 Redis 中并设置 7 天有效期,比直接把 openid 返回给前端更安全,也让服务端具备主动踢人、续期的能力。访问需要登录的接口时,前端在请求头中携带Authorization: token,网关或各服务统一解析。
4.3 小程序端的登录流程封装
小程序端的调用逻辑要处理好“静默登录”和“用户授权”的关系。首次打开商城时,不需要强制用户点击授权按钮,可以先静默登录换取 token,把商品浏览、加购这些行为记录下来;当用户要下单、领优惠券时才引导授权手机号。
function wxLogin() { return new Promise((resolve, reject) => { wx.login({ success: (res) => { if (res.code) { wx.request({ url: 'https://api.example.com/api/user/login', method: 'POST', data: { code: res.code }, success: (resp) => { const token = resp.data.data.token wx.setStorageSync('token', token) resolve(token) } }) } } }) }) }这里要注意,wx.login的 code 是临时的,而且同一时刻只能有一个 code 生效。如果用户在短时间内反复调用wx.login,前一个 code 会立即失效,所以前端要加防重入控制,避免并发请求导致登录失败。
4.4 微信服务号网页授权与小程序登录的区别
热词里有“微信服务号能配置几个网页授权地址”,这里顺便说清楚。服务号的网页授权回调域名只能配置一个,这和本次的小程序商城登录体系有本质区别:小程序不需要配置网页授权域名,登录完全基于code2session完成;而服务号用于 H5 商城时,网页授权域名只能配置一个,这意味着同一套服务号无法同时支持多个 H5 商城域名。如果是多品牌商城共用一个服务号,就要考虑用中间页跳转的方式来绕过这个限制。
5. 下单与库存扣减:订单状态机与分布式事务的现实选择
5.1 订单状态机的核心流转
订单服务是微服务商城的核心模块。订单不能只是简单的增删改查,必须按照状态机来流转,否则售后、取消、超时关闭这些逻辑会变成一团乱麻。小程序商城的订单状态一般包含这些节点:
- 待支付(PENDING_PAYMENT)
- 已支付(PAID)
- 已发货(SHIPPED)
- 已签收(RECEIVED)
- 已完成(COMPLETED)
- 已取消(CANCELLED)
- 退款中(REFUNDING)
- 已退款(REFUNDED)
合法的状态流转必须满足:待支付可以取消或支付;已支付可以发货;已发货可以签收;退款必须发生在已支付之后。任何非法的状态迁移,比如从待支付直接跳转到已签收,都应该在代码层面被拦截。
实现状态机时,我习惯用枚举来定义状态和允许的迁移:
public enum OrderStatus { PENDING_PAYMENT { @Override public boolean canTransitTo(OrderStatus target) { return target == PAID || target == CANCELLED; } }, PAID { @Override public boolean canTransitTo(OrderStatus target) { return target == SHIPPED || target == REFUNDING; } }, SHIPPED { @Override public boolean canTransitTo(OrderStatus target) { return target == RECEIVED || target == REFUNDING; } }, RECEIVED { @Override public boolean canTransitTo(OrderStatus target) { return target == COMPLETED || target == REFUNDING; } }; public abstract boolean canTransitTo(OrderStatus target); }这样设计的好处是状态迁移规则全部集中在枚举内部,后续加一个“已取消订单重新支付”的新需求,只需改这个枚举,不需要在业务代码里到处找if判断。
5.2 分布式事务:Seata AT 模式还是消息队列
订单创建涉及订单服务写订单表、库存服务扣库存表,两个服务各自持有一个数据库。这里存在分布式事务问题。常见的选型有两种。
方式一:Seata AT 模式
Seata 的 AT 模式对业务侵入最小,通过拦截 SQL 自动生成 undo_log,实现反向补偿。用@GlobalTransactional注解标记业务方法即为全局事务入口:
@GlobalTransactional(name = "create-order", timeoutMills = 30000) public void createOrder(OrderCreateDTO dto) { // 1. 创建订单(订单服务本地事务) orderMapper.insert(order); // 2. 扣减库存(通过 Feign 调用库存服务) stockFeignClient.deduct(stockDeductDTO); }AT 模式的优点是开发效率高,但要注意它的代价:全局锁对并发性能有损耗,秒杀场景下大规模扣库存时,数据库行锁和全局锁叠加可能导致吞吐量明显下降。另外,AT 模式要求数据库支持 undo_log 表,如果用的是云数据库且权限受限,建表会有麻烦。
方式二:本地消息表 + 消息队列
更推荐线上商城采用本地消息表方案兜底。下单时在同一数据库事务里写入订单表和消息表,然后通过 RocketMQ 发送“扣库存”消息,库存服务消费消息执行扣减。如果扣减失败,消息重试;重试多次仍失败,转人工处理。
这个方案的优点是不需要全局事务协调器,性能损耗极小,适合高并发交易场景;缺点是最终一致性的时延取决于消息消费速度,极端情况下用户下单后查库存可能需要短暂重试。
5.3 超卖问题的前置拦截与库存扣减策略
库存扣减必须放在数据库层做控制,不能先查库存再判断,那必然出现超卖。标准做法是带条件更新:
UPDATE stock SET available = available - #{quantity}, locked = locked + #{quantity} WHERE sku_id = #{skuId} AND available >= #{quantity}这条 SQL 的WHERE条件中available >= #{quantity}是防超卖的关键。MySQL 的行锁保证同一时刻只有一个事务能成功更新同一行,当库存不足时受影响行数为 0,业务代码据此判断扣减失败。
在高并发秒杀场景下,上述 SQL 会遇到热点行更新的瓶颈。常见做法是引入 Redis 预扣库存:商品详情页展示的是 Redis 中的库存数,下单时先用 Lua 脚本原子扣减 Redis 库存,扣减成功后发送 MQ 消息异步同步到数据库。Redis 单实例的 qps 可以达到十万级,比直接打数据库高一个数量级。
5.4 订单超时未支付自动关闭的两种实现
用户下单后 15 分钟未支付,订单应该自动关闭并释放库存。这个功能有两种方案。一种是定时任务扫描,每 30 秒扫一次订单表,关闭超时订单并恢复库存。这种方案的缺点是扫描会打到数据库,订单量大的时候成本很高。
另一种是 RocketMQ 延迟消息:创建订单时同步发送一条延迟消息,延迟 15 分钟后投递;消费者收到消息后判断订单当前是否还是待支付状态,是则关闭订单。延迟消息的精度比定时扫描高很多,而且不会产生无效的数据库扫描。商城场景我基本都选第二种。
6. 限流、压测与上线排查:Sentinel 落地与链路观测
6.1 用 Sentinel 护住下单链路:三个必配规则
商城一到促销活动,流量峰值是日常的十倍以上,微服务架构里的每个节点都可能成为瓶颈。Sentinel 是 Spring Cloud Alibaba 的流量防护组件,核心价值是对接口做精细化限流。
在订单服务的下单接口上,建议配置以下三类规则。
QPS 限流规则。基于接口维度的流量控制,比如设置下单接口的 QPS 阈值为 800。超过阈值的请求直接返回“系统繁忙,请稍后重试”,保护下游数据库不被瞬时流量打垮。
热点参数限流。针对具体商品做限流——比如某个秒杀商品的单 SKU 并发不能超过 300。热点参数限流的粒度比普通 QPS 限流更细,能有效防止个别爆品把整个服务拖垮。
熔断降级规则。当依赖的库存服务接口异常比例超过 50% 时,Sentinel 自动熔断该调用,后续请求直接走降级逻辑返回“库存服务繁忙”,不再实际调用下游。熔断窗口结束后自动半开试探恢复。
Sentinel 的规则可以在控制台动态推送。微服务整合 Sentinel 时建议将规则持久化到 Nacos 配置中心,否则控制台推送的规则在服务重启后会丢失。具体做法是引入sentinel-datasource-nacos依赖,在配置中指定规则存储的 Nacos 配置。
6.2 上线前的压测方法:用 JMeter 模拟小程序用户行为
微服务商城上线前必须做压测,不能等到上线后被真实用户教做人。压测方案要模拟真实的小程序用户行为,不能只压一个接口。
典型的压测场景是“浏览商品和下单的混合链路”:
- 浏览商品列表(GET /api/product/list)
- 查看商品详情(GET /api/product/detail/{id})
- 将商品加入购物车(POST /api/order/cart)
- 提交订单(POST /api/order/create)
- 模拟支付回调(POST /api/payment/notify)
用 JMeter 建立线程组模拟并发用户数,每个用户按顺序执行上述请求,线程数从 100 递增到 500、1000,观察每个接口的响应时间分位值(TP99 尤其重要)以及各微服务所在机器的 CPU 和内存使用率。
压测时要重点盯三个指标:接口的 TP99 响应时间是否低于 500 毫秒;Sentinel 的限流触发次数是否合理;数据库连接池是否打满。如果 TP99 超过 1 秒,就要分析是 OpenFeign 调用超时造成的连锁等待,还是数据库慢查询导致的。
6.3 链路追踪与问题定位的三个技巧
微服务商城最痛苦的问题是:用户反馈下单失败,但订单、库存、支付都有各自的日志,不知道故障到底出在哪个环节。所以从第一天就要引入链路追踪。
推荐使用 SkyWalking 或 Micrometer Tracing。每笔请求生成一个全局唯一的 traceId,通过请求头在各个服务间传递,日志框架中打印 traceId。排错时根据 traceId 聚合出完整调用链,准确定位耗时和异常发生在哪个服务哪个方法。
另外两个实操技巧,一个是把 Nacos 的服务列表和健康检查页面加到日常巡检脚本中,出现服务消失能第一时间告警;另一个是统一异常响应格式,各服务返回的 error code 要有前缀区分,比如 10001 是订单服务错误、20001 是库存服务错误,通过错误码一眼定位源头服务。
6.4 发货地址的防呆校验:一个容易被忽视的细节
最后一个建议,商品详情页和下单页都要做发货地址的防呆校验。小于 100 克的商品按普通快递计算,生鲜商品要提示不可跨区域配送,虚拟商品不需要填写地址。这类判断如果放在前端做,用户换端后规则就不一致了。正确做法是在 order-service 下单接口中统一校验,通过商品维度配置表决定是否要求填写收货地址。
本文还有配套的精品资源,点击获取