车服务优惠券系统提供了一种可装配且通用优惠券核心框架的实现,快速搭建满足业务需要的优惠券模板功能。仅需开发一次自定义业务组件,可灵活装配多优惠券模板,发券用券验券接口无需二次开发,平行无缝对接第三方商家券,节省开发资源,减少开发成本,提高了运营效率。
2. 系统设计
2.1优惠券的生命周期
组件创建:定义表单元素,生成自定义组件,存入组件库中。
装配模板:运营按业务需求,选择组件,组合装配在一起,生成优惠券模板。
生成批次:选择模板,关联配置业务券,比如:加油券,充电券,洗车券,代步券等。模板组合装配在一起,生成优惠券批次。
投放活动:配置运营活动,关联批次券信息。可组合装配成券包,或单张券,投放客户端或H5页面。
兑换领取:发放形式,可分为3种,兑换码兑换,运营活动用户登录领券,运营后台定向发券,满足业务规则,比如新注册用户,业务新老用户,首单用户发券进行间接发券。
核销使用:用户购买商品下单,拉取可用券,下单锁定券,支付核销券。
取消订单还券:订单取消,返还用户券,券可再次使用。
券快过期提醒:推送提醒用户使用。
结算:生成优惠券,平台与商家承担比例,执行结算补贴。
2.2优惠券系统设计思维导图
图 2-3
2.3优惠券系统基本流程图示
组件创建 >> 装配模板 >> 装配批次 >> 装配活动 >> 发券 >> 核销券 >> 结算
图 2-4
2.4优惠券系统架构图示
图 2-5
3. 核心功能
3.1创建自定义组件
自定义组件:创建优惠券组件,生成优惠券模板必要条件,因为模板生成时,需要加载装配相关的表单组件。
创建组件所需元素信息为:组件表单名称,Placeholder文本信息,JS验证功能,数据源,组件类型(包含文本框,数值框,下拉列表框,多选列表框控件,联选日期控件,满减金额控件,数据列表模态选择框等常用组件),表达式关键词等。其中配置Js验证功能,需要预先开发JS验证插件,集成常用的验证插件库中,用于模板生成时自动检查表单中元素。其中配置表达式关键词,用于生成规则条件表达式。其中设置数据源,用于初始化组件下拉列表,数据可选模态框等。组件生成后,保存到组件库表中,提供给模板创建使用。
解决的问题:
自定义组件的实现,存入组件库中,提高了代码复用性,常用的组件,任何业务券都可以复用,避免多次开发,增加了业务的灵活性,对系统进行解耦,也可以分组开发,部署。后台优惠券模板可以自由组合,根据运营需求,快速自行装配。提高运营工作效率,支持多业务券场景配置。
优惠券自定义组件生成,方便快速构建业务优惠券模板,需求灵活实现。不管是优惠类型,还是业务需求方可变的条件,我们都能从设计层面去实现它。来节约开发运营成本。并且生成可配置的多规则条件表达式。下单使用时,一次验证,所有的条件是否可达,满足优惠券下单使用。使优惠券系统结构简洁清晰,同时将可变的部分,完全开放给使用者,提供了非常大的灵活性。
创建组件页面图示如下:
图:3-1-1
组件创建流程图示如下:
图 3-1-2
3.2创建自定义模板
自定义模板创建:由3.1中创建的组件,按业务需求,进行动态装配。优惠券模板可设置3种类型,分别为商家券模板,平台券模板,公共模板。用于批次生成时,对运营进行不同角色权限隔离使用。其中配置业务类型,用于客户端拉券时按业务隔离展示使用。配置满减券,立减券,折扣券,用于区分券的使用条件。模板可选组件分为两部分,固定区域,可变区域。固定区域为优惠券中的常用组件区域,系统自动加载固定类型组件信息,见图3-2-1所示,固定类型组件是每张优惠券共有的属性。比如:优惠券,面额,库存数量,使用时间,平台与商家承担比例,折扣,最高金额,使用跳转Url等。可变区域为运营活动用券时需要验证的组件区域,比如限制的商家,门店,满减条件等组件。若组件库中组件元素为非固定类型,系统会区分当前组件为可变组件,展示在选择组件区域,提供运营人员按业务券的需求,可从组件库中进行拖拉组件到指定可变区域中。最后模板生成,系统会保存模板库中,以供批次创建生成使用。至此,模板再次加载或编辑时,其中的业务部分,基础属性部分,可变部分,系统会根据所存的模板信息,关联到批次中,同时相应的加载出元素组件。
解决的问题:
运营人员可根据业务券需求,自定义装配优惠券模板。通过界面,进行拖拉组件,支持组件元素拖拉排序,快速生成优惠券模板,简洁快捷,动态装配。存入模板库中。提供运营创建批次时,多次复用,减少开发成本,提高工作效率。
创建模板页面图示如下:
图 3-2-1
模板流程图示:
图 3-2-2
3.3创建自定义批次券包
- 自定义批次:设定批次的通用属性,动态加载优惠券模板,自动校验表单控件三部分组成。
- 通用属性配置:发券的形式,包含直接领取券与兑换券码形式。限制领取次数,可限当天每人领取次数,或者批次总领取次数。领取人群的配置限制,包含业务新老用户限制,注册新老用户的限制。短信钉钉提醒功能配置,过期券预提配置,运营库存提醒配置。商家单位配置,财务PR单号配置等。预提醒功能技术实现,通过延迟队列,定时任务执行实现。
- 动态优惠券模板:
- 优惠券批次创建,加载可用的优惠券模板。运营可根据需求,在下拉列表中选择优惠券模板。此处优惠券模板,是根据可视化组件,分布式控件动态加载渲染呈现,去除了冗余的模板组件。可以无限制的加载需要配置的优惠券。比如:加油,充电,洗车。
- 自动校验证表单:
- 由于模板是动态加载的组件,多个优惠券模板,会有很多组件,组件类型不同,一个个写表单校验,会显得很繁琐,代码臃肿,维护复杂 。现对所有的元素组件,进行动态提取组件配置的JS,然后根据JS校验类库,自动加载表单验证,维护简单,代码简洁。
解决问题:
批次可以多张券模板组合,最终生成一个券包。支持不同业务券组合,相同业务券组合。支持批次复制,模板复用。发券可以按券包形式发,也可以按单张券针对性发放。批次创建,是由模板动态加载自定义组件完成。多个券模板,如果按传统模式开发,会很多组件元素要书写,js功能也要逐个书写,每当有新券需求,或者券限制条件升级变动,都会需要大量的控件开发,费时费力。本批次的设置,代码简洁,界面明了,所见即所得,无须过度开发,一劳永逸。
批次创建图示如下:
图 3-3-1
模板加载图示如下:
图 3-3-2
批次创建流程图:
图 3-3-3
3.4创建自定义活动
创建活动,可以配置整个业务的领券中心。聚合所有待领取的券,券包展示,同时提供对外输出领券接口关键配置,需要设置活动名称,活动的起始时间,活动KEY,互斥KEY,活动限制数量,活动的跳转链接,活动所投放平台渠道。可选择关联不同的券批次模板,通过活动KEY的领取维度,锁定不同的批次券包,锁定整个活动领取量,进行发券。通过互斥KEY的维度,锁定不同的活动,用户在活动A中可以领取哪些券,在活动B可以领取哪些券。通过活动中启动,停止进行领取功能的控制,将活动中配置信息进行保存,存入活动领取中心库中。
解决的问题:
适应于一个或多个活动场景,比如领券中心,领单张券,券包,一键批量领券,新老用户领券。通过活动Key,关联出整个活动配置的券包。通过活动控制券上线下线功能。对外输出领券接口,提供领券的活动标识随机码Key,保证业务的保密性、可控性、复用性。
活动创建图示3-4如下:
图 3-4-1
活动配置流程图示:
图 3-4-2
4. 技术难点
4.1技术如何实现自定义组件
图 4-1-1
创建模板时,是由多个组件装配完成。新创建模板时,如何动态渲染加载多个组件?编辑模板如何对组件进行赋值?模板组件从技术层面是由多个Freemark模板视图文件组合而成,比如图 4-1-1 中propType_text.ftl 文件,propType_List.ftl文件,propType_Select.ftl文件等,在创建模板中页面中,会根据组件池中,根据组件类型,通过<#include>进行文件加载传递数据完成。如下图 4-1-2所示。
图 4-1-2
模板视图文件需要Html+Freemark+Jquery 预先开发创建好。其中表单控件中name为组件生成时配置的name属性,文件中所有的${}语法属性,均对应组件生成时配置属性。比如4-1-3中:${prop.placeholder} 文本框描述提示信息,${prop.flagword} 是使用条件占位符,${prop.jsfunction} js自定义验证功能方法名,${prop.id} 组件Id,${prop.type} 组件类型,${prop.formnameExt} 模板Id+组件名称,${prop.datasource}等不再一一列举。
动态组件插件示例:举个文本框组件propType_Text.ftl 示例:后端读取模板中文本框属性值传递赋值。
图4-1-3
模板视图文件中 ${prop}是整个页面渲染控件主要引擎对象。又是后端模板通用对象模型,对应的后端开发类名为:CouponTempPropView ,如下面代码所示。后端接口通过模板Id,关联所有模板中的组件属性信息。然后封装成CouponTempPropView数据对象,进行初始化值赋值。若模板页面首次新页面加载,后端接口HandlAdapter处理适配器响应CouponTempPropView对象数据,通过ViewResolver视图解析器初始化组件页面表单,进行渲染呈现。若模板页面编辑时,需要对组件表单input元素进行赋值。由${prop}对象取CouponTempPropView对象数据中的${prop.formvalue},然后jquery查找当前prop.formvalue的input隐藏域控件的value值,该value是由多个属性值组合而成,根据当前组件,需要进行拆解,然后将值传递给name=${prop.formnameExt}的input控件,进行传递赋值。到此组件技术装配属性完成。创建批次时,选择优惠券模板,动态加载标签自定义组件呈现后台UI,如何加载模板的相关的组件信息呢,同样是引用模板视图文件完成的。技术运用了动静分离技术手段及组件化模板插件响应,运营人员选择优惠券业务模板,后端数据根据运营的选择模板,程序进行准备模板及控件所需数据。通过模板数据装载,初始化需要加载的模板中关联组件,前端页面不需要堆积实现未知的组件,仅根据后端数据,注入到模板关联组件的插件中,传递数据,需要呈现哪一块插件,就会渲染所关联的组件,完全数据来驱动实现组件的呈现,实现方式和现在的Vue想法一致。完全数据初始化控件,数据传递控件的实现。
编辑批次时,关联多个模板信息,后端如何进行动态传值:由批次信息,找到批次所关联的一个或多个模板信息,再由模板查找,加载出模板对应的组件信息,组件信息包含表单元素,通过批次信息表中的存储的字段值,转化成Key-value,key为表单元素,value为对应值。对模板组件属性结构类型进行一一映射,通过Action传递前端页面进行渲染优惠券模板组件视图。最终实现和创建模板时的加载视图文件一致,不再详细赘述。
4.2技术如何动态生成条件表达式
由于优惠券批次包含多个优惠券模板,每个优惠券模板的使用条件是随业务需求变化,所以我们如果存储这样动态可变的值,数据表中每次需升级扩展字段,同时又要升级系统的数据读取Mybatis文件,以及优惠券模板文件中限制条件规则等。显然这种方式比较笨拙,粗旷,又不利于优惠券系统的维护。我们自定义的规则条件表达式,应运而生。
► 什么是规则条件表达式:
它是多个模板组件使用限制内容组合一起,生成一种可变的自定义表达式。参考借鉴逆波兰表达式生成规则而设置。
► 如何生成:
创建优惠券批次时,通过模板Id关联出多个组件信息,封装到数据模型中ListpropDtoList,然后根据条件表达式方法,筛选出组件prop.getType()类型,比如数值类型,获取prop.getFlagword条件表达式关键字为coupon_cityid,prop.getFlagdesc=10010。拼装生成条件表达式为:(coupon_cityid=10010)。生成使用描述为:限城市为北京使用。其他类型视条件生成表达式。示例如下:
► 举车品券生成示例:
在创建优惠券时,业务限定多条件表达式随批次动态生成,举例:车品券表达式:[coupon_city]_in(111010,120000)&&(coupon_[order]>10&&coupon_[order]<100)&&[coupon_tag]in(123),表达式条件翻译为:优惠券满足城市北京或天津,订单金额满10元小于1000元,且优惠券品类标签为123的商品使用,提交保存,生成优惠券批次相关信息,生成验证优惠券表达式,存储数据表中,及缓存中。
验证券表达式条件,解析表达式代码如下:
用户下单使用券,系统校验券的使用条件,由条件表达式多条件验证,从用券接口取出订单号,订单金额,订单商品,商家信息,运费服务费等作为检验条件,将请求参数存入Map字典中,验证用户优惠券时,通过Map中对应名称,正则匹配组件中标签名、标签参数、操作符、操作数值进行标签名的替换,执行commonBLL.EvalExpr表达条件运算,结果满足条件,当前券可进行下单结算使用。否则,将不能使用。
举例,车品券表达式:
([coupon_city]in(10110)&&(coupon_[order]>10&&coupon_[order]<100)&&[coupon_tag]in(123)),[coupon_city]:10110, coupon_[order]:20 , [coupon_tag]:123,最终替换为(10110=10110)&& 20>10 && 20<100 && 123 =123)。
表达式执行完成后,生成订单,省去传统多个字段,循环迭代的逻辑验证,有些字段关联的属性可能比较多,验证的时候加载的时间会加长。有条件表达式,我们不用再做上面的多次验证。只需要根据订单参数,替换可变参数,执行表达式规则计算,完成所需要的条件验证。
4.3技术如何实现发券,限制超发并发
运营活动配置券包,客户端进行活动发券。用户登录领券,领券接口验证当前请求参数,首先请求拦截进行签名校验,网关授权认证,并发管控请求限流桶,分布式Redis锁。然后进行登录用户及风控校验,校验通过,读取活动关联的批次券包,读取用户领券信息存入Redis中HashTable,通过领券信息HashMap集合进行批次限制校验,包含新老用户,批次是否停止使用,批次每天领取量,总的领取限制量,活动是否停止校验,校验通过后,组合领券基本信息,存入jetcache+redis 二级缓存中,然后异步发送MQ。MQ消费者读取信息入库,同时创建多线程进行同步通知第三方券入库,领券完成。
解决的问题:
领取接口整个流程,增加分布式Redis锁+请求限流桶,比如秒杀券活动,短时间会增加百万+的流量,由于服务器数据承载响应能力,带宽流量有限,加入切面拦截请求,做容错处理,同时加入分布式Redis原子锁,进行并发控制,防止券因并发而超发的现象发生。同时验证领取限制条件,解决了并发领取,权限领取,异步领取,拦截了一些风险用户,实时反馈给用户领取结果信息。整个过程,从1级,2级缓存中读取更新数据,根据后台配置分层进行校验,极大提高了请求的响应速度,代码更加简洁,安全性、可扩展性、可维护性增强。
领券流程图示如下:
图 4-3-1
► 如何互斥?
一般在秒杀商品,直降商品,下单用券时,会进行拉取可用券列表时,对优惠券列表先进行按金额排序 ,轮循验证当前券金额,与秒杀金额,直降金额进行比较,取最优惠的价格给用户展示使用。节省运营投入成本。
有时平台券与商家券,如果都是平台采购券的运营场景,也需要用互斥规则。分别拉取平台券,与商家券可用券列表,再进行取最大的券进行比对。择取优惠力度最大券提供给用户使用。
► 如何叠加?
平台券与商家券,通常下单时,可以共同叠加使用。下单时,需要同时关联两张券的权益码。两张券同时变成核销状态。需要同时验证订单的总金额是否满足券使用条件,一般用在满减或凑单时使用。
► 如何平台券转发多家商家券?
车服务加油业务,油站属于不同供应商,运营采购多家供应商的券,由于采购库存有限。如何让用户在选择油站用券时,仅能给用户发一张商家券使用,不浪费采购库存,又能促进业务拉新。我们在系统引用了平台券关联多商家券的模式,最终落地使用商家券。
比如:加油优惠券模板,新增供应商选择组件,在运营后台创建加油券时候,选择关联三家供应商。用户通过领券活动得到一张平台券,这张平台券使用条件限制了三家供应商,用户下单用这张券时,根据油站所属供应商,平台券校验当前供应商可用条件,校验通过调用商家发券接口发对应供应商商家券,商家券核销完成后,更新当前平台券。到此完成平台券转发商家券的逻辑。配置如图所示:
图 4-3-2
4.4技术如何实现用券验券
用户请求下单,拉取用户可用券,从缓存中取出用户所有券,根据业务规则动态加载请求参数,比如加油券,验证订单金额,油品类型,供应商,油站等条件,利用键值对key-value的形式,存储请求参数,然后读取每一张券的规则条件表达式。比如(coupon_seller in(12,23)) &&([order]>=50)&&(oil_product=3),通过正则表达式,取表达式中标签占位符,由键值对中获取key值替换占位符,然后执行表达式检验,完成验券拉券过程。
用户选择券,进行创建订单时,再次请求验证当前券,判断表达式条件是否满足,券是否过期,是否被停用,检验领券账户,防止被篡改。然后订单表中锁定当前券码。
用户支付时,支付成功回调,再次验证当前券码是否可用,核销当前券码,券码表中同时更新订单号,使用时间,用券金额,券变更为核销状态。当支付失败,视订单未完成,订单将定时自动取消,券被归还。
解决的问题:
接口中根据业务不同,用券的场景不同,传递的参数通过键值对形式动态传递。用于替代券条件表达式占位符,快速验券。不用关心哪个业务,哪个条件进行固定判断。完全依赖Key-value中请求参数,动态验证,实现接口装配。核销的时候,订单与券进行双向绑定,增加安全性,可靠性。
用券流程图示如下:
图 4-4-1
5. 总结
本系统将优惠券业务进行了高度的提炼和抽象,将系统中的变与不变进行了完全隔离。
通过不断的开发积累组件化控件,使得运营可以更灵活的设计营销方案,极大的提高了代码复用和系统灵活性和效率,优惠券模板只需拖拽即可完成人机交互功能。
通过对优惠券系统模板的高度提炼抽象,使其与业务系统完全解耦,从上面的介绍中我们可以看到,业务系统只有一个动作,整理配置属性参数,获取结果,除此之外,业务系统不需要知道优惠券系统的任何其他细节,这就保证了两个系统完全分离,分开开发、独立部署,互不影响,提高开发效率和系统稳定性。
本系统具有明显的降低开发成本、提高开发效率、提升系统稳定性的优点。更重要的是提供完全开放的优惠券可定义的控件,自由可变优惠券模板生成,即使后期业务需求变更,也不会重新回归测试开发。只需要修改模板条件。即可0开发成本新增优惠券玩法,极大的满足了运营频繁多变的优惠券营销需求。
作者简介
吴彦斌
■ 曾任职于服务端研发部-服务端买用技术团队-用车组。
■ 高级研发工程师,2015年加入公司,主要从事优惠券系统的研发,订单系统的研发,物流管理系统的研发。
张东生
■ 服务端研发部-服务端买用技术团队-用车组。
■ 服务端买用技术团队用车组组长,负责之家用户用车场景的各用车服务权益产品及用车工具产品的研发和架构工作。