说实话,接到这个办公用品租赁管理系统需求的时候,甲方负责人只给了一句话:"我们要做一个微服务架构的系统,前后端分离,员工用小程序借办公用品。"至于微服务怎么拆、小程序做什么、管理端长什么样,全是开放的。这类"半命题作文"项目在真实工作中不算少见,而它恰恰最考验技术选型和架构落地的能力。
这篇文章我会完整复盘这套基于SpringBoot+Vue+SpringCloud的小程序办公用品租赁管理系统的设计思路与实操过程,从业务模块拆分讲到SpringCloud各组件的落地,再到前端双端(Vue管理端+微信小程序)的协同开发,最后把分布式事务、分布式锁、分布式ID这几个绕不开的坑也一并说清楚。如果你正准备上手微服务实战项目,或者接了类似的"系统开发"需求不知道从哪下手,这篇应该能给你一份可以直接参考的路线图。
1. 办公用品租赁的业务底色:既是电商系统,更是资产管理系统
1.1 "以租代购"模式里,系统到底要管什么
办公用品租赁和普通电商卖货有一个本质区别:电商卖的是"所有权转移",下单、支付、发货、收货就结束了;而租赁卖的是"使用权+服务",商品发出去了,东西还是公司的资产,要按月记租金,要追踪设备在哪、谁在用、什么时候到期,还要处理续租、归还、报损这一整条链路。
我梳理业务的时候,把核心角色分成三类:员工(承租方)、行政管理员(审批与资产管理方)、财务(账务与报表使用方)。员工的入口是微信小程序,管理员的入口是Vue搭建的PC管理后台,财务更多是查看报表和审批租金减免,也会用到PC端。这样一拆,系统的功能地图就清晰了:
- 员工端小程序:浏览资产、提交租赁申请、查看审批进度、确认收货、申请续租/归还、报修报损
- 管理端Vue:资产入库、库存管理、审批受理、出库/回收登记、租金计算、订单管理、数据报表
- 后端公共服务:用户认证、组织架构、消息通知、操作日志、文件上传
1.2 一条完整的租赁业务链路长什么样
我画业务时序的时候,把整个租赁过程拆成了十一个节点,这直接决定了后面的数据库设计和接口设计:
浏览资产列表(小程序)→ 提交租赁申请(生成订单,状态为待审批)→ 管理员审批(通过/驳回)→ 审批通过后锁定库存并出库 → 员工确认收货 → 使用中(按月计租)→ 到期前系统提醒 → 员工申请续租/归还 → 归还验收(完好/损坏)→ 入库/报损 → 订单归档。
这十一个节点不是一个服务能承包的。资产库存和订单状态是两套数据模型,审批流又要独立追踪每一条申请记录的操作人、审批意见和时间线。如果硬做单体,代码不是不能跑,但后续每加一个新资产类型、每改一条审批规则,都要动核心业务表,风险会越来越高。这也是甲方坚持微服务最根本的出发点。
1.3 微服务不是目的,隔离业务复杂度才是
我不止一次在项目里跟人说:微服务是手段,不是目的。办公用品租赁系统选择SpringCloud这套方案,核心理由有三条。
第一是业务域的独立性足够强。资产、订单、审批、通知各自有完整的数据流转闭环,拆开后服务之间只通过接口通信,一个域的变化不会直接拖垮另一个域。第二是团队协作的现实需要。这个项目前端有Vue管理端、微信小程序两条线,后端有多个人并行开发,微服务的代码仓库分离可以让每块业务的迭代节奏互相不干扰。第三是部署粒度的灵活性。比如月底财务要跑租金账单,报表服务压力大,可以单独把这个服务扩容出多个实例,而不必把整个系统都扩容一遍。
但我也得泼一盆冷水:如果团队只有两三个人,业务量一天撑死几十单,老老实实做单体+前后端分离就够了。微服务的注册中心、网关、分布式事务、链路追踪带来的运维复杂度,对小团队是实打实的负担。这个项目之所以上微服务,是因为甲方本身有多条业务线、未来要复用资产管理和订单能力,分布式架构是他们的战略需求,不是技术炫技。
2. 服务如何拆、数据库如何分:边界划分与核心表设计
2.1 按业务能力拆服务,而不是按页面拆
很多人第一次做微服务容易犯一个错:按"页面"或"功能菜单"去拆服务。比如"登录服务""资产列表服务""订单列表服务",结果就是服务数量失控,服务之间互相调来调去,一个流程要跨越五六个服务,性能和一致性双双崩盘。正确的方法是按业务能力(Domain Capability)划分,每个服务要有自己完整的业务闭环和数据主权。
我最终把后端分成六个服务,每个服务独立数据库,代码仓库也一一对应:
- lab-auth:认证与用户服务,管员工账号、微信登录、组织架构、角色权限
- lab-asset:资产服务,管办公用品的分类、规格、库存、资产实例、出入库记录、报损记录
- lab-order:订单服务,管租赁订单创建、状态流转、续租、归还、租金计算
- lab-approval:审批服务,管审批模板、审批节点、审批记录,独立出来是因为不同公司的审批流程差异特别大
- lab-notify:通知服务,对接微信小程序订阅消息、短信、站内信
- lab-report:报表服务,管月度租金报表、资产使用率、部门租赁排行
拆完之后有一个很直观的收益:审批规则再怎么改,资产服务和订单服务一行代码都不用动。
2.2 资产服务的两级模型:品类与实例
资产这块的数据模型我费了比较多心思。办公用品租赁有个特点:同一款型号的显示器可能入库了200台,这200台是"可替换"的资产实例,但每一台又有独立的资产编号、SN码、入库时间和当前状态。所以资产服务必须拆成"品类(Product)"和"资产实例(AssetInstance)"两级模型。
品类表记录基础信息:名称、规格参数、图片、月租金单价、押金金额、所属分类。资产实例表则记录每一件实体资产:资产编号(生成规则类似固定资产编号)、品牌型号快照、入库时间、当前状态(在库/预占/出租中/维修中/已报废)、当前持有人、所在的订单号。这样设计的好处是,员工在小程序上看到的是"品类"维度的商品卡片,但管理员在后台能看到每一台设备的去向,盘点的时候扫资产编号就能定位。
2.3 订单服务必须存"业务快照",不能只存关联ID
订单表的设计有一个容易被忽略的细节:一定要保存下单那一刻的商品快照、租金单价和押金规则,而不是只保存一个"资产ID"外键。为什么?因为资产的价格、租金标准可能会调整,如果员工下单时月租金是150元,一个月后管理员把价格调成了180元,订单如果只存了资产ID,到期结账的时候租金按什么算?必然扯皮。所以订单服务里我会冗余存一份"资产名称+品牌型号+月租单价+押金"等字段,业务发生时点是什么价格,订单就锁死什么价格。
订单状态我用一个状态字段贯穿,取值包括:待审批、已驳回、待出库、使用中、续租审批中、待归还、归还验收中、已完结、已报损。整个状态流转的控制逻辑放在订单服务里,配合后面会讲到的状态机来约束非法跳转。
2.4 拆分后的第一道坎:服务间怎么通信
服务拆了、库也拆了,接下来就是服务间通信。我的策略很简单:核心业务链路(创建订单、审批通过)用同步接口,用OpenFeign调用;非核心链路(审批通过后通知员工、订单到期提醒、报表数据汇总)用异步消息,先保证主流程的一致性。
这里有一个很容易踩的决策坑:有人在系统还没跑起来的时候就疯狂引入MQ(消息队列),把下单都改成异步,结果查数据查不到、本地调试一片混乱。我的经验是,微服务落地第一版尽量用同步调用把业务跑通,同步解决不了的问题(比如超时重试导致的重复下单、大批量通知)再引入异步。过度设计比不设计更容易让项目烂尾。
3. SpringCloud组件选型:Nacos、Gateway、Feign、Sentinel的搭配方案
3.1 版本选型:SpringBoot和SpringCloud必须按兼容矩阵走
SpringCloud是个全家桶,版本和SpringBoot严格对应,选错了就是启动直接报错或者各种NoClassDefFoundError。这个项目我用的是SpringBoot 2.7.x + SpringCloud 2021.0.x(对应的版本代号是Jubilee),搭配SpringCloud Alibaba 2021.0.1.0,这一套组合在2023年之前是非常成熟稳定的组合,网上资料多,出了问题也好搜。
有人可能会问,2024、2025年做新项目为什么不直接上SpringBoot 3.x?SpringCloud 2023.x也已经很成熟了。我当时的考虑是这个团队之前都是SpringBoot 2的老项目,团队成员对旧版本更熟,而且SpringBoot 2.7仍然在社区维护周期内。如果你是从零开始、团队没历史包袱,直接SpringBoot 3.x + SpringCloud 2023.x + JDK 21也行,但要注意Nacos等服务端版本的兼容性,别只升级客户端不升级Server。
3.2 注册中心与配置中心直接用Nacos,别再用Eureka
搜索"微服务SpringBoot+SpringCloud"的朋友,经常会看到老教程里推荐Eureka做注册中心,但我的建议是:新项目直接用Nacos,别再走Eureka了。原因很简单,Nacos把注册中心和配置中心合二为一,省掉一个Config Server组件,还内置了命名空间隔离多环境。我当时在项目里直接用Nacos管理了三个环境(dev、test、prod),通过命名空间(Namespace)隔开,每个环境的配置互不可见,这一点比Eureka+Config Server的组合省事太多。
服务端的部署用Docker跑一个单机Nacos即可,关键配置在客户端的bootstrap.yml里:
spring: application: name: lab-order-service cloud: nacos: discovery: server-addr: 192.168.1.100:8848 namespace: dev-namespace config: server-addr: 192.168.1.100:8848 namespace: dev-namespace file-extension: yaml shared-configs: ->spring: cloud: gateway: routes: - id: lab-auth uri: lb://lab-auth predicates: - Path=/api/auth/** filters: - StripPrefix=1 - id: lab-asset uri: lb://lab-asset predicates: - Path=/api/asset/** filters: - StripPrefix=1 - id: lab-order uri: lb://lab-order predicates: - Path=/api/order/** filters: - StripPrefix=1我在这里没有用全局过滤器做登录拦截,而是把登录鉴权逻辑放在一个GlobalFilter里,只校验白名单之外的请求是否携带合法Token。像小程序端的租用列表页、资产详情页这种公开数据接口,也加入了白名单,避免让前端每次请求都要带Token。这里有一个细节提醒:Gateway的全局过滤器里别做太多业务逻辑,否则网关会变成一个"上帝服务",拖垮整体性能。
3.4 OpenFeign做服务间调用,必须配置超时和链路传递
服务间调用我统一用OpenFeign,配合Nacos做服务发现。比如订单服务需要查询资产信息、扣减库存,就写一个FeignClient指向lab-asset服务:
@FeignClient(name = "lab-asset", fallback = AssetFeignFallback.class) public interface AssetFeignClient { @PostMapping("/internal/asset/checkAndDeduct") DeductResult checkAndDeduct(@RequestBody DeductRequest request); }这里必须补两个配置,不然线上会踩坑:一是Feign默认的超时时间很短,要调大连接超时和读取超时;二是要把TraceId(链路追踪ID)和用户身份从上游传递到下游。我是通过实现RequestInterceptor接口,在请求头里加上TraceId和UserInfo来实现的。这样一旦线上出问题,可以顺着TraceId把一次完整请求的调用链路串联起来。
FeignClient接口我会单独放在一个公共模块里,避免在消费方服务内部去写接口定义。这样服务提供方改了接口,调用方的编译期就能发现问题。
3.5 Sentinel兜底:限流降级不能等上线再补
很多项目在开发阶段根本不管限流降级,觉得那是运维的事,直到线上某个服务被大量请求打爆才追悔莫及。我在这个项目里引入Sentinel做了三层防护:网关层根据URL路径配置QPS限流,核心服务(订单、资产)配置并发线程数和熔断规则,Feign的降级实现则在调用失败时返回兜底数据。
比如资产服务的库存扣减接口,我给它的QPS阈值设为500,超过之后直接返回"系统繁忙,请稍后重试",而不是让请求继续打到数据库层把连接池耗尽。Sentinel的接入成本非常低,一个starter依赖加几行配置就能跑起来,但收益在线上高并发场景下是救命级别的。
4. 核心服务的关键实现:订单状态机、库存预占与双端协同
4.1 订单状态机的约束力:避免状态乱跳
订单的状态流转是整个系统最核心的规则。员工提交申请后订单是"待审批",管理员审批通过后变成"待出库",出库登记后变成"使用中",到期申请归还后变成"待归还",验收完成后变成"已完结"。这个流转如果不在代码层面约束住,靠开发人员"自觉"去写判断,早晚会出脏数据。
我写了一个简单的状态机组件,核心结构是"当前状态+事件→目标状态"的映射表:
public enum OrderStatus { PENDING_APPROVAL("待审批"), REJECTED("已驳回"), PENDING_SHIP("待出库"), IN_USE("使用中"), PENDING_RETURN("待归还"), RETURN_CHECK("归还验收中"), COMPLETED("已完结"), SCRAPPED("已报损"); private static final Map<OrderStatus, Set<OrderStatus>> TRANSITIONS = new EnumMap<>(OrderStatus.class); static { TRANSITIONS.put(PENDING_APPROVAL, EnumSet.of(REJECTED, PENDING_SHIP)); TRANSITIONS.put(PENDING_SHIP, EnumSet.of(IN_USE)); TRANSITIONS.put(IN_USE, EnumSet.of(PENDING_RETURN, SCRAPPED)); TRANSITIONS.put(PENDING_RETURN, EnumSet.of(RETURN_CHECK)); TRANSITIONS.put(RETURN_CHECK, EnumSet.of(COMPLETED, SCRAPPED, IN_USE)); } public boolean canTransitTo(OrderStatus target) { Set<OrderStatus> allowed = TRANSITIONS.get(this); return allowed != null && allowed.contains(target); } }所有订单状态变更的入口,都会先调用canTransitTo校验,不允许的状态迁移直接抛业务异常。这套设计特别适合跟后端的业务逻辑审计结合:每一次状态变更都记录操作人、变更时间、变更前后值,出问题能溯源。
4.2 库存预占:下单时不真正扣减,审批通过才扣
库存处理是租赁系统的高频并发点。我采用了"预占+扣减"两段式方案:员工提交租赁申请时,资产服务先进行"可用库存预占"(把数量从"可用"移到"预占"),审批通过后预占转为正式"出库扣减",审批驳回则释放预占额度。这样既能防止多个人同时抢同一批库存,又不会因为审批流程较长让库存一直被无效占用。
预占动作我用带条件的UPDATE语句实现,天然防止超卖:
UPDATE asset_stock SET pre_occupied_count = pre_occupied_count + #{count} WHERE product_id = #{productId} AND available_count >= #{count} AND is_deleted = 0如果更新返回的影响行数为0,说明库存不够,直接给前端返回"当前库存不足"的友好提示。这里比"先查询再更新"的方式安全很多,因为在并发条件下"查询后判断"的中间状态很容易被另外一个请求插进来,"判断时库存够、更新时库存已经被抢走"的情况在传统写法里很常见。一个UPDATE语句原子解决。
4.3 Vue管理端:页面结构与接口封装经验
Vue管理端服务于管理员,核心页面包括登录页、工作台、资产管理、订单管理、审批中心、用户管理、报表中心。路由设计上,我把布局拆成Layout组件嵌套子路由的经典结构,每个菜单对应一个懒加载的页面组件:
const routes = [ { path: '/admin', component: Layout, redirect: '/admin/dashboard', children: [ { path: 'asset', name: 'AssetManage', component: () => import('@/views/asset/AssetList.vue'), meta: { title: '资产管理', icon: 'box' } }, { path: 'order', name: 'OrderManage', component: () => import('@/views/order/OrderList.vue'), meta: { title: '订单管理', icon: 'file-text' } }, { path: 'approval', name: 'ApprovalCenter', component: () => import('@/views/approval/ApprovalList.vue'), meta: { title: '审批中心', icon: 'check-square' } } ] } ]API层我做了统一的axios封装,request拦截器里自动附带Token,response拦截器里统一处理HTTP 401跳转登录和业务错误码提示,这样每个页面的业务代码只需要关心成功数据,不用到处处理异常分支。UI组件库用了Element Plus,表格、弹窗、表单这类中后台组件开箱即用,开发效率提升明显。
4.4 小程序端:登录态与订阅消息的坑
小程序端我用的原生微信小程序 + Vant Weapp组件库,没有额外选uni-app或Taro。因为这个项目的定位很明确:就是服务企业内部员工的轻量工具型小程序,不追求跨端复用。如果是未来要做多端投放(支付宝小程序、抖音小程序),那得重新评估跨端框架。
小程序登录流程踩过一个经典的坑:wx.login返回的code只能使用一次,必须立刻发给后端,后端再调用微信接口换取openid和session_key。如果中间加了自己的逻辑导致code被重复使用,微信会直接报错。如果业务需要存储登录态,后端应该自己生成随机的accessToken返回给小程序,而不是把session_key直接暴露给前端。
小程序端的业务页面包括租赁商品列表、商品详情、提交租赁申请、订单列表、订单详情、消息通知页。提交租赁申请时会调起微信的手机号快速验证组件,直接拿到脱敏手机号作为联系方式,体验比手动输入好很多。申请提交成功后的审核进度,通过订阅消息推送给员工,这一步需要员工在下单时主动授权"允许审核结果通知"。
4.5 前后端对接规范:一个统一返回体解决80%的联调矛盾
前后端联调最容易吵架的地方就是接口格式不统一。这个项目从第一天起就定死了全局统一返回体:
public class Result<T> { private int code; // 0表示成功,非0表示业务失败 private String message; private T data; private long timestamp; }分页接口统一返回PageResult结构,包含total、records、current、size四个固定字段。所有接口的时间字段用标准字符串"yyyy-MM-dd HH:mm:ss"输出,避免前端解析时间戳踩时区坑。定好这套约定后,后端不需要在每个业务方法里写一堆包装代码,前端也不需要针对每个接口写不同的解析逻辑,联调时间至少省了一半。
5. 分布式场景的三个硬骨头:事务、锁、分布式ID
5.1 分布式事务:用Seata AT模式解决跨库事务
服务拆了之后,最让人头疼的就是事务一致性。比如"创建租赁订单"这个操作横跨订单服务和资产服务——订单服务要插入订单数据,资产服务要扣减库存预占。两个操作各在自己的数据库里,本地事务管不到对方,必须引入分布式事务方案。
我选了Seata的AT模式,这是目前Java微服务领域最成熟的方案之一,对业务代码的侵入性极小。项目里只需要引入依赖并在启动类上加一个全局事务注解:
@GlobalTransactional(name = "create-rent-order", rollbackFor = Exception.class) public Long createRentOrder(RentOrderCreateCommand command) { // 1. 调用资产服务,预占库存 assetFeignClient.checkAndDeduct(command.toDeductRequest()); // 2. 本地插入订单 orderMapper.insert(order); // 3. 写入审批申请 approvalFeignClient.createApproval(command.toApprovalRequest()); return order.getId(); }Seata AT模式的工作机制是:全局事务发起方记录undo_log,各参与方在本地事务执行的同时生成前置镜像和后置镜像,如果后续某一个分支失败,Seata会依据undo_log自动回滚所有已执行的操作。需要注意,AT模式要求每张参与分布式事务的业务表都建立undo_log表,数据库必须是MySQL 5.7以上且开启binlog。如果你的业务无法接受AT模式的性能开销,再考虑TCC模式,但那套需要自己写Confirm和Cancel方法,开发成本显著更高。
5.2 分布式锁:库存扣减和防重提交都要用到
很多人在单体项目里用synchronized锁库存,但服务一拆成多实例,synchronized就锁不住了。同一时刻可能有多个订单服务实例同时扣减同一批库存,本地锁根本拦不住。我的解决方案是基于Redis的分布式锁,直接使用Redisson框架,而不是手写SETNX。Redisson内部实现了看门狗机制:默认锁30秒自动过期,但业务还没执行完时,看门狗会自动续期,避免锁提前失效导致并发穿透。如果锁的持有者宕机了,看门狗也就停了,锁最长30秒自动释放,不会死锁。这个设计比"固定过期时间"可靠得多。
使用示例如下:
RLock lock = redissonClient.getLock("asset:deduct:" + productId); boolean locked = lock.tryLock(3, 30, TimeUnit.SECONDS); if (!locked) { throw new BizException("系统繁忙,请稍后重试"); } try { // 执行库存扣减逻辑 } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } }分布式锁有个使用上的边界要拎清楚:它解决的是"多实例并发互斥"问题,不是为了替代数据库的原子操作。我的做法是"分布式锁+条件UPDATE"双层防御,锁减少冲突,UPDATE条件保证极端情况下的最终一致性。
5.3 分布式ID:雪花算法为什么是默认选择
订单表的主键ID我用的是雪花算法(Snowflake)生成的分布式ID,配合MyBatis-Plus的ASSIGN_ID策略,不需要额外写代码。雪花ID是64位Long型,由"时间戳+机器ID+序列号"三个部分构成,单机每毫秒可以生成数千个不重复ID,全局唯一且趋势递增。为什么不继续用数据库自增主键?因为订单表分布在多个实例上,如果各自用自增ID,未来做分库分表或者数据汇总时必然撞ID,而雪花ID天然规避了这个冲突。
5.4 接口幂等:重复下单的隐形杀手
微服务架构下,Feign调用超时后会重试,小程序弱网环境下用户可能连点两次提交按钮,这些都是"重复创建订单"的经典来源。我在订单服务里引入了幂等表机制:创建订单时,前端会生成一个UUID作为幂等键放在请求头里,后端在处理前先查幂等表,如果这个UUID已经处理过就直接返回上一次的结果,不再重复创建订单。幂等表和订单表在同一个数据库里,用同一个本地事务保证原子性,这个方案实现简单、性能可靠,比纯Redis判断更稳妥。
6. 部署落地:多环境配置、容器化与小程序联调要点
6.1 多环境的配置管理:Nacos命名空间隔离
前面提过Nacos的namespace按环境隔离,这里具体展开一下。我在Nacos里创建了三个命名空间:dev、test、prod,每个命名空间里各自维护数据库连接、Redis地址、第三方密钥等配置。本地开发时,开发者可以通过JVM启动参数-Dspring.cloud.nacos.config.namespace=dev-namespace来指定连到哪个环境,不需要改代码。
这个设计被很多团队忽略,代码里写死一套dev的数据库地址,到了测试环境要改配置文件、上线又要改一遍,改来改去总有人提交错配置。Nacos命名空间隔离一次配置到位,后期几乎不用再操心环境配置错乱的问题,强烈推荐。
6.2 Docker Compose编排:一套脚本拉起全部依赖
服务都做成了独立的Spring Boot Jar包,我用Docker Compose统一编排,定义五个容器:Nacos服务端、MySQL、Redis、网关服务、各个微服务实例。每个微服务的Dockerfile很简单,基础镜像用eclipse-temurin:8-jdk,把Jar包拷贝进去,暴露指定端口。
FROM eclipse-temurin:8-jdk MAINTAINER lab-project WORKDIR /app COPY target/lab-order-service.jar /app/app.jar EXPOSE 8102 ENTRYPOINT ["java", "-jar", "/app/app.jar", "--spring.profiles.active=prod"]用Compose的好处是:新同事加入项目,不需要在本地装MySQL、Redis、Nacos,一条docker compose up -d就能把依赖全部拉起来。生产环境的容器编排,如果你所在的团队运维能力足够,可以考虑升级到Kubernetes,但这不是必选项,规模没到那个量级之前,Compose+单机Docker足够稳定。
6.3 小程序发布联调:合法域名和真机预览
小程序前后端联调有两个必须提前处理的细节。一是小程序请求的合法域名必须配置为HTTPS域名,且域名需要备案;本地开发阶段可以在开发者工具里勾选"不校验合法域名",但真机预览时如果不校验,手机端会直接请求失败。二是生产环境小程序必须走HTTPS,网关层我把HTTP请求统一转发到服务,小程序端请求的域名解析到Nginx或云负载均衡,SSL证书挂在入口处。
这两个问题如果在开发后期才暴露,改起来会非常被动,建议项目一开始就申请好域名并配好证书,联调期间直接用HTTPS地址,避免上线前临时换地址引发各种兼容问题。
7. 复盘与避坑:从开发到上线,我踩过的那些真实问题
7.1 坑一:Nacos服务端版本与客户端版本的兼容性
项目初期我本地直接用Nacos 2.2.0服务端,客户端用的是SpringCloud Alibaba 2021.0.1.0自带的,启动时报了一堆"caused by: java.lang.NoSuchMethodError"。查了半天发现是Nacos客户端和服务端gRPC协议版本不匹配。这类问题定位起来特别耗时间,最直接的规避方式:去看SpringCloud Alibaba官方给出的版本对应关系表,照着推荐版本走,不要自己组合最新版。
7.2 坑二:Feign调用的超时时间
Feign默认的连接超时是2秒、读取超时是5秒,而资产服务的库存预占接口在高峰期可能超过这个时间。我线上遇到过审批通过后自动扣库存的调用直接超时,但实际扣减操作在资产服务端已经执行成功了,这就导致订单服务这边抛了异常触发Seata回滚,但资产服务那边收到回滚指令时发现undo_log已经被消费了,报"回滚失败"。这个问题的根子不在Seata,而在超时设置不合理。核心接口的超时时间我统一调成了连接3秒、读取15秒,内部接口如果涉及批量操作还要再拉长。另外,Feign调用超时后不要盲目重试,要么配合幂等表做安全重试,要么直接失败让上游做补偿。
7.3 坑三:Vue动态路由与权限控制的取舍
管理端的权限一开始打算用动态路由,根据用户角色从后端拉取菜单再动态注册路由,这样不同角色看到不同的菜单。实际做下来发现,动态路由的方案在刷新页面时会白屏,需要把路由持久化到Vuex或localStorage里,处理起来比较绕。后来我简化成"静态路由+按钮级权限指令":所有页面路由注册好,后端返回的角色权限码,前端用指令控制按钮显隐,菜单则根据权限码filter显示。对于企业内部管理系统来说,这个方案完全够用,还省掉了动态路由的复杂性。
7.4 坑四:小程序上线前的一次"已用完的code"事故
联调阶段出现过一次登录失败率高的问题。原因是前端有人把"获取手机号"和"登录"两个流程放在了一起,wx.login生成的code先被后端换了一次openid,紧接着页面onLoad里保存的code又被另一个接口拿去换了一次,第二次必然失败。后来统一了登录流程:进入小程序时先静默登录换取accessToken,需要手机号时再单独调用一键登录组件,两者彻底解耦,问题消失。
7.5 如果再来一次,我会怎么做
做完这个项目再回头看,最大的感触是:微服务架构的难点从来不在SpringCloud配置,而在于把业务边界划清楚,把"数据归谁管"弄明白。如果业务规划阶段多花一周反复推演服务边界和核心表设计,后面开发阶段至少能省一个月。另外,如果你的团队对微服务不熟悉,我建议先用一个非核心的简单服务(比如通知服务)跑通Nacos+Gateway+Feign的完整链路,再逐步把核心服务迁过去。一把梭全部上微服务,坑会一起爆,到时候排查都无从下手。
这套系统最终稳定上线,支撑了企业内几百名员工的办公用品租赁流转。我个人在实际开发里最深刻的体会是:工具永远只是工具,真正让系统立住的,是对业务的一遍遍梳理和对细节的不妥协。今天写的这些内容,既是复盘,也算给后来者的一张地图。配置和代码片段可以直接抄,但业务边界一定要自己用心推演。