这些天在学习Go,也写了几篇关于阅读Gin后端项目代码的博客。但编程这种,一定要实际上手练习,要不然都是纸上谈兵。于是就想上手自己实际写一些代码来练练手。思来想去,不知道能写些什么来练手。后来突然想到,之前写过用Python做微信聊天机器人(博客传送门),当时代码没有放到git上,后来重置了服务器导致代码全部没了。现在正好苦于不知道做什么项目练手,可以用Go也实现一套微信聊天机器人。

说干就干,照着之前自己写的博客,看了下当时Python的代码。转而用Go优化了下并实现。
微信app
Gin的log

0. 回顾流程

根据之前Python写的自动发消息的机器人可知,要想发消息就需要三个参数:company_id、secret、angent_id。 对于这三个参数如何获取,可参考文章开头的传送门。整个发送消息过程就是 首先通过company_id和secret来调用接口获取token,再通过token和angent_id来给对应接口发送post请求,就可以把post请求体中的信息发送到微信上。

1. 项目基础配置

由于目前对Go的项目布局学习的还不是特别熟练,而且对于项目基础部分如果从头开始做的话,需要耗费大量时间。因此我使用了基于开源gin项目进行二次开发的方法,实现这个机器人。

前几天在学习Gin时,发现了一位老哥封装了个Gin脚手架,可以达到开箱即用目的。项目地址: github传送门。 里边把读取配置文件,编写路由,连接数据库等多个操作均进行了实现。因此可以基于这个项目来进行二次开发,做微信机器人。

internal
config.yamlconfig/autoloadweCaht.go
package autoload

type WeChatConfig struct {
	AgentId   string `ini:"wechat" yaml:"agent_id"`
	Secret    string `ini:"wechat" yaml:"secret"`
	CompanyId string `ini:"wechat" yaml:"company_id"`
}

var WeChat = WeChatConfig{}
config/config.go
config.Config.WeChat.CompanyId //yaml中的company_id字段
2. Redis封装

因为要给微信发送消息,首先要获取到token,而官方介绍此token的有效时长为2小时。在之前Python的项目中,是直接将token写到了文件中,通过文件来读取。在此项目中,我想直接使用redis来存储。因为使用redis来存储的话,可以设置key值时长,过了这个时长就自动清除,这样就方便了许多。

data/redis.go
func SetRedis(key string, value string, t int64) bool {
	expire := time.Duration(t) * time.Second
	if err := rdb.Set(ctx, key, value, expire).Err(); err != nil {
		return false
	}
	return true
}

func GetRedis(key string) string {
	result, err := rdb.Get(ctx, key).Result()
	if err != nil {
		return ""
	}
	return result
}

func DelRedis(key string) bool {
	_, err := rdb.Del(ctx, key).Result()
	if err != nil {
		return false
	}
	return true
}
data.SetRedis(xxx)
3.消息体封装

在最终给微信服务器发送post请求时,对应的请求体格式如下:

{
        "touser": "@all",
        "msgtype": "text",
        "agentid": "xxxxx",
        "text": {"content": "xxxx"}
    }
send_msg.go
package model

type wcSendcontent struct {
	Content string `json:"content"`
}

type WcSendMsg struct {
	ToUser  string        `json:"touser"`
	MsgType string        `json:"msgtype"`
	AgentId string        `json:"agentid"`
	Text    wcSendcontent `json:"text"`
}

func (t *WcSendMsg) SetMessage(message string) {
	t.Text.Content = message
}

这里针对message信息,专门对外暴露了一个方法来进行设置。

4. 核心代码
weChat.goSendWeChat
package service

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"

	c "github.com/wannanbigpig/gin-layout/config"
	"github.com/wannanbigpig/gin-layout/data"
	"github.com/wannanbigpig/gin-layout/internal/model"
	log "github.com/wannanbigpig/gin-layout/internal/pkg/logger"
	"github.com/wannanbigpig/gin-layout/pkg/utils"
	"go.uber.org/zap"
)

/**
 * @description: 给企微发送消息
 * @param {string} message 消息内容
 * @param {string} msgType 消息类型
 * @return {*}
 */
func SendWeChat(message string, msgType string) error {
	redis_key := "access_token"
	// 尝试从redis中读取token
	accessToken := data.GetRedis(redis_key)
	http := &utils.HttpRequest{}
	// 若redis中的token已过期,则重新请求api获取token
	if accessToken == "" {
		log.Logger.Info("access token is null, will recall")
		getTokenUrl := fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=%s&corpsecret=%s",
			c.Config.WeChat.CompanyId, c.Config.WeChat.Secret)
		log.Logger.Info("token_url", zap.String("url", getTokenUrl))
		http.Request("GET", getTokenUrl, nil)
		ret := make(map[string]interface{})
		if err := http.ParseJson(&ret); err != nil {
			return err
		}
		marshal, _ := json.Marshal(ret)
		log.Logger.Info(string(marshal))
		accessToken = fmt.Sprintf("%v", ret["access_token"])
		// 写入redis 有效期2小时
		data.SetRedis(redis_key, accessToken, 7200)
	}
	msg := &model.WcSendMsg{
		ToUser:  "@all",
		MsgType: msgType,
		AgentId: c.Config.WeChat.AgentId,
	}
	msg.SetMessage(message)
	sendMsgUrl := fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=%v", accessToken)
	log.Logger.Info("sendMsgUrl = " + string(sendMsgUrl))
	header := map[string]string{"Content-Type": "application/json"}
	bytesData, _ := json.Marshal(msg)
	http.Request("POST", sendMsgUrl, bytes.NewReader(bytesData), header)
	log.Logger.Info("bytes data = " + string(bytesData))
	ret := make(map[string]interface{})
	err := http.ParseJson(&ret)
	if err != nil {
		return err
	}
	if ret["errcode"].(float64) != 0 {
		errmsg := fmt.Sprintf("%v", ret["errmsg"])
		return errors.New(errmsg)
	}
	return nil
}

从上面代码中可以看出,首先是通过redis来获取token,若没有则请求api获取token,并将其写入到redis中,有效期为2小时。然后生成一个之前封装的消息的结构体,将AgentId和message进行填充后,通过发送post请求,已达到发消息的目的。

5.本地测试
weChat.go
package wechat

import (
	"github.com/gin-gonic/gin"
	"github.com/wannanbigpig/gin-layout/internal/pkg/error_code"
	log "github.com/wannanbigpig/gin-layout/internal/pkg/logger"
	r "github.com/wannanbigpig/gin-layout/internal/pkg/response"
	"github.com/wannanbigpig/gin-layout/internal/service"
)

func SendMsg(c *gin.Context) {
	msg, ok := c.GetQuery("msg")
	if !ok {
		msg = "please input message"
	}
	log.Logger.Info("send wechat message: " + msg)
	err := service.SendWeChat(msg, "text")
	if err != nil {
		r.Resp().FailCode(c, error_code.FAILURE, err.Error())
		return
	}
	r.Success(c, "success")
}
weChatRouter.go
package routers

import (
	"github.com/gin-gonic/gin"
	w "github.com/wannanbigpig/gin-layout/internal/controller/wechat"
)

func setWeChatRouter(r *gin.Engine) {
	// version 1
	v1 := r.Group("wechat")
	{
		v1.GET("/send", w.SendMsg)
	}
}
wechat/sendrouters/router.go
curl --location --request GET "http://${IP}:${PORT}/wechat/send?msg=Hello,Golang"

执行这个命令,就可以得到本文开头的截图。

当然,这个api接口主要是为了让我们验证,实际项目运行时,建议不要这么搞。因为这接口没有任何鉴权的措施,如果对外暴露了出去,那么别人也可以肆意的调用这个接口给你的企微发送消息。