做过最后悔的架构决策:把营销逻辑塞进了下单接口
做了好几年电商系统回过头看最后悔的一个架构决策是把营销活动的逻辑写在了订单系统的下单接口里。年轻时觉得这么做是自然而然的因为用户下单的时候总得知道这笔订单有没有优惠、优惠多少钱。去营销系统查一下活动信息在下单流程里处理一下代码量也不大很快就上线了。问题是一开始的确没有暴露出设计弊端来。等到营销活动从两三种变成七八种运营那边三天两头提新活动需求订单系统的发版节奏就完全被营销带着走了。每次营销需求变更订单系统就得跟着改、跟着测、跟着发。订单团队自己的迭代计划根本排不下去。当时的做法我们当时的系统里下单流程承担了太多不属于它的职责。打开订单生成服务的代码一个类1600多行注入的依赖有二十几个其中一大半是营销相关的服务。看一下这个类注入的依赖列表就能感受到问题AutowiredprivateFullOffOrderServicefullOffOrderService;AutowiredprivateSeckillOrderServiceseckillOrderService;AutowiredprivateCutpriceOrderServicecutpriceOrderService;AutowiredprivateTejiaServicetejiaService;AutowiredprivateCouponV3ServicecouponV3Service;满减服务、秒杀服务、砍价服务、特价服务、优惠券服务全部注入在订单生成服务里面。这还只是其中一部分后面又陆续加了限时购、抽奖、好物活动越加越多。失控真的失控了。这些服务不是独立的微服务它们就定义在订单工程内部跟订单服务打包部署在一起。营销活动的所有业务逻辑包括活动校验、价格计算、限购判断全部跑在订单进程里。下单方法是怎么膨胀到700行的最核心的问题是订单生成的入口方法。这个方法负责创建大订单主订单、中订单(按照供应商拆单的)、小订单(sku维度的)的数据结构计算最终支付金额。问题在于它不只是做订单相关的事情。每处理一个商品都要判断这个商品参与了哪种营销活动然后走不同的价格计算逻辑。方法内部有8个布尔标志位分别对应8种活动类型booleanisSeckillOrderseckillOrderService.isSeckillOrder(reqSku);booleanisCutpriceOrdercutpriceOrderService.isCutpriceOrder(reqSku);booleanisTejiaOrdertejiaService.isTejiaOrder(reqSku);booleanisFullOffOrderfullOffOrderService.isFullOffLittleOrder(reqSku);booleanisJobCenterOrderjobCenterOrderService.isJobCenterOrder(reqSku);然后是一个巨大的if-else链按照活动类型走不同的分支。秒杀订单取秒杀价砍价订单算砍价优惠满减订单做满减分摊特价订单设特价……每加一种新活动这个方法就多一个分支多几十行代码。满减的计算逻辑最复杂需要在活动维度做金额分摊上不封顶和封顶两种规则多梯度满减的匹配加起来将近100行。这100行是纯粹的营销计算逻辑跟订单创建没有任何关系但它就写在订单生成的核心方法里。代价是什么发版节奏完全被绑架。运营团队每周都在策划新的营销活动。这周上一个限时折扣下周搞一个新人专享价过几天又要调整满减的门槛。每次营销规则变了订单系统的代码就得改。改完要测试测试要把下单核心流程也回归一遍。订单团队自己的需求永远在排队总有营销需求在插队。测试范围被动扩大。改一个满减规则按理说只要测满减相关的逻辑。代码写在订单系统里发版就得把秒杀、砍价、优惠券这些也过一遍怕互相影响。改了10行营销代码回归测试覆盖整个下单流程。故障域扩大。有一次营销系统的一个活动配置出了问题某个活动的结束时间设成了过去的时间。订单系统在下单时会去校验活动时间发现活动已过期直接抛异常。从监控上看是下单接口大面积报错排查了一圈才发现根源是运营配错了一个活动时间。订单系统替营销系统背了锅。团队协作成本高。订单团队和营销团队需要频繁对齐接口。营销系统加了新活动订单这边要加对应的处理逻辑。两边发版要互相配合任何一边延期都影响另一边。两个团队的迭代节奏绑在一起谁都快不起来。另外是硬编码的问题除了依赖注入和方法膨胀还有一个细节很能说明问题。下单前的活动校验逻辑用的是硬编码的魔法数字switch(activityType){case0:break;// 正常商品case1:checkSecKillSku();break;// 秒杀case3:checkFullOffSku();break;// 满减case8:checkCutPriceSku();break;// 砍价case9:checkTejiaSku();break;// 特价}0、1、3、8、9这些数字代表不同的活动类型每新增一种活动类型就得加一个case。这种做法在小规模的时候看不出问题活动类型多了以后这段代码就成了一个谁都不想碰但又不得不改的地方。下单接口的方法签名也能看出耦合程度。微信下单接口有17个参数其中6个跟营销活动直接相关WxPrePayVOwxPrePay(intuserId,ListOrderReqSkuskuList,OrderReqCommonInfocommonInfo,...,IntegergrouponId,IntegergrouponActivityId,IntegergroupPrice,IntegergrouponType,MapInteger,OrderSkuActivityInfoResDTOsecKillActivityInfoMap);一个下单接口需要传入拼团ID、拼团活动ID、拼团价格、拼团类型、秒杀活动信息。这些参数跟订单创建本身没有关系它们的存在纯粹是因为订单系统要替营销系统干活。正确做法问题的根源是订单系统承担了不属于它的职责。它应该只关心「这笔订单优惠了多少钱」不应该关心「这笔优惠是怎么算出来的」。解法是引入一个独立的结算服务让它来做订单系统和营销系统之间的隔离层。结算服务负责对接营销系统查询当前生效的活动规则根据购物车或订单里的商品信息计算出命中了哪些优惠、对应优惠多少钱。计算结果以优惠明细的形式传给订单系统。订单系统拿到优惠明细后只需要把数据存储起来不再需要知道营销系统的任何细节。它不知道当前有什么活动在运行不知道满减门槛是多少不知道优惠券的使用规则。它只知道这笔订单参与了几个优惠一共减了多少钱。调用链变成用户提交订单 → 前端先调结算服务拿到优惠信息 → 把优惠信息连同订单数据一起传给订单系统 → 订单系统创建订单。引入结算服务后订单系统注入的依赖从二十几个降到个位数。那些满减服务、秒杀服务、砍价服务的引用全部从订单工程里移除转移到结算服务中去。下单方法从700行降到200行以内if-else分支全部消失因为活动类型的判断和价格计算已经不在订单侧了。发版节奏的变化最直观。订单系统回到它该有的状态接口契约定好之后除非订单业务模型本身发生变化否则不需要频繁发版。营销系统随便折腾新活动只要结算服务跟着改就行订单系统完全不受影响。维度耦合状态解耦后订单系统发版频率每周1~2次大部分是营销需求每月1~2次只跟订单业务相关下单方法行数700行8个活动分支200行以内无活动分支注入依赖数量20个一半以上是营销服务个位数全部是订单领域服务营销活动变更影响订单系统必须跟着改、跟着发只影响结算服务订单系统无感知故障隔离营销配置错误导致下单失败结算服务降级处理订单不受影响小结回过头来看当初把营销逻辑写进下单接口在业务早期营销活动只有两三种的时候这么做确实最快。直接调一下营销接口处理一下价格代码量小上线快。问题出在后面没有及时重构。活动类型从3种变成8种的过程中每次只加一点点代码每次都觉得「就加一个case而已」累积起来就是一个700行的方法和二十几个依赖。架构腐化往往不是一次错误决策造成的而是在一次次「就加一点点」的过程中慢慢形成的。每种新活动上线的时候加一个if分支的成本是最低的没有人会觉得需要为此做一次架构调整。等到发现问题的时候耦合已经深入骨髓。该不该引入结算服务这样的中间层判断标准不是代码复杂度而是看两边的变更频率差异。订单系统属于中后台下沉服务应该追求稳定。营销系统天然就是高频变化的运营每天都要调整活动策略。一个求稳一个求变这两个系统耦合在一起稳的那个必然被变的那个拖着走。把它们拆开让各自在自己的节奏里迭代才是长期可维护的方案。

相关新闻