如何设计一个ToC的弹窗

本文主要分享了如何设计一个具有高可扩展性的弹窗功能。

本设计参考了优惠券功能的设计思路,有兴趣的朋友可以看看优惠券的分享: juejin.cn/post/722895...

一、需求介绍

假如你的项目需要实现以下弹窗,你会怎么去实现?

需求:

1.展示1张图片,点击图片后跳转到另外1个页面(支持H5、小程序、其他小程序),有个关闭按钮,点击关闭按钮停留在当前页面

2.该弹窗功能提供给运营通过管理后台进行配置

3.弹窗频率需要有所控制,比如1天1次,有优先级,比如运营配置了3个弹窗,不能同时弹,需要按优先级每次进入页面的时候才弹(参考某团的体验)

4.支持灵活配置弹窗条件,比如地域属性、平台属性、时间属性、指定某些用户、用户来源等

5.在配置弹窗过程中有些变量属性,需要运行时才能确定,比如跳转链接中增加当前登录用户ID或者token作为参数

二、数据库设计(存储设计好坏往往决定一个功能日后的可维护、可扩展)

1.错误的表设计(按需求一一列举各种条件作为表结构的字段)

id 有效期 图片url H5链接 小程序链接 频率控制 优先级 条件1 条件2 ... 条件n
1 2023-09-24 image.com/aa/bb.jpg /page/mini 1次/天 99 ios 深圳市

相信很多朋友都是这样设计弹窗表的,这种表设计非常大的问题就是每次需要增加1个新的弹窗条件的话就要新增1个字段,非常不利于功能扩展。

2.正确的表设计(抽象区分'配置数据'和'弹窗条件')

配置数据:使用1个字段,以json结构进行存储,可以做到灵活增加数据或改变数据,配置数据不涉及数据过滤,所以不涉及性能问题

id 有效期 优先级 背景图json
1 2023-09-24 99 {"imgUrl":"image.com/aa/bb.jpg",..."}

弹窗条件:由列式转化为行式存储,增加条件不需要增加字段,而是加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是固定字段)

js 复制代码
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}是一个动态参数,运行过程中替换掉:

js 复制代码
{
    "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次已经能满足需求,如有特殊需求也可以特殊设计

js 复制代码
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:github.com/897665787/s...

gitee:gitee.com/jq_di/sprin...

js 复制代码
springcloud-template
└── template-tool
     └──controller
          └── PopupController -- 最优弹窗查询API
     └── popup
          └── PopCondition -- 抽象定义使用条件
          └── PopParam -- 弹窗条件参数
          └── PopService -- 弹窗查询的核心代码
          └── ReplaceParam -- 参数替换接口
          └── condition -- 弹窗条件实现类(条件就新增到这下面)
               └── FrequencyCondition -- 弹窗频率
               └── PushAreaCondition -- 推送地区
               └── ... -- 其他弹窗条件
          └── param -- 参数替换实现类(复杂条件就新增到这下面)
               └──NewUserCouponReplaceParam  -- 新人券包
               └── ... -- 其他参数替换
相关推荐
IT_陈寒1 小时前
闭包陷阱让我加了两天班,JavaScript你真行
前端·人工智能·后端
易协同低代码2 小时前
通达OA核心类库TD类深度解析
后端
Gopher_HBo3 小时前
Go语言学习笔记(十八)Gin处理Session
后端
谭光志3 小时前
工具塞满上下文窗口怎么办?深度拆解 AI Agent Tool Search 按需加载实现原理
前端·后端·ai编程
她说..3 小时前
Java 默认值设置方式
java·开发语言·后端·springboot
foggyprojects4 小时前
从0开始,一句话启动AI DataAgent
后端·数据分析·ai编程
郡杰4 小时前
一些基础和问题解决
后端
陈随易4 小时前
前端项目部署只要30秒
前端·后端·程序员
YIAN4 小时前
从零手写文件读取 MCP 服务:一文吃透 Model Context Protocol 全链路通信原理
前端·后端·mcp
Imchendiana4 小时前
《狂人日记NO.9》— 前后端一把梭,我的全栈实录
前端·后端