diff --git a/.gitignore b/.gitignore index ac37619..a46a259 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ demo.php show.php -.DS_Store \ No newline at end of file +.DS_Store +.idea +go-wecomchan/wecomchan +go-wecomchan/wecomchan.exe \ No newline at end of file diff --git a/go-wecomchan/Dockerfile b/go-wecomchan/Dockerfile index 7e487b1..beb15eb 100644 --- a/go-wecomchan/Dockerfile +++ b/go-wecomchan/Dockerfile @@ -1,5 +1,8 @@ FROM golang:1.16.5-alpine3.13 as gobuilder +# 替换为国内源 +RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories + ENV GO111MODULE="on" ENV GOPROXY="https://goproxy.cn,direct" ENV CGO_ENABLED=0 diff --git a/go-wecomchan/README.md b/go-wecomchan/README.md index 546613a..2743dd9 100644 --- a/go-wecomchan/README.md +++ b/go-wecomchan/README.md @@ -18,15 +18,14 @@ docker构建镜像使用,需要安装docker,不依赖golang以及网络。 修改的sendkey,企业微信公司ID 等默认值为你的企业中的相关信息,如不设置运行时和打包后都可通过环境变量传入。 ```golang -var SENDKEY string = GetEnvDefault("SENDKEY", "set_a_sendkey") -var WECOM_CID string = GetEnvDefault("WECOM_CID", "企业微信公司ID") -var WECOM_SECRET string = GetEnvDefault("WECOM_SECRET", "企业微信应用Secret") -var WECOM_AID string = GetEnvDefault("WECOM_AID", "企业微信应用ID") -var WECOM_TOUID string = GetEnvDefault("WECOM_TOUID", "@all") -var REDIS_ADDR string = GetEnvDefault("REDIS_ADDR", "localhost:6379") -var REDIS_STAT string = GetEnvDefault("REDIS_STAT", "OFF") -var REDIS_PASSWORD string = GetEnvDefault("REDIS_PASSWORD", "") - +var Sendkey = GetEnvDefault("SENDKEY", "set_a_sendkey") +var WecomCid = GetEnvDefault("WECOM_CID", "企业微信公司ID") +var WecomSecret = GetEnvDefault("WECOM_SECRET", "企业微信应用Secret") +var WecomAid = GetEnvDefault("WECOM_AID", "企业微信应用ID") +var WecomToUid = GetEnvDefault("WECOM_TOUID", "@all") +var RedisStat = GetEnvDefault("REDIS_STAT", "OFF") +var RedisAddr = GetEnvDefault("REDIS_ADDR", "localhost:6379") +var RedisPassword = GetEnvDefault("REDIS_PASSWORD", "") ``` ## 直接使用 @@ -49,10 +48,12 @@ var REDIS_PASSWORD string = GetEnvDefault("REDIS_PASSWORD", "") 新增打包好的镜像可以直接使用 -`docker pull aozakiaoko/go-wecomchan` - +- v1_推送文本:`docker pull aozakiaoko/go-wecomchan` Docker Hub 地址为:[https://hub.docker.com/r/aozakiaoko/go-wecomchan](https://hub.docker.com/r/aozakiaoko/go-wecomchan) +- v2_推送文本or图片:`docker pull fcbhank/go-wecomchan` +Docker Hub 地址为:[https://hub.docker.com/r/fcbhank/go-wecomchan](https://hub.docker.com/r/fcbhank/go-wecomchan) + 1. 构建镜像 `docker build -t go-wecomchan .` @@ -70,10 +71,12 @@ docker run -dit -e SENDKEY=set_a_sendkey \ -e REDIS_STAT=ON \ -e REDIS_ADDR="localhost:6379" \ -e REDIS_PASSWORD="" \ --p 8080:8080 aozakiaoko/go-wecomchan +# v1 aozakiaoko/go-wecomchan +# v2 fcbhank/go-wecomchan +-p 8080:8080 go-wecomchan ``` -如不使用redis不要传入最后三个关于redis的环境变量 +如不使用redis不要传入最后三个关于redis的环境变量(REDIS_STAT|REDIS_ADDR|REDIS_PASSWORD) 4. 环境变量说明 @@ -84,9 +87,9 @@ docker run -dit -e SENDKEY=set_a_sendkey \ |WECOM_SECRET|企业微信应用Secret| |WECOM_AID|企业微信应用ID| |WECOM_TOUID|需要发送给的人,详见[企业微信官方文档](https://work.weixin.qq.com/api/doc/90000/90135/90236#%E6%96%87%E6%9C%AC%E6%B6%88%E6%81%AF)| +|REDIS_STAT|是否启用redis换缓存token,ON-启用 OFF或空-不启用| |REDIS_ADDR|redis服务器地址,如不启用redis缓存可不设置| -|REDIS_STAT|是否启用redis换缓存token| -|REDIS_PASSWORD|redis的连接密码| +|REDIS_PASSWORD|redis的连接密码,如不启用redis缓存可不设置| ## 使用docker-compose 部署 @@ -95,9 +98,20 @@ docker run -dit -e SENDKEY=set_a_sendkey \ `docker-compose up -d` ## 调用方式 - +- v1_推送文本 访问 `http://localhost:8080/wecomchan?sendkey=你配置的sendkey&&msg=需要发送的消息&&msg_type=text` +- v2_推送文本or图片 + +```bash +# 推送文本消息 +curl --location --request GET 'http://localhost:8080/wecomchan?sendkey={你的sendkey}&msg={你的文本消息}&msg_type=text' + +# 推送图片消息 +curl --location --request POST 'http://localhost:8080/wecomchan?sendkey={你的sendkey}&msg_type=image' \ +--form 'media=@"test.jpg"' +``` + ## 后续预计添加 * [x] Dockerfile 打包镜像(不依赖网络环境) diff --git a/go-wecomchan/wecomchan.go b/go-wecomchan/wecomchan.go index ffe10be..b5a5c6a 100644 --- a/go-wecomchan/wecomchan.go +++ b/go-wecomchan/wecomchan.go @@ -1,159 +1,259 @@ -package main - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io/ioutil" - "log" - "net/http" - "os" - "reflect" - "time" - - "github.com/go-redis/redis/v8" -) - -var SENDKEY string = GetEnvDefault("SENDKEY", "set_a_sendkey") -var WECOM_CID string = GetEnvDefault("WECOM_CID", "企业微信公司ID") -var WECOM_SECRET string = GetEnvDefault("WECOM_SECRET", "企业微信应用Secret") -var WECOM_AID string = GetEnvDefault("WECOM_AID", "企业微信应用ID") -var WECOM_TOUID string = GetEnvDefault("WECOM_TOUID", "@all") -var REDIS_ADDR string = GetEnvDefault("REDIS_ADDR", "localhost:6379") -var REDIS_STAT string = GetEnvDefault("REDIS_STAT", "OFF") -var REDIS_PASSWORD string = GetEnvDefault("REDIS_PASSWORD", "") -var ctx = context.Background() - -func GetEnvDefault(key, defVal string) string { - val, ex := os.LookupEnv(key) - if !ex { - return defVal - } - return val -} - -func praser_json(json_str string) map[string]interface{} { - var wecom_response map[string]interface{} - if string(json_str) != "" { - err := json.Unmarshal([]byte(string(json_str)), &wecom_response) - if err != nil { - log.Println("生成json字符串错误") - } - } - return wecom_response -} - -func get_token(corpid, app_secret string) string { - resp, err := http.Get("https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=" + corpid + "&corpsecret=" + app_secret) - if err != nil { - log.Println(err) - } - defer resp.Body.Close() - resp_data, err := ioutil.ReadAll(resp.Body) - if err != nil { - log.Println(err) - } - token_response := praser_json(string(resp_data)) - return token_response["access_token"].(string) -} - -func redis_client() *redis.Client { - rdb := redis.NewClient(&redis.Options{ - Addr: REDIS_ADDR, - Password: REDIS_PASSWORD, // no password set - DB: 0, // use default DB - }) - return rdb -} - -func post_msg(text_msg, msg_type, post_url string) string { - type msg struct { - Content string `json:"content"` - } - type JsonData struct { - Touser string `json:"touser"` - Agentid string `json:"agentid"` - Msgtype string `json:"msgtype"` - Text msg `json:"text"` - Duplicate_check_interval int `json:"duplicate_check_interval"` - } - post_data := JsonData{ - Touser: WECOM_TOUID, - Agentid: WECOM_AID, - Msgtype: msg_type, - Duplicate_check_interval: 600, - Text: msg{Content: text_msg}, - } - - post_json, _ := json.Marshal(post_data) - log.Println(string(post_json)) - msg_req, err := http.NewRequest("POST", post_url, bytes.NewBuffer(post_json)) - if err != nil { - log.Println(err) - } - msg_req.Header.Set("Content-Type", "application/json") - client := &http.Client{} - resp, err := client.Do(msg_req) - if err != nil { - panic(err) - } - defer msg_req.Body.Close() - body, _ := ioutil.ReadAll(resp.Body) - return string(body) -} - -func IsZero(v interface{}) (bool, error) { - t := reflect.TypeOf(v) - if !t.Comparable() { - return false, fmt.Errorf("type is not comparable: %v", t) - } - return v == reflect.Zero(t).Interface(), nil -} - -func main() { - var access_token string - if REDIS_STAT == "ON" { - log.Println("从redis获取token") - rdb := redis_client() - vals, err := rdb.Get(ctx, "access_token").Result() - if err == redis.Nil { - log.Println("access_token does not exist") - } - access_token = string(vals) - } - if access_token == "" { - access_token = get_token(WECOM_CID, WECOM_SECRET) - } - wecom_chan := func(res http.ResponseWriter, req *http.Request) { - post_url := "https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=" + access_token - req.ParseForm() - sendkey := req.FormValue("sendkey") - if sendkey != SENDKEY { - log.Panicln("sendkey 错误,请检查") - } - msg := req.FormValue("msg") - msg_type := req.FormValue("msg_type") - post_status := post_msg(msg, msg_type, post_url) - log.Println(post_status) - post_response := praser_json(string(post_status)) - log.Println(post_response) - errcode := post_response["errcode"] - ok, err := IsZero(errcode) - if err != nil { - fmt.Printf("%v", err) - } else { - if ok && REDIS_STAT == "ON" { - log.Println("pre to set redis key") - rdb := redis_client() - set, err := rdb.SetNX(ctx, "access_token", access_token, 7000*time.Second).Result() - log.Println(set) - if err != nil { - log.Println(err) - } - } - } - } - http.HandleFunc("/wecomchan", wecom_chan) - log.Fatal(http.ListenAndServe(":8080", nil)) -} +package main + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io/ioutil" + "log" + "mime/multipart" + "net/http" + "os" + "reflect" + "time" + + "github.com/go-redis/redis/v8" +) + +/*------------------------------- 环境变量配置 begin -------------------------------*/ + +var Sendkey = GetEnvDefault("SENDKEY", "set_a_sendkey") +var WecomCid = GetEnvDefault("WECOM_CID", "企业微信公司ID") +var WecomSecret = GetEnvDefault("WECOM_SECRET", "企业微信应用Secret") +var WecomAid = GetEnvDefault("WECOM_AID", "企业微信应用ID") +var WecomToUid = GetEnvDefault("WECOM_TOUID", "@all") +var RedisStat = GetEnvDefault("REDIS_STAT", "OFF") +var RedisAddr = GetEnvDefault("REDIS_ADDR", "localhost:6379") +var RedisPassword = GetEnvDefault("REDIS_PASSWORD", "") +var ctx = context.Background() + +/*------------------------------- 环境变量配置 end -------------------------------*/ + +/*------------------------------- 企业微信服务端API begin -------------------------------*/ + +var GetTokenApi = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=%s&corpsecret=%s" +var SendMessageApi = "https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=%s" +var UploadMediaApi = "https://qyapi.weixin.qq.com/cgi-bin/media/upload?access_token=%s&type=%s" + +/*------------------------------- 企业微信服务端API end -------------------------------*/ + +type Msg struct { + Content string `json:"content"` +} +type Pic struct { + MediaId string `json:"media_id"` +} +type JsonData struct { + ToUser string `json:"touser"` + AgentId string `json:"agentid"` + MsgType string `json:"msgtype"` + DuplicateCheckInterval int `json:"duplicate_check_interval"` + Text Msg `json:"text"` + Image Pic `json:"image"` +} + +// GetEnvDefault 获取配置信息,未获取到则取默认值 +func GetEnvDefault(key, defVal string) string { + val, ex := os.LookupEnv(key) + if !ex { + return defVal + } + return val +} + +// ParseJson 将json字符串解析为map +func ParseJson(jsonStr string) map[string]interface{} { + var wecomResponse map[string]interface{} + if string(jsonStr) != "" { + err := json.Unmarshal([]byte(string(jsonStr)), &wecomResponse) + if err != nil { + log.Println("生成json字符串错误") + } + } + return wecomResponse +} + +// GetRemoteToken 从企业微信服务端API获取access_token,存在redis服务则缓存 +func GetRemoteToken(corpId, appSecret string) string { + getTokenUrl := fmt.Sprintf(GetTokenApi, corpId, appSecret) + log.Println("getTokenUrl==>", getTokenUrl) + resp, err := http.Get(getTokenUrl) + if err != nil { + log.Println(err) + } + defer resp.Body.Close() + respData, err := ioutil.ReadAll(resp.Body) + if err != nil { + log.Println(err) + } + tokenResponse := ParseJson(string(respData)) + log.Println("企业微信获取access_token接口返回==>", tokenResponse) + accessToken := tokenResponse["access_token"].(string) + + if RedisStat == "ON" { + log.Println("prepare to set redis key") + rdb := RedisClient() + // access_token有效时间为7200秒(2小时) + set, err := rdb.SetNX(ctx, "access_token", accessToken, 7000*time.Second).Result() + log.Println(set) + if err != nil { + log.Println(err) + } + } + return accessToken +} + +// RedisClient redis客户端 +func RedisClient() *redis.Client { + rdb := redis.NewClient(&redis.Options{ + Addr: RedisAddr, + Password: RedisPassword, // no password set + DB: 0, // use default DB + }) + return rdb +} + +// PostMsg 推送消息 +func PostMsg(postData JsonData, postUrl string) string { + postJson, _ := json.Marshal(postData) + log.Println("postJson ", string(postJson)) + log.Println("postUrl ", postUrl) + msgReq, err := http.NewRequest("POST", postUrl, bytes.NewBuffer(postJson)) + if err != nil { + log.Println(err) + } + msgReq.Header.Set("Content-Type", "application/json") + client := &http.Client{} + resp, err := client.Do(msgReq) + if err != nil { + log.Fatalln("企业微信发送应用消息接口报错==>", err) + } + defer msgReq.Body.Close() + body, _ := ioutil.ReadAll(resp.Body) + mediaResp := ParseJson(string(body)) + log.Println("企业微信发送应用消息接口返回==>", mediaResp) + return string(body) +} + +// CheckOrUploadMedia 核对消息类型,如果为图片则上传临时素材并返回mediaId +func CheckOrUploadMedia(msgType string, req *http.Request, accessToken string) string { + if msgType != "image" { + log.Println("消息类型不是图片") + return "" + } + + // 企业微信图片上传不能大于2M + _ = req.ParseMultipartForm(2 << 20) + imgFile, imgHeader, err := req.FormFile("media") + log.Printf("文件大小==>%d字节", imgHeader.Size) + if err != nil { + log.Fatalln("图片文件出错==>", err) + return "" + } + buf := new(bytes.Buffer) + writer := multipart.NewWriter(buf) + if createFormFile, err := writer.CreateFormFile("media", imgHeader.Filename); err == nil { + readAll, _ := ioutil.ReadAll(imgFile) + createFormFile.Write(readAll) + } + writer.Close() + + uploadMediaUrl := fmt.Sprintf(UploadMediaApi, accessToken, msgType) + log.Println("uploadMediaUrl==>", uploadMediaUrl) + newRequest, _ := http.NewRequest("POST", uploadMediaUrl, buf) + newRequest.Header.Set("Content-Type", writer.FormDataContentType()) + log.Println("Content-Type ", writer.FormDataContentType()) + client := &http.Client{} + resp, err := client.Do(newRequest) + respData, _ := ioutil.ReadAll(resp.Body) + mediaResp := ParseJson(string(respData)) + log.Println("企业微信上传临时素材接口返回==>", mediaResp) + if err != nil { + log.Fatalln("上传临时素材出错==>", err) + return "" + } else { + return mediaResp["media_id"].(string) + } +} + +// IsZero 判断企业微信服务端是否正常响应 +func IsZero(v interface{}) (bool, error) { + t := reflect.TypeOf(v) + if !t.Comparable() { + return false, fmt.Errorf("type is not comparable: %v", t) + } + return v == reflect.Zero(t).Interface(), nil +} + +// 获取企业微信的access_token +func getAccessToken() string { + accessToken := "" + if RedisStat == "ON" { + log.Println("尝试从redis获取token") + rdb := RedisClient() + value, err := rdb.Get(ctx, "access_token").Result() + if err == redis.Nil { + log.Println("access_token does not exist, need get it from remote API") + } + accessToken = value + } + if accessToken == "" { + log.Println("get access_token from remote API") + accessToken = GetRemoteToken(WecomCid, WecomSecret) + } else { + log.Println("get access_token from redis") + } + return accessToken +} + +// InitJsonData 初始化Json公共部分数据 +func InitJsonData(msgType string) JsonData { + return JsonData{ + ToUser: WecomToUid, + AgentId: WecomAid, + MsgType: msgType, + DuplicateCheckInterval: 600, + } +} + +// 主函数入口 +func main() { + // 设置日志内容显示文件名和行号 + log.SetFlags(log.LstdFlags | log.Lshortfile) + accessToken := getAccessToken() + wecomChan := func(res http.ResponseWriter, req *http.Request) { + _ = req.ParseForm() + sendkey := req.FormValue("sendkey") + if sendkey != Sendkey { + log.Panicln("sendkey 错误,请检查") + } + msgContent := req.FormValue("msg") + msgType := req.FormValue("msg_type") + log.Println("mes_type=", msgType) + mediaId := CheckOrUploadMedia(msgType, req, accessToken) + log.Println("企业微信上传临时素材接口返回的media_id==>", mediaId) + + // 准备发送应用消息所需参数 + postData := InitJsonData(msgType) + postData.Text = Msg{ + Content: msgContent, + } + postData.Image = Pic{ + MediaId: mediaId, + } + sendMessageUrl := fmt.Sprintf(SendMessageApi, accessToken) + + postStatus := PostMsg(postData, sendMessageUrl) + postResponse := ParseJson(postStatus) + errcode := postResponse["errcode"] + _, err := IsZero(errcode) + if err != nil { + log.Printf("%v", err) + } + res.Header().Set("Content-type", "application/json") + _, _ = res.Write([]byte(postStatus)) + } + http.HandleFunc("/wecomchan", wecomChan) + log.Fatal(http.ListenAndServe(":8080", nil)) +}