一段骚操作 我又搞出了啥

又一周没写文章了,本着我一天一周两周一更新一贯频繁懒散的风格,开始了今天的文章。

需求的诞生

上周末在复盘的时候,因为我的一些笔记都是记在印象笔记里的,包括日常工作,学习安排计划、读书计划以及代办事项。有些时候,没有特别留意的话,容易忘记一些琐事。苹果备忘录?不行,它不会消息提醒你。苹果还有一个提醒事项,截个图感受下:

不行不行不行,这个太麻烦了,写完还要选时间。我就不能一句话写上明天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

写在最后

最后,我突然发现我过段时间还有一件很重要的事要进行提醒。刚好这个功能做完了。那么:relieved::smirk:…………(此处省略一万个心理活动),我果断拿出手机,点开 iphone 提醒应用,赶紧记录下来。


真香警告⚠️。

微信现在有专题文章了,挺好的。到时候可以分享自己看过的书单。这段时间在看 《go并发编程》和《黑天鹅》。希望之后每看一本书能做个专题的总结,方便以后查看。这篇文章原文发在一段骚操作 我又搞出了啥

本作品采用《CC 协议》,转载必须注明作者和本文链接

吴亲库里

讨论数量: 7

以前搞过公众号的通知 大概是:
sms 5-26 18:00 不要浪费公司电量

做成网站, 太麻烦了吧,每次都要打开浏览器。

微信经常用,所以才会产生这个想法。

1个月前 评论
Epona

fileIMG_0442.jpg

Hey, Siri 😂

1个月前 评论
Remember (楼主) 1个月前
Epona (作者) 1个月前
Jouzeyu

file

哦?有些强迫症的我不允许有消息没有被清空

1个月前 评论
小李世界

我就不能一句话写上明天 9 点通知我大乐透开奖,你就给我安排上吗?

这个是可以的

1个月前 评论
ALMAS

话说为何不发到Go板块儿?

1个月前 评论
ALMAS (作者) 1个月前
Remember (楼主) 1个月前

可以声请个微信公众号的测试号,有所有接口权限 mp.weixin.qq.com/debug/cgi-bin/san...

1个月前 评论

我的小爱同学也行

1个月前 评论

请勿发布不友善或者负能量的内容。与人为善,比聪明更重要!