说实话,看到"基于微信小程序二手物品调剂系统_SpringBoot+Vue+Springcloud微服务分布式"这个题目,我第一反应是:这又是一个典型的"标题看起来很唬人,实际落地全看细节"的项目。二手物品调剂本身不是什么新业务,和闲鱼、转转的模式大同小异,但一旦给它挂上"SpringCloud微服务分布式"这个帽子,事情就开始变得有意思了——原本一个单体架构两天就能写完的CRUD系统,瞬间要面对服务拆分、服务间通信、分布式事务、网关路由、认证鉴权等一系列问题。
我当初做完这套系统后最大的感触是:微服务架构本身不难,难的是想清楚"哪些地方真的需要微服务,哪些地方只是给毕业设计凑技术亮点"。这篇文章我就把自己从零到一实现这套系统的完整过程写出来,包括业务建模、技术选型、数据库设计、前后端对接、部署上线以及踩过的一堆坑。无论你是拿它做毕业设计,还是想练手微服务项目,这篇都能帮你少走不少弯路。
1. 这个项目到底在做什么:把业务想明白再动手
很多人拿到这种题目,第一反应是搜框架、找代码,但真正的问题在于:业务没有边界,技术就无从谈起。二手物品调剂系统,核心业务是"调剂"两个字,它和纯粹的二手交易还有点区别——调剂更强调"物品的流动和匹配",既可以是低价出售,也可以是物物交换,甚至可以是免费赠送。所以功能设计上不能只做一个交易闭环,还得有发布、展示、沟通、线下对接等一系列辅助能力。
1.1 业务功能地图:从发布闲置到完成交易
我当时梳理业务时,把整个系统拆成了三条主链路:
第一条链路是物品发布与展示。用户在小程序端拍照上传闲置物品,填写标题、描述、新旧程度、原价、期望价格或期望交换物品,选择交易方式(出售/交换/赠送),然后提交审核。后台审核通过后,物品进入广场列表,其他用户可以通过分类、关键词、价格区间等条件筛选。这是整个系统最核心的信息流。
第二条链路是意向沟通与对接。用户看到感兴趣的物品后,可以收藏、留言,也可以直接发起"我想要"的请求。双方可以在小程序内置的会话窗口里沟通,约定线下交易时间地点,或者协商价格。这条链路决定了系统是"信息中介"而不是"电商平台",所以不需要做支付、物流,大大降低了复杂度。
第三条链路是订单与评价。双方达成意向后,系统生成一笔调剂记录(可以是出售单,也可以是交换单),交易完成后互相评价,积累信用。信用体系对二手调剂系统非常重要,因为平台不介入交易担保,用户的信任度全靠评价和交易历史支撑。
围绕这三条链路,后台管理端要干的活也很明确:物品审核、用户管理(封号/解封)、分类管理、轮播图配置、交易记录统计、举报处理。
提示:如果你是在做毕设,业务功能务必要有"审核"和"统计"这两个模块。评阅老师最喜欢看的就是后台有审核流程、前台有状态变化,这能展示你的系统是"闭环"的,而不是一个花架子。
1.2 为什么是这三个端:小程序、Vue后台与微服务的分工逻辑
这套系统的架构是典型的前后端分离+多端适配:
- 微信小程序端:面向普通用户,承担发布、浏览、沟通、评价等C端功能。微信生态自带流量和登录体系,用户零门槛使用,这是二手调剂类产品最适合的载体。
- Vue管理后台:面向运营人员,承担物品审核、用户管理等B端功能。Vue适合做中后台系统是因为生态成熟,Element Plus组件库一拖就走,开发效率极高。
- SpringBoot+SpringCloud后端:为两个前端提供统一API服务。为什么一定要拆微服务?说实话,一个二手调剂系统的业务量,单体架构完全够用。但在学习/毕设场景下,微服务的作用是展示你在"高内聚低耦合"层面的设计能力,以及应对未来业务扩展的架构思路。
我用一句话总结这套分工逻辑:小程序做用户体验,Vue做运营管控,微服务做能力复用。比如用户服务、物品服务、交易服务、消息服务、文件服务分别独立部署,将来某一块业务流量大了可以单独扩展,而不会拖垮整个系统。
2. 要上微服务,先问自己四个问题
很多人在微服务这里栽跟头,是因为一上来就照着网上的教程搭Nacos、配Gateway、写Feign,但根本不知道自己为什么要这么干。我动手之前先问了四个问题,答案是:拆几个服务、每个服务放什么职责、服务之间怎么通信、数据一致性问题怎么兜底。这四个问题想清楚,骨架就出来了。
2.1 服务拆分:五个业务簇,不要为了微服务而微服务
我的拆分原则非常简单:按业务域拆分,而不是按代码层级拆分。最终我拆成了五个微服务:
| 服务名 | 核心职责 | 主要接口 |
|---|---|---|
| gateway-service | 统一入口、路由转发、跨域处理、JWT校验 | 无业务接口 |
| user-service | 用户注册登录、个人信息、信用评价 | /api/user/** |
| item-service | 物品发布、分类检索、物品详情、上下架 | /api/item/** |
| trade-service | 意向单、调剂订单、交易状态流转、评价 | /api/trade/** |
| message-service | 站内信、买卖双方会话记录 | /api/message/** |
文件上传我单独做了一个file-service吗?没有。图片上传走的是网关转发到文件服务,但没必要自己写一套图片存储,直接用了FastDFS/MinIO搭了一个轻量的文件服务,或者退一步用本地磁盘+Nginx静态映射也能撑住小规模场景。为了不过度设计,文件上传我并入了item-service,只在服务器上单独划了一个upload目录。
每个服务内部保持经典的三层结构:Controller -> Service -> Mapper。不要在一个服务里写另一个服务的Mapper,这是微服务的第一条铁律。
2.2 服务间通信与数据一致性:真实项目中哪些可以用简化方案
服务拆开了,但功能还是完整的,服务之间必然要通信。我的选择是:
- 同步通信用OpenFeign。比如查询物品详情时,需要展示卖家的昵称和信用分,item-service就要远程调用user-service的接口。Feign的好处是声明式HTTP客户端,接口写起来和本地调用差不多。
- 异步通信用RabbitMQ。比如交易完成后,trade-service发一条消息,message-service消费并生成站内信,user-service消费并更新双方的信用分。这个过程不需要用户等待,异步解耦很合适。
注意:异步通信这块如果你是毕设,可以用Spring Boot默认集成RabbitMQ的方式来做,不需要上什么分布式事务框架。生产环境可以上Seata或RocketMQ事务消息,但毕设/练手场景下,"本地消息表+MQ重试"这个简化方案完全够讲清原理,还更容易评优。
数据一致性的问题其实没想象中那么可怕。你要清楚:只有真正跨库的数据操作才需要分布式事务。我的系统设计里,唯一涉及分布式事务的场景是"用户在交易服务中创建订单后,需要扣减物品服务中的物品库存(可调剂数量)"。我的做法是:创建订单和扣减库存用"本地消息表+定时任务补偿"的方式,即trade-service本地事务写订单并插入一条消息记录,定时任务扫描消息表,将扣减指令发送到MQ,item-service消费后执行库存扣减。如果扣减失败,重试三次后标记人工处理。
这个方案已经被很多企业验证过,虽然是简化版,但比直接给Seata交学费稳妥得多。
3. 数据库设计:微服务下的库表边界
数据库设计是最能体现一个开发者"有没有动过脑子"的地方。微服务架构下,每个服务必须有自己的数据库,不能跨库join。你要在业务上让数据自洽,但同时在数据上彻底隔离。
3.1 一服务一库的划分与主键策略
五个服务对应五个库:
user_db:用户表、评价表、收藏表、举报表item_db:物品表、分类表、图片表、浏览记录表trade_db:意向单表、调剂订单表、交易留言表message_db:会话表、消息表- 系统配置和轮播图这类公共配置,我放到了
item_db里,没有单独建库。
主键策略上,我强烈建议用雪花ID,而不是数据库自增ID。原因很好理解:微服务下每个服务库独立生成ID,如果用自增ID,将来数据合并、分库分表、跨服务引用都得炸。雪花ID由时间戳+机器ID+序列号组成,64位long型,既能全局唯一,又带时间有序性,MyBatis-Plus直接内置了@TableId(type = IdType.ASSIGN_ID),一行注解搞定。
3.2 核心表结构:二手物品的场景特殊在哪
二手物品表是最关键的一张表,和普通商品表有很大区别。我当时字段是这么设计的:
CREATE TABLE `item` ( `id` bigint(20) NOT NULL COMMENT '雪花ID', `user_id` bigint(20) NOT NULL COMMENT '发布者ID', `title` varchar(100) NOT NULL COMMENT '标题', `description` text COMMENT '描述', `category_id` bigint(20) DEFAULT NULL COMMENT '分类ID', `cover_image` varchar(255) DEFAULT NULL COMMENT '封面图URL', `condition_level` tinyint(4) DEFAULT '5' COMMENT '新旧程度:1-全新,10-报废边缘', `original_price` decimal(10,2) DEFAULT NULL COMMENT '原价', `expected_price` decimal(10,2) DEFAULT NULL COMMENT '期望出售价', `exchange_item` varchar(200) DEFAULT NULL COMMENT '期望交换的物品描述', `trade_type` tinyint(4) DEFAULT '1' COMMENT '交易方式:1-出售,2-交换,3-赠送', `status` tinyint(4) DEFAULT '0' COMMENT '状态:0-待审核,1-展示中,2-已下架,3-已调剂,4-审核拒绝', `view_count` int(11) DEFAULT '0' COMMENT '浏览次数', `favorite_count` int(11) DEFAULT '0' COMMENT '收藏次数', `transaction_count` int(11) DEFAULT '1' COMMENT '可调剂数量,默认为1', `create_time` datetime DEFAULT NULL, `update_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;这套设计里有几个点值得说一下:
trade_type字段很关键。二手调剂区别于普通电商的核心就是"交换"这种非标交易方式。你发布物品的时候,允许用户填写"期望交换的物品",这个字段是别的系统里没有的,也是评委最容易问到的业务特色。condition_level用1-10表示新旧程度,比"几乎全新/轻微使用痕迹/明显使用痕迹"这种枚举灵活得多,前端可以用滑块组件录入。status的状态机是:待审核 -> 展示中 -> 已调剂/已下架/审核拒绝。这个状态流转在代码里要严格控制,不能出现"展示中直接跳到审核拒绝"这种非法跳转。
其他的表不一一细说了,但有两个设计经验想分享。第一个是图片表单独建,不要往item表里存多个图片URL字段。一个物品可以有多张图,存JSON或者逗号拼接都是给自己挖坑,单独一张item_image表(item_id、image_url、sort_order)最干净。第二个是交易订单表要加一个type字段区分"出售单"和"交换单",交换单没有金额,但是有双方的物品ID,字段要预留。
4. SpringBoot后端落地:从骨架搭建到业务闭环
数据库设计完,就开始写后端代码。这里我要重点讲两个东西:一是技术版本搭配,二是登录认证和核心接口的设计思路。很多人骨架搭不起来,或者各种莫名其妙的报错,十有八九是版本兼容问题。
4.1 版本组合:SpringBoot、SpringCloud、SpringCloud Alibaba的搭配
2026年这个时间节点上,我推荐的稳定组合是:
| 组件 | 版本 | 说明 |
|---|---|---|
| JDK | 1.8 或 17 | 建议1.8,兼容性最好 |
| SpringBoot | 2.7.18 | 2.x最后一个稳定版本 |
| SpringCloud | 2021.0.8 | 对应Hoxton之后的新版本号体系 |
| SpringCloud Alibaba | 2021.0.5.0 | 兼容SpringBoot 2.7 |
| Nacos | 2.2.3 | 注册中心+配置中心 |
| MyBatis-Plus | 3.5.3 | 不用写XML也能CRUD |
| MySQL | 5.7 或 8.0 | 8.0支持更好 |
注意:我知道网上已经有SpringBoot 3.x、SpringCloud 2023.x甚至更新版本的教程了,但如果你目的是"快速稳定地做出一个能跑通的项目",听我一句劝,不要用太新的版本。SpringBoot 3.x强制要求JDK 17,而且很多第三方组件还没完全适配,你遇到一个报错就得查半天,纯粹是浪费时间。国内很多企业现在还在用SpringBoot 2.x,做毕设完全够用。等把原理吃透了再升3.x不迟。
Nacos在SpringCloud Alibaba体系里承担了两个角色:注册中心(替代Eureka)和配置中心(替代Spring Cloud Config)。我的做法是每个服务都引入spring-cloud-starter-alibaba-nacos-discovery和spring-cloud-starter-alibaba-nacos-config,Bootstrap配置里指定Nacos地址和命名空间,服务名(spring.application.name)全局唯一。
4.2 登录与认证:JWT在网关和微服务之间怎么流转
登录认证是微服务架构里最容易翻车的地方。单体项目里你写个拦截器,校验用户是否登录,很简单;微服务里每个服务都是独立的进程,session不共享,怎么办?我的方案是网关统一认证 + JWT无状态令牌 + 上下文透传。
整体流程是这样的:
- 用户在小程序端通过
wx.login()拿到code,后端调微信接口换取openid,然后查库知道是哪个用户。 - 认证服务生成一个JWT令牌(包含用户ID、openid、角色),返回给小程序。小程序把令牌存在Storage里,每次请求header携带
Authorization: Bearer <token>。 - 所有请求先经过Gateway网关,网关的GlobalFilter解析JWT,校验签名和过期时间。校验通过后,把用户ID放到header的
userId字段里,转发给下游服务。 - 下游服务从header里取
userId,作为当前操作人。整个过程不查数据库、不查redis,全靠JWT本身携带的信息,所以网关转发前可以筛掉大量无效请求。
@Component public class AuthGlobalFilter implements GlobalFilter, Ordered { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { String token = exchange.getRequest().getHeaders().getFirst("Authorization"); if (StringUtils.isBlank(token) || !token.startsWith("Bearer ")) { // 白名单路径放行,比如登录、注册、物品列表 if (isWhitelist(exchange.getRequest().getPath().value())) { return chain.filter(exchange); } return unauthorized(exchange); } try { Claims claims = JwtUtil.parseToken(token.replace("Bearer ", "")); ServerWebExchange mutated = exchange.mutate() .request(r -> r.header("userId", claims.get("userId").toString())) .build(); return chain.filter(mutated); } catch (Exception e) { return unauthorized(exchange); } } }Feign调用的时候有个很大的坑:服务A通过Feign调服务B时,header默认不往后传。也就是说,B服务拿不到userId。解决办法是自定义一个RequestInterceptor,把当前请求的userId塞进Feign的RequestTemplate:
@Bean public RequestInterceptor userInfoRequestInterceptor() { return template -> { ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); if (attrs != null) { String userId = attrs.getRequest().getHeader("userId"); if (StringUtils.isNotBlank(userId)) { template.header("userId", userId); } } }; }这个坑我当初踩了很久:明明单个接口调得好好的,一走到Feign链路就报"当前用户不存在",排查了一下午才发现是header没透传。
4.3 核心接口设计:列表带分页,详情、发布、下单的状态机跳转
接口设计的核心是让前端拿到的数据是组装好的,而不是一堆裸数据。你千万不要设计成让前端拿到userId后自己去查用户昵称。
物品列表接口就是典型例子。小程序首页是"加载更多"模式的列表,所以接口直接设计成page和size参数,返回records、total、hasMore三个字段。每个记录里,除了物品信息,还要带上发布者的昵称、头像,以及该物品的收藏状态(当前用户是否已收藏)。
{ "code": 0, "data": { "records": [ { "id": "1582681912335761410", "title": "九成新华为平板", "coverImage": "https://xxx.com/images/123.jpg", "expectedPrice": 1200.00, "tradeType": 1, "conditionLevel": 2, "viewCount": 35, "seller": { "id": "1582681912335308800", "nickname": "张三", "avatar": "https://xxx.com/avatar/1.png", "creditScore": 98 }, "favorited": true } ], "total": 126, "hasMore": true } }物品详情也一样,一个接口把物品信息+多张图片+卖家信息+卖家的其他在售物品(推荐位)全部返回,前端一次请求就能渲染整页。
订单状态流转我设计了这样一个状态机:
待接单(0) -> 已接单(1) -> 已交付(2) -> 已评价(3) \-> 已取消(-1)创建订单时,用户填写期望交易方式、约定时间地点等信息。卖家在小程序里"接单",双方线下碰面交易,买家确认"已交付",最后互相评价。每个状态变更都要在Service层校验前置状态,防止前端直接跳过步骤改状态,这种细节写完记得在小程序端用手动改接口的方式测一测,能拦截住不少安全问题。
5. 微信小程序端开发:从注册到小程序上线遇到的问题
小程序是整个C端体验的门面。我在这部分踩过最多的坑,不是业务代码写不出来,而是微信平台的限制和生态习惯。
5.1 原生小程序还是uni-app,我为什么选原生
网上铺天盖地的uni-app教程,说一套代码能编译到多个平台。但如果你只是做微信小程序,我建议直接上原生微信小程序。原因很简单:原生框架的调试体验最流畅,微信开发者工具的报错信息最准确,不用中间隔一层编译,出问题好排查。将来闲了再学uni-app也不迟。
项目结构上,我用的是原生小程序的经典分包模式:
pages/ index/ 首页(物品广场) item/ 物品详情 publish/ 发布闲置 message/ 消息列表 chat/ 聊天窗口 order/ 我的订单 profile/ 个人中心每个页面由.wxml、.wxss、.js、.json四个文件组成,这套语法对有过Vue或React基础的人来说,半天就能上手。
5.2 列表加载更多与顶部导航栏的实用适配方案
首页的"加载更多"是搜索热词里出现频率非常高的一项,因为实现起来有无数种翻车方式。最简单的方案是用onReachBottom页面生命周期,在触底时判断hasMore,然后发起下一页请求。核心逻辑如下:
onReachBottom() { if (this.data.loading || !this.data.hasMore) return; this.setData({ page: this.data.page + 1 }); this.loadItems(); },这里有个性能优化点:用节流标志loading防止触底事件连续触发导致重复请求。我见过太多人写"加载更多"没加锁,结果一滑到底疯狂请求,服务器直接429。这个错误新手犯得最多。
顶部导航栏的适配是另一个高频坑。微信小程序的导航栏分为"自定义导航"和"系统导航"两种。如果你选了自定义导航(通常是为了好看),就要处理状态栏高度和胶囊按钮位置:
const systemInfo = wx.getSystemInfoSync(); const capsule = wx.getMenuButtonBoundingClientRect(); this.setData({ statusBarHeight: systemInfo.statusBarHeight, navBarHeight: capsule.top - systemInfo.statusBarHeight + capsule.height });这两个参数拿到后,导航栏的高度就固定了,不同机型都能完美适配。我最初忽略了getMenuButtonBoundingClientRect这个API,结果iPhone和安卓的胶囊位置对不齐,界面看起来歪歪扭扭,用户体验极差。
5.3 小程序登录态与后端JWT的对接细节
小程序登录的完整链路是:wx.login()获取code -> 请求后端/api/user/login,后端拿code请求https://api.weixin.qq.com/sns/jscode2session换取openid -> 生成JWT返回前端。前端拿到JWT后存在Storage里。
这里有个很关键的细节:wx.login()获取的code有效期只有5分钟,且只能用一次。如果你在页面加载时先登录,等用户操作完再调后端,code很可能已经过期了。正确的做法是App启动时就完成静默登录,拿到JWT后再让用户操作。
还有一个本地开发时的痛点:微信开发者工具里,小程序默认要求请求的域名必须是HTTPS且已在后台配置白名单。本地开发时有个取巧的办法——在开发者工具右上角的"详情 -> 本地设置"里勾选"不校验合法域名",这样就能用http://localhost:8080调试了。注意这只是开发环境的配置,真机预览时一定要用真实域名。
6. Vue管理后台:中后台开发的效率密码
后台管理端是运营人员用的,要求就一句话:功能齐全、开发快、看得过去。我选的是Vue3 + Vite + Element Plus + Pinia这套组合,没有用webpack也没有用Vue2。
6.1 后台脚手架选型:不推荐从零手写后台
我不是说不让你从零写Vue后台,而是不建议你在"业务项目"阶段从零手写。市面上有现成且开源的项目可以直接作为基底,比如vue-element-admin、若依、vue-pure-admin。我当时基于其中一个比较成熟的模板改的,模板里已经集成了登录、路由守卫、Sidebar、Tabs等中后台的公共设施,我只需要把精力放在业务页面开发上。
如果你在2026年做这个,我多说一句:若依微服务Plus版是很热门的选择,它已经把SpringBoot+SpringCloud+Vue的模板整合好了,代码生成器也很成熟。但用它有个弊端——代码不是你写的,评阅时一旦被深挖就容易露馅。我的建议是:可以借鉴它的结构,但核心代码一定要自己敲一遍。
6.2 动态路由、权限控制与Axios拦截器的实现要点
后台的权限模型是"管理员/普通运营"两级。动态路由的意思是:根据用户角色,决定他能看到哪些菜单、能进入哪些路由。实现上,我在路由守卫beforeEach里根据角色动态addRoute:
router.beforeEach((to, from, next) => { const token = getToken(); if (!token) { if (to.path === '/login') return next(); return next('/login'); } const role = userStore.role; if (!router.hasRoute(to.name) && needDynamicRoute(to.path)) { // 根据角色生成可访问路由并addRoute const routes = generateRoutes(role); routes.forEach(r => router.addRoute('layout', r)); return next({ ...to, replace: true }); } next(); });Axios拦截器主要在两个场景发挥价值:请求时自动携带JWT token;响应时统一处理401(token过期跳登录)和业务错误码(弹出ElMessage提示)。这样做的好处是每个页面都不需要写重复的token注入和错误处理逻辑。
6.3 后台运营界面:物品审核、用户管理、报表统计
后台的核心页面有四个:
- 物品审核:表格列出待审核物品,点击详情查看图片、描述、发布者信息,然后通过或驳回。驳回时必须填写原因,小程序端会展示驳回提示。这个流程一定要完整,二手调剂平台如果没有审核,很快会被广告和违规信息填满。
- 用户管理:用户列表,支持封禁/解封。封禁时要同步处理该用户的在展示中的物品(置为下架),这里涉及跨库操作,我的实现方式是用户服务调用物品服务的Feign接口,因为封禁本来就是低频操作,同步调用完全可接受。
- 分类管理:后台维护分类树,支持增删改排序。
- 数据看板:展示注册用户数、物品发布数、交易完成数、待审核数等统计指标。用ECharts画几张简单的折线图和饼图,图表一出,整个系统的完成度立刻提升一个档次。
7. 部署与测试:从本机到服务器的完整链路
系统开发完不等于结束,还要能部署、能演示、能上线。
7.1 本机联调:三个端一起跑起来的端口规划
我开发时规划了这样一套端口:
| 服务 | 端口 |
|---|---|
| Nacos | 8848 |
| gateway-service | 8080 |
| user-service | 8081 |
| item-service | 8082 |
| trade-service | 8083 |
| message-service | 8084 |
| Vue后台(dev) | 5173 |
| 小程序(开发者工具) | 无固定端口 |
所有前端请求统一走网关8080,避免前端和后端服务端口耦合。Vue后台的请求通过Vite proxy代理到网关,小程序端则直接写死网关的域名。
7.2 微信小程序调试的域名要求与内网穿透方案
微信小程序正式上线要求所有请求必须是HTTPS域名,并且要在小程序管理后台配置request合法域名和uploadFile合法域名。开发阶段测试,除了"不校验合法域名"这个开关外,还可以用内网穿透工具(我常用的是natapp)把本机8080映射到一个公网域名上,手机真机预览就能直接调通接口。这个方法非常适合演示场景,让手机连上同一个WiFi,整个系统在教室里就能现场演示。
提示:如果用Charles抓包调试小程序,一定要先在手机上安装Charles的SSL证书,并且微信开发者工具里关闭"校验合法域名"选项。不然你只能看到加密的乱码数据,抓包就失去了意义。
7.3 服务器部署:把微服务编排到Docker Compose
服务器部署我用的方案是Docker Compose。虽然每个微服务可以用java -jar单独跑,但服务一多,手动管理就特别折磨人。Docker Compose能把Nacos、MySQL、RabbitMQ以及5个SpringBoot服务全部编排在一个docker-compose.yml里,一条命令启动所有服务:
version: '3.8' services: nacos: image: nacos/nacos-server:v2.2.3 environment: - MODE=standalone ports: - "8848:8848" mysql: image: mysql:8.0 environment: - MYSQL_ROOT_PASSWORD=root123 volumes: - ./mysql/data:/var/lib/mysql ports: - "3306:3306" mq: image: rabbitmq:3.12-management ports: - "5672:5672" - "15672:15672" gateway-service: build: ./gateway-service depends_on: [nacos, mysql] ports: - "8080:8080" # ... 其他服务同理部署完用Nginx做一层反向代理和静态文件代理,前端Vue打包后的dist目录放到Nginx的html目录,配置一个HTTPS证书(可以用免费的),整个系统就算上线了。
8. 踩坑复盘:这几类坑我希望你一定绕开
最后这部分,我把自己实际开发过程中踩过且印象最深的几个坑复盘一下。它们不解决的话,系统功能再全也白搭。
8.1 请求头丢失与跨域:网关层最隐蔽的两个坑
第一个坑是跨域。你开发时Vue后台本地跑在5173,后端接口在8080,浏览器跨域是必然的。我的做法是在网关层统一添加CORS配置,不要在业务服务里自己配跨域,否则网关配一次、服务配一次,请求头会被弄丢,而且排查起来极难。跨域只在网关处理,业务服务不启CORS,这是我给自己定的死规矩。
第二个坑就是前面提到的Feign调用时header丢失。网关校验完JWT后把userId放到header,但如果下游服务用Feign调用另一个服务时不透传header,那个服务就不知道当前用户是谁。我当时的解决方案是自定义RequestInterceptor,每次Feign请求自动从当前请求上下文里取出userId并拷贝过去。
8.2 微服务事务失效的真相:本地事务管不到别人家
这是最容易被问、也最容易被忽略的一个点。你在trade-service的Service方法上写@Transactional,方法里调用item-service的Feign接口扣库存,你以为这是一个"事务",但实际上没用。Feign调用是跨进程的HTTP请求,@Transactional只管本地这个服务的数据库事务,管不住item-service那边的操作。所以我在设计时非常明确:每个服务只保证自己的数据一致性,跨服务的一致性走业务补偿(本地消息表+定时任务+MQ)。这个问题能在答辩时回答得清楚,你就是真的理解微服务了。
8.3 图片与文件处理:别在高并发场景用base64
小程序端上传图片,很多教程会让前端用FileSystemManager.readFile把图片读取成base64字符串,再POST到后端。这个方案在小图片时没问题,但手机拍照的图片通常几MB,base64编码后体积还会增加约三分之一,请求体和接口性能极其难看。正确做法是用wx.uploadFile,走multipart/form-data格式上传,后端用MultipartFile接收,直接存磁盘或对象存储,返回URL给前端。你去看热搜词里就有"vue image能显示pdf吗"这一类问题,本质上都是对前端资源处理不熟悉造成的,建议把上传流程彻底跑通,包括大图压缩、防盗链、访问权限,这些细节才是系统专业度的分水岭。
8.4 配置热更新失效与版本不兼容:SpringCloud常见病的排查思路
Nacos配置中心默认支持配置热更新——改完配置不用重启服务就能生效。但我遇到过改了数据库连接配置后怎么刷新都不生效的情况。查了半天发现,Nacos默认的配置热更新只对@ConfigurationProperties注解的类有效,而对@Value注入的字段不会自动刷新。这是Spring Cloud Alibaba一个很经典的坑,解决方案是引入扩展@NacosValue实现动态刷新,或者按配置变更的频次决定走配置中心还是走环境变量,这点很容易被人忽略,但是答辩时可以作为一个"你深入研究过配置中心"的信号。
另外就是版本兼容问题。SpringBoot 2.7.x配SpringCloud 2021.0.8,是经过很多人验证过的稳定组合。如果非要尝试SpringBoot 3.x,那就要把SpringCloud和SpringCloud Alibaba版本一起升上去,并且确认JDK 17的环境没问题,否则一堆隐性的不兼容会在某个奇怪的时刻突然冒出来,极其消磨信心。我说实话,新版本未必不好,但做项目追求的是可控和稳定,把坑留给那些想尝鲜的人吧。
最后说几句
做完这个项目,我最大的体会是:一个看似简单的业务,套上微服务架构后,复杂度是成倍上升的。但正是这种复杂度,逼着你去思考服务边界、数据一致性、调用链路上那些单机时代根本不会遇到的难题。如果你正在做这个项目,我的建议是:不要沉迷于"用最全的技术栈",先把核心业务跑通,再根据实际需要引入微服务组件。一个能运行的、逻辑自洽的系统,永远比一个技术堆砌但跑不起来的花架子值钱得多。另外,答辩时如果有人问"流量这么小为什么要用微服务",你完全可以诚实地说:这是为了检验自己在大规模分布式场景下的架构设计能力,顺便给未来的真实业务扩展留好空间——这句话,评委通常会很买账。