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

目标:

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

使用的库:

思路

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

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

接入钉钉机器人

钉钉机器人文档

按照文档在群里新建机器人即可。我开启的是webhook自定义机器人,outgoing提送地址就是项目接收信息地址,比如:http://cron.puresai.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配置文件。

来看看效果

img

总结

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

查看github源码

啰嗦

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


防止漏打卡,利用gin和cron来做一个智能提醒
https://blog.puresai.com/2021/02/12/goexample3/
作者
puresai
许可协议