5.4. 防止漏打卡,利用gin和cron来做一个智能提醒

未匹配的标注

无水印图片找不到了,文中图片来自知乎自己以前的账号,现在账号叫开源到。

目标:

  • 每天10点提醒我打卡
  • 查询杭州天气

使用的库:

思路

round2里面我们做了个框架,我们不妨以此为基础,来完成这个demo。我们通过解析不同时段的提醒任务,规律地存储到redis的有序集合,10s去查询一次有没有需要提醒的任务,如有发送到钉钉。

(代码额外说明:redis我更新成了v8版本,命令前需要加上下文,注意一下)

接入钉钉机器人

钉钉机器人文档

按照文档在群里新建机器人即可。我开启的是webhook自定义机器人,outgoing提送地址就是项目接收信息地址,比如:http://cron.13sai.com/dingdingPost

建议设置成加签或ip限制,以防被恶意攻击

关键字

// util/common.go
// 就列了一些常见的,可自行扩展
func UpdateKeywords() {
    redis := model.RedisClient.Pipeline()
    key := KeyWords
    redis.HSet(model.Ctx, key, "分钟后", "1|60")
    redis.HSet(model.Ctx, key, "时后", "1|3600")
    redis.HSet(model.Ctx, key, "天后", "1|86400")
    redis.HSet(model.Ctx, key, "每天", "-1|1")
    redis.HSet(model.Ctx, key, "每周一", "2|0")
    redis.HSet(model.Ctx, key, "每周二", "2|1")
    redis.HSet(model.Ctx, key, "每周三", "2|2")
    redis.HSet(model.Ctx, key, "每周四", "2|3")
    redis.HSet(model.Ctx, key, "每周五", "2|4")
    redis.HSet(model.Ctx, key, "每周六", "2|5")
    redis.HSet(model.Ctx, key, "每周日", "2|6")
    redis.HSet(model.Ctx, key, "周一", "3|0")
    redis.HSet(model.Ctx, key, "周二", "3|1")
    redis.HSet(model.Ctx, key, "周三", "3|2")
    redis.HSet(model.Ctx, key, "周四", "3|3")
    redis.HSet(model.Ctx, key, "周五", "3|4")
    redis.HSet(model.Ctx, key, "周六", "3|5")
    ...
    redis.HSet(model.Ctx, key, "今天", "4|0")
    redis.HSet(model.Ctx, key, "明天", "4|1")
    redis.HSet(model.Ctx, key, "后天", "4|2")
    redis.HSet(model.Ctx, key, "取消", "0|0")
    redis.Exec(model.Ctx)
}

关键字,可以自行扩展,可能会有覆盖的情况,这里需要抉择,是匹配第一个还是匹配字数最多的,我此处选择后者的。

解析内容

钉钉文档的outgoing说明不全,或者是藏在哪里我没找到,可以使用@机器人接收信息打印看一下。

//关注senderId发送人id,text发送内容,senderNick发送人昵称即可

{
    "conversationId":"xxx",
    "atUsers":[
        {
            "dingtalkId":"xxx"
        }],
    "chatbotUserId":"xxx",
    "msgId":"xxx",
    "senderNick":"sai0556",
    "isAdmin":false,
    "sessionWebhookExpiredTime":1594978626787,
    "createAt":1594973226742,
    "conversationType":"2",
    "senderId":"xxx",
    "conversationTitle":"智能备忘录",
    "isInAtList":true,
    "sessionWebhook":"xxx",
    "text":{
        "content":" hello gin-frame"
    },
    "msgtype":"text"
}

定义一个struct,接收消息

type DingDingMsgContent struct {
    SenderNick string `json:"senderNick"`
    SenderId string `json:"senderId"`
    Text struct {
        Content string `json:"content"`
    } `json:"text"`
}

func DingDing(c *gin.Context) {
    data, _ := ioutil.ReadAll(c.Request.Body)
    form := DingDingMsgContent{}
    err := json.Unmarshal([]byte(data), &form)
    // err := c.ShouldBindJSON(&form)
    if  err != nil {
        fmt.Println(err)
        return
    }

    ....
}

解析,注意定义了一些特殊情况,比如绑定手机,取消任务等,做对应的特殊处理,绑定手机是为了@ 某人,否则消息容易被忽略。

func parseContent(form DingDingMsgContent) (err error) {
    str := form.Text.Content
    redis := db.RedisClient
    fmt.Println(str)

    // 要先绑定哟,不然无法@到对应的人
    index := strings.Index(str, "绑定手机")
    if index > -1 {
        reg := regexp.MustCompile("1[0-9]{10}")
        res := reg.FindAllString(str, 1)
        if len(res) < 1 || res[0] == "" {
            err = errors.New("手机格式不正确")
            return
        }
        redis.HSet(db.Ctx, util.KeyDingDingID, form.SenderId, res[0])
        util.SendDD("绑定成功")
        return
    }

    hExist := redis.HExists(db.Ctx, util.KeyDingDingID, form.SenderId)
    if !hExist.Val() {
        err = errors.New("绑定手机号才能精确提醒哦,发送--绑定手机 13456567878--@我即可")
        return 
    }

    index = strings.Index(util.StrSub(str, 0, 10), "我的提醒")
    fmt.Println(index, "---", util.StrSub(str, 0, 6))
    if index > -1 {
        www := util.QueryAllQueue(form.SenderId);
        if len(www) < 1 {
            err = errors.New("暂无任务")
            return
        } 
        msg := ""
        for key,value := range www {
            fmt.Println(strings.Index(value, "@"))
            value := value[0:strings.Index(value, "@")]
            fmt.Println(value)
            msg = util.StrCombine(msg, "任务id:", key, ",任务内容:", value, "{br}")
        }
        err = errors.New(msg)
        return
    }

    index = strings.Index(util.StrSub(str, 0, 10), "查看任务")
    fmt.Println(index, "---", util.StrSub(str, 0, 6))
    if index > -1 {
        www := util.QueryAllQueue(form.SenderId);
        if len(www) < 1 {
            err = errors.New("暂无任务")
            return
        } 
        msg := ""
        for key,value := range www {
            fmt.Println(strings.Index(value, "@"))
            value := value[0:strings.Index(value, "@")]
            fmt.Println(value)
            msg = util.StrCombine(msg, "任务id:", key, ",任务内容:", value, "{br}")
        }
        err = errors.New(msg)
        return
    }

    index = strings.Index(util.StrSub(str, 0, 10), "取消所有任务")
    fmt.Println(index, "---", util.StrSub(str, 0, 6))
    if index > -1 {
        if er := util.CancelAllQueue(form.SenderId); er != nil {
            err = er
            return
        }
        err = errors.New("取消成功")
        return
    }

    index = strings.Index(util.StrSub(str, 0, 10), "取消")
    if index > -1 {
        reg := regexp.MustCompile("[a-z0-9]{32}")
        res := reg.FindAllString(str, 1)
        if len(res) < 1 {
            err = errors.New("任务id不正确")
            return
        }
        if er := util.CancelQueue(res[0], form.SenderId); er != nil {
            err = er
            return
        }
        err = errors.New("取消成功")
        return

    }

    return
}

// 提醒内容
func tips(form DingDingMsgContent) (err error)  {
    rd := db.RedisClient
    str := form.Text.Content

    mobile := rd.HGet(db.Ctx, util.KeyDingDingID, form.SenderId).Val()
    key := util.KeyWords
    list, _ := rd.HGetAll(db.Ctx, key).Result()
    now := time.Now().Unix()
    tipsType := 1
    k := ""
    v := ""
    fmt.Println("str", str)

    index := 0

    for key, value := range list {
        index = util.UnicodeIndex(str, key)
        if index > -1 && util.StrLen(key) > util.StrLen(k) {
            fmt.Println("index", index, str, key, value)
            k = key
            v = value
        }
    }

    msg := ""
    var score int64
    if k != "" {
        kLen := util.StrLen(k)
        msg = util.StrSub(str, index+kLen)

        val := strings.Split(v, "|")
        unit := val[1]
        units,_ := strconv.Atoi(unit)

        switch val[0] {
            // 多少时间后
            case "1":
                reg := regexp.MustCompile("[0-9]{1,2}")
                res := reg.FindAllString(str, 1)
                minute, _ := strconv.Atoi(res[0])
                score = now + int64(units*minute)
            // 每周
            case "2":
                reg := regexp.MustCompile("[0-9]{1,2}")
                res := reg.FindAllString(util.StrSub(msg, 0, 7), -1)
                hour := 9
                minute := 0
                if len(res) > 0 {
                    hour, _ = strconv.Atoi(res[0])
                }
                if len(res) > 1 {
                    minute, _ = strconv.Atoi(res[1])
                }
                now = util.GetWeekTS(int64(units))
                score = now + int64(60*minute + 3600*hour)
                tipsType = 2

            // 下周
            case "3":
                reg := regexp.MustCompile("[0-9]{1,2}")
                res := reg.FindAllString(util.StrSub(msg, 0, 7), -1)
                hour := 9
                minute := 0
                if len(res) > 0 {
                    hour, _ = strconv.Atoi(res[0])
                }
                if len(res) > 1 {
                    minute, _ = strconv.Atoi(res[1])
                }
                now = util.TodayTS()
                score = now + int64(60*minute + 3600*hour + units*86400)
            case "4":
                reg := regexp.MustCompile("[0-9]{1,2}")
                res := reg.FindAllString(util.StrSub(msg, 0, 7), -1)
                hour := 9
                minute := 0
                if len(res) > 0 {
                    hour, _ = strconv.Atoi(res[0])
                }
                if len(res) > 1 {
                    minute, _ = strconv.Atoi(res[1])
                }
                now = util.TodayTS() + 86400*int64(units)
                score = now + int64(60*minute + 3600*hour)
            case "-1": 
                reg := regexp.MustCompile("[0-9]{1,10}")
                res := reg.FindAllString(util.StrSub(msg, 0, 7), -1)
                fmt.Println("res", res)
                hour := 9
                minute := 0
                if len(res) > 0 {
                    hour, _ = strconv.Atoi(res[0])
                }
                if len(res) > 1 {
                    minute, _ = strconv.Atoi(res[1])
                }
                now = util.TodayTS() + 86400
                score = now + int64(60*minute + 3600*hour)
                fmt.Println(now, score, minute, hour)
                tipsType = 3
            default:
        }
    } else {
        reg := regexp.MustCompile("(([0-9]{4})[-|/|年])?([0-9]{1,2})[-|/|月]([0-9]{1,2})日?")
        pi := reg.FindAllStringSubmatch(str, -1)
        if (len(pi) > 0 ) {
            date := pi[0]
            if date[2] == "" {
                date[2] = "2020"
            }
            location, _ := time.LoadLocation("Asia/Shanghai")
            tm2, _ := time.ParseInLocation("2006/01/02", fmt.Sprintf("%s/%s/%s", date[2], date[3], date[4]), location)
            score = util.GetZeroTime(tm2).Unix()

            msg = reg.ReplaceAllString(str, "")
            fmt.Println(msg)

        } else {
            msg = str
            score = util.TodayTS()
        }

        reg = regexp.MustCompile("[0-9]{1,10}")
        res := reg.FindAllString(util.StrSub(msg, 0, 7), -1)
        fmt.Println("res", res)
        hour := 9
        minute := 0
        if len(res) >= 1 {
            hour, _ = strconv.Atoi(res[0])
            fmt.Println("hour", hour, minute)
        }
        if len(res) > 1 {
            minute, _ = strconv.Atoi(res[1])
        }
        score += int64(60*minute + 3600*hour)
    }

    if msg == "" {
        err = errors.New("你说啥")
        return
    }
    index = util.UnicodeIndex(msg, "提醒我")
    index2 := util.UnicodeIndex(msg, "提醒")
    if index2 < 0 {
        err = errors.New("大哥,要我提醒你干啥呢?请发送--下周一13点提醒我写作业")
        return
    }

    if index < 0 && index2 > -1 {
        msg = util.StrSub(msg, index2+2)
    } else {
        msg = util.StrSub(msg, index+3)
    }

    fmt.Println(msg, mobile)
    msg = util.StrCombine(msg, "@", mobile)

    fmt.Println(score, msg, tipsType, err)
    if err != nil {
        util.SendDD(err.Error())
        return
    }

    member := util.StrCombine(strconv.Itoa(tipsType), msg)
    rd.ZAdd(db.Ctx, util.KeyCrontab, &redis.Z{
        Score: float64(score),
        Member: member,
    })

    uniqueKey := util.Md5(member)
    rd.HSet(db.Ctx, util.StrCombine(util.KeyUserCron, form.SenderId), uniqueKey, member)
    util.SendDD(fmt.Sprintf("设置成功(取消请回复:取消任务%s)--%s提醒您%s", uniqueKey, time.Unix(score, 0).Format("2006/01/02 15:04:05"), msg))
    return 
}

发送钉钉消息

这里就是对接钉钉接口,解析给需要提醒的人就行,就不做过多说明了。

func SendDD(msg string) {
        // 打印出来看看是个啥
    fmt.Println("dingding-----------")
    fmt.Println(msg)
    tips := make(map[string]interface{})
    content := make(map[string]interface{})
    tips["msgtype"] = "markdown"
    // @ 是用来提醒群里对应的人
    arr := strings.Split(msg, "@")
    // [提醒]是机器人关键字,个人建议设置机器人限制ip或使用token,比较靠谱
    content["text"] = fmt.Sprintf("%s", strings.Replace(arr[0], "{br}", " \n\n", -1))
    content["title"] = "鹅鹅鹅"

    if len(arr) > 1 {
        mobile := make([]string, 0)
        at := make(map[string]interface{})
        mobile = append(mobile, arr[1])
        at["atMobiles"] = mobile
        tips["at"] = at
        content["text"] = fmt.Sprintf("%s @%s", content["text"], arr[1])
    }

    tips["markdown"] = content

    bytesData, err := json.Marshal(tips)
    if err != nil {
        fmt.Println(err.Error() )
        return
    }
    reader := bytes.NewReader(bytesData)
    url := viper.GetString("dingding_url")
    request, err := http.NewRequest("POST", url, reader)
    if err != nil {
        return
    }
    request.Header.Set("Content-Type", "application/json;charset=UTF-8")
    client := http.Client{}
    _, err = client.Do(request)
    if err != nil {
        fmt.Println(err.Error())
        return
    }
    // 偷懒不重试了
    // respBytes, err := ioutil.ReadAll(resp.Body)
    // if err != nil {
    //     fmt.Println(err.Error())
    //     return
    // }
    // //byte数组直接转成string,优化内存
    // str := (*string)(unsafe.Pointer(&respBytes))
    // fmt.Println(*str)
}

定时发送与任务取消

这就是发送提醒的核心代码了,详细使用说明可以看下:

Golang cron 定时任务使用

func Cron() {
    c := cron.New()
    spec := "*/10 * * * * ?"
    c.AddJob(spec, Queue{})
    c.Start()
}

type Queue struct {
}

func (q Queue) Run() {
    now := time.Now().Unix()
    rd := model.RedisClient
    op := &redis.ZRangeBy{
        Min: "0",
        Max: strconv.FormatInt(now, 10),
    }
    ret, err := rd.ZRangeByScoreWithScores(model.Ctx, KeyCrontab, op).Result()
    if err != nil {
        fmt.Printf("zrangebyscore failed, err:%v\n", err)
        return
    }
    for _, z := range ret {
        fmt.Println(z.Member.(string), z.Score)
        QueueDo(z.Member.(string), z.Score)
    }
}

func QueueDo(msg string, score float64) {
    msgType := msg[0:1]
    SendDD(msg[1:])
    rd := model.RedisClient
    rd.ZRem(model.Ctx, KeyCrontab, msg)

    switch msgType {
        case "2":
            rd.ZAdd(model.Ctx, KeyCrontab, &redis.Z{
                Score: score + 7*86400,
                Member: msg,
            })
        case "3":
            rd.ZAdd(model.Ctx, KeyCrontab, &redis.Z{
                Score: score + 86400,
                Member: msg,
            })
        default:
            rd.ZRem(model.Ctx, KeyCrontab, msg)
    }
}

// 取消提醒
func CancelQueue(uniqueKey string, SenderId string) (err error) {
    rd := model.RedisClient
    member := rd.HGet(model.Ctx, StrCombine(KeyUserCron, SenderId), uniqueKey).Val()
    if member == "" {
        fmt.Println(StrCombine(KeyUserCron, SenderId), uniqueKey)
        err = errors.New("没有此任务")
        return
    }
    fmt.Println(member, "member")
    rd.ZRem(model.Ctx, KeyCrontab, member)
    rd.HDel(model.Ctx, StrCombine(KeyUserCron, SenderId), uniqueKey)
    err = errors.New("取消成功")
    return 
}

// 取消所有
func CancelAllQueue(SenderId string) (err error) {
    rd := model.RedisClient
    list, _ := rd.HGetAll(model.Ctx, StrCombine(KeyUserCron, SenderId)).Result()
    for _, value := range list {
        rd.ZRem(model.Ctx, KeyCrontab, value)
    }

    rd.Del(model.Ctx, StrCombine(KeyUserCron, SenderId))
    err = errors.New("已经取消所有提醒任务")
    return 
}

func QueryAllQueue(SenderId string) (map[string]string) {
    rd := model.RedisClient
    list, _ := rd.HGetAll(model.Ctx, StrCombine(KeyUserCron, SenderId)).Result()
    // fmt.Println(list)
    return list
}

天气与聊天给你是接了一个免费智能接口,有兴趣可查看github配置文件。

来看看效果

总结

这个demo其实主要点就是解析钉钉推送内容做对应的处理,因关键字过多,代码其实有点啰嗦,你可以自行优化,对接智能接口和钉钉接口,还是定时任务其实都是相对简单的,当然,这只是很基础的功能,你可以自行扩展。另外,这次之列出了主要代码,没有做十分详尽的说明,有兴趣可以查看源码。

查看github源码,有用不妨star一个

啰嗦

这个demo的起初也是我们几个同事老忘记打卡,有了这个demo,起初只能提醒打卡,后面陆续加入了取消、查看、查询天气等功能,大家学习技术的时候也可以考虑应用到生活场景当中,这样学习起来也比较有有趣,实践中也会发现很多想不到的问题,最后,祝大家工作愉快,不忘打卡。

关注和赞赏都是对笔者最大的支持

关注和赞赏都是对笔者最大的支持

本文章首发在 LearnKu.com 网站上。

上一篇 下一篇
讨论数量: 0
发起讨论 只看当前版本


暂无话题~