防止漏打卡,利用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)
}
定时发送与任务取消
这就是发送提醒的核心代码了,详细使用说明可以看下:
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其实主要点就是解析钉钉推送内容做对应的处理,因关键字过多,代码其实有点啰嗦,你可以自行优化,对接智能接口和钉钉接口,还是定时任务其实都是相对简单的,当然,这只是很基础的功能,你可以自行扩展。另外,这次之列出了主要代码,没有做十分详尽的说明,有兴趣可以查看源码。
啰嗦
这个demo的起初也是我们几个同事老忘记打卡,有了这个demo,起初只能提醒打卡,后面陆续加入了取消、查看、查询天气等功能,大家学习技术的时候也可以考虑应用到生活场景当中,这样学习起来也比较有有趣,实践中也会发现很多想不到的问题,最后,祝大家工作愉快,不忘打卡。