技术分享
如何采用一套程序代码,实现系统的“千人千面”
00 分钟
2023-5-25
2023-5-25
type
status
date
May 25, 2023 09:31 AM
slug
summary
tags
category
icon
password
日期
链接地址
Column
标签
本文主要涉及两种千人千面情况,第一种是面对用户来说,每个用户的系统页面都不同;第二种是系统运行层面,即一套系统代码需要针对于客户部署成千上百套的实例,需要保证每套程序主流程一致,但是业务出现差异时,程序要体现不同的运行流程。故下面的文章主要是针对这两种情况进行阐述。

应用场景

什么情况下需要进行千人千面管理呢,作者遇到的主要是下面几个场景:
  1. 应用软件:用户菜单和按钮级别的差异,这种情况一般是一套软件,需要面对不同的用户,用户有众多的角色之分。之后针对于不同角色需要赋予资源信息,而资源信息包括菜单资源、按钮资源、表格列等。该种方案较为简单,通过传统的角色管理方式即可完成,这里不做赘述;
  1. 社交平台或店铺系统:此种软件系统用户可以对自己的页面进行动态的编辑,如拖拽等效果,此种系统技术难度较大,后面会做些方案性的描述;
  1. 业务软件:如 Wms、Tms 等商业软件,该种软件需要售卖给不同的客户,软件本身是有固定的系统功能流程,但是每个客户都会有自己的定制化需求,故需要在现有系统功能流程中增加新需求的程序流程等,这种新增的程序流程需要做严格的审核,是否属于业务主流程程序中的新增功能,如果是,则增加到现有主流程程序中;如果不是,就需要采用额外的手段进行功能体现。商业软件可以针对于特定的客户做特定版本的管理,即该版本仅是此客户使用,商业软件都是一锥子买卖,这种方式是行得通的。但如果系统代码是由一个团队长期维护,如系统是公司内部单独使用,同时又为客户提供定制化的系统服务,所有系统部署、维护等都是该团队职责的情况下,保证程序版本的唯一性是十分重要的。如大型的物流公司,如顺丰、中通、圆通、京东等,会有很多类型的 Wms 仓库,如冷鲜仓、服装仓、食品仓、大件仓、医药仓等,因为仓的商品品类不同,故生产模式也不同,这些差异都必须在系统中得到体现。而此时就必须保证一套代码来支撑此种场景,此种情况就是此篇文章的重点,即一套程序代码,实现千客 千面。

方案描述 - 页面千人千面

下面首先说一下第二个场景的方案:
很早之前作者采用开源框架 Jetspeed 实现过用户随意定义页面的效果,即用户可以随意修改自己页面各个数据框的位置,效果如下图,图中的各个展示格都是可以拖拽的效果,即用户可以对自己的页面进行修改,可以删除某一个展示格,每个用户的页面都是可以自己修改的。
notion image
下图是对应的表结构,首先,一个页面是一个 Page 记录,每个 Page 里面包含了多个 Fragment,其中 Fragment 还可以包含多个 Fragment,每个 Fragment 都有对应的相对位置 (X,Y,Z) 坐标,并有 Row 和 Column 来管理 Fragment 在上级 Fragment 中的相对行列位置等。同时,还可以在 Page 表中指定 class,即 Page 对应的处理类,而在 Fragment 表中包含了 Fragment_String_Id,该字段是用来标识该 Fragment 的数据的处理类 Id,可以理解为 Spring 中的 Bean Id。该种方式是采用表结构来记录。
notion image
除了采用表来存储结构外,还支持文件的方式存储,即整个页面的布局采用 XML 等文件结构来标识,之后可以采用文件系统来存储,也可以在 MongoDb 这种数据库中进行存储。在 XML 文件中定义如表结构的结构属性,涵盖的属性和表结构一样,主要是存储方式的差异。
结构的定义中仅是包含每个页面展示格中的数据来源的标识 ID,通过该 ID 能够找到对应的数据获取类,具体如下图,在修改店铺页面时,在编辑页面时,可以选择每个页面展示格的数据源,即对展示格选取数据源后,该展示格的数据就交给该数据源来提供。
notion image
数据源选择如下图,针对于展示格选择好某一个数据源后,当页面展示该页面中的各个展示格时,就会从此数据源中获取数据,一般的数据源可以支持很多种形式,比如数据接口形式、数据插件形式、缓存接口、静态数据等。
notion image
下图是一种数据插件的方式,首先插件包的规范如下图,图中包含代码、样式文件、页面片段、插件定义文件等。
notion image
下图是插件定义的文件,即通过该方法来获取展示格的数据,可以通过数据方法类获取对应的数据,数据可以是数据实体对象,也可以是 HTML 片段(可以包含 JS、CSS、图片引用,有自己的页面格式)等。
notion image
下图是插件包定义的插件定义文件,通过解析该插件定义文件,可以识别出来插件提供的各个资源的位置等。
notion image
通过上面的方式,既可以实现用户页面的千人千面管理,同时数据的展示可以根据用户的运行时来获取差异性的数据。

方案描述 - 系统千客千面

下面来讨论一下如何实现系统的千客千面,针对于一套程序代码如果跑出千种运行效果,是较难维护的。因为可能涉及到一套部署环境需要支持多租户(多客户),这就需要在运行时根据租户信息来运行自己的特定业务等。
系统难点背景是:众多商家客户业务;商家可以提出自己的个性化业务生产流程;一个系统部署集群可以支撑一个商家,也可以支撑多个商家;程序开发人员追求的个性化生产流程的代码是否需要加入到成熟业务流程的代码中。
业务系统各个模块中的方法的基本流程如下图,包括请求参数校验、记录状态等校验、记录参数补齐、事务控制等。
请求参数校验:即对方法入参进行缺失(判空)性校验,保证此次请求后续处理过程中执行有效。
记录状态等校验:即对请求的参数记录(各种订单单据等)进行合法性判断,其中还包括防重校验等。
记录参数补齐:即对业务数据实体的转换、参数补齐(可能查询外部接口等)等。
事务控制:对请求的参数记录进行事务性数据库操作。
上面几个流程可以理解为系统代码规范,保证程序的处理过程中逻辑清晰,在增加新增业务时,按照此规范进行增加新业务功能,这样系统程序的维护就变得简单地多。注:系统业务不同,流程节点可能不一样,但是一般都能规范出像下图的程序节点,仅是节点个数多少的差异。
notion image
上面仅是在代码规范方面起到了一定的效果,对于成熟的业务流程可以很好的支持和维护。但是,面对不同的商家客户的特性化生产流程需求,传统的方法是程序员硬编码,在代码中增加了很多的 If 判断,经常出现的是 If(customer == "aaa"){......},即这个流程仅是对商家 aaa 有效果,仅是其的定制化业务流程。
对于此种情况,当商家客户越来越多时,程序的成熟业务流程将会面目全非,特别是当商家客户如果合作结束了,那该商家客户的特殊定制化业务流程的程序代码很难清理掉,因为涉及到的点非常多,研发人员也很难全面删除掉,且也不敢删,日积月累,这种问题对于后面系统的业务扩展埋下了巨雷(商业软件除外)。

系统千人千面 - 配置管理

下面说下第一种方案,如下图,按照规范业务程序流程定义标准接口,接口中包含的方法与规范流程中节点相匹配,这种方式是在现有的成熟业务主流程中增加新的个性化业务实现。如果一个新商家客户在现有成熟流程中不满足需求,即需要增加新的程序来支撑新业务,那就需要为这个新商家实现业务标准接口,对几个节点的方法中的全部或部分方法进行实现,之后通过配置的方法将新接口实现类添加到处理主流程中。
notion image
下图中是两个商家分别实现的自己特定的业务实现,在部署时可以采用配置文件对该实现类进行配置,规范流程中各个节点除了主流程实现外,还包括一个扩展点责任链入口,如果责任链中没有配置链节点,则直接向后执行,如果配置了链中节点,则按照责任链中节点依次触发链中节点。
notion image
下图是规范流程中第一个节点请求参数校验流程处理链,包含了三个商家的实现(即一套程序部署集群支撑 3 个商家的服务),当程序执行到该扩展点责任链时,会触发 aaa、bbb、ccc 的各个商家实现。如果某个商家不再合作了,需要将定制化的业务程序实现在主流程中去掉,直接将配置文件去掉即可,并将实现类删掉,保证程序的干净。
notion image

系统千人千面 - 脚本管理

下面说下第二种方案,如下图,采用对现有规范流程中各个节点进行程序埋点,程序埋点属于新颖名词,该种方式是在程序的代码中增加脚本引入的代码,下图中可以看到,和第一种方案类似,在 4 个规范流程节点中进行了 4 个脚本的埋点。
notion image
埋点程序如下面所示,maidian_order_check 该埋点脚本的标识,通过该标识能查到对应的脚本,并执行该脚本,该脚本是对某个商家的定制化流程。
脚本的实现有多种方式,可以采用 Groovy、Javac、Drools、Qlexpress 等。
下图是具体的埋点脚本样例,可以动态执行该动态脚本,而仅是在 aaa 和 bbb 商家的部署集群中才会出现该种脚本,其他的部署集群中没有该脚本定义,执行到上面代码后,获取到的脚本为空,无法得到结果。
notion image
那脚本如何管理呢?如果部署的集群达到上千时,管理非常复杂,就需要有管理的系统来管理该种脚本信息,具体如下图,分为脚本管理系统和各个业务系统集群。
脚本管理系统包括定义管理模块,包括新建定义等,可以对业务系统的实现进行定义,实现包括实体 (Domain)、服务 (Service) 等的定义。通过定义的组装可以对脚本进行新建、修改等,一个业务应用系统中某个模块中可以有多个脚本标识,脚本的实现是按业务集群的维度进行管理,即一个脚本标识,对于多个业务集群有多个定义。
当业务集群系统启动时,去脚本管理系统查询脚本,在执行具体业务时,执行动态控制的脚本即可。
notion image
通过上面的方案,可以很好地隔离业务系统的核心主流程和不同商家客户的定制化需求实现,对于商家客户的定制化程序实现,在业务系统中是存在的,但是在业务执行流程的代码中是可以通过脚本埋点的方式来做到隔离的。当商家客户合作结束后,该商家定制化的业务实现可以一次性删除掉,同时脚本可以清理掉,达到完全清除的效果。
该种方案在商业软件中一般是不会使用的,但是对于大型的互联网公司、大型物流公司等,具备一定的应用场景,特别是面对各种 To B 商家的系统,可以考虑该种方式进行管理。
各位读者可能暂时用不到该种方法,但是可以了解下思想,如果有类似场景,可以采用或变种一下使用。该篇文章就写到这里,如果大家有什么问题,可以留言,一起讨论。
下面是我写的一个样例,大家可以参考(脚本采用 Qlexpress 实现)。
首先对我们的应用能力进行声明,声明的方式采用的是注解的方式,便于在系统启动时让我们的 Qlexpress 上下文 Context 能识别出能力,并进行上下文管理;
Entity.java(实体声明注解)
notion image
Function.java(函数声明注解)
notion image
Bean.java(Spring 容器管理类注解)
notion image
BeanMethod.java(上面 Bean 声明类中的方法注解)
notion image
Order.java(实体注解实例)
notion image
OrderManager.java(Bean 和 BeanMethod 注解实例)
notion image
MarkOrderOperatorPluginDefine.java(Function 注解实例)
notion image
postProcessAfterInitialization 方法(在 Spring 容器启动时,会执行该方法,此时会处理该 Bean 的注解是否有上面自定义注解,如果存在,将其解析后,添加到 Qlexpress 上下文中)
notion image
Context.java(自定义 Qlexpress 上下文,可以对添加到上下文中的参数进行注解解析)
notion image
测试方法,即在添加了 Order 运行时参数后,执行对应的脚本 express_20190104_002 ,这样就可以在固定的程序中进行埋点,来动态执行脚本,实现千套部署,千套实现的效果。