推送开权引导的技术内幕

人人都讨厌代码腐化,人人都在腐化代码!本文介绍app消息推送开权提醒能力的服务端实现,并说明如何通过手搓一个简易的流程引擎来实现横向的业务场景隔离,纵向的业务流程编排,从而灵活支持业务需求,抑制代码腐化。

背景

消息推送是电商APP引流促活的重要手段,如何引导用户打开消息推送权限,接收APP的推送消息是各大电商APP都需要思考的问题。消息平台在过去的两个季度通过技术手段引导了近百万的用户开启推送权限:在用户关闭app消息推送权限情况下,在某些特定页面提示用户去打开权限。开权提示又可以分为弱提示,强提示。

弱提示如下,只展示开权提醒。

强提示如下,展示开权弹框。

产品需求

从产品侧来分析需求,我们希望能够有效引导用户开权,同时又不能过于频繁的打扰用户,引起用户反感。因此我们的提示要有针对性,且要能够控制提示频率,具体需求点如下:

  1. 不同页面提示文案不同,第一期迭代支持【消息】【商品】【订单】三个页面的开权提醒。每个页面的提示文案不同。如消息页面,提示:“开启消息通知,互动消息、优惠活动不错过~”,订单页面,提示:“开启消息通知,及时了解订单物流状态”等等。
  2. 提示文案支持动态配置,业务方需要修改提示文案时,可以通过修改配置快速生效。
  3. 提示次数要可控,不能频繁提示打扰用户。
    1. 一天最多一次
    2. 一周内最多n次,n可配置
    3. 个别页面可以永久提示,不同页面的频控次数可配置
    4. 用户主动关闭提示后一段时间内不能再提示

请求流程

  • 用户进入特定页面后,客户端判断若用户未打开推送权限,并且满足某些特定业务需求,比如在商品页面,若用户主动收藏过该商品,则认为需要给用户开权提示。
  • 此时客户端调用服务端接口来获取开权提示的文案。
  • 服务端接收到请求,会先进行防疲劳判断,比如本周该用户看到过开权提示的次数超过7次了,那就不再提示。当然不同页面防疲劳规则也不同,对于消息页面,业务就需要开权提示一直展示。

获取提示文案流程

  • 客户端获取开权提示后,会在业务页面展示出来,并且回调服务端,告诉服务端该用户在某个页面成功展示过一次提示。
  • 用户看到提示后,可以选择去打开推送权限,也可以忽略甚至关闭掉开权提示。这时候客户端需要回调给服务端,记录用户主动关闭一次提示。用户关闭超过3次,则半年内不再提示。当然不同的业务具体规则会有变化,且要支持可配置。

提示回调流程

服务端设计

只考虑单一页面的提示,那么方案很简单,流程如下

伪代码如下

public String getTip(String page){获取配置();if(单页面防疲劳()){return "";}获取提示文案();修改单页面防疲劳数据();修改全局防疲劳数据();}

如果需求变化不大,只有一个页面需要开权提示,那么以上方案完全可以满足。但是对于app开权提醒绝不可能只在一个页面提示,且不同页面的提示文案,频控方案都不相同,在这种需求背景下,以上的方案有哪些弊端呢?

  1. 代码耦合度高,易腐化

比如新增一个页面订单页面,他不需要全局防疲劳,怎么改呢?需要在主流程中增加订单页面相关的判断条件,伪代码如下。新增页面越来越多的情况下,代码会在一两个迭代周期内迅速腐化,连开发者自己都看不明白逻辑。

代码腐化的主要原因

缺乏设计。缺少必要的封装和抽象,代码逻辑完全是从业务逻辑直接翻译过来的,也就是直译型代码。这类代码直观直接,但是难以应对业务变化,一写出来就已经腐化了。

public String getTip(String page){获取配置();if(单页面防疲劳()){return "";}if(!订单页面()){if(全局防疲劳()){return "";}}获取提示文案();修改单页面防疲劳数据();if(!订单页面()){修改全局防疲劳数据();}}
  1. 测试成本高

还是以新增页面来看,对于测试来说,他不仅需要测试新页面的功能,而且还必须要回归老的消息页面的功能,因为新页面的改动影响了原有的业务流程。

我们都知道面向对象的开闭原则,对扩展开放,对修改关闭。对扩展开放好理解,因为需求一直在变,我们的代码必须能够灵活扩展以适应变化。对修改封闭,我们的扩展尽量不对现有的代码改动太大。为什么?因为修改意味着成本上升。成本不仅仅是代码实现,维护的成本,还包括测试的成本。

  1. 不易扩展,难以应对变化

考虑以下几个变化,a. 业务需要临时关闭某个页面的提示,而客户端又来不及发版, b. 调整某个页面的防疲劳次数,且不影响其他页面现有的防疲劳,c. 某个页面需要对防疲劳规则做ab实验,如何修改代码影响效率最高,影响最小?。对于目前的方案来说,这类需求都需要对主流程进行改动,必然可能会影响其他页面的功能,从而导致线上功能不稳定。

解决方案

上述几个问题相信大家都耳熟能详,根本原因还是代码设计过程中未充分解耦,随着迭代推进最终导致代码腐化,难以维护。下面分享一个比较有效的解耦设计:横向业务隔离,纵向流程编排。

场景隔离

我们用一个抽象的模型来描述这类需求。如下图,有A,B,C...等多个场景。每个场景都有step1, step2, step3,step4...等等多个步骤,不同场景可能有不同的步骤,同一个步骤在不同场景的实现可能也有细微差别。在未解耦前,我们的代码结构如下图所示,各个场景的代码通过判断语句耦合在一起。

伪代码如下

public void execute(String scene){if(!scene.equals("C")){step1();}if(scene.equals("A")){step2();}if(scene.equals("A")){//判断语句耦合不同场景的逻辑step3();}else if(scene.equals("B")){step3_EXT_B();}else if(scene.equals("C")){step3_EXT_C();}}

解耦后的效果如下,各场景在逻辑上是隔离的,每个场景有自己的业务流程。

这样解耦的好处如下

  1. 各个场景的业务流程在逻辑上是相互隔离的,不会因为修改某个场景逻辑导致所有其他场景的代码都受影响。
  2. 各场景之间的步骤可以抽象为action实例,相同action可以实现快速复用。
  3. 快速支持新场景,新场景只要提供新的场景流程实现即可,对已有场景无影响,且测试时无需回归全部场景,极大降低测试成本。

想法很好,那如何落地呢?答案是手搓流程引擎!

流程引擎

这里的流程引擎不是Activiti, JBPM这类重量级流程编排工具,相对我们的需求来说,使用这类工具有点大材小用,而且有过度设计的嫌疑,反而会大大增加开发成本。

代码腐化的另外两个原因

  1. 过度设计。多余的设计不仅不产生业务价值,而且无端提升理解维护成本,尤其需求之外的功能代码本身就是已腐化的死代码,针对这类代码要尽早做减法,应删尽删。
  2. 设计弃用。软件开发经常会碰到有些同学拿着电锯当菜刀使。比如已经引入ORM框架,他还是要手写SQL;比如有了AOP,他还是要到处嵌入重复代码。这类做法也会加剧代码腐化。

根据需求,我们只需要实现一个轻量级的流程编排工具,帮我们实现如下两点能力:

  1. 在横向上,通过不同的流程上下文装配各自的步骤节点action,把不同场景的逻辑隔离开来。
  2. 在纵向上,通过在流程上下文中的节点处理器handler,执行各流程的业务步骤。
  3. 提供上下文工厂类,根据场景code来提供不同场景的上下文实例。

整体结构如下

  1. 流程装配

新增场景时,提供场景上下文生成器实现类,在generate方法中装配流程步骤Action类实例。

@Servicepublic class AGenerator<P, R> implements IContextGenerator<P, R> {private final ProcessAwareContext processAwareContext;public AGenerator(ProcessAwareContext processAwareContext) {this.processAwareContext = processAwareContext;}@Overridepublic boolean check(P paramDTO) {return false;}@Overridepublic ProcessContext<P, R> generate(P para) throws Exception {ProcessContext<P, R> context = new ProcessContext<>(para);context.addAction(processAwareContext.getBean(Step1Action.class));context.addAction(processAwareContext.getBean(Step2Action.class));context.addAction(processAwareContext.getBean(Step3Action.class));return context;}}

场景流程的各个步骤用Action类来封装。通过步骤的封装,实现业务步骤在不同场景流程中的复用。Action类型又可以分为以下几类:

  • 网关节点:控制流程是否继续执行的节点,比如判断用户开权提示已经被频控拦截了,那么可以快速结束流程
  • 值节点:修改流程返回值的节点,即对流程返回值有影响的节点。
  • 空节点:对流程走向和返回值都无影响的节点,比如缓存数据,写数据库的节点。

上下文装配流程步骤时,根据步骤实例Action的类型,提供相应的执行器,在引擎执行时调用处理器来处理Action对应的业务操作。

每个节点类型都有对应的处理器。

  • 网关处理器:执行网关节点。网关节点的执行结果是一个布尔值,用于判断该流程是否结束,如果结束,则停止后续节点执行。目前的网关处理器设计比较轻量,后续业务有需要,可以支持复杂的网关设计,比如根据网关节点的返回值控制流程的执行路径等能力。
  • 值节点处理器:执行值节点。值节点的执行结果会被设置到上下文对象的result字段,从而设置整个流程的返回值。
  • 空节点处理器:执行空节点。这些节点不影响节点执行,也不影响流程结果,主要用于修改缓存,数据库等数据操作。
  1. 流程执行

执行场景流程时,从流程上下文工厂类获取流程上下文实例,提交给流程引擎执行。

@Overridepublic R execute(P paramDTO) {if (!validParam(paramDTO)) {return null;}try {ProcessContext<P, R> context = processFactory.get(paramDTO);ProcessEngine.execute(context);return context.getResult();} catch (Exception ex) {log.error("processExecuteFailed", ex);}return null;}

工厂类只有一个get方法,根据场景参数,获取场景对应的上下文生成器,由生成器动态装配出该场景对应的上下文对象。

 

@Service

public class ProcessFactory<P, R> {

List<IContextGenerator> generators;

public ProcessContext<P, R> get(P p){

try {

IContextGenerator generator = generators.stream().filter(ig -> ig.check(p)).findFirst().get();

if (generator != null) {

return generator.generate(p);

}

}catch (Exception ex){

}

return null;

}

}

引擎执行的逻辑很简单,从上下文对象中获取流程中的各个Action对象的处理器,依次执行处理器的handle方法,从而完成流程步骤的执行。

 

public static void execute(ProcessContext context) {

if (Collections.isEmpty(context.getHandlers())) {

return;

}

context.getHandlers().forEach(handler -> {

if (!context.isDone()) {//快速结束

((BaseHandler) handler).handle(context);

}

});

}

收益与展望

推送开权引导能力一期支持3个场景,对开权率有明显的提升效果,因此迅速吸引数十个不同场景接入。大多数场景的业务流程都是一样的,可以通过复用既有的流程上下文生成器,生成各自的流程实例来处理。对于有特殊防疲劳逻辑的场景,可以通过拼装各自的流程上下文来实现。比如

【消息中心】场景不需要全局防疲劳,亦即【消息中心】的展示次数不受全局其他场景展示次数影响,

【我的购买页】需要针对不同的ab实验支持不同的防疲劳逻辑

这些场景可以通过提供各自的上下文生成器的实现类来实现不同业务流程的编排,且新的流程不影响既有流程的执行逻辑。如此我们便实现了面向对象编程的开闭原则,既对新增的流程扩展开放,对已有的流程改变关闭,充分满足了目前多个业务场景的开权提示需求。

当然这个方案远非完美,比如新增业务流程时,我们依然需要修改代码,通过代码去编排业务流程,是否可以通过修改配置流实现流程编排,甚至通过在画布上拖拽节点来实现流程编排等等。这些就涉及到通用流程编排引擎的范畴了,有兴趣的同学可以参考activiti, JBPM这些通用流程引擎的实现,此处不做赘述。

总结

以上介绍了APP消息推送开权提示的背景和实现逻辑,并说明了服务端如何通过流程引擎实现对业务场景的隔离,从而达到降低维护、测试成本,抑制代码腐化的目的。事实上,业务场景隔离本身并不复杂,方法也很多,除了本文介绍的流程隔离外,还可以借助接口隔离,依赖包隔离,甚至微服务隔离等多种形式。核心问题在于如何预知业务的潜在变化,提前合理设计,而不要等变化发生后才去重构,事后重构往往意味着不重构

限于水平,文尽于此,欢迎大家批评指正。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/689659.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

曲线生成 | 图解B样条曲线生成原理(基本概念与节点生成算法)

目录 0 专栏介绍1 什么是B样条曲线&#xff1f;2 基函数的de Boor递推式3 B样条曲线基本概念图解4 节点生成公式 0 专栏介绍 &#x1f525;附C/Python/Matlab全套代码&#x1f525;课程设计、毕业设计、创新竞赛必备&#xff01;详细介绍全局规划(图搜索、采样法、智能算法等)…

2.Angular组件概述

组件 Angular 组件概述 组件是 Angular 应用的主要构造块。每个组件包括如下部分&#xff1a; 一个 HTML 模板&#xff0c;用于声明页面要渲染的内容一个用于定义行为的 TypeScript 类一个 CSS 选择器&#xff0c;用于定义组件在模板中的使用方式要应用在模板上的 CSS 样式&am…

java以及android类加载机制

类加载机制 一、Java类加载机制 java中&#xff0c;每一个类或者接口&#xff0c;在编译后&#xff0c;都会生成一个.class文件。 类加载机制指的是将这些.class文件中的二进制数据读入到内存中并对数据进行校验&#xff0c;解析和初始化。最终&#xff0c;每一个类都会在方…

OpenAI:Sora视频生成模型技术报告(中文)

概述 视频生成模型作为世界模拟器 我们探索视频数据生成模型的大规模训练。具体来说&#xff0c;我们在可变持续时间、分辨率和宽高比的视频和图像上联合训练文本条件扩散模型。我们利用transformer架构&#xff0c;在视频和图像潜在代码的时空补丁上运行。我们最大的模型Sor…

Springboot+vue的物流管理系统(有报告)。Javaee项目,springboot vue前后端分离项目

演示视频&#xff1a; Springbootvue的物流管理系统&#xff08;有报告&#xff09;。Javaee项目&#xff0c;springboot vue前后端分离项目 项目介绍&#xff1a; 本文设计了一个基于Springbootvue的前后端分离的物流管理系统&#xff0c;采用M&#xff08;model&#xff09;…

代码随想录算法训练营第50天(动态规划07 ● 70. 爬楼梯 (进阶) ● 322. 零钱兑换 ● 279.完全平方数

动态规划part07 70. 爬楼梯 &#xff08;进阶&#xff09;解题思路总结 322. 零钱兑换解题思路总结 279.完全平方数解题思路 70. 爬楼梯 &#xff08;进阶&#xff09; 这道题目 爬楼梯之前我们做过&#xff0c;这次再用完全背包的思路来分析一遍 文章讲解&#xff1a; 70. 爬…

用163邮箱或者outlook接收国科大邮箱的邮件

使用如图下路径&#xff0c;创建一个新的密码&#xff0c;用于在163大师邮箱或者outlook登录即可 如果不行&#xff0c;则需要手动配置邮箱服务器 参考网址&#xff1a;中国科学院邮件系统帮助中心

人工智能_普通服务器CPU_安装清华开源人工智能AI大模型ChatGlm-6B_001---人工智能工作笔记0096

使用centos安装,注意安装之前,保证系统可以联网,然后执行yum update 先去更新一下系统,可以省掉很多麻烦 20240219_150031 这里我们使用centos系统吧,使用习惯了. ChatGlm首先需要一台个人计算机,或者服务器, 要的算力,训练最多,微调次之,推理需要算力最少 其实很多都支持C…

SpringBoot的 8 个优点

目录 1、简化配置 2、快速开发 3、微服务支持 4、内嵌服务器 5、健康监测 6、热部署 7、自动化管理 8、社区支持和生态系统 SpringBoot 是一个基于 Spring 框架的快速开发框架&#xff0c;它通过提供一系列的自动配置、约定优于配置、快速集成等功能&#xff0c;简化了…

Hive切换引擎(MR、Tez、Spark)

Hive切换引擎(MR、Tez、Spark) 1. MapReduce计算引擎(默认) set hive.execution.enginemr;2. Tez引擎 set hive.execution.enginetez;1. Spark计算引擎 set hive.execution.enginespark;

探索LightGBM:类别特征与数据处理

导言 LightGBM是一种高效的梯度提升决策树算法&#xff0c;常用于分类和回归任务。在实际应用中&#xff0c;数据通常包含各种类型的特征&#xff0c;其中类别特征是一种常见的类型。本教程将详细介绍如何在Python中使用LightGBM处理类别特征和数据&#xff0c;包括数据预处理…

借助Aspose.BarCode条码控件,C# 中的文本转 QR 码生成器

二维码用于在较小的空间内存储大量数据。它们易于使用&#xff0c;可以通过智能手机或其他设备扫描来打开网站、观看视频或访问其他编码信息。在这篇博文中&#xff0c;我们将学习如何使用 C# 以编程方式生成基于文本的 QR 码。我们将提供分步指南和代码片段&#xff0c;帮助您…

嵌入式学习 Day20

一. 标准IO和文件IO的区别: 1.标准IO是库函数,是对系统调用的封装 2.文件IO是系统调用,是Linux内核中的函数接口 3.标准IO是有缓存的 4.文件IO是没有缓存的 IO: b c d - 标准IO l s p 二. 文件IO: 1.操作步骤&#xff1a; …

Java中基于Session登录验证

1. 基于Session的登录验证 基于Session的登录验证方式是最简单的一种登录校验方式。 为啥能用Session作为登录验证的一种方式&#xff0c;因为每个用户的请求都会有一个Session&#xff0c;这个对象是Servlet给我们创建的&#xff0c;不需要我们手动创建&#xff0c;并且这个…

Unity之XR Interaction Toolkit如何在VR中实现一个可以拖拽的UI

前言 普通的VR项目中,我们常见的UI都是一个3D的UI,放置在场景中的某个位置,方便我们使用射线点击。但是为了更好的体验,我们可能会有跟随头显的UI,或者可拖拽的UI,这样更方便用户去操作。 所以我们今天的需求就是:如何基于XR Interaction Toolkit 插件 在VR中使用手柄射…

zip解压缩

使用unzip库可以轻松解压zip文件&#xff0c;源码下载地址&#xff1a;http://www.codeproject.com/Articles/7530/Zip-Utils-clean-elegant-simple-C-Win #include <Windows.h> #include"unzip.h" SetCurrentDirectory("c:\\"); HZIP hz OpenZip(…

AlexNet的出现推动深度学习的巨大发展

尽管AlexNet&#xff08;2012&#xff09;的代码只比LeNet&#xff08;1998&#xff09;多出几行&#xff0c;但学术界花了很多年才接受深度学习这一概念&#xff0c;并应用其出色的实验结果。 AlexNet&#xff08;由Alex Krizhevsky、Ilya Sutskever和Geoffrey Hinton共同设计…

OpenAI 发布文生视频Sora大模型,一句话便可生成长达一分钟的视频

前几期的文章&#xff0c;我们介绍了很多文生视频的大模型&#xff0c;包括字节发布的magic video以及stable video diffusion等模型&#xff0c;都可以输入相关的文本生成对应场景的视频。 文生视频大模型必然会成为各个人工智能大厂竞争的核心领地&#xff0c;这不OpenAI刚刚…

代码随想录算法训练营第三十六天|435. 无重叠区间、763. 划分字母区间、56. 合并区间。

435. 无重叠区间 题目链接&#xff1a;无重叠区间 题目描述&#xff1a; 给定一个区间的集合 intervals &#xff0c;其中 intervals[i] [starti, endi] 。返回 需要移除区间的最小数量&#xff0c;使剩余区间互不重叠 。 解题思路&#xff1a; 本题和上一个射气球类似&#x…

什么是接口测试?怎么做接口测试?

&#x1f345; 视频学习&#xff1a;文末有免费的配套视频可观看 &#x1f345; 关注公众号【互联网杂货铺】&#xff0c;回复 1 &#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 首先&#xff0c;什么是接口呢&#xff1f; 接口一般来说有两种…