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




所有评论(0)