文章目录
- 获取企业微信用户id
- 获取企业微信access_token
-
- [使用 Redis 将 access_token缓存起来](#使用 Redis 将 access_token缓存起来)
- 代码优化
这篇文章主要是讲一下在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也行:
go
//utils/funcUtils/http.go
package funcUtils
import (
"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接口的client
client := &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文件,开始写实现方法:
go
// 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=" + workWechatAccessTokenVal
data, err := funcUtils.HttpPost(url, weworkRequestParams, "application/json")
fmt.Println(data)
fmt.Println(err)
}
上面的代码逻辑很简单,定义好需要发送POST请求的URL和参数body体,其中 access_token 先不管,先随便传一个字符串试试POST请求能不能通。
然后,定义一个路由:
go
// router/user.go
package router
func 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
,这两个参数登录企业微信开发者后台就可以获取到。接下来,直接写实现的方法:
go
// 获取企业微信的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
,然后如下配置:
go
// utils/redisUtil/goRedis.go
package redisUtil
import (
"context"
"github.com/go-redis/redis/v8"
"log"
)
var ctx = context.Background()
var GoRdb *redis.Client
func 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 之前先查一下有没有缓存,并且把整体代码结构也优化一下,该写配置文件的就写到配置文件。代码如下:
yaml
// conf/application.yml
workWechat:
requestUrl: https://qyapi.weixin.qq.com/cgi-bin
corpId: xxx
secret: xxx
go
// controllers/userController/user.go
var weworkConfig map[string]string
func 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-200
redisUtil.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=" + workWechatAccessTokenVal
data, 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 层,或者说是逻辑层:
go
// service/work_wechat/workWechatUser.go
package work_wechat
var weworkConfig map[string]string
type 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层:
go
// controllers/userController/user.go
package userController
func 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)
}
}
这样一来,代码看起来就简洁清晰了很多。
参数改为由客户端传递
上面在实现功能的时候,直接把请求参数的手机号写死在了代码里,这显然不太合理,因此需要把手机号作为参数从客户端传递。代码优化如下:
go
// controllers/userController/user.go
func GetWorkWechatUserInfo(c *gin.Context) {
// 定义一个 map 来存储请求体中的数据
var requestData map[string]interface{}
// 绑定请求体中的 JSON 数据到 map
if 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 里面:
go
var weworkConfig map[string]string
func 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 层的代码如下:
go
// service/work_wechat/workWechatUser.go
type WorkWechatConfig struct {
requestUrl string
corpId string
secret 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 层:
go
service := work_wechat.NewWorkWechatConfig() //初始化配置信息
data, err := service.GetWorkWechatUserInfo(requestData["mobile"].(string))
这样的写法, 直接就会在调用的时候初始化企微相关的配置信息。
另外,记得把Redis的配置信息也放到配置文件里面:
yaml
// conf/application.yml
redis:
host: localhost
port: 6379
password: ""
db: 1
go
// utils/redisUtil/goRedis.go
var ctx = context.Background()
var GoRdb *redis.Client
func 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