点击阅读全文
// Trying to be the person you want to be.
// 时刻夯实基础
// 时刻对新技术保持热忱
// 个人博客 http://TIGERB.cn
// 轻量级PHP框架EasyPHP 作者 http://easy-php.tigerb.cn
// 电商设计手册|SkrShop 作者 https://github.com/skr-shop/m...
// 新的目标成为一名优秀的 Gopher
TIGERB 发布了文章 · 2020-07-01
经过两年的更新「SkrShop」已经构成了下面的架构:
图中紫色的内容就是本编文章的主要内容:营销体系的基础服务「优惠券服务」。但是呢,首先要说的是关于不断被催更的事。
关于催更?
我给出了如下解释:人逢假日懒🤷♀️(我没错😭)、工作紧、需要保证质量,就酱。但是我一定能保证的是一直会更新下去,希望得到大家理解。
关于下期内容?
之前在Github上的Issues大家一致想看关于订单相关的内容,所以更新完本期「优惠券」之后就开始了订单之旅。
Issues如下:
1. https://github.com/skr-shop/manuals/issues/25
2. https://github.com/skr-shop/manuals/issues/18
进入正题,营销体系的基础服务「优惠券服务」。通过如下问题来介绍优惠券:
对于获取优惠券的用户而言:关注的是优惠券的优惠能力,所以按优惠能力维度优惠券主要分为下面三类:
优惠能力维度 | 描述 |
---|---|
满减券 | 满多少金额(不含邮费)可以减多少金额 |
现金券 | 抵扣多少现金(无门槛) |
抵扣券 | 抵扣某Sku全部金额(一个数量) |
折扣券 | 打折 |
对于发放优惠券的运营人员而言:
一种是「固定有效期」,优惠券的生效时间戳和过期时间戳,在创建优惠券的时候已经确定。用户在任意时间领取该券,该券的有效时间都是之前设置的有效时间的开始结束时间。
另一种是「动态有效期」,创建优惠券设置的是有效时间段,比如7天有效时间、12小时有效时间等。这类优惠券以用户领取优惠券的时间为优惠券的有效时间的开始时间,以以用户领取优惠券的时间+有效时间为有效时间的结束时间。
有效期维度 | 优惠券类型 | 优惠券生效时间 | 优惠券失效时间 | 描述 |
---|---|---|---|---|
固定 | 固定有效期 | 优惠券类型被创建时已确定 | 优惠券类型被创建时已确定 | 无论用户什么时间领取该优惠券,优惠券生效的时间都是设置好的统一时间 |
动态 | 动态有效期 | 用户领取优惠券时,当前时间戳 | 用户领取优惠券时,当前时间戳 + N*24*60*60 | 优惠券类型被创建时,只确定了该优惠券的有效,例如6小时、7天、一个月 |
小结如下:
运营策略 | 描述 |
---|---|
(非)指定Sku | Sku券 |
(非)指定Spu | Spu券 |
(非)指定类别 | 类别券 |
指定店铺 | 店铺券 |
全场通用 | 平台券 |
适用终端(复选框) | 描述 |
---|---|
Android | 安卓端 |
iOS | iOS端 |
PC | 网页电脑端 |
Mobile | 网页手机端 |
微信端 | |
微信小程序 | 微信小程序 |
All | 以上所有 |
适用人群 | 描述 |
---|---|
白名单 | 测试用户 |
会员 | 会员专属 |
小结如下:
领取优惠券场景 | 描述 |
---|---|
活动页面 | 大促、节假日活动页面展示获取优惠券的按钮 |
游戏页面 | 通过游戏获取优惠券 |
店铺首页 | 店铺首页展示领券入口 |
商品详情 | 商品详情页面展示领券入口 |
积分中心 | 积分兑换优惠券 |
展示优惠券场景 | 描述 |
---|---|
活动页面 | 大促、节假日活动页面展示可以领取的优惠券 |
商品详情 | 商品详情页面展示可以领取、可以使用的优惠券列表 |
个人中心-我的优惠券 | 我的优惠券列表 |
订单结算页面 | 结算页面,适用该订单的优惠券列表以及推荐 |
积分中心 | 展示可以兑换的优惠券详情 |
选择优惠券场景 | 描述 |
---|---|
商品详情 | 商品详情页面展示该用户已有的,且适用于该商品的优惠券 |
订单结算页面-优惠券列表 | 选择可用优惠券结算 |
订单结算页面-输入优惠码 | 输入优惠码结算 |
返还优惠券场景 | 描述 |
---|---|
未支付订单取消 | 未支付的订单,用户主动取消返还优惠券,或超时关单返还优惠券 |
已支付订单全款取消 | 已支付的订单,订单部分退款不返还,当整个订单全部退款返还优惠券 |
场景示例 | 描述 |
---|---|
活动页领券 | 大促、节假日活动页面展示获取优惠券的按钮 |
游戏发券 | 游戏奖励 |
商品页领券 | - |
店铺页领券 | - |
购物返券 | 购买某个Sku,订单妥投后发放优惠券 |
新用户发券 | 新用户注册发放优惠券 |
积分兑券 | 积分换取优惠券 |
小结如下:
发放方式 | 描述 |
---|---|
同步发放 | 适用于用户点击领券等实时性要求较高的获取券场景 |
异步发放 | 适用于实时性要求不高的发放券场景,比如新用户注册发券等场景 |
发放能力 | 描述 |
---|---|
单张发放 | 指定一个优惠券类型ID,且指定一个UID只发一张该券 |
批量发放 | 指定一个优惠券类型ID,且指定一批UID,每个UID只发一张该券 |
发放类型 | 描述 |
---|---|
优惠券类型标识 | 通过该优惠券类型的身份标识发放,比如创建一个优惠券类型时会生成一个16位标识码,用户通过16位标识码 领取优惠券;这里不使用自增ID(避免对外泄露历史创建了的优惠券数量), |
优惠码code | 创建一个优惠券类型时,运营人员会给该券填写一个6位左右的Ascall码,比如SKR6a6 ,用户通过该码领取优惠券 |
撤销能力 | 描述 |
---|---|
单张撤销 | 指定一个优惠券类型ID,且指定一个UID只撤销一张该券 |
批量撤销 | 指定一个优惠券类型ID,且指定一批UID,每个UID撤销一张该券 |
用户优惠券列表 | 子类 | 描述 |
---|---|---|
全部 | - | 查询该用户所有的优惠券 |
可以使用 | 全部 | 查询该用户所有可以使用的优惠券 |
- | 适用于某个spu或sku | 查询该用户适用于某个spu或sku可以使用的优惠券 |
- | 适用于某个类别 | 查询该用户适用于某个类别可以使用的优惠券 |
- | 适用于某个店铺 | 查询该用户适用于某个店铺可以使用的优惠券 |
无效 | 全部 | 查询该用户所有无效的优惠券 |
- | 过期 | 查询该用户所有过期的优惠券 |
- | 失效 | 查询该用户所有失效的优惠券 |
订单结算页面推荐一张最适合该订单的优惠券
小结如下:
一旦有发生风险的可能则触发风控:
领取 | 描述 |
---|---|
设备ID | 每天领取某优惠券的个数限制 |
UID | 每天领取某优惠券的个数限制 |
IP | 每天领取某优惠券的个数限制 |
使用 | 描述 |
---|---|
设备ID | 每天使用某优惠券的个数限制 |
UID | 每天使用某优惠券的个数限制 |
IP | 每天使用某优惠券的个数限制 |
手机号 | 每天使用某优惠券的个数限制 |
邮编 | 比如注重邮编的海外地区,每天使用某优惠券的个数限制 |
依托用户历史订单数据,得到用户成功完成交易(比如成功妥投15天+)的比率,根据此比率对用户进行等级划分,高等级进入通行Unblock名单,低等级进入Block名单,根据不同用户级别设置限制策略。等其他大数据分析手段。
根据预算值设置发券总数阈值,当触发阈值时阻断并报警。
优惠券尽量不要支持虚拟商品以防止可能被利用的不法活动。
SkrShop历史分享:https://github.com/skr-shop/m...
赞 2 收藏 2 评论 0
TIGERB 发布了文章 · 2020-06-02
嗯,我的代码没有else
系列,一个设计模式业务真实使用的golang系列。
本系列主要分享,如何在我们的真实业务场景中使用设计模式。
本系列文章主要采用如下结构:
本文主要介绍「策略模式」如何在真实业务场景中使用。
「策略模式」比较简单,大家平常工作中应该经常使用到,所以本文作为复习,帮助大家温故知新。我们先来看下定义:
不同的算法按照统一的标准封装,客户端根据不同的场景,决策使用何种算法。
上面的概念的关键词:
概念很容易理解,不多说。
「策略模式」的优势:
每一行代码下面的十字路口
当代码的下一步面临选择的时候都可以使用「策略模式」,我们把不同选择的算法按照统一的标准封装,得到一类算法集的过程,就是实现「策略模式」的过程。
我们有哪些真实业务场景可以用「策略模式」呢?
比如:
本文以支付接口举例,说明「策略模式」的具体使用。
关于怎么用,完全可以生搬硬套我总结的使用设计模式的四个步骤:
我们以某团的订单支付页面为例,页面上的每一个支付选项都是一个支付策略。如下:
用户可以使用:
用户决定使用美团支付下的银行卡支付方式的参数
用户决定使用支付宝网页版支付方式的参数
注:不一定完全准确。
我们通过梳理的文本业务流程得到了如下的业务流程图:
注:流程不一定完全准确。
「策略模式」的核心是接口:
PaymentInterface
Pay(ctx *Context) error
当前支付方式的支付逻辑Refund(ctx *Context) error
当前支付方式的退款逻辑伪代码如下:
// 定义一个支付接口
- `PaymentInterface`
+ 抽象方法`Pay(ctx *Context) error`: 当前支付方式的支付逻辑
+ 抽象方法`Refund(ctx *Context) error`: 当前支付方式的退款逻辑
// 定义具体的支付方式 实现接口`PaymentInterface`
- 具体的微信支付方式`WechatPay`
+ 实现方法`Pay`: 支付逻辑
+ 实现方法`Refund`: 支付逻辑
- 具体的支付宝支付网页版方式`AliPayWap`
+ 实现方法`Pay`: 支付逻辑
+ 实现方法`Refund`: 支付逻辑
- 具体的支付宝支付网页版方式`BankPay`
+ 实现方法`Pay`: 支付逻辑
+ 实现方法`Refund`: 支付逻辑
// 客户端代码
通过接口参数pay_type的值判断是哪种支付方式策略
同时得到了我们的UML图:
package main
import (
"fmt"
"runtime"
)
//------------------------------------------------------------
//我的代码没有`else`系列
//策略模式
//@auhtor TIGERB<https://github.com/TIGERB>
//------------------------------------------------------------
const (
// ConstWechatPay 微信支付
ConstWechatPay = "wechat_pay"
// ConstAliPayWap 支付宝支付 网页版
ConstAliPayWap = "AliPayWapwap"
// ConstBankPay 银行卡支付
ConstBankPay = "quickbank"
)
// Context 上下文
type Context struct {
// 用户选择的支付方式
PayType string `json:"pay_type"`
}
// PaymentInterface 支付方式接口
type PaymentInterface interface {
Pay(ctx *Context) error // 支付
Refund(ctx *Context) error // 退款
}
// WechatPay 微信支付
type WechatPay struct {
}
// Pay 当前支付方式的支付逻辑
func (p *WechatPay) Pay(ctx *Context) (err error) {
// 当前策略的业务逻辑写这
fmt.Println(runFuncName(), "使用微信支付...")
return
}
// Refund 当前支付方式的支付逻辑
func (p *WechatPay) Refund(ctx *Context) (err error) {
// 当前策略的业务逻辑写这
fmt.Println(runFuncName(), "使用微信退款...")
return
}
// AliPayWap 支付宝网页版
type AliPayWap struct {
}
// Pay 当前支付方式的支付逻辑
func (p *AliPayWap) Pay(ctx *Context) (err error) {
// 当前策略的业务逻辑写这
fmt.Println(runFuncName(), "使用支付宝网页版支付...")
return
}
// Refund 当前支付方式的支付逻辑
func (p *AliPayWap) Refund(ctx *Context) (err error) {
// 当前策略的业务逻辑写这
fmt.Println(runFuncName(), "使用支付宝网页版退款...")
return
}
// BankPay 银行卡支付
type BankPay struct {
}
// Pay 当前支付方式的支付逻辑
func (p *BankPay) Pay(ctx *Context) (err error) {
// 当前策略的业务逻辑写这
fmt.Println(runFuncName(), "使用银行卡支付...")
return
}
// Refund 当前支付方式的支付逻辑
func (p *BankPay) Refund(ctx *Context) (err error) {
// 当前策略的业务逻辑写这
fmt.Println(runFuncName(), "使用银行卡退款...")
return
}
// 获取正在运行的函数名
func runFuncName() string {
pc := make([]uintptr, 1)
runtime.Callers(2, pc)
f := runtime.FuncForPC(pc[0])
return f.Name()
}
func main() {
// 相对于被调用的支付策略 这里就是支付策略的客户端
// 业务上下文
ctx := &Context{
PayType: "wechat_pay",
}
// 获取支付方式
var instance PaymentInterface
switch ctx.PayType {
case ConstWechatPay:
instance = &WechatPay{}
case ConstAliPayWap:
instance = &AliPayWap{}
case ConstBankPay:
instance = &BankPay{}
default:
panic("无效的支付方式")
}
// 支付
instance.Pay(ctx)
}
代码运行结果:
[Running] go run "../easy-tips/go/src/patterns/strategy/strategy.go"
main.(*WechatPay).Pay 使用微信支付...
最后总结下,「策略模式」抽象过程的核心是:
在每一行代码下面的十字路口
interface
interface
封装分支代码,得到每一个具体策略特别说明:
1. 我的代码没有`else`,只是一个在代码合理设计的情况下自然而然无限接近或者达到的结果,并不是一个硬性的目标,务必较真。
2. 本系列的一些设计模式的概念可能和原概念存在差异,因为会结合实际使用,取其精华,适当改变,灵活使用。
我的代码没有else系列 更多文章 点击此处查看
赞 12 收藏 11 评论 0
TIGERB 发布了文章 · 2020-06-02
嗯,我的代码没有else
系列,一个设计模式业务真实使用的golang系列。
本系列主要分享,如何在我们的真实业务场景中使用设计模式。
本系列文章主要采用如下结构:
本文主要介绍「状态模式」如何在真实业务场景中使用。
「状态模式」比较简单,就是算法的选取取决于于自己的内部状态。相较于「策略模式」算法的选取由用户决策变成内部状态决策,「策略模式」是用户(客户端)选择具体的算法,「状态模式」只是通过内部不同的状态选择具体的算法。
不同的算法按照统一的标准封装,根据不同的内部状态,决策使用何种算法
具体算法的选取是由内部状态决定的
我们有哪些真实业务场景可以用「状态模式」呢?
比如,发送短信接口、限流等等。
短信接口
限流
关于怎么用,完全可以生搬硬套我总结的使用设计模式的四个步骤:
先来看看一个短信验证码登录的界面。
可以得到:
我们通过梳理的文本业务流程得到了如下的业务流程图:
「状态模式」的核心是:
一个接口:
SmsServiceInterface
一个实体类:
StateManager
伪代码如下:
// 定义一个短信服务接口
- 接口`SmsServiceInterface`
+ 抽象方法`Send(ctx *Context) error`发送短信的抽象方法
// 定义具体的短信服务实体类 实现接口`SmsServiceInterface`
- 实体类`ServiceProviderAliyun`
+ 成员方法`Send(ctx *Context) error`具体的发送短信逻辑
- 实体类`ServiceProviderTencent`
+ 成员方法`Send(ctx *Context) error`具体的发送短信逻辑
- 实体类`ServiceProviderYunpian`
+ 成员方法`Send(ctx *Context) error`具体的发送短信逻辑
// 定义状态管理实体类`StateManager`
- 成员属性
+ `currentProviderType ProviderType`当前使用的服务提供商类型
+ `currentProvider SmsServiceInterface`当前使用的服务提供商实例
+ `setStateDuration time.Duration`更新状态时间间隔
- 成员方法
+ `initState(duration time.Duration)`初始化状态
+ `setState(t time.Time)`设置状态
同时得到了我们的UML图:
package main
//------------------------------------------------------------
//我的代码没有`else`系列
//状态模式
//@auhtor TIGERB<https://github.com/TIGERB>
//------------------------------------------------------------
import (
"fmt"
"math/rand"
"runtime"
"time"
)
// Context 上下文
type Context struct {
Tel string // 手机号
Text string // 短信内容
TemplateID string // 短信模板ID
}
// SmsServiceInterface 短信服务接口
type SmsServiceInterface interface {
Send(ctx *Context) error
}
// ServiceProviderAliyun 阿里云
type ServiceProviderAliyun struct {
}
// Send Send
func (s *ServiceProviderAliyun) Send(ctx *Context) error {
fmt.Println(runFuncName(), "【阿里云】短信发送成功,手机号:"+ctx.Tel)
return nil
}
// ServiceProviderTencent 腾讯云
type ServiceProviderTencent struct {
}
// Send Send
func (s *ServiceProviderTencent) Send(ctx *Context) error {
fmt.Println(runFuncName(), "【腾讯云】短信发送成功,手机号:"+ctx.Tel)
return nil
}
// ServiceProviderYunpian 云片
type ServiceProviderYunpian struct {
}
// Send Send
func (s *ServiceProviderYunpian) Send(ctx *Context) error {
fmt.Println(runFuncName(), "【云片】短信发送成功,手机号:"+ctx.Tel)
return nil
}
// 获取正在运行的函数名
func runFuncName() string {
pc := make([]uintptr, 1)
runtime.Callers(2, pc)
f := runtime.FuncForPC(pc[0])
return f.Name()
}
// ProviderType 短信服务提供商类型
type ProviderType string
const (
// ProviderTypeAliyun 阿里云
ProviderTypeAliyun ProviderType = "aliyun"
// ProviderTypeTencent 腾讯云
ProviderTypeTencent ProviderType = "tencent"
// ProviderTypeYunpian 云片
ProviderTypeYunpian ProviderType = "yunpian"
)
var (
// stateManagerInstance 当前使用的服务提供商实例
// 默认aliyun
stateManagerInstance *StateManager
)
// StateManager 状态管理
type StateManager struct {
// CurrentProviderType 当前使用的服务提供商类型
// 默认aliyun
currentProviderType ProviderType
// CurrentProvider 当前使用的服务提供商实例
// 默认aliyun
currentProvider SmsServiceInterface
// 更新状态时间间隔
setStateDuration time.Duration
}
// initState 初始化状态
func (m *StateManager) initState(duration time.Duration) {
// 初始化
m.setStateDuration = duration
m.setState(time.Now())
// 定时器更新状态
go func() {
for {
// 每一段时间后根据回调的发送成功率 计算得到当前应该使用的 厂商
select {
case t := <-time.NewTicker(m.setStateDuration).C:
m.setState(t)
}
}
}()
}
// setState 设置状态
// 根据短信云商回调的短信发送成功率 得到下阶段发送短信使用哪个厂商的服务
func (m *StateManager) setState(t time.Time) {
// 这里用随机模拟
ProviderTypeArray := [3]ProviderType{
ProviderTypeAliyun,
ProviderTypeTencent,
ProviderTypeYunpian,
}
m.currentProviderType = ProviderTypeArray[rand.Intn(len(ProviderTypeArray))]
switch m.currentProviderType {
case ProviderTypeAliyun:
m.currentProvider = &ServiceProviderAliyun{}
case ProviderTypeTencent:
m.currentProvider = &ServiceProviderTencent{}
case ProviderTypeYunpian:
m.currentProvider = &ServiceProviderYunpian{}
default:
panic("无效的短信服务商")
}
fmt.Printf("时间:%s| 变更短信发送厂商为: %s \n", t.Format("2006-01-02 15:04:05"), m.currentProviderType)
}
// getState 获取当前状态
func (m *StateManager) getState() SmsServiceInterface {
return m.currentProvider
}
// GetState 获取当前状态
func GetState() SmsServiceInterface {
return stateManagerInstance.getState()
}
func main() {
// 初始化状态管理
stateManagerInstance = &StateManager{}
stateManagerInstance.initState(300 * time.Millisecond)
// 模拟发送短信的接口
sendSms := func() {
// 发送短信
GetState().Send(&Context{
Tel: "+8613666666666",
Text: "3232",
TemplateID: "TYSHK_01",
})
}
// 模拟用户调用发送短信的接口
sendSms()
time.Sleep(1 * time.Second)
sendSms()
time.Sleep(1 * time.Second)
sendSms()
time.Sleep(1 * time.Second)
sendSms()
time.Sleep(1 * time.Second)
sendSms()
}
代码运行结果:
[Running] go run "./easy-tips/go/src/patterns/state/state.go"
时间:2020-05-30 18:02:37| 变更短信发送厂商为: yunpian
main.(*ServiceProviderYunpian).Send 【云片】短信发送成功,手机号:+8613666666666
时间:2020-05-30 18:02:37| 变更短信发送厂商为: aliyun
时间:2020-05-30 18:02:38| 变更短信发送厂商为: yunpian
时间:2020-05-30 18:02:38| 变更短信发送厂商为: yunpian
main.(*ServiceProviderYunpian).Send 【云片】短信发送成功,手机号:+8613666666666
时间:2020-05-30 18:02:38| 变更短信发送厂商为: tencent
时间:2020-05-30 18:02:39| 变更短信发送厂商为: aliyun
时间:2020-05-30 18:02:39| 变更短信发送厂商为: tencent
main.(*ServiceProviderTencent).Send 【腾讯云】短信发送成功,手机号:+8613666666666
时间:2020-05-30 18:02:39| 变更短信发送厂商为: yunpian
时间:2020-05-30 18:02:40| 变更短信发送厂商为: tencent
时间:2020-05-30 18:02:40| 变更短信发送厂商为: aliyun
main.(*ServiceProviderAliyun).Send 【阿里云】短信发送成功,手机号:+8613666666666
时间:2020-05-30 18:02:40| 变更短信发送厂商为: yunpian
时间:2020-05-30 18:02:40| 变更短信发送厂商为: tencent
时间:2020-05-30 18:02:41| 变更短信发送厂商为: aliyun
时间:2020-05-30 18:02:41| 变更短信发送厂商为: yunpian
main.(*ServiceProviderYunpian).Send 【云片】短信发送成功,手机号:+8613666666666
最后总结下,「状态模式」抽象过程的核心是:
interface
特别说明:
1. 我的代码没有`else`,只是一个在代码合理设计的情况下自然而然无限接近或者达到的结果,并不是一个硬性的目标,务必较真。
2. 本系列的一些设计模式的概念可能和原概念存在差异,因为会结合实际使用,取其精华,适当改变,灵活使用。
我的代码没有else系列 更多文章 点击此处查看
赞 1 收藏 1 评论 0
TIGERB 发布了文章 · 2020-05-07
本文结构很简单:
5张图送你5种秒杀系统,再加点骚操作,再顺带些点心里话🤷♀️。
实现原理: 通过redis原子操作减库存
图一
优点 | 缺点 |
---|---|
简单好用 | 考验redis服务能力 |
是否公平 |
---|
公平 |
先到先得 |
我们称这类秒杀系统为:
简单秒杀系统
如果刚开始QPS并不高,redis完全抗的下来的情况,完全可以依赖这个「简单秒杀系统」。
实现原理: 服务内存限流算法 + redis原子操作减库存
图二
优点 | 缺点 |
---|---|
简单好用 | - |
是否公平 |
---|
不是很公平 |
相对的先到先得 |
我们称这类秒杀系统为:
够用秒杀系统
实现原理: 服务本地内存原子操作减库存
服务本地内存的库存怎么来的?
活动开始前分配好每台机器的库存,推送到机器上。
图三
优点 | 缺点 |
---|---|
高性能 | 不支持动态伸缩容(活动进行期间),因为库存是活动开始前分配好的 |
释放redis压力 | - |
是否公平 |
---|
不是很公平 |
不是绝对的先到先得 |
我们称这类秒杀系统为:
预备库存秒杀系统
实现原理: 服务本地协程Coroutine定时redis原子操作减部分库存到本地内存 + 服务本地内存原子操作减库存
图四
优点 | 缺点 |
---|---|
高性能 | 支持动态伸缩容(活动进行期间) |
释放redis压力 | - |
具备通用性 | - |
是否公平 |
---|
不是很公平,但是好了点 |
几乎先到先得 |
我们称这类秒杀系统为:
实时预备库存秒杀系统
实现原理: 服务本地Goroutine定时同步是否售罄到本地内存 + 队列 + 排队成功轮训(或主动Push)结果
图五
优点 | 缺点 |
---|---|
高性能 | 开发成本高(需主动通知或轮训排队结果) |
真公平 | - |
具备通用性 | - |
是否公平 |
---|
很公平 |
绝对的先到先得 |
我们称这类秒杀系统为:
公平排队秒杀系统
上面的秒杀系统还不够完美吗?
答案:是的。
还有什么优化的空间?
答案:静态化获取秒杀活动信息的接口。
静态化是什么意思?
答案:比如获取秒杀活动信息是通过接口 https://seckill.skrshop.tech/v1/acticity/get
获取的。现在呢,我们需要通过https://static-api.skrshop.tech/seckill/v1/acticity/get
这个接口获取。有什么区别呢?看下面:
服务名 | 接口 | 数据存储位置 |
---|---|---|
秒杀服务 | https://seckill.skrshop.tech/... | 秒杀服务内存或redis等 |
接口静态化服务 | https://static-api.skrshop.te... | CDN、本地文件 |
以前是这样
变成了这样
结果:可以通过接口https://static-api.skrshop.tech/seckill/v1/acticity/get
就获取到了秒杀活动信息,流量都分摊到了cdn,秒杀服务自身没了这部分的负载。
小声点说:“秒杀结果我也敢推CDN😏😏😏。”
备注:
之后我们会分享`如何用Golang设计一个好用的「接口静态化服务」`。
上面我们得到了如下几类秒杀系统
秒杀系统 |
---|
简单秒杀系统 |
够用秒杀系统 |
预备库存秒杀系统 |
实时预备库存秒杀系统 |
公平排队秒杀系统 |
我想说的是里面没有最好的方案,也没有最坏的方案,只有适合你的。
拿先到先得
来说,一定要看你们的产品对外宣传,切勿上来就追逐绝对的先到先得。其实你看所有的方案,相对而言都是“先到先得”,比如,活动开始一个小时了你再来抢,那相对于准时的用户自然抢不过,对吧。
又如预备库存秒杀系统
,虽然不支持动态伸缩容。但是如果你的环境满足如下任意条件,就完全够用了。
秒杀场景结束时间之快,通常几秒就结束了,真实活动可能会发生如下情况:
所以:
合适好用就行,切勿过度设计。
这次算是把老本都吐露出来了,真是慌得一匹。
SkrShop历史分享:https://github.com/skr-shop/m...
赞 59 收藏 39 评论 2
TIGERB 发布了文章 · 2020-04-12
嗯,我的代码没有else
系列,一个设计模式业务真实使用的golang系列。
本系列主要分享,如何在我们的真实业务场景中使用设计模式。
本系列文章主要采用如下结构:
虽然本文的题目叫做“订阅通知”,但是呢,本文却主要介绍「观察者模式」如何在真实业务场景中使用。是不是有些不理解?解释下:
观察者观察被观察者,被观察者通知观察者
我们用“订阅通知”翻译下「观察者模式」的概念,结果:
“订阅者订阅主题,主题通知订阅者”
是不是容易理解多了,我们再来拆解下这句话,得到:
两个对象
两个动作
观察者模式的优势:
其实说白了,就是分布式架构中使用消息机制MQ解耦业务的优势,是不是这么一想很容易理解了。
所有发生变更,需要通知的业务场景
详细说:只要发生了某些变化,需要通知依赖了这些变化的具体事物的业务场景。
我们有哪些真实业务场景可以用「观察者模式」呢?
比如,订单逆向流,也就是订单成立之后的各种取消操作(本文不讨论售后),主要有如下取消类型:
订单取消类型 |
---|
未支付取消订单 |
超时关单 |
已支付取消订单 |
取消发货单 |
拒收 |
在触发这些取消操作都要进行各种各样的子操作,显而易见不同的取消操作所涉及的子操作是存在交集的。其次,已支付取消订单的子操作应该是所有订单取消类型最全的,其他类型的复用代码即可,除了分装成函数片段,还有什么更好的封装方式吗?答案:「观察者模式」。
接着我们来分析下订单逆向流业务中的变与不变:
变
不变
关于怎么用,完全可以生搬硬套我总结的使用设计模式的四个步骤:
注:本文于单体架构背景探讨业务的实现过程,简单容易理解。
第一步,梳理出所有存在的的逆向业务的子操作,如下:
所有子操作 |
---|
修改订单状态 |
记录订单状态变更日志 |
退优惠券 |
还优惠活动资格 |
还库存 |
还礼品卡 |
退钱包余额 |
修改发货单状态 |
记录发货单状态变更日志 |
生成退款单 |
生成发票-红票 |
发邮件 |
发短信 |
发微信消息 |
第二步,找到不同订单取消类型和这些子操作的关系,如下:
订单取消类型(“主题”)(被观察者) | 子操作(“订阅者”)(观察者) |
---|---|
取消未支付订单 | - |
- | 修改订单状态 |
- | 记录订单状态变更日志 |
- | 退优惠券 |
- | 还优惠活动资格 |
- | 还库存 |
超时关单 | - |
- | 修改订单状态 |
- | 记录订单状态变更日志 |
- | 退优惠券 |
- | 还优惠活动资格 |
- | 还库存 |
- | 发邮件 |
- | 发短信 |
- | 发微信消息 |
已支付取消订单(未生成发货单) | - |
- | 修改订单状态 |
- | 记录订单状态变更日志 |
- | 还优惠活动资格(看情况) |
- | 还库存 |
- | 还礼品卡 |
- | 退钱包余额 |
- | 生成退款单 |
- | 生成发票-红票 |
- | 发邮件 |
- | 发短信 |
- | 发微信消息 |
取消发货单(未发货) | - |
- | 修改订单状态 |
- | 记录订单状态变更日志 |
- | 修改发货单状态 |
- | 记录发货单状态变更日志 |
- | 还库存 |
- | 还礼品卡 |
- | 退钱包余额 |
- | 生成退款单 |
- | 生成发票-红票 |
- | 发邮件 |
- | 发短信 |
- | 发微信消息 |
拒收 | - |
- | 修改订单状态 |
- | 记录订单状态变更日志 |
- | 修改发货单状态 |
- | 记录发货单状态变更日志 |
- | 还库存 |
- | 还礼品卡 |
- | 退钱包余额 |
- | 生成退款单 |
- | 生成发票-红票 |
- | 发邮件 |
- | 发短信 |
- | 发微信消息 |
注:流程不一定完全准确、全面。
结论:
我们通过梳理的文本业务流程得到了如下的业务流程图:
注:本文于单体架构背景探讨业务的实现过程,简单容易理解。
「观察者模式」的核心是两个接口:
“主题”(被观察者)接口Observable
Attach
: 增加“订阅者”Detach
: 删除“订阅者”Notify
: 通知“订阅者”“订阅者”(观察者)接口ObserverInterface
Do
: 自身的业务订单逆向流的业务下,我们需要实现这两个接口:
Observable
ObserverInterface
伪代码如下:
// ------------这里实现一个具体的“主题”------------
具体订单取消的动作实现“主题”(被观察者)接口`Observable`。得到一个具体的“主题”:
- 订单取消的动作的“主题”结构体`ObservableConcrete`
+ 成员属性`observerList []ObserverInterface`:订阅者列表
+ 具体方法`Attach`: 增加子逻辑
+ 具体方法`Detach`: 删除子逻辑
+ 具体方法`Notify`: 通知子逻辑
// ------------这里实现所有具体的“订阅者”------------
子逻辑实现“订阅者”接口`ObserverInterface`:
- 具体“订阅者”也就是子逻辑`OrderStatus`
+ 实现方法`Do`: 修改订单状态
- 具体“订阅者”也就是子逻辑`OrderStatusLog`
+ 实现方法`Do`: 记录订单状态变更日志
- 具体“订阅者”也就是子逻辑`CouponRefund`
+ 实现方法`Do`: 退优惠券
- 具体“订阅者”也就是子逻辑`PromotionRefund`
+ 实现方法`Do`: 还优惠活动资格
- 具体“订阅者”也就是子逻辑`StockRefund`
+ 实现方法`Do`: 还库存
- 具体“订阅者”也就是子逻辑`GiftCardRefund`
+ 实现方法`Do`: 还礼品卡
- 具体“订阅者”也就是子逻辑`WalletRefund`
+ 实现方法`Do`: 退钱包余额
- 具体“订阅者”也就是子逻辑`DeliverBillStatus`
+ 实现方法`Do`: 修改发货单状态
- 具体“订阅者”也就是子逻辑`DeliverBillStatusLog`
+ 实现方法`Do`: 记录发货单状态变更日志
- 具体“订阅者”也就是子逻辑`Refund`
+ 实现方法`Do`: 生成退款单
- 具体“订阅者”也就是子逻辑`Invoice`
+ 实现方法`Do`: 生成发票-红票
- 具体“订阅者”也就是子逻辑`Email`
+ 实现方法`Do`: 发邮件
- 具体“订阅者”也就是子逻辑`Sms`
+ 实现方法`Do`: 发短信
- 具体“订阅者”也就是子逻辑`WechatNotify`
+ 实现方法`Do`: 发微信消息
同时得到了我们的UML图:
package main
//------------------------------------------------------------
//我的代码没有`else`系列
//观察者模式
//@auhtor TIGERB<https://github.com/TIGERB>
//------------------------------------------------------------
import (
"fmt"
"reflect"
"runtime"
)
// Observable 被观察者
type Observable interface {
Attach(observer ...ObserverInterface) Observable
Detach(observer ObserverInterface) Observable
Notify() error
}
// ObservableConcrete 一个具体的 订单状态变化的被观察者
type ObservableConcrete struct {
observerList []ObserverInterface
}
// Attach 注册观察者
// @param $observer ObserverInterface 观察者列表
func (o *ObservableConcrete) Attach(observer ...ObserverInterface) Observable {
o.observerList = append(o.observerList, observer...)
return o
}
// Detach 注销观察者
// @param $observer ObserverInterface 待注销的观察者
func (o *ObservableConcrete) Detach(observer ObserverInterface) Observable {
if len(o.observerList) == 0 {
return o
}
for k, observerItem := range o.observerList {
if observer == observerItem {
fmt.Println(runFuncName(), "注销:", reflect.TypeOf(observer))
o.observerList = append(o.observerList[:k], o.observerList[k+1:]...)
}
}
return o
}
// Notify 通知观察者
func (o *ObservableConcrete) Notify() (err error) {
// code ...
for _, observer := range o.observerList {
if err = observer.Do(o); err != nil {
return err
}
}
return nil
}
// ObserverInterface 定义一个观察者的接口
type ObserverInterface interface {
// 自身的业务
Do(o Observable) error
}
// OrderStatus 修改订单状态
type OrderStatus struct {
}
// Do 具体业务
func (observer *OrderStatus) Do(o Observable) (err error) {
// code...
fmt.Println(runFuncName(), "修改订单状态...")
return
}
// OrderStatusLog 记录订单状态变更日志
type OrderStatusLog struct {
}
// Do 具体业务
func (observer *OrderStatusLog) Do(o Observable) (err error) {
// code...
fmt.Println(runFuncName(), "记录订单状态变更日志...")
return
}
// CouponRefund 退优惠券
type CouponRefund struct {
}
// Do 具体业务
func (observer *CouponRefund) Do(o Observable) (err error) {
// code...
fmt.Println(runFuncName(), "退优惠券...")
return
}
// PromotionRefund 还优惠活动资格
type PromotionRefund struct {
}
// Do 具体业务
func (observer *PromotionRefund) Do(o Observable) (err error) {
// code...
fmt.Println(runFuncName(), "还优惠活动资格...")
return
}
// StockRefund 还库存
type StockRefund struct {
}
// Do 具体业务
func (observer *StockRefund) Do(o Observable) (err error) {
// code...
fmt.Println(runFuncName(), "还库存...")
return
}
// GiftCardRefund 还礼品卡
type GiftCardRefund struct {
}
// Do 具体业务
func (observer *GiftCardRefund) Do(o Observable) (err error) {
// code...
fmt.Println(runFuncName(), "还礼品卡...")
return
}
// WalletRefund 退钱包余额
type WalletRefund struct {
}
// Do 具体业务
func (observer *WalletRefund) Do(o Observable) (err error) {
// code...
fmt.Println(runFuncName(), "退钱包余额...")
return
}
// DeliverBillStatus 修改发货单状态
type DeliverBillStatus struct {
}
// Do 具体业务
func (observer *DeliverBillStatus) Do(o Observable) (err error) {
// code...
fmt.Println(runFuncName(), "修改发货单状态...")
return
}
// DeliverBillStatusLog 记录发货单状态变更日志
type DeliverBillStatusLog struct {
}
// Do 具体业务
func (observer *DeliverBillStatusLog) Do(o Observable) (err error) {
// code...
fmt.Println(runFuncName(), "记录发货单状态变更日志...")
return
}
// Refund 生成退款单
type Refund struct {
}
// Do 具体业务
func (observer *Refund) Do(o Observable) (err error) {
// code...
fmt.Println(runFuncName(), "生成退款单...")
return
}
// Invoice 生成发票-红票
type Invoice struct {
}
// Do 具体业务
func (observer *Invoice) Do(o Observable) (err error) {
// code...
fmt.Println(runFuncName(), "生成发票-红票...")
return
}
// Email 发邮件
type Email struct {
}
// Do 具体业务
func (observer *Email) Do(o Observable) (err error) {
// code...
fmt.Println(runFuncName(), "发邮件...")
return
}
// Sms 发短信
type Sms struct {
}
// Do 具体业务
func (observer *Sms) Do(o Observable) (err error) {
// code...
fmt.Println(runFuncName(), "发短信...")
return
}
// WechatNotify 发微信消息
type WechatNotify struct {
}
// Do 具体业务
func (observer *WechatNotify) Do(o Observable) (err error) {
// code...
fmt.Println(runFuncName(), "发微信消息...")
return
}
// 客户端调用
func main() {
// 创建 未支付取消订单 “主题”
fmt.Println("----------------------- 未支付取消订单 “主题”")
orderUnPaidCancelSubject := &ObservableConcrete{}
orderUnPaidCancelSubject.Attach(
&OrderStatus{},
&OrderStatusLog{},
&CouponRefund{},
&PromotionRefund{},
&StockRefund{},
)
orderUnPaidCancelSubject.Notify()
// 创建 超时关单 “主题”
fmt.Println("----------------------- 超时关单 “主题”")
orderOverTimeSubject := &ObservableConcrete{}
orderOverTimeSubject.Attach(
&OrderStatus{},
&OrderStatusLog{},
&CouponRefund{},
&PromotionRefund{},
&StockRefund{},
&Email{},
&Sms{},
&WechatNotify{},
)
orderOverTimeSubject.Notify()
// 创建 已支付取消订单 “主题”
fmt.Println("----------------------- 已支付取消订单 “主题”")
orderPaidCancelSubject := &ObservableConcrete{}
orderPaidCancelSubject.Attach(
&OrderStatus{},
&OrderStatusLog{},
&CouponRefund{},
&PromotionRefund{},
&StockRefund{},
&GiftCardRefund{},
&WalletRefund{},
&Refund{},
&Invoice{},
&Email{},
&Sms{},
&WechatNotify{},
)
orderPaidCancelSubject.Notify()
// 创建 取消发货单 “主题”
fmt.Println("----------------------- 取消发货单 “主题”")
deliverBillCancelSubject := &ObservableConcrete{}
deliverBillCancelSubject.Attach(
&OrderStatus{},
&OrderStatusLog{},
&DeliverBillStatus{},
&DeliverBillStatusLog{},
&StockRefund{},
&GiftCardRefund{},
&WalletRefund{},
&Refund{},
&Invoice{},
&Email{},
&Sms{},
&WechatNotify{},
)
deliverBillCancelSubject.Notify()
// 创建 拒收 “主题”
fmt.Println("----------------------- 拒收 “主题”")
deliverBillRejectSubject := &ObservableConcrete{}
deliverBillRejectSubject.Attach(
&OrderStatus{},
&OrderStatusLog{},
&DeliverBillStatus{},
&DeliverBillStatusLog{},
&StockRefund{},
&GiftCardRefund{},
&WalletRefund{},
&Refund{},
&Invoice{},
&Email{},
&Sms{},
&WechatNotify{},
)
deliverBillRejectSubject.Notify()
// 未来可以快速的根据业务的变化 创建新的主题 从而快速构建新的业务接口
fmt.Println("----------------------- 未来的扩展...")
}
// 获取正在运行的函数名
func runFuncName() string {
pc := make([]uintptr, 1)
runtime.Callers(2, pc)
f := runtime.FuncForPC(pc[0])
return f.Name()
}
代码运行结果:
[Running] go run "../easy-tips/go/src/patterns/observer/observer.go"
----------------------- 未支付取消订单 “主题”
main.(*OrderStatus).Do 修改订单状态...
main.(*OrderStatusLog).Do 记录订单状态变更日志...
main.(*CouponRefund).Do 退优惠券...
main.(*PromotionRefund).Do 还优惠活动资格...
main.(*StockRefund).Do 还库存...
----------------------- 超时关单 “主题”
main.(*OrderStatus).Do 修改订单状态...
main.(*OrderStatusLog).Do 记录订单状态变更日志...
main.(*CouponRefund).Do 退优惠券...
main.(*PromotionRefund).Do 还优惠活动资格...
main.(*StockRefund).Do 还库存...
main.(*Email).Do 发邮件...
main.(*Sms).Do 发短信...
main.(*WechatNotify).Do 发微信消息...
----------------------- 已支付取消订单 “主题”
main.(*OrderStatus).Do 修改订单状态...
main.(*OrderStatusLog).Do 记录订单状态变更日志...
main.(*CouponRefund).Do 退优惠券...
main.(*PromotionRefund).Do 还优惠活动资格...
main.(*StockRefund).Do 还库存...
main.(*GiftCardRefund).Do 还礼品卡...
main.(*WalletRefund).Do 退钱包余额...
main.(*Refund).Do 生成退款单...
main.(*Invoice).Do 生成发票-红票...
main.(*Email).Do 发邮件...
main.(*Sms).Do 发短信...
main.(*WechatNotify).Do 发微信消息...
----------------------- 取消发货单 “主题”
main.(*OrderStatus).Do 修改订单状态...
main.(*OrderStatusLog).Do 记录订单状态变更日志...
main.(*DeliverBillStatus).Do 修改发货单状态...
main.(*DeliverBillStatusLog).Do 记录发货单状态变更日志...
main.(*StockRefund).Do 还库存...
main.(*GiftCardRefund).Do 还礼品卡...
main.(*WalletRefund).Do 退钱包余额...
main.(*Refund).Do 生成退款单...
main.(*Invoice).Do 生成发票-红票...
main.(*Email).Do 发邮件...
main.(*Sms).Do 发短信...
main.(*WechatNotify).Do 发微信消息...
----------------------- 拒收 “主题”
main.(*OrderStatus).Do 修改订单状态...
main.(*OrderStatusLog).Do 记录订单状态变更日志...
main.(*DeliverBillStatus).Do 修改发货单状态...
main.(*DeliverBillStatusLog).Do 记录发货单状态变更日志...
main.(*StockRefund).Do 还库存...
main.(*GiftCardRefund).Do 还礼品卡...
main.(*WalletRefund).Do 退钱包余额...
main.(*Refund).Do 生成退款单...
main.(*Invoice).Do 生成发票-红票...
main.(*Email).Do 发邮件...
main.(*Sms).Do 发短信...
main.(*WechatNotify).Do 发微信消息...
最后总结下,「观察者模式」抽象过程的核心是:
特别说明:
1. 我的代码没有`else`,只是一个在代码合理设计的情况下自然而然无限接近或者达到的结果,并不是一个硬性的目标,务必较真。
2. 本系列的一些设计模式的概念可能和原概念存在差异,因为会结合实际使用,取其精华,适当改变,灵活使用。
3. 观察者模式与订阅通知实际还是有差异,本文均加上了双引号。订阅通知:订阅方不是直接依赖主题方(联想下mq等消息中间件的使用);而观察者模式:观察者是直接依赖了被观察者,从上面的代码我们也可以清晰的看出来这个差异。
我的代码没有else系列 更多文章 点击此处查看
赞 5 收藏 4 评论 0
TIGERB 发布了文章 · 2020-04-11
嗯,我的代码没有else
系列,一个设计模式业务真实使用的golang系列。
本系列主要分享,如何在我们的真实业务场景中使用设计模式。
本系列文章主要采用如下结构:
本文主要介绍「组合模式」如何在真实业务场景中使用。
一个具有层级关系的对象由一系列拥有父子关系的对象通过树形结构组成。
组合模式的优势:
满足如下要求的所有场景:
Get请求获取页面数据的所有接口
前端大行组件化的当今,我们在写后端接口代码的时候还是按照业务思路一头写到尾吗?我们是否可以思索,「后端接口业务代码如何可以简单快速组件化?」,答案是肯定的,这就是「组合模式」的作用。
我们利用「组合模式」的定义和前端模块的划分去构建后端业务代码结构:
我们有哪些真实业务场景可以用「组合模式」呢?
比如我们以“复杂的订单结算页面”为例,下面是某东的订单结算页面:
从页面的展示形式上,可以看出:
页面由多个模块构成,比如:
单个模块可以由多个子模块构成
店铺模块,又由如下模块构成:
关于怎么用,完全可以生搬硬套我总结的使用设计模式的四个步骤:
按照如上某东的订单结算页面的示例,我们得到了如下的订单结算页面模块组成图:
注:模块不一定完全准确
责任链模式主要类主要包含如下特性:
成员属性
ChildComponents
: 子组件列表 -> 稳定不变的成员方法
Mount
: 添加一个子组件 -> 稳定不变的Remove
: 移除一个子组件 -> 稳定不变的Do
: 执行组件&子组件 -> 变化的套用到订单结算页面信息接口伪代码实现如下:
一个父类(抽象类):
- 成员属性
+ `ChildComponents`: 子组件列表
- 成员方法
+ `Mount`: 实现添加一个子组件
+ `Remove`: 实现移除一个子组件
+ `Do`: 抽象方法
组件一,订单结算页面组件类(继承父类、看成一个大的组件):
- 成员方法
+ `Do`: 执行当前组件的逻辑,执行子组件的逻辑
组件二,地址组件(继承父类):
- 成员方法
+ `Do`: 执行当前组件的逻辑,执行子组件的逻辑
组件三,支付方式组件(继承父类):
- 成员方法
+ `Do`: 执行当前组件的逻辑,执行子组件的逻辑
组件四,店铺组件(继承父类):
- 成员方法
+ `Do`: 执行当前组件的逻辑,执行子组件的逻辑
组件五,商品组件(继承父类):
- 成员方法
+ `Do`: 执行当前组件的逻辑,执行子组件的逻辑
组件六,优惠信息组件(继承父类):
- 成员方法
+ `Do`: 执行当前组件的逻辑,执行子组件的逻辑
组件七,物流组件(继承父类):
- 成员方法
+ `Do`: 执行当前组件的逻辑,执行子组件的逻辑
组件八,发票组件(继承父类):
- 成员方法
+ `Do`: 执行当前组件的逻辑,执行子组件的逻辑
组件九,优惠券组件(继承父类):
- 成员方法
+ `Do`: 执行当前组件的逻辑,执行子组件的逻辑
组件十,礼品卡组件(继承父类):
- 成员方法
+ `Do`: 执行当前组件的逻辑,执行子组件的逻辑
组件十一,订单金额详细信息组件(继承父类):
- 成员方法
+ `Do`: 执行当前组件的逻辑,执行子组件的逻辑
组件十二,售后组件(继承父类,未来扩展的组件):
- 成员方法
+ `Do`: 执行当前组件的逻辑,执行子组件的逻辑
但是,golang里没有的继承的概念,要复用成员属性ChildComponents
、成员方法Mount
、成员方法Remove
怎么办呢?我们使用合成复用
的特性变相达到“继承复用”的目的,如下:
一个接口(interface):
+ 抽象方法`Mount`: 添加一个子组件
+ 抽象方法`Remove`: 移除一个子组件
+ 抽象方法`Do`: 执行组件&子组件
一个基础结构体`BaseComponent`:
- 成员属性
+ `ChildComponents`: 子组件列表
- 成员方法
+ 实体方法`Mount`: 添加一个子组件
+ 实体方法`Remove`: 移除一个子组件
+ 实体方法`ChildsDo`: 执行子组件
组件一,订单结算页面组件类:
- 合成复用基础结构体`BaseComponent`
- 成员方法
+ `Do`: 执行当前组件的逻辑,执行子组件的逻辑
组件二,地址组件:
- 合成复用基础结构体`BaseComponent`
- 成员方法
+ `Do`: 执行当前组件的逻辑,执行子组件的逻辑
组件三,支付方式组件:
- 合成复用基础结构体`BaseComponent`
- 成员方法
+ `Do`: 执行当前组件的逻辑,执行子组件的逻辑
...略
组件十一,订单金额详细信息组件:
- 合成复用基础结构体`BaseComponent`
- 成员方法
+ `Do`: 执行当前组件的逻辑,执行子组件的逻辑
同时得到了我们的UML图:
package main
import (
"fmt"
"reflect"
"runtime"
)
//------------------------------------------------------------
//我的代码没有`else`系列
//组合模式
//@auhtor TIGERB<https://github.com/TIGERB>
//------------------------------------------------------------
// Context 上下文
type Context struct{}
// Component 组件接口
type Component interface {
// 添加一个子组件
Mount(c Component, components ...Component) error
// 移除一个子组件
Remove(c Component) error
// 执行组件&子组件
Do(ctx *Context) error
}
// BaseComponent 基础组件
// 实现Add:添加一个子组件
// 实现Remove:移除一个子组件
type BaseComponent struct {
// 子组件列表
ChildComponents []Component
}
// Mount 挂载一个子组件
func (bc *BaseComponent) Mount(c Component, components ...Component) (err error) {
bc.ChildComponents = append(bc.ChildComponents, c)
if len(components) == 0 {
return
}
bc.ChildComponents = append(bc.ChildComponents, components...)
return
}
// Remove 移除一个子组件
func (bc *BaseComponent) Remove(c Component) (err error) {
if len(bc.ChildComponents) == 0 {
return
}
for k, childComponent := range bc.ChildComponents {
if c == childComponent {
fmt.Println(runFuncName(), "移除:", reflect.TypeOf(childComponent))
bc.ChildComponents = append(bc.ChildComponents[:k], bc.ChildComponents[k+1:]...)
}
}
return
}
// Do 执行组件&子组件
func (bc *BaseComponent) Do(ctx *Context) (err error) {
// do nothing
return
}
// ChildsDo 执行子组件
func (bc *BaseComponent) ChildsDo(ctx *Context) (err error) {
// 执行子组件
for _, childComponent := range bc.ChildComponents {
if err = childComponent.Do(ctx); err != nil {
return err
}
}
return
}
// CheckoutPageComponent 订单结算页面组件
type CheckoutPageComponent struct {
// 合成复用基础组件
BaseComponent
}
// Do 执行组件&子组件
func (bc *CheckoutPageComponent) Do(ctx *Context) (err error) {
// 当前组件的业务逻辑写这
fmt.Println(runFuncName(), "订单结算页面组件...")
// 执行子组件
bc.ChildsDo(ctx)
// 当前组件的业务逻辑写这
return
}
// AddressComponent 地址组件
type AddressComponent struct {
// 合成复用基础组件
BaseComponent
}
// Do 执行组件&子组件
func (bc *AddressComponent) Do(ctx *Context) (err error) {
// 当前组件的业务逻辑写这
fmt.Println(runFuncName(), "地址组件...")
// 执行子组件
bc.ChildsDo(ctx)
// 当前组件的业务逻辑写这
return
}
// PayMethodComponent 支付方式组件
type PayMethodComponent struct {
// 合成复用基础组件
BaseComponent
}
// Do 执行组件&子组件
func (bc *PayMethodComponent) Do(ctx *Context) (err error) {
// 当前组件的业务逻辑写这
fmt.Println(runFuncName(), "支付方式组件...")
// 执行子组件
bc.ChildsDo(ctx)
// 当前组件的业务逻辑写这
return
}
// StoreComponent 店铺组件
type StoreComponent struct {
// 合成复用基础组件
BaseComponent
}
// Do 执行组件&子组件
func (bc *StoreComponent) Do(ctx *Context) (err error) {
// 当前组件的业务逻辑写这
fmt.Println(runFuncName(), "店铺组件...")
// 执行子组件
bc.ChildsDo(ctx)
// 当前组件的业务逻辑写这
return
}
// SkuComponent 商品组件
type SkuComponent struct {
// 合成复用基础组件
BaseComponent
}
// Do 执行组件&子组件
func (bc *SkuComponent) Do(ctx *Context) (err error) {
// 当前组件的业务逻辑写这
fmt.Println(runFuncName(), "商品组件...")
// 执行子组件
bc.ChildsDo(ctx)
// 当前组件的业务逻辑写这
return
}
// PromotionComponent 优惠信息组件
type PromotionComponent struct {
// 合成复用基础组件
BaseComponent
}
// Do 执行组件&子组件
func (bc *PromotionComponent) Do(ctx *Context) (err error) {
// 当前组件的业务逻辑写这
fmt.Println(runFuncName(), "优惠信息组件...")
// 执行子组件
bc.ChildsDo(ctx)
// 当前组件的业务逻辑写这
return
}
// ExpressComponent 物流组件
type ExpressComponent struct {
// 合成复用基础组件
BaseComponent
}
// Do 执行组件&子组件
func (bc *ExpressComponent) Do(ctx *Context) (err error) {
// 当前组件的业务逻辑写这
fmt.Println(runFuncName(), "物流组件...")
// 执行子组件
bc.ChildsDo(ctx)
// 当前组件的业务逻辑写这
return
}
// AftersaleComponent 售后组件
type AftersaleComponent struct {
// 合成复用基础组件
BaseComponent
}
// Do 执行组件&子组件
func (bc *AftersaleComponent) Do(ctx *Context) (err error) {
// 当前组件的业务逻辑写这
fmt.Println(runFuncName(), "售后组件...")
// 执行子组件
bc.ChildsDo(ctx)
// 当前组件的业务逻辑写这
return
}
// InvoiceComponent 发票组件
type InvoiceComponent struct {
// 合成复用基础组件
BaseComponent
}
// Do 执行组件&子组件
func (bc *InvoiceComponent) Do(ctx *Context) (err error) {
// 当前组件的业务逻辑写这
fmt.Println(runFuncName(), "发票组件...")
// 执行子组件
bc.ChildsDo(ctx)
// 当前组件的业务逻辑写这
return
}
// CouponComponent 优惠券组件
type CouponComponent struct {
// 合成复用基础组件
BaseComponent
}
// Do 执行组件&子组件
func (bc *CouponComponent) Do(ctx *Context) (err error) {
// 当前组件的业务逻辑写这
fmt.Println(runFuncName(), "优惠券组件...")
// 执行子组件
bc.ChildsDo(ctx)
// 当前组件的业务逻辑写这
return
}
// GiftCardComponent 礼品卡组件
type GiftCardComponent struct {
// 合成复用基础组件
BaseComponent
}
// Do 执行组件&子组件
func (bc *GiftCardComponent) Do(ctx *Context) (err error) {
// 当前组件的业务逻辑写这
fmt.Println(runFuncName(), "礼品卡组件...")
// 执行子组件
bc.ChildsDo(ctx)
// 当前组件的业务逻辑写这
return
}
// OrderComponent 订单金额详细信息组件
type OrderComponent struct {
// 合成复用基础组件
BaseComponent
}
// Do 执行组件&子组件
func (bc *OrderComponent) Do(ctx *Context) (err error) {
// 当前组件的业务逻辑写这
fmt.Println(runFuncName(), "订单金额详细信息组件...")
// 执行子组件
bc.ChildsDo(ctx)
// 当前组件的业务逻辑写这
return
}
func main() {
// 初始化订单结算页面 这个大组件
checkoutPage := &CheckoutPageComponent{}
// 挂载子组件
storeComponent := &StoreComponent{}
skuComponent := &SkuComponent{}
skuComponent.Mount(
&PromotionComponent{},
&AftersaleComponent{},
)
storeComponent.Mount(
skuComponent,
&ExpressComponent{},
)
// 挂载组件
checkoutPage.Mount(
&AddressComponent{},
&PayMethodComponent{},
storeComponent,
&InvoiceComponent{},
&CouponComponent{},
&GiftCardComponent{},
&OrderComponent{},
)
// 移除组件测试
// checkoutPage.Remove(storeComponent)
// 开始构建页面组件数据
checkoutPage.Do(&Context{})
}
// 获取正在运行的函数名
func runFuncName() string {
pc := make([]uintptr, 1)
runtime.Callers(2, pc)
f := runtime.FuncForPC(pc[0])
return f.Name()
}
代码运行结果:
[Running] go run "../easy-tips/go/src/patterns/composite/composite.go"
main.(*CheckoutPageComponent).Do 订单结算页面组件...
main.(*AddressComponent).Do 地址组件...
main.(*PayMethodComponent).Do 支付方式组件...
main.(*StoreComponent).Do 店铺组件...
main.(*SkuComponent).Do 商品组件...
main.(*PromotionComponent).Do 优惠信息组件...
main.(*AftersaleComponent).Do 售后组件...
main.(*ExpressComponent).Do 物流组件...
main.(*InvoiceComponent).Do 发票组件...
main.(*CouponComponent).Do 优惠券组件...
main.(*GiftCardComponent).Do 礼品卡组件...
main.(*OrderComponent).Do 订单金额详细信息组件...
最后总结下,「组合模式」抽象过程的核心是:
与「责任链模式」的区别:
特别说明:
1. 我的代码没有`else`,只是一个在代码合理设计的情况下自然而然无限接近或者达到的结果,并不是一个硬性的目标,务必较真。
2. 本系列的一些设计模式的概念可能和原概念存在差异,因为会结合实际使用,取其精华,适当改变,灵活使用。
我的代码没有else系列 更多文章 点击此处查看
赞 1 收藏 1 评论 0
TIGERB 发布了文章 · 2020-04-11
嗯,我的代码没有else
系列,一个设计模式业务真实使用的golang系列。
本系列主要分享,如何在我们的真实业务场景中使用设计模式。
本系列文章主要采用如下结构:
本文主要介绍「责任链模式」如何在真实业务场景中使用。
首先把一系列业务按职责划分成不同的对象,接着把这一系列对象构成一个链,然后在这一系列对象中传递请求对象,直到被处理为止。
我们从概念中可以看出责任链模式有如下明显的优势:
但是有一点直到被处理为止
,代表最终只会被一个实际的业务对象执行了实际的业务逻辑,明显适用的场景并不多。但是除此之外,上面的那两点优势还是让人很心动,所以,为了适用于目前所接触的绝大多数业务场景,把概念进行了简单的调整,如下:
首先把一系列业务按职责划分成不同的对象,接着把这一系列对象构成一个链,直到“链路结束”为止。(结束:异常结束,或链路执行完毕结束)
简单的直到“链路结束”为止
转换可以让我们把责任链模式适用于任何复杂的业务场景。
以下是责任链模式的具体优势:
满足如下要求的场景:
业务极度复杂的所有场景
任何杂乱无章的业务代码,都可以使用责任链模式(改)去重构、设计。
我们有哪些真实业务场景可以用「责任链模式(改)」呢?
比如电商系统的下单接口,随着业务发展不断的发展,该接口会充斥着各种各样的业务逻辑。
关于怎么用,完全可以生搬硬套我总结的使用设计模式的四个步骤:
步骤 | 逻辑 |
---|---|
1 | 参数校验 |
2 | 获取地址信息 |
3 | 地址信息校验 |
4 | 获取购物车数据 |
5 | 获取商品库存信息 |
6 | 商品库存校验 |
7 | 获取优惠信息 |
8 | 获取运费信息 |
9 | 使用优惠信息 |
10 | 扣库存 |
11 | 清理购物车 |
12 | 写订单表 |
13 | 写订单商品表 |
14 | 写订单优惠信息表 |
XX | 以及未来会增加的逻辑... |
业务的不断发展变化的:
比如增加的新的业务,订金预售:
4|获取购物车数据
后,需要校验商品参见订金预售活动的有效性等逻辑。注:流程不一定完全准确
我们通过梳理的文本业务流程得到了如下的业务流程图:
责任链模式主要类主要包含如下特性:
成员属性
nextHandler
: 下一个等待被调用的对象实例 -> 稳定不变的成员方法
SetNext
: 把下一个对象的实例绑定到当前对象的nextHandler
属性上 -> 稳定不变的Do
: 当前对象业务逻辑入口 -> 变化的Run
: 调用当前对象的Do
,nextHandler
不为空则调用nextHandler.Do
-> 稳定不变的套用到下单接口伪代码实现如下:
一个父类(抽象类):
- 成员属性
+ `nextHandler`: 下一个等待被调用的对象实例
- 成员方法
+ 实体方法`SetNext`: 实现把下一个对象的实例绑定到当前对象的`nextHandler`属性上
+ 抽象方法`Do`: 当前对象业务逻辑入口
+ 实体方法`Run`: 实现调用当前对象的`Do`,`nextHandler`不为空则调用`nextHandler.Do`
子类一(参数校验)
- 继承抽象类父类
- 实现抽象方法`Do`:具体的参数校验逻辑
子类二(获取地址信息)
- 继承抽象类父类
- 实现抽象方法`Do`:具体获取地址信息的逻辑
子类三(获取购物车数据)
- 继承抽象类父类
- 实现抽象方法`Do`:具体获取购物车数据的逻辑
......略
子类X(以及未来会增加的逻辑)
- 继承抽象类父类
- 实现抽象方法`Do`:以及未来会增加的逻辑
但是,golang里没有的继承的概念,要复用成员属性nextHandler
、成员方法SetNext
、成员方法Run
怎么办呢?我们使用合成复用
的特性变相达到“继承复用”的目的,如下:
一个接口(interface):
- 抽象方法`SetNext`: 待实现把下一个对象的实例绑定到当前对象的`nextHandler`属性上
- 抽象方法`Do`: 待实现当前对象业务逻辑入口
- 抽象方法`Run`: 待实现调用当前对象的`Do`,`nextHandler`不为空则调用`nextHandler.Do`
一个基础结构体:
- 成员属性
+ `nextHandler`: 下一个等待被调用的对象实例
- 成员方法
+ 实体方法`SetNext`: 实现把下一个对象的实例绑定到当前对象的`nextHandler`属性上
+ 实体方法`Run`: 实现调用当前对象的`Do`,`nextHandler`不为空则调用`nextHandler.Do`
子类一(参数校验)
- 合成复用基础结构体
- 实现抽象方法`Do`:具体的参数校验逻辑
子类二(获取地址信息)
- 合成复用基础结构体
- 实现抽象方法`Do`:具体获取地址信息的逻辑
子类三(获取购物车数据)
- 合成复用基础结构体
- 实现抽象方法`Do`:具体获取购物车数据的逻辑
......略
子类X(以及未来会增加的逻辑)
- 合成复用基础结构体
- 实现抽象方法`Do`:以及未来会增加的逻辑
同时得到了我们的UML图:
package main
//---------------
//我的代码没有`else`系列
//责任链模式
//@auhtor TIGERB<https://github.com/TIGERB>
//---------------
import (
"fmt"
"runtime"
)
// Context Context
type Context struct {
}
// Handler 处理
type Handler interface {
// 自身的业务
Do(c *Context) error
// 设置下一个对象
SetNext(h Handler) Handler
// 执行
Run(c *Context) error
}
// Next 抽象出来的 可被合成复用的结构体
type Next struct {
// 下一个对象
nextHandler Handler
}
// SetNext 实现好的 可被复用的SetNext方法
// 返回值是下一个对象 方便写成链式代码优雅
// 例如 nullHandler.SetNext(argumentsHandler).SetNext(signHandler).SetNext(frequentHandler)
func (n *Next) SetNext(h Handler) Handler {
n.nextHandler = h
return h
}
// Run 执行
func (n *Next) Run(c *Context) (err error) {
// 由于go无继承的概念 这里无法执行当前handler的Do
// n.Do(c)
if n.nextHandler != nil {
// 合成复用下的变种
// 执行下一个handler的Do
if err = (n.nextHandler).Do(c); err != nil {
return
}
// 执行下一个handler的Run
return (n.nextHandler).Run(c)
}
return
}
// NullHandler 空Handler
// 由于go无继承的概念 作为链式调用的第一个载体 设置实际的下一个对象
type NullHandler struct {
// 合成复用Next的`nextHandler`成员属性、`SetNext`成员方法、`Run`成员方法
Next
}
// Do 空Handler的Do
func (h *NullHandler) Do(c *Context) (err error) {
// 空Handler 这里什么也不做 只是载体 do nothing...
return
}
// ArgumentsHandler 校验参数的handler
type ArgumentsHandler struct {
// 合成复用Next
Next
}
// Do 校验参数的逻辑
func (h *ArgumentsHandler) Do(c *Context) (err error) {
fmt.Println(runFuncName(), "校验参数成功...")
return
}
// AddressInfoHandler 地址信息handler
type AddressInfoHandler struct {
// 合成复用Next
Next
}
// Do 校验参数的逻辑
func (h *AddressInfoHandler) Do(c *Context) (err error) {
fmt.Println(runFuncName(), "获取地址信息...")
fmt.Println(runFuncName(), "地址信息校验...")
return
}
// CartInfoHandler 获取购物车数据handler
type CartInfoHandler struct {
// 合成复用Next
Next
}
// Do 校验参数的逻辑
func (h *CartInfoHandler) Do(c *Context) (err error) {
fmt.Println(runFuncName(), "获取购物车数据...")
return
}
// StockInfoHandler 商品库存handler
type StockInfoHandler struct {
// 合成复用Next
Next
}
// Do 校验参数的逻辑
func (h *StockInfoHandler) Do(c *Context) (err error) {
fmt.Println(runFuncName(), "获取商品库存信息...")
fmt.Println(runFuncName(), "商品库存校验...")
return
}
// PromotionInfoHandler 获取优惠信息handler
type PromotionInfoHandler struct {
// 合成复用Next
Next
}
// Do 校验参数的逻辑
func (h *PromotionInfoHandler) Do(c *Context) (err error) {
fmt.Println(runFuncName(), "获取优惠信息...")
return
}
// ShipmentInfoHandler 获取运费信息handler
type ShipmentInfoHandler struct {
// 合成复用Next
Next
}
// Do 校验参数的逻辑
func (h *ShipmentInfoHandler) Do(c *Context) (err error) {
fmt.Println(runFuncName(), "获取运费信息...")
return
}
// PromotionUseHandler 使用优惠信息handler
type PromotionUseHandler struct {
// 合成复用Next
Next
}
// Do 校验参数的逻辑
func (h *PromotionUseHandler) Do(c *Context) (err error) {
fmt.Println(runFuncName(), "使用优惠信息...")
return
}
// StockSubtractHandler 库存操作handler
type StockSubtractHandler struct {
// 合成复用Next
Next
}
// Do 校验参数的逻辑
func (h *StockSubtractHandler) Do(c *Context) (err error) {
fmt.Println(runFuncName(), "扣库存...")
return
}
// CartDelHandler 清理购物车handler
type CartDelHandler struct {
// 合成复用Next
Next
}
// Do 校验参数的逻辑
func (h *CartDelHandler) Do(c *Context) (err error) {
fmt.Println(runFuncName(), "清理购物车...")
// err = fmt.Errorf("CartDelHandler.Do fail")
return
}
// DBTableOrderHandler 写订单表handler
type DBTableOrderHandler struct {
// 合成复用Next
Next
}
// Do 校验参数的逻辑
func (h *DBTableOrderHandler) Do(c *Context) (err error) {
fmt.Println(runFuncName(), "写订单表...")
return
}
// DBTableOrderSkusHandler 写订单商品表handler
type DBTableOrderSkusHandler struct {
// 合成复用Next
Next
}
// Do 校验参数的逻辑
func (h *DBTableOrderSkusHandler) Do(c *Context) (err error) {
fmt.Println(runFuncName(), "写订单商品表...")
return
}
// DBTableOrderPromotionsHandler 写订单优惠信息表handler
type DBTableOrderPromotionsHandler struct {
// 合成复用Next
Next
}
// Do 校验参数的逻辑
func (h *DBTableOrderPromotionsHandler) Do(c *Context) (err error) {
fmt.Println(runFuncName(), "写订单优惠信息表...")
return
}
// 获取正在运行的函数名
func runFuncName() string {
pc := make([]uintptr, 1)
runtime.Callers(2, pc)
f := runtime.FuncForPC(pc[0])
return f.Name()
}
func main() {
// 初始化空handler
nullHandler := &NullHandler{}
// 链式调用 代码是不是很优雅
// 很明显的链 逻辑关系一览无余
nullHandler.SetNext(&ArgumentsHandler{}).
SetNext(&AddressInfoHandler{}).
SetNext(&CartInfoHandler{}).
SetNext(&StockInfoHandler{}).
SetNext(&PromotionInfoHandler{}).
SetNext(&ShipmentInfoHandler{}).
SetNext(&PromotionUseHandler{}).
SetNext(&StockSubtractHandler{}).
SetNext(&CartDelHandler{}).
SetNext(&DBTableOrderHandler{}).
SetNext(&DBTableOrderSkusHandler{}).
SetNext(&DBTableOrderPromotionsHandler{})
//无限扩展代码...
// 开始执行业务
if err := nullHandler.Run(&Context{}); err != nil {
// 异常
fmt.Println("Fail | Error:" + err.Error())
return
}
// 成功
fmt.Println("Success")
return
}
代码运行结果:
[Running] go run "../easy-tips/go/src/patterns/responsibility/responsibility-order-submit.go"
main.(*ArgumentsHandler).Do 校验参数成功...
main.(*AddressInfoHandler).Do 获取地址信息...
main.(*AddressInfoHandler).Do 地址信息校验...
main.(*CartInfoHandler).Do 获取购物车数据...
main.(*StockInfoHandler).Do 获取商品库存信息...
main.(*StockInfoHandler).Do 商品库存校验...
main.(*PromotionInfoHandler).Do 获取优惠信息...
main.(*ShipmentInfoHandler).Do 获取运费信息...
main.(*PromotionUseHandler).Do 使用优惠信息...
main.(*StockSubtractHandler).Do 扣库存...
main.(*CartDelHandler).Do 清理购物车...
main.(*DBTableOrderHandler).Do 写订单表...
main.(*DBTableOrderSkusHandler).Do 写订单商品表...
main.(*DBTableOrderPromotionsHandler).Do 写订单优惠信息表...
Success
最后总结下,「责任链模式(改)」抽象过程的核心是:
特别说明:
1. 我的代码没有`else`,只是一个在代码合理设计的情况下自然而然无限接近或者达到的结果,并不是一个硬性的目标,务必较真。
2. 本系列的一些设计模式的概念可能和原概念存在差异,因为会结合实际使用,取其精华,适当改变,灵活使用。
我的代码没有else系列 更多文章 点击此处查看
赞 2 收藏 2 评论 2
TIGERB 发布了文章 · 2020-04-11
嗯,我的代码没有else
系列,一个设计模式业务真实使用的golang系列。
本系列主要分享,如何在我们的真实业务场景中使用设计模式。
本系列文章主要采用如下结构:
本文主要介绍「模板模式」如何在真实业务场景中使用。
抽象类里定义好算法的执行步骤和具体算法,以及可能发生变化的算法定义为抽象方法。不同的子类继承该抽象类,并实现父类的抽象方法。
模板模式的优势:
满足如下要求的所有场景:
算法执行的步骤是稳定不变的,但是具体的某些算法可能存在变化的场景。
怎么理解,举个例子:比如说你煮个面,必然需要先烧水,水烧开之后再放面进去
,以上的流程我们称之为煮面过程
。可知:这个煮面过程
的步骤是稳定不变的,但是在不同的环境烧水的方式可能不尽相同,也许有的人用天然气烧水、有的人用电磁炉烧水、有的人用柴火烧水,等等。我们可以得到以下结论:
煮面过程
的步骤是稳定不变的煮面过程
的烧水方式是可变的我们有哪些真实业务场景可以用「模板模式」呢?
比如抽奖系统的抽奖接口,为什么:
关于怎么用,完全可以生搬硬套我总结的使用设计模式的四个步骤:
我通过历史上接触过的各种抽奖场景(红包雨、糖果雨、打地鼠、大转盘(九宫格)、考眼力、答题闯关、游戏闯关、支付刮刮乐、积分刮刮乐等等),按照真实业务需求梳理了以下抽奖业务抽奖接口的大致文本流程。
了解具体业务请点击《通用抽奖工具之需求分析 | SkrShop》
主步骤 | 主逻辑 | 抽奖类型 | 子步骤 | 子逻辑 |
---|---|---|---|---|
1 | 校验活动编号(serial_no)是否存在、并获取活动信息 | - | - | - |
2 | 校验活动、场次是否正在进行 | - | - | - |
3 | 其他参数校验(不同活动类型实现不同) | - | - | - |
4 | 活动抽奖次数校验(同时扣减) | - | - | - |
5 | 活动是否需要消费积分 | - | - | - |
6 | 场次抽奖次数校验(同时扣减) | - | - | - |
7 | 获取场次奖品信息 | - | - | - |
8 | 获取node奖品信息(不同活动类型实现不同) | 按时间抽奖类型 | 1 | do nothing(抽取该场次的奖品即可,无需其他逻辑) |
8 | 按抽奖次数抽奖类型 | 1 | 判断是该用户第几次抽奖 | |
8 | 2 | 获取对应node的奖品信息 | ||
8 | 3 | 复写原所有奖品信息(抽取该node节点的奖品) | ||
8 | 按数额范围区间抽奖 | 1 | 判断属于哪个数额区间 | |
8 | 2 | 获取对应node的奖品信息 | ||
8 | 3 | 复写原所有奖品信息(抽取该node节点的奖品) | ||
9 | 抽奖 | - | - | - |
10 | 奖品数量判断 | - | - | - |
11 | 组装奖品信息 | - | - | - |
注:流程不一定完全准确
结论:
主逻辑
是稳定不变的其他参数校验
和获取node奖品信息
的算法是可变的我们通过梳理的文本业务流程得到了如下的业务流程图:
通过上面的分析我们可以得到:
一个抽象类
- 具体共有方法`Run`,里面定义了算法的执行步骤
- 具体私有方法,不会发生变化的具体方法
- 抽象方法,会发生变化的方法
子类一(按时间抽奖类型)
- 继承抽象类父类
- 实现抽象方法
子类二(按抽奖次数抽奖类型)
- 继承抽象类父类
- 实现抽象方法
子类三(按数额范围区间抽奖)
- 继承抽象类父类
- 实现抽象方法
但是golang里面没有继承的概念,我们就把对抽象类里抽象方法的依赖转化成对接口interface
里抽象方法的依赖,同时也可以利用合成复用
的方式“继承”模板:
抽象行为的接口`BehaviorInterface`(包含如下需要实现的方法)
- 其他参数校验的方法`checkParams`
- 获取node奖品信息的方法`getPrizesByNode`
抽奖结构体类
- 具体共有方法`Run`,里面定义了算法的执行步骤
- 具体私有方法`checkParams` 里面的逻辑实际依赖的接口BehaviorInterface.checkParams(ctx)的抽象方法
- 具体私有方法`getPrizesByNode` 里面的逻辑实际依赖的接口BehaviorInterface.getPrizesByNode(ctx)的抽象方法
- 其他具体私有方法,不会发生变化的具体方法
实现`BehaviorInterface`的结构体一(按时间抽奖类型)
- 实现接口方法
实现`BehaviorInterface`的结构体二(按抽奖次数抽奖类型)
- 实现接口方法
实现`BehaviorInterface`的结构体三(按数额范围区间抽奖)
- 实现接口方法
同时得到了我们的UML图:
package main
import (
"fmt"
"runtime"
)
//------------------------------------------------------------
//我的代码没有`else`系列
//模板模式
//@auhtor TIGERB<https://github.com/TIGERB>
//------------------------------------------------------------
const (
// ConstActTypeTime 按时间抽奖类型
ConstActTypeTime int32 = 1
// ConstActTypeTimes 按抽奖次数抽奖
ConstActTypeTimes int32 = 2
// ConstActTypeAmount 按数额范围区间抽奖
ConstActTypeAmount int32 = 3
)
// Context 上下文
type Context struct {
ActInfo *ActInfo
}
// ActInfo 上下文
type ActInfo struct {
// 活动抽奖类型1: 按时间抽奖 2: 按抽奖次数抽奖 3:按数额范围区间抽奖
ActivityType int32
// 其他字段略
}
// BehaviorInterface 不同抽奖类型的行为差异的抽象接口
type BehaviorInterface interface {
// 其他参数校验(不同活动类型实现不同)
checkParams(ctx *Context) error
// 获取node奖品信息(不同活动类型实现不同)
getPrizesByNode(ctx *Context) error
}
// TimeDraw 具体抽奖行为
// 按时间抽奖类型 比如红包雨
type TimeDraw struct{}
// checkParams 其他参数校验(不同活动类型实现不同)
func (draw TimeDraw) checkParams(ctx *Context) (err error) {
fmt.Println(runFuncName(), "按时间抽奖类型:特殊参数校验...")
return
}
// getPrizesByNode 获取node奖品信息(不同活动类型实现不同)
func (draw TimeDraw) getPrizesByNode(ctx *Context) (err error) {
fmt.Println(runFuncName(), "do nothing(抽取该场次的奖品即可,无需其他逻辑)...")
return
}
// TimesDraw 具体抽奖行为
// 按抽奖次数抽奖类型 比如答题闯关
type TimesDraw struct{}
// checkParams 其他参数校验(不同活动类型实现不同)
func (draw TimesDraw) checkParams(ctx *Context) (err error) {
fmt.Println(runFuncName(), "按抽奖次数抽奖类型:特殊参数校验...")
return
}
// getPrizesByNode 获取node奖品信息(不同活动类型实现不同)
func (draw TimesDraw) getPrizesByNode(ctx *Context) (err error) {
fmt.Println(runFuncName(), "1. 判断是该用户第几次抽奖...")
fmt.Println(runFuncName(), "2. 获取对应node的奖品信息...")
fmt.Println(runFuncName(), "3. 复写原所有奖品信息(抽取该node节点的奖品)...")
return
}
// AmountDraw 具体抽奖行为
// 按数额范围区间抽奖 比如订单金额刮奖
type AmountDraw struct{}
// checkParams 其他参数校验(不同活动类型实现不同)
func (draw *AmountDraw) checkParams(ctx *Context) (err error) {
fmt.Println(runFuncName(), "按数额范围区间抽奖:特殊参数校验...")
return
}
// getPrizesByNode 获取node奖品信息(不同活动类型实现不同)
func (draw *AmountDraw) getPrizesByNode(ctx *Context) (err error) {
fmt.Println(runFuncName(), "1. 判断属于哪个数额区间...")
fmt.Println(runFuncName(), "2. 获取对应node的奖品信息...")
fmt.Println(runFuncName(), "3. 复写原所有奖品信息(抽取该node节点的奖品)...")
return
}
// Lottery 抽奖模板
type Lottery struct {
// 不同抽奖类型的抽象行为
concreteBehavior BehaviorInterface
}
// Run 抽奖算法
// 稳定不变的算法步骤
func (lottery *Lottery) Run(ctx *Context) (err error) {
// 具体方法:校验活动编号(serial_no)是否存在、并获取活动信息
if err = lottery.checkSerialNo(ctx); err != nil {
return err
}
// 具体方法:校验活动、场次是否正在进行
if err = lottery.checkStatus(ctx); err != nil {
return err
}
// ”抽象方法“:其他参数校验
if err = lottery.checkParams(ctx); err != nil {
return err
}
// 具体方法:活动抽奖次数校验(同时扣减)
if err = lottery.checkTimesByAct(ctx); err != nil {
return err
}
// 具体方法:活动是否需要消费积分
if err = lottery.consumePointsByAct(ctx); err != nil {
return err
}
// 具体方法:场次抽奖次数校验(同时扣减)
if err = lottery.checkTimesBySession(ctx); err != nil {
return err
}
// 具体方法:获取场次奖品信息
if err = lottery.getPrizesBySession(ctx); err != nil {
return err
}
// ”抽象方法“:获取node奖品信息
if err = lottery.getPrizesByNode(ctx); err != nil {
return err
}
// 具体方法:抽奖
if err = lottery.drawPrizes(ctx); err != nil {
return err
}
// 具体方法:奖品数量判断
if err = lottery.checkPrizesStock(ctx); err != nil {
return err
}
// 具体方法:组装奖品信息
if err = lottery.packagePrizeInfo(ctx); err != nil {
return err
}
return
}
// checkSerialNo 校验活动编号(serial_no)是否存在
func (lottery *Lottery) checkSerialNo(ctx *Context) (err error) {
fmt.Println(runFuncName(), "校验活动编号(serial_no)是否存在、并获取活动信息...")
// 获取活动信息伪代码
ctx.ActInfo = &ActInfo{
// 假设当前的活动类型为按抽奖次数抽奖
ActivityType: ConstActTypeTimes,
}
// 获取当前抽奖类型的具体行为
switch ctx.ActInfo.ActivityType {
case 1:
// 按时间抽奖
lottery.concreteBehavior = &TimeDraw{}
case 2:
// 按抽奖次数抽奖
lottery.concreteBehavior = &TimesDraw{}
case 3:
// 按数额范围区间抽奖
lottery.concreteBehavior = &AmountDraw{}
default:
return fmt.Errorf("不存在的活动类型")
}
return
}
// checkStatus 校验活动、场次是否正在进行
func (lottery *Lottery) checkStatus(ctx *Context) (err error) {
fmt.Println(runFuncName(), "校验活动、场次是否正在进行...")
return
}
// checkParams 其他参数校验(不同活动类型实现不同)
// 不同场景变化的算法 转化为依赖抽象
func (lottery *Lottery) checkParams(ctx *Context) (err error) {
// 实际依赖的接口的抽象方法
return lottery.concreteBehavior.checkParams(ctx)
}
// checkTimesByAct 活动抽奖次数校验
func (lottery *Lottery) checkTimesByAct(ctx *Context) (err error) {
fmt.Println(runFuncName(), "活动抽奖次数校验...")
return
}
// consumePointsByAct 活动是否需要消费积分
func (lottery *Lottery) consumePointsByAct(ctx *Context) (err error) {
fmt.Println(runFuncName(), "活动是否需要消费积分...")
return
}
// checkTimesBySession 活动抽奖次数校验
func (lottery *Lottery) checkTimesBySession(ctx *Context) (err error) {
fmt.Println(runFuncName(), "活动抽奖次数校验...")
return
}
// getPrizesBySession 获取场次奖品信息
func (lottery *Lottery) getPrizesBySession(ctx *Context) (err error) {
fmt.Println(runFuncName(), "获取场次奖品信息...")
return
}
// getPrizesByNode 获取node奖品信息(不同活动类型实现不同)
// 不同场景变化的算法 转化为依赖抽象
func (lottery *Lottery) getPrizesByNode(ctx *Context) (err error) {
// 实际依赖的接口的抽象方法
return lottery.concreteBehavior.getPrizesByNode(ctx)
}
// drawPrizes 抽奖
func (lottery *Lottery) drawPrizes(ctx *Context) (err error) {
fmt.Println(runFuncName(), "抽奖...")
return
}
// checkPrizesStock 奖品数量判断
func (lottery *Lottery) checkPrizesStock(ctx *Context) (err error) {
fmt.Println(runFuncName(), "奖品数量判断...")
return
}
// packagePrizeInfo 组装奖品信息
func (lottery *Lottery) packagePrizeInfo(ctx *Context) (err error) {
fmt.Println(runFuncName(), "组装奖品信息...")
return
}
func main() {
(&Lottery{}).Run(&Context{})
}
// 获取正在运行的函数名
func runFuncName() string {
pc := make([]uintptr, 1)
runtime.Callers(2, pc)
f := runtime.FuncForPC(pc[0])
return f.Name()
}
以下是代码执行结果:
[Running] go run ".../easy-tips/go/src/patterns/template/template.go"
main.(*Lottery).checkSerialNo 校验活动编号(serial_no)是否存在、并获取活动信息...
main.(*Lottery).checkStatus 校验活动、场次是否正在进行...
main.TimesDraw.checkParams 按抽奖次数抽奖类型:特殊参数校验...
main.(*Lottery).checkTimesByAct 活动抽奖次数校验...
main.(*Lottery).consumePointsByAct 活动是否需要消费积分...
main.(*Lottery).checkTimesBySession 活动抽奖次数校验...
main.(*Lottery).getPrizesBySession 获取场次奖品信息...
main.TimesDraw.getPrizesByNode 1. 判断是该用户第几次抽奖...
main.TimesDraw.getPrizesByNode 2. 获取对应node的奖品信息...
main.TimesDraw.getPrizesByNode 3. 复写原所有奖品信息(抽取该node节点的奖品)...
main.(*Lottery).drawPrizes 抽奖...
main.(*Lottery).checkPrizesStock 奖品数量判断...
main.(*Lottery).packagePrizeInfo 组装奖品信息...
demo代码地址:https://github.com/TIGERB/eas...
合成复用
特性实现)package main
import (
"fmt"
"runtime"
)
//------------------------------------------------------------
//我的代码没有`else`系列
//模板模式
//@auhtor TIGERB<https://github.com/TIGERB>
//------------------------------------------------------------
const (
// ConstActTypeTime 按时间抽奖类型
ConstActTypeTime int32 = 1
// ConstActTypeTimes 按抽奖次数抽奖
ConstActTypeTimes int32 = 2
// ConstActTypeAmount 按数额范围区间抽奖
ConstActTypeAmount int32 = 3
)
// Context 上下文
type Context struct {
ActInfo *ActInfo
}
// ActInfo 上下文
type ActInfo struct {
// 活动抽奖类型1: 按时间抽奖 2: 按抽奖次数抽奖 3:按数额范围区间抽奖
ActivityType int32
// 其他字段略
}
// BehaviorInterface 不同抽奖类型的行为差异的抽象接口
type BehaviorInterface interface {
// 其他参数校验(不同活动类型实现不同)
checkParams(ctx *Context) error
// 获取node奖品信息(不同活动类型实现不同)
getPrizesByNode(ctx *Context) error
}
// TimeDraw 具体抽奖行为
// 按时间抽奖类型 比如红包雨
type TimeDraw struct {
// 合成复用模板
Lottery
}
// checkParams 其他参数校验(不同活动类型实现不同)
func (draw TimeDraw) checkParams(ctx *Context) (err error) {
fmt.Println(runFuncName(), "按时间抽奖类型:特殊参数校验...")
return
}
// getPrizesByNode 获取node奖品信息(不同活动类型实现不同)
func (draw TimeDraw) getPrizesByNode(ctx *Context) (err error) {
fmt.Println(runFuncName(), "do nothing(抽取该场次的奖品即可,无需其他逻辑)...")
return
}
// TimesDraw 具体抽奖行为
// 按抽奖次数抽奖类型 比如答题闯关
type TimesDraw struct {
// 合成复用模板
Lottery
}
// checkParams 其他参数校验(不同活动类型实现不同)
func (draw TimesDraw) checkParams(ctx *Context) (err error) {
fmt.Println(runFuncName(), "按抽奖次数抽奖类型:特殊参数校验...")
return
}
// getPrizesByNode 获取node奖品信息(不同活动类型实现不同)
func (draw TimesDraw) getPrizesByNode(ctx *Context) (err error) {
fmt.Println(runFuncName(), "1. 判断是该用户第几次抽奖...")
fmt.Println(runFuncName(), "2. 获取对应node的奖品信息...")
fmt.Println(runFuncName(), "3. 复写原所有奖品信息(抽取该node节点的奖品)...")
return
}
// AmountDraw 具体抽奖行为
// 按数额范围区间抽奖 比如订单金额刮奖
type AmountDraw struct {
// 合成复用模板
Lottery
}
// checkParams 其他参数校验(不同活动类型实现不同)
func (draw *AmountDraw) checkParams(ctx *Context) (err error) {
fmt.Println(runFuncName(), "按数额范围区间抽奖:特殊参数校验...")
return
}
// getPrizesByNode 获取node奖品信息(不同活动类型实现不同)
func (draw *AmountDraw) getPrizesByNode(ctx *Context) (err error) {
fmt.Println(runFuncName(), "1. 判断属于哪个数额区间...")
fmt.Println(runFuncName(), "2. 获取对应node的奖品信息...")
fmt.Println(runFuncName(), "3. 复写原所有奖品信息(抽取该node节点的奖品)...")
return
}
// Lottery 抽奖模板
type Lottery struct {
// 不同抽奖类型的抽象行为
ConcreteBehavior BehaviorInterface
}
// Run 抽奖算法
// 稳定不变的算法步骤
func (lottery *Lottery) Run(ctx *Context) (err error) {
// 具体方法:校验活动编号(serial_no)是否存在、并获取活动信息
if err = lottery.checkSerialNo(ctx); err != nil {
return err
}
// 具体方法:校验活动、场次是否正在进行
if err = lottery.checkStatus(ctx); err != nil {
return err
}
// ”抽象方法“:其他参数校验
if err = lottery.checkParams(ctx); err != nil {
return err
}
// 具体方法:活动抽奖次数校验(同时扣减)
if err = lottery.checkTimesByAct(ctx); err != nil {
return err
}
// 具体方法:活动是否需要消费积分
if err = lottery.consumePointsByAct(ctx); err != nil {
return err
}
// 具体方法:场次抽奖次数校验(同时扣减)
if err = lottery.checkTimesBySession(ctx); err != nil {
return err
}
// 具体方法:获取场次奖品信息
if err = lottery.getPrizesBySession(ctx); err != nil {
return err
}
// ”抽象方法“:获取node奖品信息
if err = lottery.getPrizesByNode(ctx); err != nil {
return err
}
// 具体方法:抽奖
if err = lottery.drawPrizes(ctx); err != nil {
return err
}
// 具体方法:奖品数量判断
if err = lottery.checkPrizesStock(ctx); err != nil {
return err
}
// 具体方法:组装奖品信息
if err = lottery.packagePrizeInfo(ctx); err != nil {
return err
}
return
}
// checkSerialNo 校验活动编号(serial_no)是否存在
func (lottery *Lottery) checkSerialNo(ctx *Context) (err error) {
fmt.Println(runFuncName(), "校验活动编号(serial_no)是否存在、并获取活动信息...")
return
}
// checkStatus 校验活动、场次是否正在进行
func (lottery *Lottery) checkStatus(ctx *Context) (err error) {
fmt.Println(runFuncName(), "校验活动、场次是否正在进行...")
return
}
// checkParams 其他参数校验(不同活动类型实现不同)
// 不同场景变化的算法 转化为依赖抽象
func (lottery *Lottery) checkParams(ctx *Context) (err error) {
// 实际依赖的接口的抽象方法
return lottery.ConcreteBehavior.checkParams(ctx)
}
// checkTimesByAct 活动抽奖次数校验
func (lottery *Lottery) checkTimesByAct(ctx *Context) (err error) {
fmt.Println(runFuncName(), "活动抽奖次数校验...")
return
}
// consumePointsByAct 活动是否需要消费积分
func (lottery *Lottery) consumePointsByAct(ctx *Context) (err error) {
fmt.Println(runFuncName(), "活动是否需要消费积分...")
return
}
// checkTimesBySession 活动抽奖次数校验
func (lottery *Lottery) checkTimesBySession(ctx *Context) (err error) {
fmt.Println(runFuncName(), "活动抽奖次数校验...")
return
}
// getPrizesBySession 获取场次奖品信息
func (lottery *Lottery) getPrizesBySession(ctx *Context) (err error) {
fmt.Println(runFuncName(), "获取场次奖品信息...")
return
}
// getPrizesByNode 获取node奖品信息(不同活动类型实现不同)
// 不同场景变化的算法 转化为依赖抽象
func (lottery *Lottery) getPrizesByNode(ctx *Context) (err error) {
// 实际依赖的接口的抽象方法
return lottery.ConcreteBehavior.getPrizesByNode(ctx)
}
// drawPrizes 抽奖
func (lottery *Lottery) drawPrizes(ctx *Context) (err error) {
fmt.Println(runFuncName(), "抽奖...")
return
}
// checkPrizesStock 奖品数量判断
func (lottery *Lottery) checkPrizesStock(ctx *Context) (err error) {
fmt.Println(runFuncName(), "奖品数量判断...")
return
}
// packagePrizeInfo 组装奖品信息
func (lottery *Lottery) packagePrizeInfo(ctx *Context) (err error) {
fmt.Println(runFuncName(), "组装奖品信息...")
return
}
func main() {
ctx := &Context{
ActInfo: &ActInfo{
ActivityType: ConstActTypeAmount,
},
}
switch ctx.ActInfo.ActivityType {
case ConstActTypeTime: // 按时间抽奖类型
instance := &TimeDraw{}
instance.ConcreteBehavior = instance
instance.Run(ctx)
case ConstActTypeTimes: // 按抽奖次数抽奖
instance := &TimesDraw{}
instance.ConcreteBehavior = instance
instance.Run(ctx)
case ConstActTypeAmount: // 按数额范围区间抽奖
instance := &AmountDraw{}
instance.ConcreteBehavior = instance
instance.Run(ctx)
default:
// 报错
return
}
}
// 获取正在运行的函数名
func runFuncName() string {
pc := make([]uintptr, 1)
runtime.Callers(2, pc)
f := runtime.FuncForPC(pc[0])
return f.Name()
}
以下是代码执行结果:
[Running] go run ".../easy-tips/go/src/patterns/template/templateOther.go"
main.(*Lottery).checkSerialNo 校验活动编号(serial_no)是否存在、并获取活动信息...
main.(*Lottery).checkStatus 校验活动、场次是否正在进行...
main.(*AmountDraw).checkParams 按数额范围区间抽奖:特殊参数校验...
main.(*Lottery).checkTimesByAct 活动抽奖次数校验...
main.(*Lottery).consumePointsByAct 活动是否需要消费积分...
main.(*Lottery).checkTimesBySession 活动抽奖次数校验...
main.(*Lottery).getPrizesBySession 获取场次奖品信息...
main.(*AmountDraw).getPrizesByNode 1. 判断属于哪个数额区间...
main.(*AmountDraw).getPrizesByNode 2. 获取对应node的奖品信息...
main.(*AmountDraw).getPrizesByNode 3. 复写原所有奖品信息(抽取该node节点的奖品)...
main.(*Lottery).drawPrizes 抽奖...
main.(*Lottery).checkPrizesStock 奖品数量判断...
main.(*Lottery).packagePrizeInfo 组装奖品信息...
demo2代码地址:https://github.com/TIGERB/eas...
最后总结下,「模板模式」抽象过程的核心是把握不变与变:
Run
方法里的抽奖步骤 -> 被继承复用
变:不同场景下 -> 被具体实现
checkParams
参数校验逻辑getPrizesByNode
获取该节点奖品的逻辑特别说明:
1. 我的代码没有`else`,只是一个在代码合理设计的情况下自然而然无限接近或者达到的结果,并不是一个硬性的目标,务必较真。
2. 本系列的一些设计模式的概念可能和原概念存在差异,因为会结合实际使用,取其精华,适当改变,灵活使用。
我的代码没有else系列 更多文章 点击此处查看
赞 0 收藏 0 评论 1
TIGERB 发布了文章 · 2020-01-02
上篇文章《通用抽奖工具之需求分析》我们已经通过一些常见的抽奖场景,得到了符合这些抽奖场景的抽奖工具五要素:
抽奖五要素 | 要素名称 |
---|---|
第一要素 | 活动 |
第二要素 | 场次 |
第三要素 | 奖品 |
第四要素 | 中奖概率 |
第五要素 | 均匀投奖 |
以及创建一个抽奖活动的5个基本步骤,如下:
上篇文章回顾 《通用抽奖工具之需求分析》
需求已经分析完了,今天我们就来看看这通用抽奖工具具体的设计,分为如下三个部分:
第一要素活动配置
的抽奖活动表
:
-- 通用抽奖工具(万能胶Glue) glue_activity 抽奖活动表
CREATE TABLE `glue_activity` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '活动ID',
`serial_no` char(16) unsigned NOT NULL DEFAULT '' COMMENT '活动编号(md5值中间16位)',
`name` varchar(255) unsigned NOT NULL DEFAULT '' COMMENT '活动名称',
`description` varchar(255) unsigned NOT NULL DEFAULT '' COMMENT '活动描述',
`activity_type` tinyint(1) unsigned NOT NULL DEFAULT '1' COMMENT '活动抽奖类型1: 按时间抽奖 2: 按抽奖次数抽奖 3:按数额范围区间抽奖',
`probability_type` tinyint(1) unsigned NOT NULL DEFAULT '1' COMMENT '中奖概率类型1: static 2: dynamic',
`times_limit` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '抽奖次数限制,0默认不限制',
`start_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '活动开始时间',
`end_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '活动结束时间',
`create_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间',
`create_by` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建人staff_id',
`update_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间',
`update_by` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '修改人staff_id',
`status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '状态 -1:deleted, 0:disable, 1:enable',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='抽奖活动表';
第二要素场次配置
的抽奖场次表
:
-- 通用抽奖工具(万能胶Glue) glue_session 抽奖场次表
CREATE TABLE `glue_session` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '场次ID',
`activity_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '活动ID',
`times_limit` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '抽奖次数限制,0默认不限制',
`start_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '场次开始时间',
`end_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '场次结束时间',
`create_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间',
`create_by` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建人staff_id',
`update_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间',
`update_by` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '修改人staff_id',
`status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '状态 -1:deleted, 0:disable, 1:enable',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='抽奖场次表';
第三、四要素奖品配置
的抽奖场次奖品表
:
-- 通用抽奖工具(万能胶Glue) glue_session_prizes 抽奖场次奖品表
CREATE TABLE `glue_session_prizes` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增ID',
`session_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '场次ID',
`node` varchar(255) unsigned NOT NULL DEFAULT '' COMMENT '节点标识 按时间抽奖: 空值, 按抽奖次数抽奖: 第几次参与值, 按数额范围区间抽奖: 数额区间上限值',
`prize_type` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '奖品类型 1:优惠券, 2:积分, 3:实物, 4:空奖 ...',
`name` varchar(255) unsigned NOT NULL DEFAULT '' COMMENT '奖品名称',
`pic_url` varchar(255) unsigned NOT NULL DEFAULT '' COMMENT '奖品图片',
`value` varchar(255) unsigned NOT NULL DEFAULT '' COMMENT '奖品抽象值 优惠券:优惠券ID, 积分:积分值, 实物: sku ID',
`probability` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '中奖概率1~100',
`create_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间',
`create_by` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建人staff_id',
`update_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间',
`update_by` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '修改人staff_id',
`status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '状态 -1:deleted, 0:disable, 1:enable',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='抽奖场次奖品表';
第五要素均匀投奖
的抽奖场次奖品定时投放器表
:
-- 通用抽奖工具(万能胶Glue) glue_session_prizes_timer 抽奖场次奖品定时投放器表
CREATE TABLE `glue_session_prizes_timer` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增ID',
`session_prizes_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '抽奖场次奖品ID',
`delivery_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '定时投放奖品数量的时间',
`prize_quantity` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '奖品数量',
`create_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间',
`create_by` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建人staff_id',
`update_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间',
`update_by` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '修改人staff_id',
`status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '状态 -1:deleted, 0:wait, 1:success',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='抽奖场次奖品定时投放器表';
其他表,抽奖记录&奖品发放记录表:
-- 通用抽奖工具(万能胶Glue) glue_user_draw_record 用户抽奖记录表
CREATE TABLE `glue_user_draw_record` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增ID',
`activity_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '活动ID',
`session_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '场次ID',
`prize_type_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '奖品类型ID',
`user_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建人user_id',
`create_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间',
`update_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间',
`status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '状态 -1:未中奖, 1:已中奖 , 2: 发奖失败 , 3: 已发奖',
`log` text COMMENT '操作信息等记录',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户抽奖记录表';
1、获取活动信息 GET {version}/glue/activity
请求参数:
字段 | 类型 | 是否必传 | 描述 |
---|---|---|---|
serial_no | string | Y | 活动编号 |
响应内容:
{
"code": "200",
"msg": "OK",
"result": {
"serial_no": "string, 活动编号",
"type": "number, 活动抽奖类型1: 按时间抽奖 2: 按抽奖次数抽奖 3:按数额范围区间抽奖",
"name": "string, 活动名称",
"description": "string, 活动描述",
"start_time": "number, 活动开始时间",
"end_time": "number, 活动开始时间",
"remaining_times": "number, 活动抽奖次数限制,0不限制",
"sessions_list":[
{
"start_time": "number, 场次开始时间",
"end_time": "number, 场次开始时间",
"remaining_times": "number, 场次抽奖次数限制,0不限制",
"prizes_list": [
{
"name": "string, 奖品名称",
"pic_url": "string, 奖品图片"
}
]
}
]
}
}
2、抽奖 POST {version}/glue/activity/draw
请求参数:
字段 | 类型 | 是否必传 | 描述 |
---|---|---|---|
serial_no | string | Y | 活动编号 |
uid | number | Y | 用户ID |
响应内容:
// 中奖
{
"code": "200",
"msg": "OK",
"result": {
"serial_no": "string, spu id",
"act_remaining_times": "number, 本活动抽奖剩余次数,0不限制",
"session_remaining_times": "number, 本场次抽奖剩余次数,0不限制",
"prizes_info":
{
"name": "string, 奖品名称",
"pic_url": "string, 奖品图片"
}
}
}
// 未中奖
{
"code": "401",
"msg": "",
"result": {
}
}
活动营销系统中的第一个字系统通用抽奖工具今天讲完了,希望对大家有一定的帮助或启示。
通用抽奖工具的代码设计特别适合设计模式中的模板模式
,你们觉着呢😏😏😏。所以,新的一年我会再写一篇《[Skr-Shop]通用抽奖工具之代码设计》吗?
(O_O)?
最后后,祝大家2020年新年🆕快乐~
[Skr Shop] 项目地址长按进入:https://github.com/skr-shop/m...
Skr Shop系列更多文章:
赞 13 收藏 11 评论 7
TIGERB 发布了文章 · 2019-12-26
首先我们先来回顾下营销体系的组成:
营销体系 |
---|
活动营销系统 |
销售营销系统 |
今天带来的是活动营销系统下的第一个独立子系统通用抽奖工具的介绍,本篇文章主要分为如下4部分:
下面是我列出来的一些常见的抽奖场景,红包雨、糖果雨、打地鼠、大转盘(九宫格)、考眼力、答题闯关、游戏闯关、支付刮刮乐、积分刮刮乐等等活动营销场景。
活动名称 | 描述 |
---|---|
红包雨 | 每日整点抢红包🧧抽奖,每个整点一般可参与一次 |
糖果雨 | 每日整点抢糖果🍬抽奖,每个整点一般可参与一次 |
打地鼠 | 每日整点打地鼠抽奖,每个整点一般可参与一次 |
大转盘(九宫格) | 某个时间段,转盘抽奖,每个场一般可参N次 |
考眼力 | 某个时间段,旋转杯子猜小球在哪个被子里,猜对可抽奖,一般每日可参与N次 |
答题闯关 | 每过一关,可参与抽奖,越到后面奖品越贵重 |
游戏闯关 | 每过一关,可参与抽奖,越到后面奖品越贵重 |
支付刮刮乐 | 支付订单后可刮奖,支付金额越大奖品越贵重 |
积分刮刮乐 | 积分刮奖,消费积分额度越大奖品越贵重 |
通过上面的活动描述,我们把整个抽奖场景归为以下三类:
类型 | 活动名称 | 维度 |
---|---|---|
按时间抽奖 | 红包雨、糖果雨、打地鼠、幸运大转盘(九宫格)、考眼力 | 时间维度 |
按抽奖次数抽奖 | 答题闯关、游戏闯关 | 参与该活动次数维度 |
按数额范围区间抽奖 | 支付刮刮乐、积分刮刮乐 | 数额区间维度 |
接着我们来看下每类抽奖活动具体的抽奖需求配置。
本小节每类抽奖活动的需求配置,分为如下三个部分:
按时间抽奖
的需求配置类型 | 活动名称 | 特点 |
---|---|---|
按时间抽奖 | 红包雨、糖果雨、打地鼠、幸运大转盘(九宫格)、考眼力 | 时间维度 |
按时间抽奖 | 是否多场次 | 单场次次数限制(次) | 总场次次数限制(次) |
---|---|---|---|
红包雨 | 是 | 1 | N |
糖果雨 | 是 | 1 | N |
打地鼠 | 是 | N | N |
幸运大转盘(九宫格) | 否 | N | N |
考眼力 | 否 | N | N |
通过上面的分析我们得到了活动和场次的概念: 一个活动需要支持多场次的配置。
红包雨的需求配置示例:
活动特征:红包雨需要支持多场次。
比如双十二期间三天、每天三场整点红包雨配置如下:
活动、场次配置:
双十二红包雨 |
---|
活动配置: |
2019-12-10 ~ 2019-12-12 |
场次配置: |
10:00:00 ~ 10:01:00 |
12:00:00 ~ 12:01:00 |
18:00:00 ~ 18:01:00 |
奖品配置:
场次 | 奖品1 | 奖品2 | --- | 奖品N |
---|---|---|---|---|
场次10:00:00 ~ 10:01:00 | 优惠券2元 | 空奖 | --- | 无 |
场次12:00:00 ~ 12:01:00 | 优惠券5元 | 空奖 | --- | 无 |
场次18:00:00 ~ 18:01:00 | 优惠券10元 | 优惠券20元 | --- | 空奖 |
上面配置的结果如下:
2019-12-10日三场整点红包雨:
2019-12-10 10:00:00 ~ 10:01:00
2019-12-10 12:00:00 ~ 12:01:00
2019-12-10 18:00:00 ~ 18:01:00
2019-12-11日三场整点红包雨:
2019-12-11 10:00:00 ~ 10:01:00
2019-12-11 12:00:00 ~ 12:01:00
2019-12-11 18:00:00 ~ 18:01:00
2019-12-12日三场整点红包雨:
2019-12-12 10:00:00 ~ 10:01:00
2019-12-12 12:00:00 ~ 12:01:00
2019-12-12 18:00:00 ~ 18:01:00
幸运大转盘的需求配置示例:
活动特征:幸运大转盘不需要多场次。
比如年货节2020-01-20 ~ 2020-02-10期间幸运大转盘配置如下:
活动、场次配置:
双十二幸运大转盘 |
---|
活动配置: |
2019-12-10 ~ 2019-12-12 |
场次配置: |
00:00:00 ~ 23:59:59 |
奖品配置:
场次 | 奖品1 | 奖品2 | --- | 奖品N |
---|---|---|---|---|
场次00:00:00 ~ 23:59:59 | 优惠券2元 | 空奖 | --- | 无 |
上面配置的结果如下:
幸运大转盘抽奖活动将于 2019-12-10 00:00:00 ~ 2019-12-12 23:59:59 进行
注意与思考:双十二幸运大转盘不需要多个场次,只配置一个场次即可,完全复用活动场次模型。
按抽奖次数抽奖
的需求配置类型 | 活动名称 | 特点 |
---|---|---|
按抽奖次数抽奖 | 答题闯关、游戏闯关 | (成功参与)当前活动次数维度 |
答题闯关的需求配置示例:
活动特征:每一关的奖品不同,一般越到后面中大奖的几率越大。
活动、场次配置:
双十二答题闯关 |
---|
活动配置: |
2019-12-10 ~ 2019-12-12 |
场次配置: |
00:00:00 ~ 23:59:59 |
奖品配置:
双十二答题闯关 | 奖品 |
---|---|
第一关 | 优惠券2元 |
第二关 | 优惠券5元 |
第三关 | 优惠券10元 |
第四关 | 优惠券20元 |
第五关 | 优惠券50元 |
第六关 | 优惠券100元 |
注意与思考:同理活动&场次配置完全复用,同幸运大转盘配置(不需要支持多场次)。
按数额范围区间抽奖
的需求配置:类型 | 活动名称 | 特点 |
---|---|---|
按数额范围区间抽奖 | 支付刮刮乐、积分刮刮乐 | 数额区间维度 |
支付刮刮乐的需求配置示例:
活动特征:不同的订单金额,一般金额越大中大奖的几率越大。
活动、场次配置:
双十二答题闯关 |
---|
活动配置: |
2019-12-10 ~ 2019-12-12 |
场次配置: |
00:00:00 ~ 23:59:59 |
奖品配置:
订单金额 | 奖品1 | 奖品2 | --- | 奖品N |
---|---|---|---|---|
0~100 | 优惠券2元 | 空奖 | --- | 无 |
100~200 | 优惠券5元 | 空奖 | --- | 无 |
200~1000 | 优惠券10元 | 优惠券20元 | --- | 空奖 |
1000以上 | 优惠券50元 | 笔记本电脑 | --- | 空奖 |
注意与思考:同理活动&场次配置完全复用,同幸运大转盘配置(不需要支持多场次)。
总结: 通过上面的分析我们得到了抽奖工具的两个要素活动和场次。
抽奖抽什么?
常见奖品类型 |
---|
优惠券 |
积分 |
实物 |
空奖 |
总结: 我们得到了抽奖工具的另一个要素奖品。
通过上面的分析我们已经得到了抽奖的三要素
那还有什么要素我们还没聊到呢?接下来来看。
抽奖自然离不开奖品的中奖概率的设置。关于中奖概率我们支持如下灵活的配置:
比如我们某次大促活动红包雨的配置如下:
活动配置 | 描述 |
---|---|
活动时间 | 2019-12-10~2019-12-12 |
活动名称 | 2019双十二大促整点红包雨 |
活动描述 | 2019双十二大促全端整点红包雨活动 |
手动设置奖品概率 | 是 |
场次 | 奖品类型 | 具体奖品 | 奖品数量 | 中奖概率 |
---|---|---|---|---|
10:00:00 ~ 10:01:00 | 优惠券 | 2元优惠券 | 2000 | 50% |
- | 优惠券 | 5元优惠券 | 1000 | 20% |
- | 空奖 | - | 5000 | 30% |
12:00:00 ~ 12:01:00 | 优惠券 | 2元优惠券 | 2000 | 50% |
- | 优惠券 | 5元优惠券 | 1000 | 20% |
- | 空奖 | - | 5000 | 30% |
18:00:00 ~ 18:01:00 | 优惠券 | 2元优惠券 | 2000 | 50% |
- | 优惠券 | 5元优惠券 | 1000 | 20% |
- | 空奖 | - | 5000 | 30% |
备注:每轮场次中奖概率之和必须为100%,否则剩余部分默认添加为空奖的中奖概率。
如何均匀的抽走奖品?
答案: 均匀投奖。
具体方式为拆分总奖品数量,到各个细致具体的时间段。以双十二幸运大转盘为例:
场次 | 奖品类型 | 具体奖品 | 奖品数量 | 中奖概率 | 投奖时间(默认提前5分钟投奖) | 投奖数量 |
---|---|---|---|---|---|---|
00:00:00 ~ 23:59:59 | 优惠券 | 2元优惠券 | 2000 | 50% | - | - |
- | - | - | - | - | 00:00:00 | 2000 |
- | - | - | - | - | 06:00:00 | 2000 |
- | - | - | - | - | 12:00:00 | 2000 |
- | - | - | - | - | 18:00:00 | 2000 |
这里我们就得到了抽奖的第五个要素:均匀投奖。
通过上面的分析,我们得到抽奖五要素如下:
抽奖五要素 | 要素名称 |
---|---|
第一要素 | 活动 |
第二要素 | 场次 |
第三要素 | 奖品 |
第四要素 | 中奖概率 |
第五要素 | 均匀投奖 |
同时我们通过抽奖五要素也得到了通用抽奖工具配置一场抽奖活动的5个基本步骤:
最后,接着一篇文章,我们将来介绍通用抽奖工具的DB设计和配置后台设计。
[Skr Shop] 项目地址长按进入:https://github.com/skr-shop/m...
Skr Shop系列更多文章:
赞 8 收藏 4 评论 0
推荐关注