前言

golang在对接话费充值这一块时,一定会选择同时接入多个渠道。这是基于以下原因:

  • 有的渠道便宜,有的渠道很贵。
  • 有的渠道对移动/联通/电信手机,不支持,或存在极大的失败率。
  • 渠道的不可靠性,需要我们同时接入多个渠道。

接入多个渠道,预先定好谁先谁后,然后写充值和回调逻辑,不是很简单吗,为什么特意形成解决方案?

这是因为:

  • 我们接入新的渠道时,不应该侵入历史接入的渠道业务代码。
  • 以前接入渠道的人,可能离职转岗,总之做了交接。
  • 充值渠道的价格浮动,可靠性浮动,可能需要变更优先顺序,这个变更操作不应该很复杂。

那么,我们要如何以最简单的形式,以兜底的形式,接入多个渠道呢?

实现分析

我们需要基于接口,来实现这一整套流程,将具体某个渠道的业务 ,和调用位置分离。

流程分析:

单渠道充值流程:

多渠道兜底流程分析

实现
channelI.go

  • channelI 订制了渠道接口,要求所有渠道都接入该接口
package core// 话费渠道接口
type HuafeiChannelI interface {// 渠道keyChannelKey() string// 冲话费的电话, 金额(分), 订单号。 返回requestBuf, responseBuf,errorCharge(phone string, amount int, orderId string) ([]byte, []byte, error)// 回调地址NotifyURL() string
}

wrapChannel.go

  • wrapChannel.go 对channel进行了封装,形成了链表节点。指向了下一次尝试的话费渠道对象。
package coretype wrapChannel struct {channel HuafeiChannelInext    *wrapChannel
}func newWrapChannel(channel HuafeiChannelI) *wrapChannel {return &wrapChannel{channel: channel,}
}func (o *wrapChannel) setNext(next *wrapChannel) {o.next = next
}

好了,下面是我们的核心。话费调度器
manager.go

package coreimport ("fmt"
)// 话费管理器,app全局单例
// 内部的状态全量运行期只读,所以不用加锁
type HuafeiManager struct {channels map[string]HuafeiChannelI // 存放话费充值渠道对象wrapChannels map[string]*wrapChannel // 用于定位某个回调对象sorted       []*wrapChannel          // 用于渠道回调失败降级定序closeCallback bool // 本参数仅用于测试,true时,将不会执行以下所有回调allFailFunc func(fen int, attach map[string]interface{}) // 全部失败时,则会调用allFailFunc。建议在充值话费前,优先扣除掉抵扣物,而不是抵扣物抵扣回滚onCharge func(channel HuafeiChannelI, req []byte, resp []byte, orderId string) // 每次调用充值时,都会call一次onCharge回调onNotify func(channel HuafeiChannelI, req []byte, orderId string) // 每次收到回调,都会call一次onNotify回调onSuccess func(channel HuafeiChannelI, attach map[string]interface{}) // 收到回调,并且结果为成功,则会触发OnSuccess
}// 初始化管理器对象
func NewHuafeiManager() *HuafeiManager {return &HuafeiManager{channels:     make(map[string]HuafeiChannelI),wrapChannels: make(map[string]*wrapChannel),sorted:       make([]*wrapChannel, 0, 10),}
}// 进入测试模式,将不触发回调
func (hm *HuafeiManager) WithoutCallback() {hm.closeCallback = true
}// 设置全部失败的回调
func (hm *HuafeiManager) SetAllFailFunc(f func(fen int, attach map[string]interface{})) {hm.allFailFunc = f
}// 设置充值的回调
func (hm *HuafeiManager) SetOnCharge(f func(channel HuafeiChannelI, req []byte, resp []byte, orderId string)) {hm.onCharge = f
}// 设置收到notify的回调
func (hm *HuafeiManager) SetOnNotify(f func(channel HuafeiChannelI, req []byte, orderId string)) {hm.onNotify = f
}// 设置到账的回调
func (hm *HuafeiManager) SetOnSuccess(f func(channel HuafeiChannelI, attach map[string]interface{})) {hm.onSuccess = f
}func (hm *HuafeiManager) AllFailFunc(fen int, attach map[string]interface{}) {if hm.closeCallback == true {fmt.Println("all fail")return}hm.allFailFunc(fen, attach)
}func (hm *HuafeiManager) OnCharge(channel HuafeiChannelI, req []byte, resp []byte, orderId string) {if hm.closeCallback == true {fmt.Println(fmt.Sprintf("%s_charge_req:", channel.ChannelKey()), string(req))fmt.Println(fmt.Sprintf("%s_charge_resp:", channel.ChannelKey()), string(resp))return}hm.onCharge(channel, req, resp, orderId)
}func (hm *HuafeiManager) OnNotify(channel HuafeiChannelI, req []byte, orderId string) {if hm.closeCallback == true {fmt.Println(fmt.Sprintf("%s_notify:", channel.ChannelKey()), string(req))return}hm.onNotify(channel, req, orderId)
}func (hm *HuafeiManager) OnSuccess(channel HuafeiChannelI, attach map[string]interface{}) {if hm.closeCallback == true {fmt.Println(fmt.Sprintf("%s_success:", channel.ChannelKey()))return}hm.onSuccess(channel, attach)
}// 增加一个话费渠道
func (hm *HuafeiManager) Add(channel HuafeiChannelI) {channelKey := channel.ChannelKey()hm.channels[channelKey] = channelwraped := newWrapChannel(channel)hm.wrapChannels[channelKey] = wrapedif len(hm.sorted) > 0 {hm.sorted[len(hm.sorted)-1].setNext(wraped)}hm.sorted = append(hm.sorted, wraped)
}// 充值
type ChargeResp struct {ChannelResponses []ChannelResponse // 第一次发起充值话费时的request和responseKvs map[string]string //  key为 xxx_charge_request, xxx_charge_response, xxx_notify_request 三类Err error
}type ChannelResponse struct {ChannelKey  string `json:"channel_key"`RequestBody []byte `json:"request_body"`Response    []byte `json:"rsp"`
}func (hm HuafeiManager) Charge(phone string, fen int, orderId string, attach map[string]interface{}) {var failTimes intfor i, _ := range hm.sorted {channel := hm.sorted[i].channelrequestBuf, responseBuf, e := channel.Charge(phone, fen, orderId)hm.OnCharge(channel, requestBuf, responseBuf, orderId)if e == nil {break}failTimes ++}if failTimes >= len(hm.channels) {hm.AllFailFunc(fen, attach)}return
}func (hm *HuafeiManager) HandleNotifyFail(channel HuafeiChannelI, phone string, fen int, orderId string, attach map[string]interface{}) {wc, exist := hm.wrapChannels[channel.ChannelKey()]if !exist {return}if wc.next == nil {hm.AllFailFunc(fen, attach)return}hm.chargeWithStart(wc.next.channel, phone, fen, orderId, attach)
}// 从有序渠道里,某一个渠道开始滚动充值
func (hm *HuafeiManager) chargeWithStart(channel HuafeiChannelI, phone string, amount int, orderId string, attach map[string]interface{}) {// 不处理未标记key的渠道if channel.ChannelKey() == "" {return}// 找到基准渠道的有序数组的indexstartKey := channel.ChannelKey()var hitIndex = 0for i, v := range hm.sorted {if v.channel.ChannelKey() == startKey {hitIndex = ibreak}}// 选择需要滚动的子序列var subSorted = make([]*wrapChannel, 0, 10)for i := hitIndex; i < len(hm.sorted); i ++ {subSorted = append(subSorted, hm.sorted[i])}if len(subSorted) == 0 {hm.AllFailFunc(amount, attach)return}var failTimes intfor i, _ := range subSorted {requestBuf, responseBuf, e := subSorted[i].channel.Charge(phone, amount, orderId)hm.OnCharge(subSorted[i].channel, requestBuf, responseBuf, orderId)// 成功 breakif e == nil {break}failTimes ++}if failTimes >= len(subSorted) {hm.AllFailFunc(amount, attach)return}return
}

最佳实践

笔者在生产中,接入了【欧飞】【力方】【大猿人】三类话费渠道。

在调度时,用法

package huafeitool
var HuafeiTool *core.HuafeiManagerfunc init() {HuafeiTool = core.NewHuafeiManager()// 按照Add顺序进行兜底。HuafeiTool.Add(&dayuanren.DayuanrenChannel{})HuafeiTool.Add(&lifang.LifangChannel{})HuafeiTool.Add(&oufei.OufeiChannel{})// 全部失败时回调,行为包括【等价物补回】【订单标记失败】【玩家邮件通知】HuafeiTool.SetAllFailFunc(func(fen int, attach map[string]interface{}) {gameId := commonv2.GetInt(attach, "game_id")userId := commonv2.GetInt(attach, "user_id")orderId := commonv2.GetString(attach, "order_id")var hasWithDraw boolif gameId != 0 && userId != 0 {// 余额抵扣物补回// 标记是否补回成功}// 订单标记失败if orderId != "" {if hasWithDraw {// 订单状态修改为失败,并且已经补回道具} else {// 订单标记失败,并且补回失败}}// 发送邮件通知if hasWithDraw && gameId != 0 && userId != 0 {// 发送邮件通知}})// 触发充值时的回调HuafeiTool.SetOnCharge(func(channel core.HuafeiChannelI, req []byte, resp []byte, orderId string) {// 某一个渠道充值时,应该把request和response标记打入订单中,方便回溯})// 触发notify时的回调HuafeiTool.SetOnNotify(func(channel core.HuafeiChannelI, req []byte, orderId string) {// 某一个渠道到账回调收到时,应该将request打入订单中,方便回溯})// 触发成功的回调HuafeiTool.SetOnSuccess(func(channel core.HuafeiChannelI, attach map[string]interface{}) {// 修改订单状态成功// 发放邮件通知})
}

充值时,使用管理器来充值

huafeitool.HuafeiTool.Charge("<手机号>", 100, "<订单号>", map[string]interface{}{// 附加信息"order_id": <订单号>,"user_id": <玩家id>,
})

回调逻辑(以大猿人举例)

func (o *DayuanrenChannel) Notify(manager *core.HuafeiManager) gin.HandlerFunc {return func(c *gin.Context) {type Param struct {OrderNumber string `json:"order_number"`OutTradeNum string `json:"out_trade_num"`Otime       int    `json:"otime"`State       int    `json:"state"`}var param Paramparam.OrderNumber = c.DefaultQuery("order_number", "")param.OutTradeNum = c.DefaultQuery("out_trade_num", "")param.Otime, _ = strconv.Atoi(c.DefaultQuery("otime", "-1"))param.State, _ = strconv.Atoi(c.DefaultQuery("state", "-1")) //1-成功 2-失败manager.OnNotify(&DayuanrenChannel{}, []byte(c.Request.URL.RawQuery), param.OutTradeNum)HandleCallback(param.OutTradeNum, param.State, manager)c.String(200, "success")}
}

优化后

通过Add顺序,来决定话费兜底顺序

HuafeiTool.Add(&dayuanren.DayuanrenChannel{})
HuafeiTool.Add(&lifang.LifangChannel{})
HuafeiTool.Add(&oufei.OufeiChannel{})

解决方案(7) golang话费充值多渠道兜底相关推荐

  1. Golang中的自动伸缩和自防御设计

    Raygun服务由许多活动组件构成,每个组件用于特定的任务.其中一个模块是用Golang编写的,负责对iOS崩溃报告进行处理.简而言之,它接受本机iOS崩溃报告,查找相关的dSYM文件,并生成开发者可 ...

  2. Golang 多版本管理

    如果你是一个 Golang 的用户,那么你大概率会遇到管理和维护 Golang 版本的诉求,如果你恰好同时需要开发调试两个不同版本的项目,在不考虑强制跳版本的情况下,你或许就需要使用"Gol ...

  3. 如何缓解Golang大型游戏服务器的GC压力

    背景 Golang的垃圾回收器使用的是并行三色标记回收算法.该算法对比分代算法的最大问题就是,无法区分年轻代和老年代对象,如果老年代对象非常多的话,新生代对象的回收效率就会下降.如果程序没有减慢对象分 ...

  4. Golang交叉编译Sqlite3踩坑记录

    Golang交叉编译Sqlite3踩坑记录 ,windows下编译golang go-sqlite3解决方案 众所周知Golang能够在一个平台编译不同平台可执行程序进行发布 然而在遇到需要内置处理程 ...

  5. 从系统精壮性到系统稳定性

    那条横线是整个服务的一个可靠负载边界,由于网络抖动,造成客户端重试,进而造成了一波重视流量的小高峰,这个小高峰变成了压垮骆驼的最后稻草,一个服务节点打垮,流量被负载到其他正常节点,正常节点继续被打垮, ...

  6. SAP Hybris电子商务最新功能

    SAP Hybris 电子商务6.0中国加速器是专为中国市场设计的电子商务平台,可满足企业在全渠道销售和订单履行方面的所有需求.新版的中国加速器基于SAP Hybris核心加速器之上进行开发,通过添加 ...

  7. 【Free5GC】环境安装搭建

    1.安装Ubuntu虚拟机步骤 1.1.下载最新Ubuntu Server LTS镜像文件 搜索 "ubuntu server download",到 Ubuntu 官网 下载最新 ...

  8. 各厂内推整理 | 第五期

    点击上方"朱小厮的博客",选择"设为星标" 从去年开始,整个互联网行业的态势就不容乐观,很多公司都停止了招聘甚至出现了大面积的裁员潮,找工作变得越来越困难. 皮 ...

  9. 各厂内推整理 (新增宇宙条)| 第四期

    点击上方"朱小厮的博客",选择"设为星标" 从去年开始,整个互联网行业的态势就不容乐观,很多公司都停止了招聘甚至出现了大面积的裁员潮,找工作变得越来越困难. 皮 ...

最新文章

  1. C++11中rvalue references的使用
  2. Visual Studio警告IDE0006的解决办法
  3. linux2.6.37内核接两个硬盘导致读写效率变低的问题
  4. 深入理解Oracle字符串函数Translate()
  5. Spring 的设计初衷
  6. 非暴力拆解:小熊派NB-IoT通信扩展板
  7. nginx不缓存html页面耗性能,加速nginx性能: 开启gzip和缓存
  8. 管理感悟:不要问没经过思考的问题
  9. c语言sigaction,C语言中的Sigaction和setitimer
  10. 备考OCJP认证知识点总结(五)
  11. win7计算机系统减肥,win7系统精简瘦身的操作方法
  12. 缺氧游戏 不给计算机加水,缺氧 泥土用完了怎么办 | 手游网游页游攻略大全
  13. 风格迁移篇--StarGAN:用于多域图像到图像翻译的统一生成对抗网络
  14. Cabbage教学(2)——类型转换与字符串操作
  15. java servlet验证码_Servlet 实现验证码
  16. win10可以上网但显示无法连接到Internet
  17. 华为H5快游戏如何接入广告服务
  18. TypeWriter: Neural Type Prediction with Search-based Validation基于搜索的神经网络预测器
  19. Python_从零开始学习_(27) 字符串
  20. IntelliJ IDEA 自动导包设置以及idea import导包顺序Java

热门文章

  1. JTAG基本原理及仿真器性能比较
  2. BSA-Xylan 牛血清白蛋白-木聚糖,血清白蛋白HSA/卵清白蛋白OVA/乳清白蛋白偶联糖
  3. 计算机windons无法启动,电脑开机出现windows未能启动的解决方法
  4. MATLAB字符串学习笔记
  5. 基因组变异检测SNPcalling(GATK)
  6. 短网址缩短网址源码Shortny v2.0.1
  7. 用c语言编写两整数乘积,c语言两个数相乘求积 c语言输入两个整数求乘积
  8. 用户界面设计九大原则
  9. Pyinstaller打包引用其他文件(.py或其他格式)的.py文件
  10. jieba分词的最详细解读