文章目录
- 获取企业微信用户id
- 发送HTTP的GET和POST请求的方法
- 实现方法和调试
- 获取企业微信access_token
- 使用 Redis 将 access_token缓存起来
- 代码优化
- 代码逻辑拆分service层和controller层
- 参数改为由客户端传递
- 企微配置文件初始化
这篇文章主要是讲一下在Go语言Gin框架中对接企业微信获取数据的一个代码示例,主要涉及到Redis的基本用法、HTTP的GET和POST请求、对接企业微信接口获取数据并返回JSON。
获取企业微信用户id
拿到需求,首先第一感觉就是查找企业微信(以下简称“企微”)的文档,文档链接如下:https://developer.work.weixin.qq.com/document/path/95402
请求方式:POST(HTTPS)
请求地址:https://qyapi.weixin.qq.com/cgi-bin/user/getuserid?access_token=ACCESS_TOKEN
请求包体:
{"mobile": "13430388888"
}
OK, 拿到了接口文档,那么下一步就是要发送一个HTTP请求了。
发送HTTP的GET和POST请求的方法
我们先来封装一个发送HTTP的GET和POST请求的方法,这类方法网上一查就能搜到一大堆,也不需要死记硬背,实在不行问一下AI也行:
//utils/funcUtils/http.gopackage funcUtilsimport ("bytes""encoding/json""fmt""io/ioutil""net/http""time"
)// HttpGet 发送GET请求
// url:请求地址
func HttpGet(url string) (map[string]interface{}, error) {client := &http.Client{Timeout: 5 * time.Second}fmt.Println("HttpGet请求三方接口信息:", url)resp, err := client.Get(url)if err != nil {fmt.Println("HttpGet调用三方接口出现了错误:", err)return nil, err}res, err := ioutil.ReadAll(resp.Body)fmt.Println("HttpGet三方接口返回信息:", bytes.NewBuffer(res), err)// 将 JSON 字符串转换为 map[string]interface{}var dataMap map[string]interface{}if err := json.Unmarshal([]byte(res), &dataMap); err != nil {fmt.Printf("Failed to unmarshal JSON: %v\n", err)return nil, err}return dataMap, nil
}// HttpPost 发送POST请求
// url: 请求地址
// data: POST请求提交的数据
// contentType: 请求体格式,如:application/json
func HttpPost(url string, data interface{}, contentType string) (map[string]interface{}, error) {//创建调用API接口的clientclient := &http.Client{Timeout: 5 * time.Second}jsonStr, _ := json.Marshal(data)fmt.Println("HttpPost请求三方接口信息:", url, bytes.NewBuffer(jsonStr))resp, err := client.Post(url, contentType, bytes.NewBuffer(jsonStr))if err != nil {fmt.Println("HttpPost调用三方接口出现了错误:", err)return nil, err}res, err := ioutil.ReadAll(resp.Body)fmt.Println("HttpPost三方接口返回信息:", bytes.NewBuffer(res), err)// 将 JSON 字符串转换为 map[string]interface{}var dataMap map[string]interface{}if err := json.Unmarshal([]byte(res), &dataMap); err != nil {fmt.Printf("Failed to unmarshal JSON: %v\n", err)return nil, err}return dataMap, err
}
实现方法和调试
接下来,新增一个controller文件,开始写实现方法:
// controllers/userController/user.go// 获取企业微信用户详情
func GetWorkWechatUserInfo(c *gin.Context) {var weworkRequestParams = map[string]string{"mobile": "132xxxxx", //传入一个手机号}workWechatAccessTokenVal := "xxxx" //企微accessToken,可以先写死url := "https://qyapi.weixin.qq.com/cgi-bin/user/getuserid?access_token=" + workWechatAccessTokenValdata, err := funcUtils.HttpPost(url, weworkRequestParams, "application/json")fmt.Println(data)fmt.Println(err)
}
上面的代码逻辑很简单,定义好需要发送POST请求的URL和参数body体,其中 access_token 先不管,先随便传一个字符串试试POST请求能不能通。
然后,定义一个路由:
// router/user.gopackage routerfunc UserRouter(e *gin.Engine) {user := e.Group("/user"){user.POST("/workWechatUserInfo", userController.GetWorkWechatUserInfo)}
}
运行一下,启动后用 postman 发送 POST请求访问 http://127.0.0.1:8080/user/workWechatUserInfo
看看控制台输出:
企微返回了一个json数据,说明我们的POST请求发送成功了。接下来,按照企微返回的提示信息,我们来获取一下 access_token。
获取企业微信access_token
同样的,找到企微的文档如下:https://developer.work.weixin.qq.com/document/path/91039
请求方式: GET(HTTPS)
请求地址: https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=ID&corpsecret=SECRET
这次是个 GET请求,刚好上面我们已经把GET请求的方法封装好了,直接调用就行。这里需要传两个参数:corpid
和 corpsecret
,这两个参数登录企业微信开发者后台就可以获取到。接下来,直接写实现的方法:
// 获取企业微信的access_token
func GetWorkWechatAccessToken() string {corpId := "test" //替换成你自己的数据,下同secret := "test"url := fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=%v&corpsecret=%v", corpId, secret)data, err := funcUtils.HttpGet(url)fmt.Println(data)fmt.Println(err)
}
然后在上面的 GetWorkWechatUserInfo()
方法中来调用这个方法:
可以看到,已经成功获取到企微返回的 access_token 了。但是这里需要仔细阅读企微的文档:注意:不能频繁调用gettoken接口,否则会受到频率拦截。
也就是说,这个接口返回的 access_token 在 7200秒之内是不变的,因此不能每次都去请求企微接口获取 access_token,需要放到缓存里。那么,最简单的,就是直接用 Redis给它存起来。
使用 Redis 将 access_token缓存起来
使用Redis也很简单,安装扩展 go get github.com/go-redis/redis/v8
,然后如下配置:
// utils/redisUtil/goRedis.gopackage redisUtilimport ("context""github.com/go-redis/redis/v8""log"
)var ctx = context.Background()
var GoRdb *redis.Clientfunc init() {// 初始化 Redis 客户端GoRdb = redis.NewClient(&redis.Options{Addr: "localhost:6379", // Redis 服务器地址Password: "", // 密码(如果没有密码则为空)DB: 0, // 使用的数据库编号})// 测试连接_, err := GoRdb.Ping(ctx).Result()if err != nil {log.Fatalf("Failed to connect to Redis: %v", err)}log.Println("Connected to Redis successfully")
}
在 GetWorkWechatUserInfo()
这个方法调用企微接口获取 access_token 之前先查一下有没有缓存,并且把整体代码结构也优化一下,该写配置文件的就写到配置文件。代码如下:
// conf/application.ymlworkWechat:requestUrl: https://qyapi.weixin.qq.com/cgi-bincorpId: xxxsecret: xxx
// controllers/userController/user.govar weworkConfig map[string]stringfunc init() {common.InitConfig()weworkConfig = make(map[string]string)weworkConfig["requestUrl"] = viper.GetString("workWechat.requestUrl")weworkConfig["corpId"] = viper.GetString("workWechat.corpId")weworkConfig["secret"] = viper.GetString("workWechat.secret")
}// 获取企业微信的access_token
func GetWorkWechatAccessToken() string {url := fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=%v&corpsecret=%v", weworkConfig["corpId"], weworkConfig["secret"])data, err := funcUtils.HttpGet(url)if err != nil {fmt.Println("GetWorkWechatAccessToken调用三方接口出现了错误:", err)return ""}if data["access_token"].(string) == "" {fmt.Println("GetWorkWechatAccessToken获取到的access_token为空")return ""}return data["access_token"].(string)
}// 获取企业微信用户详情
func GetWorkWechatUserInfo(c *gin.Context) {var ctx = context.Background()workWechatAccessTokenKey := "workWechatAccessTokenKey"workWechatAccessTokenVal := redisUtil.GoRdb.Get(ctx, workWechatAccessTokenKey).Val()if workWechatAccessTokenVal == "" {workWechatAccessTokenVal = GetWorkWechatAccessToken()if workWechatAccessTokenVal == "" {controllers.ReturnError(c, 400, fmt.Sprintf("获取企业微信access_token失败"))}expiration, _ := time.ParseDuration("7000s") //7200-200redisUtil.GoRdb.Set(ctx, workWechatAccessTokenKey, workWechatAccessTokenVal, expiration)}var weworkRequestParams = map[string]string{"mobile": "132xxxx",}url := "https://qyapi.weixin.qq.com/cgi-bin/user/getuserid?access_token=" + workWechatAccessTokenValdata, err := funcUtils.HttpPost(url, weworkRequestParams, "application/json")if err != nil {fmt.Println("GetWorkWechatUserInfo调用企微接口出现了错误:", err)controllers.ReturnError(c, 400, "获取失败(-1)")}if data["userid"].(string) == "" {fmt.Println("GetWorkWechatUserInfo获取到的userid为空")controllers.ReturnError(c, 400, "获取失败(-2)")}result := map[string]string{"work_wechat_user_id": data["userid"].(string),}controllers.ReturnSuccess(c, 200, "获取成功", result, 1)
}
再次运行一下:
至此,已经成功获取到了企业微信的用户id。
代码优化
实现了基本功能之后,回过头来,我们再来阅读一下整体的代码,发现还有很大的优化空间。
代码逻辑拆分service层和controller层
上面代码中的 GetWorkWechatUserInfo()
方法 把Gin框架的入口逻辑和实现的业务逻辑混在了一起,不方便代码的维护和复用。因此,可以拆分出来一层 service 层,或者说是逻辑层:
// service/work_wechat/workWechatUser.gopackage work_wechatvar weworkConfig map[string]stringtype WorkWechatUser struct {}func NewWorkWechatUser() *WorkWechatUser {return &WorkWechatUser{}
}func (we *WorkWechatUser) init() {common.InitConfig()weworkConfig = make(map[string]string)weworkConfig["requestUrl"] = viper.GetString("workWechat.requestUrl")weworkConfig["corpId"] = viper.GetString("workWechat.corpId")weworkConfig["secret"] = viper.GetString("workWechat.secret")
}// 获取企业微信的access_token
func (ctx *WorkWechatUser) GetWorkWechatAccessToken() (string, error) {//... 忽略部分代码return data["access_token"].(string), nil
}// 获取企业微信用户详情
func (ctx *WorkWechatUser) GetWorkWechatUserInfo() (map[string]string, error) {//... 忽略部分代码//返回结果result := map[string]string{"work_wechat_user_id": data["userid"].(string),}return result, nil
}
可以看的出来,在 service 中,不涉及任何 Gin 框架的逻辑,这样也方便我们这部分的业务逻辑代码的复用。接下来定义Gin框架的controller层,用来调用上面的service层:
// controllers/userController/user.gopackage userControllerfunc GetWorkWechatUserInfo(c *gin.Context) {service := work_wechat.NewWorkWechatUser()data, err := service.GetWorkWechatUserInfo()if err != nil {controllers.ReturnError(c, 400, fmt.Sprintf("获取失败:%v", err))} else {controllers.ReturnSuccess(c, 200, "获取成功", data, 1)}
}
这样一来,代码看起来就简洁清晰了很多。
参数改为由客户端传递
上面在实现功能的时候,直接把请求参数的手机号写死在了代码里,这显然不太合理,因此需要把手机号作为参数从客户端传递。代码优化如下:
// controllers/userController/user.go
func GetWorkWechatUserInfo(c *gin.Context) {// 定义一个 map 来存储请求体中的数据var requestData map[string]interface{}// 绑定请求体中的 JSON 数据到 mapif err := c.ShouldBindJSON(&requestData); err != nil {log.Printf("Failed to bind JSON: %v", err)c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to bind JSON"})return}if requestData["mobile"] == "" {controllers.ReturnError(c, 400, fmt.Sprintf("缺少参数mobile"))return}//.... 后续逻辑...
}
企微配置文件初始化
在最开始的时候,我把企微相关的配置信息写在了一个 map 里面:
var weworkConfig map[string]stringfunc init() {common.InitConfig()weworkConfig = make(map[string]string)weworkConfig["requestUrl"] = viper.GetString("workWechat.requestUrl")weworkConfig["corpId"] = viper.GetString("workWechat.corpId")weworkConfig["secret"] = viper.GetString("workWechat.secret")
}
但实际上,如果把配置信息放在 service 初始化的结构体里面可能会更优雅一些,因此,继续优化 service 层的代码如下:
// service/work_wechat/workWechatUser.gotype WorkWechatConfig struct {requestUrl stringcorpId stringsecret string
}func NewWorkWechatConfig() *WorkWechatConfig {return &WorkWechatConfig{requestUrl: viper.GetString("workWechat.requestUrl"),corpId: viper.GetString("workWechat.corpId"),secret: viper.GetString("workWechat.secret"),}
}func (ctx *WorkWechatConfig) GetWorkWechatAccessToken() (string, error) {url := fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=%v&corpsecret=%v", ctx.corpId, ctx.secret)data, err := funcUtils.HttpGet(url)//.... 后续逻辑
}
这样,对于 controller 层:
service := work_wechat.NewWorkWechatConfig() //初始化配置信息
data, err := service.GetWorkWechatUserInfo(requestData["mobile"].(string))
这样的写法, 直接就会在调用的时候初始化企微相关的配置信息。
另外,记得把Redis的配置信息也放到配置文件里面:
// conf/application.ymlredis:host: localhostport: 6379password: ""db: 1
// utils/redisUtil/goRedis.govar ctx = context.Background()
var GoRdb *redis.Clientfunc InitRedis() {// 初始化 Redis 客户端GoRdb = redis.NewClient(&redis.Options{Addr: fmt.Sprintf("%s:%s", viper.GetString("redis.host"), viper.GetString("redis.port")), //Redis服务器地址,格式:"localhost:6379"Password: viper.GetString("redis.password"), // 密码(如果没有密码则为空)DB: viper.GetInt("redis.db"), // 使用的数据库编号})
}
个人感悟:很多逻辑看似简单,但不能只看不练,只有多加练习,才能学以致用,融会贯通。纸上得来终觉浅,绝知此事要躬行。刚开始做一件事情,比如学习一门新的编程语言或者做一个小功能,先不要想着怎么把它做的最好,而是要先做出来v0.1版本,先别管好不好,先能运行起来,后面再慢慢优化。很多人并不是有多少过人之处,只不过比其他人更加刻苦的去练习而已,练习的次数多了,自然就熟练了。
正所谓:“无他,唯手熟尔。”
完整源代码链接:https://gitee.com/rxbook/go-demo-2025