☰
Spring Cloud微服务架构实战:微信小程序二手交易系统从单体到分布式
2026/9/30 3:47:07 网站建设 项目流程

这段时间一直在折腾一个二手物品调剂系统,本来只是想做个简单的毕业设计级别项目,结果越做越往企业级靠了。技术栈从最初的 SpringBoot + Vue 单机版,一路演变成了微信小程序 + SpringBoot + Vue + Spring Cloud 微服务分布式架构。整个过程踩了不少坑,也理清了很多之前模糊的概念,这篇就系统梳理一下这个项目的完整落地过程。

先说下这个项目到底是干什么的:核心是一个二手物品交易平台,用户通过微信小程序完成扫码登录、浏览商品、发布闲置、下单购买、在线支付等操作;管理端使用 Vue 搭建后台,负责商品审核、用户管理、交易统计;后端采用 Spring Cloud 微服务架构,将用户、商品、订单、支付、搜索等模块拆分成独立服务,通过 Nacos 注册发现、Gateway 统一网关、Feign 服务间调用、Sentinel 熔断限流来支撑整个业务链路。

这套方案适合谁参考?如果你正在做类似的校园二手交易、社区闲置流转项目,或者想从单体架构向微服务过渡,又或者准备应对面试中“为什么拆分服务、拆了之后怎么保证数据一致性”这类问题,这篇内容应该对你有帮助。

1. 需求拆解与微服务架构选型思路

1.1 二手调剂业务到底要解决什么问题

做系统之前,先把业务捋清楚。所谓“二手物品调剂”,核心场景就是:学生或社区居民有闲置物品(教材、电子产品、生活用品、甚至代步工具),想快速转手;另一方想低价买到还不错的二手货。中间需要平台完成信息撮合、交易担保、信任背书。

所以系统必须覆盖四条核心链路:

  • 发布链路:用户拍摄物品照片 → 填写描述和价格 → 提交审核 → 上架展示
  • 发现链路:首页推荐、分类筛选、关键词搜索、物品详情浏览
  • 交易链路:下单 → 支付(担保交易)→ 发货/收货 → 确认完成或发起售后
  • 管理链路:管理员审核商品 → 处置违规 → 用户封禁 → 运营数据统计

这些链路之间耦合度差异很大。比如搜索和推荐流量大、变动频繁;订单和支付强一致、不容出错;用户服务是所有链路的底座;商品服务既要承接高频浏览也要处理审核状态流转。如果全塞进一个单体应用里,开发期看起来很快,但到了后期,任何一个模块的内存溢出或数据库慢查询都会拖垮整个系统。这也是我最终选择微服务的最直接原因——不是跟风,而是业务复杂度到了一定程度,物理上需要隔离。

1.2 单体到微服务的演进边界在哪里

说句实在话,如果业务只有几百个用户、日活个位数,单体架构完全够用,强行拆微服务属于自找麻烦。但这是个二手交易平台,涉及多方角色、多端操作,而且我明确希望它未来可以扩展成校园级甚至社区级产品,所以拆分是合理的。

我给自己定了几条拆分原则:

  • 按业务域拆:用户、商品、订单、支付、存储/文件、搜索,彼此独立
  • 数据库独立:每个服务有自己的库,禁止跨库 join,需要聚合数据走接口或事件
  • 高流量与高一致性分离:商品浏览是读多写少,订单支付是强一致场景,分开部署便于独立扩容和优化
  • 管理端控制台独立成服务:管理操作和用户操作权限边界完全不同

最终的服务划分如下:

服务名职责关键依赖
user-service微信登录、用户信息、地址管理、信任分MySQL、Redis
product-service商品发布、审核、上下架、分类、图片MySQL、MinIO
order-service订单创建、状态流转、超时取消MySQL、Redis、Seata
payment-service微信支付统一下单、回调处理、退款MySQL、Feign
search-service商品搜索、推荐列表、类目聚合Elasticsearch
admin-service管理后台接口、权限校验、数据统计MySQL
gateway-server统一入口、鉴权、路由、限流Spring Cloud Gateway
auth-centerJWT 签发、刷新、Token 校验Redis

拆成 8 个服务之后,开发并行度一下就打开了,小程序端、管理端、各服务可以同步推进,互不阻塞。当然,代价也明显——部署复杂度、联调成本、分布式事务问题全都来了。这个取舍后面细讲。

1.3 技术选型的核心考量

选型这块,我是结合团队熟悉度和生态成熟度来定的,没有刻意追求冷门框架。

后端框架:

  • SpringBoot 2.7.x:生态最全,资料最多,遇到问题基本都能搜到答案
  • Spring Cloud Alibaba:Nacos 做注册中心和配置中心,Sentinel 做流控熔断,Seata 做分布式事务——整套组件在中文社区中落地案例丰富,出了问题相对好排查
  • MyBatis-Plus:单表 CRUD 基础能力开箱即用,分页插件好用,省掉大量重复代码
  • Redis:缓存热点商品、存会话、实现分布式锁

前端两端:

  • 微信小程序原生(WXML + WXSS + JS):官方支持最好,调试方便,不引入额外框架
  • Vue 3 + Element Plus + ECharts:管理后台的标准组合,表格、表单、图表都有现成组件

数据库与中间件:

  • MySQL 8.0:主数据库,每个微服务独立实例,通过 Seata 协调分布式事务
  • Elasticsearch 7.x:商品搜索。这里有一个重要决策:初期我没上 ES,用的是 MySQL LIKE 查询,后来商品量上来之后发现搜个“自行车”要扫几万条记录,响应时间直接飙到 3 秒以上,才下决心引入了 ES。对于搜索场景,这是必需的
  • MinIO:私有化对象存储,存商品图片,兼容 S3 API。没有用公有云 OSS,主要考虑到可以内网部署部署成本更低,而且数据和接口完全可以自控
  • RabbitMQ:异步解耦,比如下单成功后发消息给积分服务、通知服务

2. 数据库设计、接口规范与微信小程序端实现

2.1 核心库表设计思路

微服务拆分之后,数据库必须跟着拆。每个服务各自管好自己的那一摊,这一点早期一定要坚持,不然后面数据耦合在一起,拆都不知道从哪里下手。

重点说几个有代表性的表设计:

用户服务:

CREATE TABLE `user_account` ( `id` bigint NOT NULL AUTO_INCREMENT, `openid` varchar(64) NOT NULL COMMENT '微信openid,唯一', `nickname` varchar(64) DEFAULT '', `avatar_url` varchar(255) DEFAULT '', `phone` varchar(20) DEFAULT NULL, `credit_score` int DEFAULT 100 COMMENT '信任分,押金抵扣依据', `status` tinyint DEFAULT 1 COMMENT '1正常 0冻结', `create_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_openid` (`openid`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户账号表';

openid 是微信体系下用户的唯一标识,设计上要建唯一索引,防止重复登录场景下插入两条用户数据。

商品服务:

CREATE TABLE `product_info` ( `id` bigint NOT NULL AUTO_INCREMENT, `user_id` bigint NOT NULL COMMENT '发布者ID', `title` varchar(100) NOT NULL, `description` text, `category_id` int NOT NULL, `price` decimal(10,2) NOT NULL, `original_price` decimal(10,2) DEFAULT NULL, `images` json DEFAULT NULL COMMENT '图片URL数组', `status` tinyint DEFAULT 0 COMMENT '0待审核 1在售 2已售 3下架 4违规', `view_count` int DEFAULT 0, `create_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`), KEY `idx_category_status` (`category_id`, `status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品表';

这里有个细节:图片用 JSON 字段存储数组。虽然不符合严格的数据库范式,但业务查询永远是一次性取全部图片,没必要拆一张子表再聚合,实际运行效果挺好。商品列表页只需要主图,我通过 JSON 函数取第一条,配合覆盖索引性能也扛得住。

订单服务:

CREATE TABLE `order_info` ( `id` bigint NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '业务订单号', `product_id` bigint NOT NULL, `seller_id` bigint NOT NULL, `buyer_id` bigint NOT NULL, `amount` decimal(10,2) NOT NULL, `status` tinyint DEFAULT 0 COMMENT '0待支付 1已支付待发货 2已发货 3已确认 4已取消 5售后中', `pay_time` datetime DEFAULT NULL, `create_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';

订单号不能用数据库自增 ID,必须业务生成。我用的是“日期 + 随机数 + 用户ID尾号”组合,保证高并发下不重复,同时带上时间信息方便排查。

2.2 前后端接口规范:RESTful + 统一响应体

微服务之间的接口规范和前后端接口规范,必须在开工之前就定死。不然后面联调就是灾难。

我们统一约定:

  • 接口路径:/api/{version}/{service}/{resource},例如/api/v1/product/list
  • 请求方式:GET 查、POST 增、PUT 改、DELETE 删
  • 响应结构统一:
{ "code": 200, "message": "success", "data": { } }

错误码分段管理:1xxx用户端错误、2xxx商品端、3xxx订单支付端、9xxx系统级异常。这样拿到错误码就能马上定位大概哪个模块出了问题。

一个关键约定:所有时间字段统一返回时间戳毫秒值。因为小程序端日期格式化跟 Web 端不一样,返回字符串格式很容易被各端解析坑到,直接用时间戳由前端统一处理最省事。

2.3 微信小程序端核心页面与交互

微信小程序端用了原生框架,页面不算多,但踩过的细节问题不少。

核心页面清单:

  • 首页:搜索框 + 分类宫格 + 瀑布流商品列表,下拉刷新、触底加载分页
  • 商品详情:轮播图、价格、描述、卖家信息、同款推荐
  • 发布闲置:图片上传(最多9张)、标题、描述、价格、原价、分类选择
  • 订单列表:待付款 / 待发货 / 待收货 / 已完成,四个 tab 切换
  • 个人中心:头像昵称、发布记录、购买记录、信任分、地址管理

小程序登录流程,这里很多新手会搞错:

正确流程不是在小程序端直接拿 code 去后端换 openid 就完事,而是:

  1. 小程序端调用wx.login()获取临时 code
  2. 把 code 传给后端 auth-center 服务
  3. 后端用 code + appid + secret 调微信code2session接口,换取 openid 和 session_key
  4. 后端用 openid 查询/创建用户,生成 JWT 返回给小程序
  5. 小程序把 JWT 存到 storage,后续请求带上 token

注意:code 只能使用一次,且 5 分钟有效期。调微信接口失败时一定要有重试和日志,不然用户莫名其妙登录不上,排查起来很痛苦。

图片上传也是小程序端一个容易出问题的点。小程序wx.uploadFile上传图片时,不能走常规的Content-Type: application/json,必须用 form-data 格式,后台接收 MultipartFile。我之前把图片 base64 直接编码塞 JSON 里传,后端解析慢、传输体积大,图片一多直接卡死,后来全改成上传到 MinIO 拿回 URL 的方式,体验提升明显。

一个瀑布流布局的细节:小程序原生的 scroll-view 实现双列瀑布流,需要对图片高度做预判。如果每张图片都在渲染后动态获取高度,会出现大量图片跳动。我的方案是后端返回图片时附上宽高比(上传时用工具读取图片尺寸存库),前端用mode="widthFix"调整高度,基本稳住。

3. 微服务核心链路实现与踩坑实录

3.1 服务注册发现、统一网关与鉴权

服务注册这块,用的 Nacos 2.x。每个服务启动时自动注册到 Nacos,服务消费者通过服务名调用,不再写死 IP:Port。这样做的好处很明显:弹性扩容缩容对上层透明,比如订单服务压力大了,直接多拉两个实例,网关和 Feign 会自动负载均衡到新实例。

网关层是Spring Cloud Gateway,所有请求先过网关再到具体服务。网关主要干了三件事:

  • 路由转发:根据路径前缀把请求转发到对应微服务
  • 统一鉴权:校验 Token 是否有效,无效的直接拦截返回 401
  • 限流:基于 Redis + Lua 脚本实现 IP 维度限流,每个 IP 每秒最多 20 次请求,防刷

鉴权 Token 我用的JWT(JSON Web Token),结构是三段:Header.Payload.Signature。服务端不保存状态,验签通过就放行。要特别注意 JWT 的密钥一定放配置文件,不要硬编码在代码里,而且要定期更换。

网关鉴权过滤器核心逻辑可以简化成:

@Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { String path = exchange.getRequest().getURI().getPath(); // 白名单直接放行 if (WHITE_LIST.contains(path)) { return chain.filter(exchange); } String token = exchange.getRequest().getHeaders().getFirst("Authorization"); if (StringUtils.isBlank(token) || !jwtUtil.validateToken(token)) { exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } // 解析用户信息,放进请求头转发给下游服务 ServerHttpRequest request = exchange.getRequest().mutate() .header("X-User-Id", jwtUtil.getUserId(token)) .build(); return chain.filter(exchange.mutate().request(request).build()); }

这套链路踩过最大的坑是:网关是 WebFlux 响应式模型,不能直接用传统的 Servlet 注解和 ThreadLocal。提供者从登录状态取值时,用的是RequestContextHolder.getRequestAttributes(),这在 WebFlux 里是空指针。最后是统一改成从网关透传的 Header 中读取用户 ID,彻底避开了线程模型差异问题。

3.2 服务间调用与分布式事务处理

服务间调用用的是OpenFeign,声明式 HTTP 客户端。举个订单流程的例子:

用户下单时,order-service 需要同时确认商品状态、扣减库存(或标记预占)、创建订单。这里 order-service 和 product-service、user-service 都有交互。Feign 接口定义大概长这样:

@FeignClient(name = "product-service", path = "/api/v1/product") public interface ProductClient { @PutMapping("/preoccupy/{productId}") Result<Boolean> preoccupy(@PathVariable("productId") Long productId, @RequestParam("orderNo") String orderNo); }

一开始我以为 Feign 调用很简单,实际上坑不少:

坑一:超时时间默认太短。Feign 默认连接超时 10 秒,读超时 60 秒。但当一个服务处理慢(比如大图片上传、报表统计),调用方很快抛超时。我后来统一配置了:连接超时 3 秒,读超时 10 秒,并且对重要接口做单独调整。

坑二:传递 Token 问题。用户请求经过网关到 order-service,order-service 再 Feign 调 product-service,这个链路中间 Token 会丢。需要写一个 Feign 拦截器,把上游请求头里的 Token 和用户信息复制到下游请求里。

坑三:全局异常处理。Feign 调用返回的 JSON 解析失败、连接断开,需要做降级处理。Feign 的 fallback 工厂一定要开启feign.hystrix.enabled=true(或 sentinel 适配),否则下游一挂,上游直接抛 500。

分布式事务是这个项目最复杂的一块。下订单同时要扣库存、生成订单、后续还要减积分,这明显不是一个本地事务能搞定的。我对比了几种方案:

方案实时一致性性能实现复杂度适用场景
本地消息表最终一致高中对延迟不敏感的业务
Seata AT 模式最终一致中低通用分布式事务
RocketMQ 事务消息最终一致高高消息驱动场景

最后选了Seata AT 模式。AT 模式的核心思想是:业务代码不用侵入,Seata 通过解析 SQL 自动生成回滚日志。下单方法加个@GlobalTransactional注解,方法内的所有本地事务(扣库存、建订单)会被纳入同一个全局事务,只要有一个失败,全部回滚,包括对商品服务的远程调用。

不过 Seata 也不是银弹。AT 模式全局锁对高并发有一定影响,比如秒杀场景下性能会明显下降。我最终的做法是:纯粹的读多写少操作不走分布式事务,强一致链路才用 Seata。搜索和展示场景直接读缓存,订单链路用分布式事务保证一致性。

3.3 缓存设计与并发控制

Redis 在这套系统里承担了四个角色:

  • 热点商品缓存:商品详情读多写少,按“商品ID + 更新时间”作 key,缓存 10 分钟
  • 分类列表缓存:首页分类信息基本不变,缓存 1 小时,后台改分类时手动刷新
  • 登录状态存储:JWT 无状态之后,用户退出和封禁状态需要靠 Redis 记录黑名单
  • 分布式锁:防止重复提交订单,防止并发抢同一个商品

其中一个比较关键的点是防超卖和防重复下单。场景是这样的:一个二手商品,可能同时被多个用户看中下单。如果后端不加控制,就可能出现库存为 1 的商品被下单两次的情况。

我用 Redis 分布式锁来做控制,核心思想是不让两个请求同时扣减同一个商品的库存:

String lockKey = "product:lock:" + productId; boolean locked = redisTemplate.opsForValue() .setIfAbsent(lockKey, "1", 30, TimeUnit.SECONDS); if (!locked) { return Result.error("商品正在被其他用户购买,请稍后再试"); } try { // 检查商品状态并预占库存 Integer stock = productService.preoccupyStock(productId, orderNo); if (stock == null || stock <= 0) { return Result.error("商品已售出"); } // 创建订单... } finally { redisTemplate.delete(lockKey); }

这里要强调:分布式锁必须设置过期时间。如果不设置过期时间,一旦持有锁的服务进程卡死或 GC 停顿,其他请求就永远拿不到锁,系统直接死锁。加锁和解锁的原子性也一定要保证,不能分开两步操作。

后来我优化了一个细节:锁的粒度从“商品 ID”细化到“商品 ID + 买家 ID”,这样同一个用户可以同时买多个商品,但同一个人不能对同一个商品重复下单,体验更好。

3.4 图片存储与对象存储方案

二手商品图片特别多,一个商品发布时可能上传 9 张图。本地磁盘存储肯定不行:一是分布式环境下服务实例多了以后文件不共享;二是扩容时文件丢失;三是磁盘空间难管理。

我用的是MinIO,部署一个单节点实例,Docker 一条命令拉起来,内网访问,成本低、可控性强。核心配置:

@Configuration public class MinioConfig { @Value("${minio.endpoint}") private String endpoint; @Value("${minio.access-key}") private String accessKey; @Value("${minio.secret-key}") private String secretKey; @Bean public MinioClient minioClient() { return MinioClient.builder() .endpoint(endpoint) .credentials(accessKey, secretKey) .build(); } }

上传接口生成一个预签名 URL(Presigned URL),小程序端生成一个带签名的链接,直接通过这个链接上传文件,后端不用经过服务器转发,减轻后端带宽压力。同时设置 bucket 的权限是私有读,访问图片时走预签名 URL,过一段时间自动过期,这样也有一定防盗链作用。

图片需要做压缩处理:小程序端上传前用 canvas 压缩到合理尺寸,后端再对超过限制的图片进行缩放。这样做的直接好处是列表页图片加载速度明显变快,用户流量也省了。

3.5 支付与退款流程要点

微信支付对接是项目里最不能出错的部分。整个支付流程梳理下来分四步:

  1. 用户在小程序端发起支付:小程序调用后端 payment-service,传入订单号、金额、openid
  2. 后端调微信支付统一下单接口生成预支付订单,返回 prepay_id 等参数
  3. 小程序端拿到参数调wx.requestPayment拉起支付面板
  4. 支付结果由微信服务器异步回调到后端,后端更新订单状态,再通知卖家

这里有一个特别容易踩坑的地方:微信支付异步回调必须验签。回调通知里会带Wechatpay-Signature头,必须用微信平台证书公钥验签,验签通过才能处理业务逻辑。很多人图省事不验签,结果被伪造回调刷单甚至盗刷,必须引以为戒。

支付完成后订单状态不能直接改成“已支付”,必须由回调来改。支付的“最终状态”以回调为准,前端返回结果只做界面提示。

退款流程:买家发起退款申请(订单状态变为售后中),卖家同意之后,payment-service 调微信支付退款接口,金额原路返回,同步记录退款流水。整个退款过程也要处理好状态机,防止重复退款。

4. Vue 管理后台、环境配置与部署要点

4.1 Vue 3 后台管理系统的页面模块

管理后台用 Vue 3 + Element Plus,页面整体分四块:

  • 仪表盘:商品发布趋势、成交金额、用户增长曲线,用 ECharts 画图
  • 商品管理:待审核列表、在售列表、违规下架,支持一键审核批量操作
  • 用户管理:用户列表、信任分调整、账户冻结/解冻
  • 系统设置:分类管理、公告管理、支付参数配置

权限控制不能只在前端做。用户登录后,后端返回用户的角色标识,前端路由守卫根据角色控制菜单显示。但这只是体验层的,真正的权限控制在后端:每个接口都要校验当前用户角色,没有权限直接拒绝。我见过太多前端隐藏按钮就算了、后端接口不校验的案例,别人用 Postman 直接调接口就能绕过。

**Vue 3 组合式 API(Composition API)**确实比选项式更顺手,特别是管理后台这种大量表单和表格的交互,逻辑复用用 composables 提取出来,比如管理后台的商品审核列表,页面代码结构可以拆分得很干净。Vue Router 的动态路由也要注意:菜单和路由应该从后端接口动态拉取,而不是全部写死在前端,这样加一个新菜单不用发版。

一个提升效率的点:后台列表页用了el-table 的 v-loading指令管理加载状态,再封装一个统一的useTable组合式函数,把分页、搜索、刷新逻辑抽出来,后面新增管理页面能省不少代码。

4.2 前后端联调与跨域问题

前后端联调的痛苦点,基本都集中在 CORS 和请求拦截上。

开发环境中,小程序端请求走的是request封装,域名需要在小程序后台配置为合法请求域名。本地开发可以在开发者工具中勾选“不校验合法域名”,但真机预览必须配置 HTTPS 域名。如果买不起 HTTPS 证书,可以用内网穿透工具把本地服务映射出去,不过免费的穿透工具连接稳定性不太行,线上环境还是建议用正式域名加 HTTPS。

Web 管理后台因为跑在浏览器里,会碰跨域问题。常见的解决方式是后端网关层开启 CORS 配置:

@Bean public CorsWebFilter corsWebFilter() { CorsConfiguration config = new CorsConfiguration(); config.addAllowedOriginPattern("*"); config.addAllowedMethod("*"); config.addAllowedHeader("*"); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return new CorsWebFilter(source); }

但生产环境我建议把 allowedOrigins 配置成具体的域名,不要用*。*加上allowCredentials(true)在浏览器里是被拒绝的,而且也不安全。

4.3 部署环境与自动化脚本

部署架构如下:

  • 前端(Vue 管理后台)打包为静态文件,用 Nginx 托管,反向代理到 Gateway
  • Gateway 和所有微服务打成 Docker 镜像,用 docker-compose 编排
  • MySQL、Redis、Nacos、Seata、MinIO、RabbitMQ 都用 Docker 部署
  • Elasticsearch 单独部署,内存配置需要格外注意,默认的堆内存太大,小机器跑不动

一个部署中的关键细节:服务启动顺序有讲究。Nacos 必须先启动,然后 Seata 和微服务;微服务里 Gateway 可以和业务服务并行启动,但只有当 Nacos 注册成功后,服务之间才能互相发现。如果启动顺序乱了,服务反复重启好多次才能恢复,我干脆写了个 shell 脚本按依赖顺序启动:

#!/bin/bash echo "启动 Nacos..." docker-compose up -d nacos sleep 30 echo "启动基础设施..." docker-compose up -d mysql redis minio rabbitmq sleep 10 echo "启动 Seata..." docker-compose up -d seata sleep 20 echo "启动微服务..." docker-compose up -d user-service product-service order-service payment-service sleep 30 echo "启动 Gateway..." docker-compose up -d gateway-server

实测下来这套顺序基本稳定。注意:服务的配置文件里要用服务名而不是 IP 地址连接中间件,比如数据库地址写成jdbc:mysql://mysql:3306,这依赖 Docker 内置 DNS,只要服务都在同一网络内就可以解析。

4.4 从源码到可运行:环境配置清单

如果你要照着这个项目复现,环境配置是第一个门槛。我会把关键配置写在这里方便对照:

后端统一配置样例(application.yml):

spring: cloud: nacos: discovery: server-addr: 192.168.31.100:8848 datasource: url: jdbc:mysql://192.168.31.100:3306/order_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: xxxxxx redis: host: 192.168.31.100 port: 6379 mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

Nacos 配置中心:数据库连接、Redis 地址、JWT 密钥等环境相关的配置,放到 Nacos 配置中心管理,不要写死在本地 application.yml 里。这样改配置不需要重新打包镜像,直接在 Nacos 控制台改,然后刷新。

本地开发常见的环境坑:

  • JDK 版本不统一:项目要求 JDK 8 或 11,但有的同事装的 JDK 17,编译直接报错
  • Maven 仓库源太慢:配置阿里云镜像源,构建速度快非常多
  • Lombok 插件没装:IDE 爆红,但 mvn 命令行编译其实能过,一开始会误以为代码有问题
  • 数据库连接时区问题:连接串必须加serverTimezone=Asia/Shanghai,否则时间错 8 小时

5. 常见问题排查与避坑经验

5.1 高频故障排查表

下面这些是我在实际开发和压测中遇到的问题,整理成速查表,遇到类似情况可以直接按图索骥:

现象可能原因排查思路与解决
小程序请求接口一直超时1. 域名不是 HTTPS 且未配置合法域名
2. 网关和服务之间网络不通
3. 本地调试开了代理
先看微信开发者工具的 network 面板;再确认服务是否注册到 Nacos;最后 telnet 测试端口连通性
网关返回 401Token 过期、Token 未带、JWT 密钥不一致检查请求头 Authorization;看网关日志的校验失败原因;确认各服务 JWT 密钥一致
Feign 调用报连接超时下游服务处理慢,读超时设置过短调整 Feign 超时配置;优先检查下游 SQL 是否有慢查询;增加日志追踪调用链
下单后库存没扣减分布式事务未生效确认 Seata 全局事务注解在接口方法上,确认事务分组配置一致;看 Seata 日志是否有全局锁冲突
搜索商品不全增量同步延迟检查 RabbitMQ 消费是否积压;手动触发一次全量重建索引
小程序登录码无效code 被重复使用wx.login 一次获取的 code 只能用一次,检查是否在日志中重复请求
管理后台图表数据不准统计 SQL 写错或时区不对检查 SQL 的 GROUP BY 是否按天分组,确认数据库时区设置为东八区
上传图片偶发失败MinIO 连接池占用检查 MinIO 连接数是否够用,增加预签名 URL 有效期至 5 分钟
服务重启后注册不上Nacos 临时实例正常,持久实例数据问题检查 Nacos 控制台是否能看到服务列表;确认各服务有独立命名空间和 group

5.2 微服务链路追踪与日志排查

微服务拆了之后,最头疼的其实是出问题的时候不知道怎么定位。一次用户投诉“下单失败”,你根本不知道是 order-service 的问题、product-service 的问题,还是 Seata 全局事务超时的问题。

我踩了这个坑后接入了Sleuth + Zipkin,给每个请求生成一个全局 TraceId。日志打印带上 TraceId,用 ELK(Elasticsearch + Logstash + Kibana)做统一日志平台。出问题的时候,拿一个小程序端返回的 requestId(TraceId),在 Kibana 里按 TraceId 一搜,所有相关服务的日志按时间排序展开,哪个环节报错一目了然。

日志打印规范也要定好,至少打印四类信息:请求入口(参数摘要)、业务关键变更(订单状态变化)、远程调用结果(Feign 调用耗时和响应码)、异常堆栈(完整保留,不要只打 message)。同时所有日志必须带上traceId、userId、orderNo这些便于检索的字段。用 Logback 的 pattern 统一配置。这样排查问题效率提升了一个数量级。

5.3 微服务拆分的代价与应对

写到这里我必须坦白:微服务虽好,但不是没有代价。这些代价如果你没有提前想清楚,项目后期会非常难受。

首先是运维复杂度。单体架构一个 jar 一个进程,微服务变成了 8 个 jar + 一堆中间件。我部署用的 docker-compose 还算轻量,但如果你上了 Kubernetes,学习成本会更高。所以我的建议是:不要一上来就上 K8s,先从 docker-compose 开始,等实例多了再平滑迁移。

其次是接口一致性测试更难。单体架构一个环境编译、启停、冒烟就完事,微服务要先把 Nacos、MySQL、Redis、Seata 全部拉起,然后按依赖顺序启动各个服务。建议写一个start-all.sh一键启动脚本,把中间件和服务都管起来,不然每次开发前置准备就是 20 分钟。

再次是团队沟通成本。服务接口的定义、异常码的规划、数据格式的一致性,这些必须要有一个统一的契约文档。我在项目里建了一个 OpenAPI 文档(Swagger),每个服务启动后访问/swagger-ui.html就能看到该服务的全部接口,联调时对着文档走,基本不用互相追问。

5.4 最后的几句话:经验沉淀

回头看这个项目,最大的收获不是把某个技术栈用熟了,而是理解了架构选型背后的逻辑。微信小程序是前端触点,Vue 是运营端工具,SpringBoot 是每个微服务的地基,Spring Cloud 是把地基连成网络的水泥和钢筋。它们组合在一起,不是技术堆砌,而是为了一个目标——让二手物品流通得更顺畅、更透明、更可信。

如果你也准备做类似项目,我的建议是:先别急着上微服务,把单体版本的业务跑通,画出核心链路图,再考虑哪些模块必须独立。拆服务不是目的,解决业务问题是目的。项目里用的 Spring Cloud Alibaba 组件目前生态成熟度很高,但也不要为了用而用,保持“够用就行,复杂留白”的心态。

后续这个系统还可以继续扩展:接入消息推送(买家下单后提醒卖家发货)、引入推荐算法(根据浏览历史推荐相似商品)、增加社区模块(用户发布求购帖)等。方向很多,但核心架构已经铺好了路,往哪个方向走,就看实际业务需求了。

按照我的亲身体验,项目中途最危险的就是“架构洁癖”和“追求完美”,总想一步到位把每个模块都做得企业级,结果进度一拖再拖。先跑通、再优化、最后重构,这才是个人项目出活最快的方式。这套从微信小程序到微服务的完整链路,踩坑虽多,但每个坑都垫高了后续开发的高度。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询