本文主要分享了如何设计一个具有高可扩展性的弹窗功能。
本设计参考了优惠券功能的设计思路,有兴趣的朋友可以看看优惠券的分享:如何设计一个可扩展的优惠券功能_java优惠券系统设计-CSDN博客
一、需求介绍
假如你的项目需要实现以下弹窗,你会怎么去实现?
需求:
1.展示1张图片,点击图片后跳转到另外1个页面(支持H5、小程序、其他小程序),有个关闭按钮,点击关闭按钮停留在当前页面
2.该弹窗功能提供给运营通过管理后台进行配置
3.弹窗频率需要有所控制,比如1天1次,有优先级,比如运营配置了3个弹窗,不能同时弹,需要按优先级每次进入页面的时候才弹(参考某团的体验)
4.支持灵活配置弹窗条件,比如地域属性、平台属性、时间属性、指定某些用户、用户来源等
5.在配置弹窗过程中有些变量属性,需要运行时才能确定,比如跳转链接中增加当前登录用户ID或者token作为参数
二、数据库设计(存储设计好坏往往决定一个功能日后的可维护、可扩展)
1.错误的表设计(按需求一一列举各种条件作为表结构的字段)
id | 有效期 | 图片url | H5链接 | 小程序链接 | 频率控制 | 优先级 | 条件1 | 条件2 | ... | 条件n |
1 | 2023-09-24 | https://image.com/aa/bb.jpg | /page/mini | 1次/天 | 99 | ios | 深圳市 |
相信很多朋友都是这样设计弹窗表的,这种表设计非常大的问题就是每次需要增加1个新的弹窗条件的话就要新增1个字段,非常不利于功能扩展。
2.正确的表设计(抽象区分‘配置数据’和‘弹窗条件’)
配置数据:使用1个字段,以json结构进行存储,可以做到灵活增加数据或改变数据,配置数据不涉及数据过滤,所以不涉及性能问题
id | 有效期 | 优先级 | 背景图json |
1 | 2023-09-24 | 99 | {"imgUrl":"https://image.com/aa/bb.jpg","type":"redirct_mini","value":"/page/mini"} |
弹窗条件:由列式转化为行式存储,增加条件不需要增加字段,而是加1行数据
id | popup_id | 条件 | 条件值 |
1 | 1 | 条件1 | 1次/天 |
2 | 1 | 条件2 | ios |
3 | 1 | 条件3 | 深圳市 |
核心设计思想:
1.把数据库表的列式条件转化为行式条件,比如 where a=? and b=?,转化为2行数据作为条件(当然转化后不再是sql能表达的)
2.运用设计模式(类似责任链模式)把1中的行式条件转化为一段代码,可简单可复杂,通过关联关系来做到可插拔
3.完整的表结构设计(remark、create_time、update_time是固定字段)
CREATE TABLE `mk_popup_pop_condition` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
`bean_name` varchar(32) NOT NULL DEFAULT '' COMMENT 'bean名称(PopCondition的实现类)',
`descrpition` varchar(255) DEFAULT NULL COMMENT '描述',
`remark` varchar(255) DEFAULT NULL COMMENT '备注',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `uniq_beanname` (`bean_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='弹窗条件';
CREATE TABLE `mk_popup` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
`begin_time` datetime NOT NULL COMMENT '有效期开始时间',
`end_time` datetime NOT NULL COMMENT '有效期结束时间',
`status` varchar(8) NOT NULL DEFAULT '' COMMENT '状态(off:下架,on:上架)',
`priority` int(11) NOT NULL DEFAULT '0' COMMENT '优先级(值越大,优先级越高)',
`title` varchar(32) NOT NULL DEFAULT '' COMMENT '标题',
`text` varchar(128) NOT NULL DEFAULT '' COMMENT '文案',
`bg_img` varchar(255) NOT NULL DEFAULT '' COMMENT '背景图json数据',
`close_btn` varchar(255) NOT NULL DEFAULT '' COMMENT '关闭按钮json数据',
`remark` varchar(255) DEFAULT NULL COMMENT '备注',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_begintime` (`begin_time`),
KEY `idx_endtime` (`end_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='弹窗';
CREATE TABLE `mk_popup_condition` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
`popup_id` int(11) NOT NULL COMMENT 'mk_popup.id',
`pop_condition` varchar(32) NOT NULL DEFAULT '' COMMENT '弹窗条件(bean名称,mk_popup_pop_condition.bean_name)',
`pop_condition_value` varchar(255) DEFAULT '' COMMENT '弹窗条件值',
`remark` varchar(255) DEFAULT NULL COMMENT '备注',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_popupid` (`popup_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='弹窗-弹窗条件';
CREATE TABLE `mk_popup_log` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
`business_type` varchar(64) NOT NULL DEFAULT '' COMMENT '业务类型(popup:mk_popup,user_popup:mk_user_popup)',
`business_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '业务ID',
`user_id` int(11) NOT NULL DEFAULT '0' COMMENT 'bu_user_info.id',
`deviceid` varchar(64) NOT NULL DEFAULT '' COMMENT '设备号',
`remark` varchar(255) DEFAULT NULL COMMENT '备注',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_userid` (`user_id`),
KEY `idx_deviceid` (`deviceid`),
KEY `idx_businessid_businesstype` (`business_id`,`business_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户弹窗记录';
表说明:
mk_popup_pop_condition:主要是记录有哪些弹窗条件,管理后台中配置弹窗时有用
mk_popup:弹窗配置,运营配置数据就往这里写数据
mk_popup_condition:某个弹窗的弹窗条件(1对多),条件组合配置就是利用该表
mk_popup_log:用户弹窗记录,用于控制弹窗频率
数据样例说明:
弹窗配置
弹窗条件
1) 弹窗1只有1个条件FrequencyCondition,可以理解为只控制弹窗频率为2次/天
2) 弹窗2有3个条件,需要符合场景为小程序首页,地域属性为深圳才弹,并且控制弹窗频率为1次/天
三、代码设计
1.抽象弹窗条件,定义一个接口,其子类对应mk_popup_pop_condition的数据
PopParam说明:
2.核心代码(PopService)中提供1个方法‘用户最优的弹窗’(此处代码过多,只贴核心部分,完整的可以通过gitee查看源码)
设计思想:
1.查询有效期内上架的所有弹窗(按优先级倒排,找到第1个就是最优的了),遍历每个弹窗的多个弹窗条件,判断有1个条件不满足就放弃该弹窗,进入下一个判断
2.运行时替换一些配置的参数,简单的如{userId},复杂的参数就实现ReplaceParam接口,下文有响应结果demo
tips:此处利用CompletableFuture优化性能,实现了多线程并发判断每个弹窗条件,只要有1个条件不满足就立马返回。
3.如何新增1种弹窗条件?
实现接口PopCondition,重写canPop方法,以下为‘场景’弹窗条件的代码
把Bean名称配置到弹窗条件表mk_popup_condition即可使用该条件,无需新增其他代码,也不改动主流程代码。
4.Controller固定入口
5.API定义
前端将弹窗封装成1个公共组件,只需请求固定接口/popup/best即可,在需要弹窗的页面直接引用该组件
入参:
字段 | 说明 |
sence | 前端每个页面定义1个唯一值 |
经纬度 | 非固定参数,可不传 |
地域信息 | 非固定参数,可不传 |
渠道信息 | 非固定参数,可不传 |
... | 非固定参数,可不传 |
响应:
字段 | 说明 |
model | 前端渲染模板定义,需要与前端协商,不同的值代表不同的渲染模板 |
title | 标题,本来想用于背景图的标题,但其实可以把标题设计到背景图中 |
text | 文案,同标题 |
bgImg | 背景图JSON,存储背景图位置的数据和动作 |
bgImg.imgUrl | 背景图 |
bgImg.type | 点击背景图做什么?(close:关闭,api:请求API,subscribe:订阅消息,redirect_http:跳转http链接,redirect_mini:跳转小程序链接,redirect_other_mini:跳转其他小程序链接) |
bgImg.value | 点击背景图做什么携带的信息 |
closeBtn | 关闭按钮JSON,存储关闭按钮位置的动作(一般都是关闭,也可以做一些骚操作,比如点击关闭按钮跳转到某个页面,或者没有关闭按钮,一定要点击背景图) |
popupLogId | 弹窗记录ID,本设计中查询到弹窗就会往mk_popup_log插入记录,而不是用户看到弹窗才记录,如果需要了解用户是否有真正看到弹窗,需要与前端协商一起实现,这个ID可以作为下个api的传参 |
示例:
https://domain.com/popup/best?sence=mini_home&cityCode=440300
四、需求升级
1.点击图片后不跳转到另外1个页面,而是实现特殊逻辑(如拉起小程序订阅消息)
解:需要前端协作实现,增加bgImg.type的定义值,数据通过bgImg.value返回给前端
2.点击图片后不跳转到另外1个页面,而是切换到下一张图片,点击后下一张图片后才跳转页面(比如发优惠券,第一张图片展示立即领取,点击后请求api进行发券,随后切换至去使用的图片,点击后再跳转到对应用券页面)
解:增加下一步操作设计,next结构与最外层的响应一样,类似递归结构
3.图片位置除了背景图,还需要展示其他的信息,比如新人礼包弹窗展示优惠券(需要实现ReplaceParam来完成)
1)实现接口ReplaceParam,bean名称为参数名,重写replace方法,以下为‘新人券包’的demo代码
2)配置demo(即bg_img字段值),{newUserCoupon}是一个动态参数,运行过程中替换掉:
{
"imgUrl": "{\"imgUrl\":\"https://img.domain.com/aaa.png\",\"newUserCoupon\":{newUserCoupon}}",
"type": "api",
"value": "https://domain.com/api/coupon/recive?id=8"
}
问:imgUrl为什么要配置为json字符串?
答:灵活运用固定字段响应数据给到前端,不同业务场景可定义不同的字段返回,只要跟前端约定好即可
数据响应示例:
4.在运行时给用户做一些弹窗埋点,比如完成某个事件后给用户埋一个弹窗,用户下次进入应用就给他弹
解:增加1张表mk_user_popup,字段与mk_popup差不多,但是不需要配置mk_popup_condition,因为这种弹窗只弹1次已经能满足需求,如有特殊需求也可以特殊设计
CREATE TABLE `mk_user_popup` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
`user_id` int(11) NOT NULL COMMENT 'bu_user_info.id',
`begin_time` datetime NOT NULL COMMENT '有效期开始时间',
`end_time` datetime NOT NULL COMMENT '有效期结束时间',
`status` varchar(8) NOT NULL DEFAULT '' COMMENT '状态(off:下架,on:上架)',
`priority` int(11) NOT NULL DEFAULT '0' COMMENT '优先级(值越大,优先级越高)',
`title` varchar(32) NOT NULL DEFAULT '' COMMENT '标题',
`text` varchar(128) NOT NULL DEFAULT '' COMMENT '文案',
`bg_img` varchar(255) NOT NULL DEFAULT '' COMMENT '背景图json数据',
`close_btn` varchar(255) NOT NULL DEFAULT '' COMMENT '关闭按钮json数据',
`remark` varchar(255) DEFAULT NULL COMMENT '备注',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_userid` (`user_id`),
KEY `idx_userid_begintime` (`user_id`,`begin_time`),
KEY `idx_userid_endtime` (`user_id`,`end_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户弹窗';
PopService中增加以下代码
tips:还有些步骤型的逻辑,每步骤都有1个弹窗,比如进行到第1步,给用户埋了1个弹窗,但用户一直没有进入应用,后面流转到第2步了,又埋了1个弹窗。此时如果用户进入应用,就会先后看到2个弹窗,其实第1步的弹窗对于用户来说已经没有意义了(造成不好的用户体验),直接给用户弹第2步的弹窗即可。所以可以设计取消弹窗的操作,在埋第2步弹窗的同时把第1步弹窗取消掉。
扩展思考:
1.弹窗做成不同的表现形式,比如提示框效果,过一段时间自动消失
2.弹窗自动关闭,比如倒计时n秒后自动关闭
怎么样?如果你觉得有用的话,还不快快收藏起来!!!
附:涉及的代码目录
github:https://github.com/897665787/springcloud-template
gitee:springcloud-template: 一个基于springcloud netflix微服务框架,记录了关于微服务开发的一些最佳应用,欢迎大家学习指导。
springcloud-template
└── template-tool
└──controller
└── PopupController -- 最优弹窗查询API
└── popup
└── PopCondition -- 抽象定义使用条件
└── PopParam -- 弹窗条件参数
└── PopService -- 弹窗查询的核心代码
└── ReplaceParam -- 参数替换接口
└── condition -- 弹窗条件实现类(条件就新增到这下面)
└── FrequencyCondition -- 弹窗频率
└── PushAreaCondition -- 推送地区
└── ... -- 其他弹窗条件
└── param -- 参数替换实现类(复杂条件就新增到这下面)
└──NewUserCouponReplaceParam -- 新人券包
└── ... -- 其他参数替换