简介:这份资源是《基于微服务的小程序商城系统详解》配套资料,面向具备一定 Java 与分布式基础的开发者、电商项目学习者及需要搭建小程序商城的团队。内容围绕用户中心、商品中心、订单中心、支付中心等核心模块展开,讲解微服务拆分、服务治理、监控追踪与微信端、管理平台的整体设计思路,帮助读者理解模块化解耦、独立部署与弹性扩展的实现方式。压缩包为 zip 格式,整体约 58.77MB,上游未提供文件总数与类型明细,故不展开具体文件构成。目前已有 367 人学习下载,适合作为微服务电商架构的参考案例。读者可从中获取各业务中心的职责划分、订单与支付流程的衔接逻辑、服务治理与故障排查思路,以及微信小程序商城从商品管理到交易闭环的完整方案,便于对照自身项目进行架构优化与功能扩展。
1. 从单体到微服务:小程序商城系统为什么要拆
去年帮一个做社区团购的团队救火,他们的商城小程序上线三个月,日订单从几百冲到两万多,然后整个系统开始"抽搐":用户点开商品详情要转三秒,下单偶尔提示"系统繁忙",最要命的是每次改一个优惠券规则,得把整个后端重新发版,发一次停服十分钟。翻他们的代码仓库,一个 Spring Boot 工程里塞了商品、订单、库存、支付、用户、营销六个模块,两万多行代码,编译一次要四分钟。这就是典型的单体架构撞上了业务增长的天花板。
基于微服务的小程序商城系统,说白了就是把这样一个大后端按业务边界切成若干个能独立部署、独立扩容的小服务,小程序端通过网关统一调用。它解决的不是"能不能跑"的问题,而是"改得动、扛得住、扩得快"的问题。适合谁?适合后端已经跑通、订单量开始让单体吃力的团队,也适合一开始就想把架构做对、不想后期推倒重来的创业者。如果你还在验证商业模式、日订单不到一千,我劝你先别拆,单体足够用,拆早了纯属给自己找罪受。
2. 微服务拆分:商城系统按什么边界切、切几刀
2.1 拆分粒度:按业务能力切,不按技术分层切
新手最容易犯的错,是按"controller 一层、service 一层、dao 一层"去拆,拆完发现每个服务都缺胳膊少腿,一个下单请求要跨五个服务来回调,链路长得像春运。正确的切法是按业务能力(Business Capability)切,商城系统天然有这么几条清晰的边界:
| 服务名 | 职责 | 独立数据库 | 扩容特征 |
|---|---|---|---|
| 用户服务 | 登录、手机号绑定、会员等级 | user_db | 读多写少 |
| 商品服务 | SPU/SKU、类目、上下架 | product_db | 读多写少,可加缓存 |
| 订单服务 | 下单、状态机、超时取消 | order_db | 写密集,大促扩容重点 |
| 库存服务 | 扣减、回滚、防超卖 | stock_db | 写密集,强一致要求高 |
| 支付服务 | 微信支付回调、对账 | pay_db | 低频但绝不能丢 |
| 营销服务 | 优惠券、满减、秒杀 | promo_db | 脉冲式流量 |
切几刀的原则是:一个服务对应一个能独立讲清楚的业务故事,团队里一个人能说清它干什么。订单和库存我建议分开,虽然它们交互频繁,但库存的并发模型和订单完全不同,混在一起大促时互相拖累。用户和营销可以合并,如果团队人手紧张,营销本质是挂在用户身上的规则。
2.2 服务间通信:同步调用用 Feign,异步解耦用消息队列
拆完之后,下单这个动作要跨订单、库存、营销、支付四个服务。哪些用同步、哪些用异步,直接决定系统的抗压能力。
同步调用用 Spring Cloud OpenFeign,适合"必须立刻知道结果"的场景,比如下单前查库存够不够:
// 库存服务的 Feign 客户端,声明式调用,像调本地方法一样 @FeignClient(name = "stock-service", fallback = StockFeignFallback.class) public interface StockFeignClient { // 预扣库存,返回是否成功 @PostMapping("/stock/deduct") Result<Boolean> deduct(@RequestBody StockDeductDTO dto); }@FeignClient的name对应注册中心里的服务名,fallback是熔断降级类,库存服务挂了不能把订单服务一起拖死。这里有个血泪经验:Feign 默认超时是 1 秒,大促时库存服务稍微抖一下,订单就大面积失败,一定要在配置里把超时调到 3 到 5 秒,并且配上重试。
异步解耦用 RocketMQ 或 RabbitMQ,适合"通知性质、可以晚一点"的场景。下单成功后发一条消息,营销服务去核销优惠券、用户服务去加积分,这些都不该阻塞用户看到"下单成功":
// 订单创建成功后,发消息通知其他服务,不等待结果 rocketMQTemplate.asyncSend("order-created-topic", OrderCreatedEvent.builder() .orderId(order.getId()) .userId(order.getUserId()) .couponId(order.getCouponId()) // 有券才核销 .build(), new SendCallback() { @Override public void onSuccess(SendResult result) { log.info("订单事件发送成功: {}", order.getId()); } @Override public void onException(Throwable e) { // 发送失败要落库补偿,不能只打日志 orderEventMapper.saveFailedEvent(order.getId(), e.getMessage()); } });参数说明:asyncSend不阻塞主线程,回调里失败必须落库,靠定时任务补偿重发,这是保证最终一致性的后悔药。别用syncSend,那等于把异步又变回同步,白拆了。
2.3 网关与注册中心:小程序所有请求的唯一入口
小程序端不能直连各个微服务,否则域名管理、鉴权、限流会散落一地。统一走 Spring Cloud Gateway,注册中心用 Nacos:
# gateway 路由配置,按路径前缀转发到对应服务 spring: cloud: gateway: routes: - id: order-service uri: lb://order-service # lb 表示从注册中心负载均衡 predicates: - Path=/api/order/** filters: - StripPrefix=1 # 去掉 /api 前缀再转发 - name: RequestRateLimiter # 限流,防刷 args: redis-rate-limiter.replenishRate: 100 # 每秒放行 100 个 redis-rate-limiter.burstCapacity: 200 # 突发上限 200lb://是负载均衡协议,Nacos 里注册了几个 order-service 实例就轮询几个。StripPrefix=1去掉一层前缀,让后端服务不用感知网关的路径约定。限流参数replenishRate是令牌桶的填充速率,burstCapacity是桶容量,大促时这两个值要按压测结果调,调小了正常用户被限,调大了防不住刷子。
3. 小程序端对接:登录、手机号与请求封装
3.1 微信登录换 token 的完整链路
小程序登录不是传统的账号密码,而是wx.login拿 code,后端拿 code 去微信服务器换 openid,再签发自己的 token。这条链路是微服务商城的第一道门,走错了后面全乱。
// 小程序端:登录并拿到业务 token wx.login({ success: (res) => { // res.code 只能用一次,五分钟内有效 wx.request({ url: 'https://your-gateway.com/api/auth/login', method: 'POST', data: { code: res.code }, success: (resp) => { // 后端返回自定义 token,存起来,后续请求带上 wx.setStorageSync('token', resp.data.token); wx.setStorageSync('userId', resp.data.userId); } }); } });后端 auth 服务的逻辑是:拿 code 调微信的jscode2session接口,换回openid和session_key,查用户表,没有就注册,有就更新登录时间,然后用 JWT 签发 token。这里有个坑:session_key绝对不能下发给小程序端,它是对称加密用户数据的密钥,泄露等于把用户手机号送人。
3.2 获取手机号的解密流程
微信小程序获取手机号,前端调getPhoneNumber拿到加密的encryptedData和iv,传给后端用session_key解密:
// 后端解密手机号,session_key 从服务端缓存取,不能信前端传的 String decryptPhone(String encryptedData, String iv, String sessionKey) { try { AES aes = new AES(Mode.CBC, Padding.PKCS5Padding, Base64.decode(sessionKey), Base64.decode(iv)); String json = aes.decryptStr(encryptedData); // 解出来是 {"phoneNumber":"138...","purePhoneNumber":"138..."} return JSONUtil.parseObj(json).getStr("purePhoneNumber"); } catch (Exception e) { // 解密失败通常是 session_key 过期,让前端重新 login throw new BizException("手机号解密失败,请重新登录"); } }参数说明:sessionKey必须从服务端 Redis 里按 openid 取,不能由前端传,否则任何人都能伪造。iv是初始向量,每次加密都不同,前端原样传即可。解密失败九成是session_key过期,因为用户可能几天没打开小程序,这时候要引导重新wx.login。
3.3 请求封装与 token 自动续期
小程序里每个页面都手写wx.request是灾难,封装一个统一请求方法,自动带 token、自动处理 401:
// 统一请求封装,token 失效自动重新登录 function request(options) { const token = wx.getStorageSync('token'); return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + options.url, method: options.method || 'GET', data: options.data, header: { 'Authorization': 'Bearer ' + token }, success: (res) => { if (res.statusCode === 401) { // token 过期,清掉重新登录,再重放这次请求 wx.removeStorageSync('token'); reLogin().then(() => request(options).then(resolve)); return; } resolve(res.data); }, fail: reject }); }); }逻辑说明:401 时先清 token 再重新登录,登录成功后递归重放原请求,用户无感知。注意递归要有次数上限,否则登录接口本身返回 401 会死循环。这个封装是小程序端最值得先做的事,能省掉后面无数重复代码。
4. 避坑与排查:微服务商城上线后最容易翻车的五件事
4.1 分布式事务没处理,下单扣了库存却没生成订单
现象:用户下单,库存扣了,但订单表没记录,用户投诉"钱扣了没订单"。原因:订单服务和库存服务是两个数据库,本地事务管不住跨服务操作。解决:用 Seata 的 AT 模式,或者更轻量的本地消息表加定时补偿。我一般推荐后者,因为 Seata 对性能有损耗,商城这种高并发场景不划算。具体做法是订单服务本地事务里同时写订单和一条"待扣库存"消息,定时任务扫消息去调库存服务,成功就标记完成,失败就重试。
4.2 Feign 超时和重试配置不当,大促雪崩
现象:大促时一个服务响应变慢,调用它的服务线程池被占满,连锁反应整个系统挂掉。原因:Feign 默认超时 1 秒、默认不重试,但很多人手动开了重试却没设上限。解决:超时设 3 秒,重试最多 1 次,并且必须配熔断降级。用 Sentinel 或 Hystrix,当失败率超过阈值直接返回兜底数据,比如商品详情查不到就返回缓存里的旧数据,别让请求堆积。
4.3 小程序端 token 存错地方,切后台就掉登录
现象:用户切到微信聊天再回来,就要重新登录。原因:token 存在了内存变量里,小程序切后台可能被回收。解决:token 必须存wx.setStorageSync,它是持久化的。但要注意 storage 有 10MB 上限,别往里塞大对象。另外 token 有效期别设太短,商城类小程序设 7 天比较合理,配合静默续期。
4.4 库存超卖,秒杀时卖出去的数量超过实际库存
现象:库存 100 件,秒杀结束卖出 120 件。原因:查库存和扣库存是两步,并发下都查到有库存就都扣了。解决:扣库存必须用数据库原子操作,UPDATE stock SET num = num - 1 WHERE sku_id = ? AND num > 0,看影响行数判断是否成功。别用"先 select 再 update",那是超卖的经典写法。更狠一点用 Redis 预扣,Lua 脚本保证原子性。
4.5 服务拆太细,一个下单请求跨了八个服务
现象:下单接口响应时间从 200ms 涨到 2 秒。原因:拆过头了,每个服务一次网络调用,八次叠加起来延迟爆炸。解决:合并边界模糊的服务,比如把营销合并进订单,把类目合并进商品。判断标准是:如果两个服务的数据总是一起被查询、一起被修改,它们就该在一起。微服务不是越细越好,是边界越清晰越好。
5. 本地多服务联调:一个文件夹统一启动的技巧
拆成微服务后,本地开发最烦的是要开七八个 IDEA 窗口,内存直接爆掉。我现在的习惯是把所有服务放在一个父工程文件夹里,用 VS Code 的launch.json统一管理启动配置,一个窗口全搞定。
{ "version": "0.2.0", "configurations": [ { "type": "java", "name": "GatewayApplication", "request": "launch", "mainClass": "com.mall.gateway.GatewayApplication", "projectName": "gateway-service" }, { "type": "java", "name": "OrderApplication", "request": "launch", "mainClass": "com.mall.order.OrderApplication", "projectName": "order-service", "vmArgs": "-Dserver.port=8082" }, { "type": "java", "name": "StockApplication", "request": "launch", "mainClass": "com.mall.stock.StockApplication", "projectName": "stock-service", "vmArgs": "-Dserver.port=8083" } ], "compounds": [ { "name": "全部启动", "configurations": ["GatewayApplication", "OrderApplication", "StockApplication"] } ] }compounds是关键,它把多个启动配置打包成一个,点一下"全部启动"就按顺序拉起所有服务。vmArgs里指定端口,避免和本地其他项目冲突。这套配置的前提是每个服务都是独立的 Maven 模块,父 pom 里用<modules>聚合,VS Code 的 Java 插件能自动识别。
联调时还有两个习惯值得养成。第一,Nacos 本地起一个单机版,所有服务注册到本地,别连测试环境的注册中心,否则你的请求会打到别人机器上,排查半天发现是别人的代码在跑。第二,每个服务的日志输出到独立文件,用logback-spring.xml按服务名区分,出问题时直接看对应文件,别在一个控制台里翻。
最后说个验证方法:拆完之后,用 JMeter 压一下下单接口,看 P99 延迟和错误率。如果 P99 超过 500ms 或者错误率超过 1%,说明拆分边界或者通信方式有问题,回去看是不是同步调用太多、是不是该异步的没异步。我踩过最深的坑就是拆完不压测,上线才发现 Feign 调用链太长,那时候改架构的成本比一开始就设计对高十倍。希望帮到你。
本文还有配套的精品资源,点击获取