├── boot ├── .gitkeep └── boot.go ├── i18n └── .gitkeep ├── app ├── model │ └── .gitkeep ├── service │ └── .gitkeep ├── cron │ └── notify │ │ ├── data.go │ │ └── consumer.go └── api │ ├── hello │ └── hello.go │ └── notify │ └── notify.go ├── config ├── .gitkeep └── config.toml ├── docker └── .gitkeep ├── document └── .gitkeep ├── router ├── .gitkeep └── router.go ├── template └── .gitkeep ├── vendor └── .gitkeep ├── public ├── html │ └── .gitkeep ├── plugin │ └── .gitkeep └── resource │ ├── css │ └── .gitkeep │ ├── js │ └── .gitkeep │ └── image │ └── .gitkeep ├── go.mod ├── .gitattributes ├── .gitignore ├── main.go ├── library ├── Di │ └── Di.go └── response │ └── response.go ├── Dockerfile └── README.MD /boot/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /i18n/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/model/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /config/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docker/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /document/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /router/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /template/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/service/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/html/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/plugin/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/resource/css/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/resource/js/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/resource/image/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module gf-app 2 | 3 | require github.com/gogf/gf v1.11.5 4 | 5 | go 1.12 6 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.js linguist-language=GO 2 | *.css linguist-language=GO 3 | *.html linguist-language=GO -------------------------------------------------------------------------------- /app/cron/notify/data.go: -------------------------------------------------------------------------------- 1 | package notify 2 | 3 | type TaskData struct { 4 | Url string 5 | Data interface{} 6 | TryTime int 7 | NextDoTime int64 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .buildpath 2 | .hgignore.swp 3 | .project 4 | .orig 5 | .swp 6 | .idea/ 7 | .settings/ 8 | .vscode/ 9 | vender/ 10 | log/ 11 | composer.lock 12 | gitpush.sh 13 | pkg/ 14 | bin/ 15 | cbuild 16 | */.DS_Store 17 | main 18 | .vscode 19 | go.sum -------------------------------------------------------------------------------- /app/api/hello/hello.go: -------------------------------------------------------------------------------- 1 | package hello 2 | 3 | import ( 4 | "github.com/gogf/gf/net/ghttp" 5 | ) 6 | 7 | // Hello is a demonstration route handler for output "Hello World!". 8 | func Hello(r *ghttp.Request) { 9 | r.Response.Writeln("Hello World!") 10 | } 11 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | _ "gf-app/boot" 5 | _ "gf-app/router" 6 | "github.com/gogf/gf/frame/g" 7 | "github.com/gogf/gf/os/glog" 8 | ) 9 | 10 | func main() { 11 | glog.SetDebug(false) 12 | g.Server().SetDumpRouterMap(false) 13 | 14 | g.Server().Run() 15 | } 16 | -------------------------------------------------------------------------------- /library/Di/Di.go: -------------------------------------------------------------------------------- 1 | package di 2 | 3 | import "github.com/gogf/gf/container/gmap" 4 | 5 | var( 6 | globData = gmap.New(true) 7 | ) 8 | 9 | 10 | func Set(name string, value interface{}) { 11 | globData.Set(name, value) 12 | } 13 | 14 | func Get(name string) interface{} { 15 | return globData.Get(name) 16 | } 17 | 18 | -------------------------------------------------------------------------------- /router/router.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "gf-app/app/api/hello" 5 | "gf-app/app/api/notify" 6 | "github.com/gogf/gf/frame/g" 7 | "github.com/gogf/gf/net/ghttp" 8 | ) 9 | 10 | func init() { 11 | s := g.Server() 12 | s.Group("/", func(group *ghttp.RouterGroup) { 13 | group.ALL("/", hello.Hello) 14 | }) 15 | 16 | s.Group("/notify/", func(group *ghttp.RouterGroup) { 17 | group.ALL("QueryResidualLength", notify.QueryResidualLength) 18 | group.ALL("OpenQueue", notify.OpenQueue) 19 | group.ALL("RefuseQueue", notify.RefuseQueue) 20 | group.ALL("PushQueue", notify.PushQueue) 21 | }) 22 | 23 | } 24 | -------------------------------------------------------------------------------- /library/response/response.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | import ( 4 | "github.com/gogf/gf/net/ghttp" 5 | ) 6 | 7 | // 数据返回通用JSON数据结构 8 | type JsonResponse struct { 9 | Code int `json:"code"` // 错误码((0:成功, 1:失败, >1:错误码)) 10 | Message string `json:"message"` // 提示信息 11 | Data interface{} `json:"data"` // 返回数据(业务接口定义具体数据结构) 12 | } 13 | 14 | // 标准返回结果数据结构封装。 15 | func Json(r *ghttp.Request, code int, message string, data ...interface{}) { 16 | responseData := interface{}(nil) 17 | if len(data) > 0 { 18 | responseData = data[0] 19 | } 20 | r.Response.WriteJson(JsonResponse{ 21 | Code: code, 22 | Message: message, 23 | Data: responseData, 24 | }) 25 | } 26 | 27 | // 返回JSON数据并退出当前HTTP执行函数。 28 | func JsonExit(r *ghttp.Request, err int, msg string, data ...interface{}) { 29 | Json(r, err, msg, data...) 30 | r.Exit() 31 | } 32 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM loads/alpine:3.8 2 | 3 | LABEL maintainer="john@goframe.org" 4 | 5 | ############################################################################### 6 | # INSTALLATION 7 | ############################################################################### 8 | 9 | # 设置固定的项目路径 10 | ENV WORKDIR /var/www/gf-app 11 | 12 | # 添加应用可执行文件,并设置执行权限 13 | ADD ./bin/linux_amd64/main $WORKDIR/main 14 | RUN chmod +x $WORKDIR/main 15 | 16 | # 添加I18N多语言文件、静态文件、配置文件、模板文件 17 | ADD i18n $WORKDIR/i18n 18 | ADD public $WORKDIR/public 19 | ADD config $WORKDIR/config 20 | ADD template $WORKDIR/template 21 | 22 | ############################################################################### 23 | # START 24 | ############################################################################### 25 | WORKDIR $WORKDIR 26 | CMD ./main 27 | -------------------------------------------------------------------------------- /config/config.toml: -------------------------------------------------------------------------------- 1 | # HTTP Server 2 | [server] 3 | Address = ":8199" 4 | ServerRoot = "public" 5 | ServerAgent = "gf-app" 6 | LogPath = "/tmp/log/gf-app/server" 7 | 8 | # Logger. 9 | [logger] 10 | Path = "/tmp/log/gf-app" 11 | Level = "all" 12 | Stdout = true 13 | 14 | # Template. 15 | [viewer] 16 | Path = "template" 17 | DefaultFile = "index.html" 18 | Delimiters = ["${", "}"] 19 | 20 | # Database. 21 | [database] 22 | link = "mysql:root:12345678@tcp(127.0.0.1:3306)/test" 23 | debug = true 24 | # Database logger. 25 | [database.logger] 26 | Path = "/tmp/log/gf-app/sql" 27 | Level = "all" 28 | Stdout = true 29 | 30 | [compiler] 31 | name = "siam-app" 32 | version = "1.0.0" 33 | arch = "amd64" 34 | system = "linux,windows" 35 | output = "" 36 | path = "./bin" 37 | extra = "-ldflags \"-s -w\"" -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # 回调任务消费 2 | 3 | 类似微信支付回调,多条推送队列 4 | 5 | 将一个推送任务推送进来,消费者将会POST任务数据到指定的url(通知客户) 6 | 7 | 客户接受接口需要响应指定内容 默认"success"字段,如果没有响应该内容,则认定回调失败,将会按一定间隔再次触发推送,直到超出次数或者推送成功 8 | 9 | ## 环境依赖 10 | 11 | 基于`goframe`框架,请先查看该框架的开发文档、gf工具使用文档 12 | 13 | 打包构建:`gf build main.go` 14 | 15 | 上传构建包和其他目录(配置目录 public目录等) 运行即可 16 | 17 | ## 默认配置 18 | 19 | 默认失败4次就丢弃,每次间隔为:{0, 5, 10, 30} 20 | 21 | 默认推送的url返回"success"即为成功,若要修改,在app/cron/notify/consumer.go的`doJob`方法中找到相关逻辑修改 `if responseContext != "success" {` 22 | 23 | 24 | ## 相关api url 25 | 26 | - 打开推送入口 /notify/OpenQueue (默认打开) 27 | - 关闭推送入口 /notify/RefuseQueue 28 | - 推送数据进队列 /notify/PushQueue post两个字段 29 | - - url 30 | - - data 数组 31 | 32 | 可以参考api接口的逻辑,实现TCP版推送入口,增加响应速率。 33 | 34 | ## 相关代码所在文件 35 | 36 | boot/boot.go 开启监听的队列 37 | 38 | app/cron/notify/consumer.go 设置每次队列失败后增加的间隔 39 | 40 | ## 如何新增次数 41 | 42 | - app/cron/notify/consumer.go 新增delayTimeArray数组的元素 43 | - boot/boot.go 增加初始化队列、消费者开启(传入队列,最后一个队列传递自身就好了 反正不会用到) 44 | 45 | 46 | -------------------------------------------------------------------------------- /boot/boot.go: -------------------------------------------------------------------------------- 1 | package boot 2 | 3 | import ( 4 | "gf-app/app/cron/notify" 5 | di "gf-app/library/Di" 6 | "github.com/gogf/gf/container/gqueue" 7 | ) 8 | 9 | func init() { 10 | // ================= 方案1 go 内存队列 需要自己完成数据落地备份 以免进程挂掉或者重启的时候数据丢失 =============== 11 | // 初始化队列 12 | di.Set("queue_status", true) 13 | di.Set("queue_normal", gqueue.New()) 14 | di.Set("queue_resend1", gqueue.New()) 15 | di.Set("queue_resend2", gqueue.New()) 16 | di.Set("queue_resend3", gqueue.New()) 17 | 18 | // 启动时数据恢复 19 | // 定时数据落地 20 | 21 | // ================= 方案2 其他队列如redis等 go服务挂了不会影响数据储存 =============== 22 | 23 | // 定时生产者 24 | //gtimer.AddSingleton(10000*time.Millisecond, func() { 25 | // queue := di.Get("queue_normal").(*gqueue.Queue) 26 | // taskData := notify.TaskData{ 27 | // Url: "http://www.baidu.com", 28 | // Data: gtime.Datetime(), 29 | // TryTime: 0, 30 | // NextDoTime: gtime.Timestamp(), 31 | // } 32 | // queue.Push(taskData) 33 | //}) 34 | 35 | // 启动监听任务定时器 36 | queue := di.Get("queue_normal").(*gqueue.Queue) 37 | queueResend := di.Get("queue_resend1").(*gqueue.Queue) 38 | queueResend2 := di.Get("queue_resend2").(*gqueue.Queue) 39 | queueResend3 := di.Get("queue_resend3").(*gqueue.Queue) 40 | 41 | go notify.Consumer(queue, "normal", queueResend) 42 | go notify.Consumer(queueResend, "resend1", queueResend2) 43 | go notify.Consumer(queueResend2, "resend2", queueResend3) 44 | // 最后一个队列了,nextQueue 传递自身就可以了 45 | go notify.Consumer(queueResend3, "resend3", queueResend3) 46 | } 47 | -------------------------------------------------------------------------------- /app/api/notify/notify.go: -------------------------------------------------------------------------------- 1 | package notify 2 | 3 | import ( 4 | "fmt" 5 | "gf-app/app/cron/notify" 6 | di "gf-app/library/Di" 7 | "github.com/gogf/gf/container/gqueue" 8 | "github.com/gogf/gf/net/ghttp" 9 | "github.com/gogf/gf/os/gtime" 10 | "github.com/gogf/gf/util/gvalid" 11 | ) 12 | 13 | // 查询队列剩余数量,要重启服务之前应该先关闭添加入口,然后查询队列剩余数量 为0 后 间隔3s(防止有任务消费一半) 再stop服务 14 | func QueryResidualLength(r *ghttp.Request) { 15 | queue := di.Get("queue_normal").(*gqueue.Queue) 16 | queueResend1 := di.Get("queue_resend1").(*gqueue.Queue) 17 | queueResend2 := di.Get("queue_resend2").(*gqueue.Queue) 18 | queueResend3 := di.Get("queue_resend3").(*gqueue.Queue) 19 | 20 | text := `当前剩余 21 | normal -> %d 22 | resend1 -> %d 23 | resend2 -> %d 24 | resend3 -> %d 25 | ` 26 | 27 | r.Response.Writeln(fmt.Sprintf(text, queue.Len(), queueResend1.Len(), queueResend2.Len(), queueResend3.Len())) 28 | } 29 | 30 | // 关闭添加入口 31 | func RefuseQueue(r *ghttp.Request) { 32 | di.Set("queue_status", false) 33 | r.Response.Writeln("ok") 34 | } 35 | 36 | // 打开添加入口 37 | func OpenQueue(r *ghttp.Request) { 38 | di.Set("queue_status", true) 39 | r.Response.Writeln("ok") 40 | } 41 | 42 | // 推送进队列 43 | func PushQueue(r *ghttp.Request) { 44 | queueStatus := di.Get("queue_status") 45 | 46 | if queueStatus == false { 47 | r.Response.Writeln("queue_status is false") 48 | return 49 | } 50 | 51 | // 校验参数 52 | url := r.Get("url") 53 | data := r.Get("data") 54 | 55 | if e := gvalid.Check(url, "url", nil); e != nil { 56 | r.Response.Writeln(e.String()) 57 | return 58 | } 59 | 60 | queue := di.Get("queue_normal").(*gqueue.Queue) 61 | 62 | taskData := notify.TaskData{ 63 | Url: url.(string), 64 | Data: data, 65 | TryTime: 0, 66 | NextDoTime: gtime.Timestamp(), 67 | } 68 | queue.Push(taskData) 69 | 70 | r.Response.Writeln("ok") 71 | } 72 | -------------------------------------------------------------------------------- /app/cron/notify/consumer.go: -------------------------------------------------------------------------------- 1 | package notify 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gogf/gf/container/gqueue" 6 | "github.com/gogf/gf/net/ghttp" 7 | "github.com/gogf/gf/os/gtime" 8 | "time" 9 | ) 10 | 11 | var ( 12 | // 第一次失败马上重发 13 | // 2失败延迟5s 14 | // 3失败延迟10s 15 | // 4失败延迟30s 16 | delayTimeArray = [4]int{0, 5, 10, 30} 17 | ) 18 | 19 | /** 20 | * 队列监听入口 21 | */ 22 | func Consumer(queue *gqueue.Queue, name string, nextQueue *gqueue.Queue) { 23 | for { 24 | v := <-queue.C 25 | if v != nil { 26 | data, ok := v.(TaskData) 27 | if ok == false { 28 | // log 29 | fmt.Println("error") 30 | continue 31 | } 32 | 33 | go doJob(queue, name, nextQueue, data) 34 | } 35 | time.Sleep(time.Millisecond) 36 | } 37 | } 38 | 39 | /** 40 | * 实际任务内容 41 | */ 42 | func doJob(queue *gqueue.Queue, name string, nextQueue *gqueue.Queue, data TaskData) { 43 | // 队列最前面的还不可以消费,整个协程堵塞 等待 44 | if data.NextDoTime > gtime.Timestamp() { 45 | sleepTime := data.NextDoTime - gtime.Timestamp() 46 | time.Sleep(time.Duration(sleepTime) * time.Second) 47 | } 48 | 49 | isSuccess := false 50 | responseContext := "" 51 | 52 | if response, err := ghttp.Post(data.Url, data.Data); err != nil { 53 | afterJob(name, data, false, err.Error()) 54 | return 55 | } else { 56 | defer response.Close() 57 | responseContext = response.ReadAllString() 58 | if responseContext != "success" { 59 | isSuccess = false 60 | } else { 61 | isSuccess = true 62 | } 63 | } 64 | 65 | if isSuccess { 66 | afterJob(name, data, true) 67 | return 68 | } 69 | 70 | // 超出设置次数了 71 | if data.TryTime >= len(delayTimeArray) { 72 | fmt.Printf("超出次数 %d 结束 \n", data.TryTime) 73 | return 74 | } 75 | 76 | // 如果失败 投递到下一个梯队队列中 77 | delayTime := delayTimeArray[data.TryTime] 78 | 79 | // 第一次失败投递自身 80 | data.TryTime += 1 81 | data.NextDoTime = int64(int(gtime.Timestamp()) + delayTime) 82 | 83 | if data.TryTime == 1 { 84 | queue.Push(data) 85 | } else { 86 | nextQueue.Push(data) 87 | } 88 | 89 | afterJob(name, data, false, responseContext) 90 | } 91 | 92 | // 后置操作,比如汇总结果到日志平台 93 | func afterJob(name string, data TaskData, result bool, content ...string) { 94 | fmt.Println(gtime.Datetime()) 95 | fmt.Printf("%s -> result %t content : %s \n", name, result, content) 96 | fmt.Println(data) 97 | } 98 | --------------------------------------------------------------------------------