2021-08-17 01:55:55 +08:00
|
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
|
|
|
|
"context"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"fmt"
|
|
|
|
|
"io/ioutil"
|
|
|
|
|
"log"
|
2021-08-19 13:45:35 +08:00
|
|
|
|
"math"
|
2021-08-17 01:55:55 +08:00
|
|
|
|
"mime/multipart"
|
|
|
|
|
"net/http"
|
|
|
|
|
"os"
|
|
|
|
|
"reflect"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/go-redis/redis/v8"
|
2021-08-30 10:57:10 +08:00
|
|
|
|
"github.com/julienschmidt/httprouter"
|
2021-08-17 01:55:55 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
/*------------------------------- 环境变量配置 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 -------------------------------*/
|
|
|
|
|
|
2021-08-19 13:45:35 +08:00
|
|
|
|
const RedisTokenKey = "access_token"
|
|
|
|
|
|
2021-08-17 01:55:55 +08:00
|
|
|
|
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)
|
2021-08-19 13:45:35 +08:00
|
|
|
|
accessToken := tokenResponse[RedisTokenKey].(string)
|
2021-08-17 01:55:55 +08:00
|
|
|
|
|
|
|
|
|
if RedisStat == "ON" {
|
|
|
|
|
log.Println("prepare to set redis key")
|
|
|
|
|
rdb := RedisClient()
|
|
|
|
|
// access_token有效时间为7200秒(2小时)
|
2021-08-19 13:45:35 +08:00
|
|
|
|
set, err := rdb.SetNX(ctx, RedisTokenKey, accessToken, 7000*time.Second).Result()
|
2021-08-17 01:55:55 +08:00
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
2021-08-19 13:45:35 +08:00
|
|
|
|
// UploadMedia 上传临时素材并返回mediaId
|
|
|
|
|
func UploadMedia(msgType string, req *http.Request, accessToken string) (string, float64) {
|
2021-08-17 01:55:55 +08:00
|
|
|
|
// 企业微信图片上传不能大于2M
|
|
|
|
|
_ = req.ParseMultipartForm(2 << 20)
|
|
|
|
|
imgFile, imgHeader, err := req.FormFile("media")
|
|
|
|
|
log.Printf("文件大小==>%d字节", imgHeader.Size)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Fatalln("图片文件出错==>", err)
|
2021-08-19 13:45:35 +08:00
|
|
|
|
// 自定义code无效的图片文件
|
|
|
|
|
return "", 400
|
2021-08-17 01:55:55 +08:00
|
|
|
|
}
|
|
|
|
|
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)
|
2021-08-19 13:45:35 +08:00
|
|
|
|
return "", mediaResp["errcode"].(float64)
|
2021-08-17 01:55:55 +08:00
|
|
|
|
} else {
|
2021-08-19 13:45:35 +08:00
|
|
|
|
return mediaResp["media_id"].(string), float64(0)
|
2021-08-17 01:55:55 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-08-19 13:45:35 +08:00
|
|
|
|
// ValidateToken 判断accessToken是否失效
|
|
|
|
|
// true-未失效, false-失效需重新获取
|
|
|
|
|
func ValidateToken(errcode interface{}) bool {
|
|
|
|
|
codeTyp := reflect.TypeOf(errcode)
|
|
|
|
|
log.Println("errcode的数据类型==>", codeTyp)
|
|
|
|
|
if !codeTyp.Comparable() {
|
|
|
|
|
log.Printf("type is not comparable: %v", codeTyp)
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 如果errcode为42001表明token已失效,则清空redis中的token缓存
|
|
|
|
|
// 已知codeType为float64
|
|
|
|
|
if math.Abs(errcode.(float64)-float64(42001)) < 1e-3 {
|
|
|
|
|
if RedisStat == "ON" {
|
|
|
|
|
log.Printf("token已失效,开始删除redis中的key==>%s", RedisTokenKey)
|
|
|
|
|
rdb := RedisClient()
|
|
|
|
|
rdb.Del(ctx, RedisTokenKey)
|
|
|
|
|
log.Printf("删除redis中的key==>%s完毕", RedisTokenKey)
|
|
|
|
|
}
|
|
|
|
|
log.Println("现需重新获取token")
|
|
|
|
|
return false
|
2021-08-17 01:55:55 +08:00
|
|
|
|
}
|
2021-08-19 13:45:35 +08:00
|
|
|
|
return true
|
2021-08-17 01:55:55 +08:00
|
|
|
|
}
|
|
|
|
|
|
2021-08-19 13:45:35 +08:00
|
|
|
|
// GetAccessToken 获取企业微信的access_token
|
|
|
|
|
func GetAccessToken() string {
|
2021-08-17 01:55:55 +08:00
|
|
|
|
accessToken := ""
|
|
|
|
|
if RedisStat == "ON" {
|
|
|
|
|
log.Println("尝试从redis获取token")
|
|
|
|
|
rdb := RedisClient()
|
2021-08-19 13:45:35 +08:00
|
|
|
|
value, err := rdb.Get(ctx, RedisTokenKey).Result()
|
2021-08-17 01:55:55 +08:00
|
|
|
|
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)
|
2021-08-30 10:57:10 +08:00
|
|
|
|
wecomChan := func(res http.ResponseWriter, req *http.Request, ps httprouter.Params) {
|
2021-08-17 01:55:55 +08:00
|
|
|
|
_ = req.ParseForm()
|
2021-08-30 10:57:10 +08:00
|
|
|
|
sendkey := req.Form.Get("sendkey")
|
|
|
|
|
urlpath := ps.ByName("urlpath")
|
|
|
|
|
if len(sendkey) == 0 && len(urlpath) > 6 && urlpath != "wecomchan" {
|
|
|
|
|
sendkey = urlpath[1 : len(urlpath)-5]
|
|
|
|
|
}
|
2021-08-17 01:55:55 +08:00
|
|
|
|
if sendkey != Sendkey {
|
|
|
|
|
log.Panicln("sendkey 错误,请检查")
|
|
|
|
|
}
|
2021-08-30 10:57:10 +08:00
|
|
|
|
req.ParseForm()
|
|
|
|
|
msgContent := req.Form.Get("msg")
|
|
|
|
|
if len(msgContent) == 0 {
|
|
|
|
|
msgContent = req.Form.Get("title") + "\n" + req.Form.Get("desc")
|
|
|
|
|
}
|
|
|
|
|
msgType := req.Form.Get("msg_type")
|
2021-08-30 11:19:41 +08:00
|
|
|
|
if len(msgType) == 0 {
|
|
|
|
|
msgType = "text"
|
|
|
|
|
}
|
2021-08-17 01:55:55 +08:00
|
|
|
|
log.Println("mes_type=", msgType)
|
2021-08-19 13:45:35 +08:00
|
|
|
|
// 默认mediaId为空
|
|
|
|
|
mediaId := ""
|
2021-08-30 10:57:10 +08:00
|
|
|
|
// 获取token
|
|
|
|
|
accessToken := GetAccessToken()
|
|
|
|
|
// 默认token有效
|
|
|
|
|
tokenValid := true
|
2021-08-19 13:45:35 +08:00
|
|
|
|
if msgType != "image" {
|
|
|
|
|
log.Println("消息类型不是图片")
|
|
|
|
|
} else {
|
|
|
|
|
// token有效则跳出循环继续执行,否则重试3次
|
|
|
|
|
for i := 0; i <= 3; i++ {
|
|
|
|
|
var errcode float64
|
|
|
|
|
mediaId, errcode = UploadMedia(msgType, req, accessToken)
|
|
|
|
|
log.Printf("企业微信上传临时素材接口返回的media_id==>[%s], errcode==>[%f]\n", mediaId, errcode)
|
|
|
|
|
tokenValid = ValidateToken(errcode)
|
|
|
|
|
if tokenValid {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
accessToken = GetAccessToken()
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-08-17 01:55:55 +08:00
|
|
|
|
|
|
|
|
|
// 准备发送应用消息所需参数
|
|
|
|
|
postData := InitJsonData(msgType)
|
|
|
|
|
postData.Text = Msg{
|
|
|
|
|
Content: msgContent,
|
|
|
|
|
}
|
|
|
|
|
postData.Image = Pic{
|
|
|
|
|
MediaId: mediaId,
|
|
|
|
|
}
|
|
|
|
|
|
2021-08-19 13:45:35 +08:00
|
|
|
|
postStatus := ""
|
|
|
|
|
for i := 0; i <= 3; i++ {
|
|
|
|
|
sendMessageUrl := fmt.Sprintf(SendMessageApi, accessToken)
|
|
|
|
|
postStatus = PostMsg(postData, sendMessageUrl)
|
|
|
|
|
postResponse := ParseJson(postStatus)
|
|
|
|
|
errcode := postResponse["errcode"]
|
|
|
|
|
log.Println("发送应用消息接口返回errcode==>", errcode)
|
|
|
|
|
tokenValid = ValidateToken(errcode)
|
|
|
|
|
// token有效则跳出循环继续执行,否则重试3次
|
|
|
|
|
if tokenValid {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
// 刷新token
|
|
|
|
|
accessToken = GetAccessToken()
|
2021-08-17 01:55:55 +08:00
|
|
|
|
}
|
2021-08-19 13:45:35 +08:00
|
|
|
|
|
2021-08-17 01:55:55 +08:00
|
|
|
|
res.Header().Set("Content-type", "application/json")
|
|
|
|
|
_, _ = res.Write([]byte(postStatus))
|
|
|
|
|
}
|
2021-08-30 10:57:10 +08:00
|
|
|
|
router := httprouter.New()
|
|
|
|
|
router.GET("/*urlpath", wecomChan)
|
|
|
|
|
router.POST("/*urlpath", wecomChan)
|
|
|
|
|
log.Fatal(http.ListenAndServe(":8080", router))
|
2021-08-17 01:55:55 +08:00
|
|
|
|
}
|