一段骚操作 我又搞出了啥
又一周没写文章了,本着我一天一周两周一更新一贯频繁懒散的风格,开始了今天的文章。
需求的诞生
上周末在复盘的时候,因为我的一些笔记都是记在印象笔记里的,包括日常工作,学习安排计划、读书计划以及代办事项。有些时候,没有特别留意的话,容易忘记一些琐事。苹果备忘录?不行,它不会消息提醒你。苹果还有一个提醒事项,截个图感受下:
不行不行不行,这个太麻烦了,写完还要选时间。我就不能一句话写上明天9点通知我大乐透开奖,你就给我安排上吗?本着一个不会制造需求的程序员不是一个好产品思想观念,我决定一波操作完成这个需求。
开始落地
好了,现在要实现用户在公众号 库里的深夜食堂 发送一段带有时间的文字,根据上面的时间,在特定的时间内进行通知,提醒对方有未完成的代办事项。本来想着用微信公众号的订阅消息实现,但是奈何自己手上只有一个未认证的个人订阅号,没有订阅消息的权限。只能被动的接收和回复用户发送的消息。那就只能你了,兄弟。
没有了订阅消息,也就意味着我需要接入外部的通知手段来实现。无非就在于短信和邮箱。还有心灵感应?这个我只有一颗心,忙不过来。邮箱通知,如果微信没开启的话,也收不到,那就只剩下短信了。好了,现在你在微信公众号发送一段需要通知的内容和时间,然后我在对应的时间点发送给你短信,就像这样:
然后第二天六点30分你会准时准点收到一条短信。
开始有了一丝丝的沙雕样子,嗯,这正是我想要的效果。根据发送公众号发送内容,提取出用户交代的时间以及联系方式。时间方面,只要把正常人表达的时间规则提取出来就完事了。比如说明天上午9点、2020-05-20 05:20、后天上午9点20分或者明天19:70,这四种表达应该包含了大部分表达式了,也正是现在程序所支持的。
你总不能来个:
下星期一的前一天提醒我告诉小姐姐,先别急着嫁人,再等我一期双色球。
你要是这么写,那我也只能返回给你:
开始实现
其实核心是由Go标准库代码包的 time 实现的。我们把用户每条需要发送的通知存入到数据库中。初始化项目的时候,专门拉出一个 Goroutine 实现一个三小时触发一次的断续器,查看数据库中接下来三小时内需要发送通知的消息。
func Scheduler() {
//3小时醒过来一次查看下一个三小时内要发送的短信
for {
if isFirst == false {
timer := time.NewTicker(3 * time.Hour)
<-timer.C
}
isFirst = false
now := GetLocalTimeNow()
hh, _ := time.ParseDuration("1h")
threeTime := now.Add(hh * 3)
//查询最近三小时内有没有要发送的短信
rows, err := models.Db.Query(
"select * from todos where notice_time>? and notice_time<? and status=?",
SetFormatTime(now), SetFormatTime(threeTime), 2)
if err != nil {
log.Println(err.Error())
}
for rows.Next() {
var todo = models.Todo{}
var email = &Email{}
var phone = &Phone{}
if err = rows.Scan(&todo.Id, &todo.Content, &todo.CreatedAt,
&todo.NoticeTime, &todo.Status, &todo.Phone, &todo.Email); err != nil {
log.Println(err.Error())
}
email.Body = todo.Content
phone.Phone = todo.Phone
phone.Id = todo.Id
go func(todo2 models.Todo, email2 *Email, phone2 *Phone) {
SendEmailOrPhone(todo2, email2, phone2)
}(todo, email, phone)
todos = append(todos, todo)
}
}
}
每一条开启一个 Goroutine 调用 SendEmailOrPhone(),里面再计算出具体距离通知的时间,分别启动一个时间间隔的定时器。
func SendEmailOrPhone(todo models.Todo, email *Email, phone *Phone) {
//查看这条通知离现在的时间 定时器安排一下发送
now := GetLocalTimeNow()
noticeTime, _ := time.ParseInLocation("2006-01-02 15:04:05",
todo.NoticeTime.Format("2006-01-02 15:04:05"), time.Local)
diff := noticeTime.Sub(now)
timer = time.NewTimer(diff)
<-timer.C
//暂时关闭邮箱提醒
//email.SendNotice()
phone.SendNotice(todo.Id)
log.Println("此次发送的手机号是:%d", phone)
log.Println("主键id是:", todo.Id)
}
有人会说,如果这时候在这三小时内,有新的通知进来,而且需要通知时间还是当前断续器内需要发送的,那么岂不是就通知不到了?因为下一次的断续器肯定不会查询到这条通知。对应这样的通知,除了插入到表的操作,另外也单独给它启动一个定时器,发送成功的通知得标记一下,防止二次发送。核心部分:
//通知时间小于现在的3小时,直接搞个定时器
func isCreateTimerForSendNotice(lastId int64, sendTime string, createdTime time.Time, phone string) time.Duration {
log.Println(sendTime)
cstSh, _ := time.LoadLocation("Asia/Shanghai")
noticeTime, err := time.ParseInLocation("2006-01-02 15:04:05", sendTime+":00", cstSh)
if err != nil {
fmt.Println(err.Error())
}
diff := noticeTime.Sub(createdTime)
//直接给他一个定时器 执行 即使下一个断续器启动 检索信息的时候这条通知已经标注已通知了
if diff.Hours() < 3 && diff.Hours() > 0 {
var noticePhone = &Phone{}
noticePhone.Phone = phone
noticePhone.Id = lastId
go func() {
//到点执行
timer := time.NewTimer(diff)
<-timer.C
noticePhone.SendNotice(lastId)
}()
}
return diff
}
如果发送成功的话,再标注一下对应消息已通知。现在还有另一个问题,如果此时由于网络和一些奇怪的问题,导致发送失败,那这时候小姐姐接收不到短信还不得伤心死,不行,我决定不允许这种事情发生。
所以还需要加入重试机制。这个重试,我们可以使用 channel 来完成。在 Go 的哲学中:
Don’t communicate by sharing memory; share memory by communicating. (不要通过共享内存来通信,而应该通过通信来共享内存。)
接受者负责接收数据,然后重发短信,可以设置重发间隔值,发送失败可能短时间也会失败,再设置一个最大允许重试的次数。我们可以去使用 channel 高级一点的用法,通过传参去约束函数只能进行收或者发。
var ErrNoticeChannel = make(chan Phone, 10)
//错误的通知集合
var CountErr = make(map[int64]int, 10)
//接收
func getHandleErrNotice() <-chan Phone {
return ErrNoticeChannel
}
//发送
func SendHandleChannel(ch chan<- Phone, phone2 Phone) {
ch <- phone2
}
//处理发送失败通知
func HandlerErrNotice() {
handles := getHandleErrNotice()
for sendPhone := range handles {
timer := time.NewTimer(time.Second * 30)
<-timer.C
if CountErr[sendPhone.Id] >= 3 {
log.Printf("id为%d的通知重试超过三次了", sendPhone.Id)
lock.RLock()
delete(CountErr, sendPhone.Id)
lock.RUnlock()
continue
}
log.Println("重试中")
lock.RLock()
CountErr[sendPhone.Id] += 1
lock.RUnlock()
sendPhone.SendNotice(sendPhone.Id)
}
}
为什么要这样约束呢?一般在实际开发场景中,面向接口而不是实现。我们可能会这样定义一个接口:
type Notice interface {
SendInt(ch chan<- Phone)
}
那么一个类型如果想成为一个接口类型的实现类型,必然要实现它的所有方法。如果我们在某个接口定义了单向的通道,等同于定义了此接口的实现类通道类型。
现在还可以看到,这里对 map 类型变量 CountErr 操作的时候加了锁,因为 map 并不是并发安全的。但是,其实这里,我并没有在程序别处操作这个变量,仅仅在这个接收通道有对 map 进行操作,那么就可以不必使用互斥锁。这么快就忘记 Go 的哲学了?然后我们只要在发送失败的时候把数据传入通道即可。
// ......
// ......
// ......
// 通过 client 对象调用想要访问的接口,需要传入请求对象
response, err := client.SendSms(request)
// 处理异常
if _, ok := err.(*errors.TencentCloudSDKError); ok {
SendHandleChannel(ErrNoticeChannel, *phone)
fmt.Printf("An API error has returned: %s", err)
return
}
// ......
// ......
// ......
写到这里,这个简单的功能也就实现了(当然也忽略了微信接口对接,撸文档就完事了)。文本框输入实现我不说你也知道,正则匹配。没有啥分词系统,没有啥自然语言处理。而且支持的不够完善。目前能支持这些时间格式:明天上午9点、2020-05-20 05:20、后天上午9点20分或者明天19:70。
源码地址:github.com/wuqinqiang/remind-go
写在最后
最后,我突然发现我过段时间还有一件很重要的事要进行提醒。刚好这个功能做完了。那么…………(此处省略一万个心理活动),我果断拿出手机,点开 iphone 提醒应用,赶紧记录下来。
真香警告⚠️。
微信现在有专题文章了,挺好的。到时候可以分享自己看过的书单。这段时间在看 《go并发编程》和《黑天鹅》。希望之后每看一本书能做个专题的总结,方便以后查看。这篇文章原文发在一段骚操作 我又搞出了啥
本作品采用《CC 协议》,转载必须注明作者和本文链接
推荐文章: