commit 686071002a21451a940fa9c9912df0ffdfa8b487 Author: wzj <244142824@qq.com> Date: Sat Nov 27 17:04:57 2021 +0800 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a46a259 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +demo.php +show.php +.DS_Store +.idea +go-wecomchan/wecomchan +go-wecomchan/wecomchan.exe \ No newline at end of file diff --git a/20210208142819.png b/20210208142819.png new file mode 100644 index 0000000..f699765 Binary files /dev/null and b/20210208142819.png differ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8016b23 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Easy + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/ONLINE.md b/ONLINE.md new file mode 100644 index 0000000..6002d30 --- /dev/null +++ b/ONLINE.md @@ -0,0 +1,13 @@ +# 在线服务搭建指南(PHP版) + +## 安装条件 + +- PHP7.4+ +- JSON &&CURL 模块 +- 可访问外部网络的运行环境 + +## 安装说明 + +1. 用编辑器打开 `index.php`,按提示修改头部 define 的值( sendkey自己随意写,其他参见企业微信配置文档 ) +1. 将 `index.php` 上传运行环境 +1. 通过 `http://指向运行环境的域名/?sendkey=你设定的sendkey&text=你要发送的内容` 即可发送内容 diff --git a/README.md b/README.md new file mode 100644 index 0000000..60b8b5f --- /dev/null +++ b/README.md @@ -0,0 +1,328 @@ +# Wecom酱 + +通过企业微信向微信推送消息的解决方案。包括: + +1. 配置说明(本页下方) +2. 推送函数(支持多种语言,见本页下方) +3. 自行搭建的在线服务源码 + 1. [PHP版搭建说明](ONLINE.md) + 2. [Go版说明](go-wecomchan/README.md) + 3. [腾讯云云函数搭建说明](go-scf/) + +## 🎈 本项目属于方糖推送生态。该生态包含项目如下: + +- [Server酱Turbo](https://sct.ftqq.com):支持企业微信、微信服务号、钉钉、飞书群机器人等多通道的在线服务,无需搭建直接使用,每天有免费额度 +- [Wecom酱](https://github.com/easychen/wecomchan):通过企业微信推送消息到微信的消息推送函数和在线服务方案,开源免费,可自己搭建。支持多语言。 +- [Tele酱](https://github.com/easychen/telechan):可以通过 Vercel 免费部署,且部署后 API 在国内网络可访问的 Telegram 多账户消息推送机器人 + +## 企业微信应用消息配置说明 + +优点: + +1. 一次配置,持续使用 +1. 配置好以后,只需要微信就能收消息,不再需要安装企业微信客户端 + +PS:消息接口无需认证即可使用,个人用微信就可以注册 + +### 具体操作 + +#### 第一步,注册企业 + +用电脑打开[企业微信官网](https://work.weixin.qq.com/),注册一个企业 + +#### 第二步,创建应用 + +注册成功后,点「管理企业」进入管理界面,选择「应用管理」 → 「自建」 → 「创建应用」 + +![](https://theseven.ftqq.com/20210208143228.png) + +应用名称填入「Server酱」,应用logo到[这里](./20210208142819.png)下载,可见范围选择公司名。 + + +![](https://theseven.ftqq.com/20210208143327.png) + +创建完成后进入应用详情页,可以得到应用ID( `agentid` )①,应用Secret( `secret` )②。 + +注意:`secret`推送到手机端时,只能在`企业微信客户端`中查看。 + +![](https://theseven.ftqq.com/20210208143553.png) + +#### 第三步,获取企业ID + +进入「[我的企业](https://work.weixin.qq.com/wework_admin/frame#profile)」页面,拉到最下边,可以看到企业ID③,复制并填到上方。 + +推送UID直接填 `@all` ,推送给公司全员。 + +#### 第四步,推送消息到微信 + +进入「我的企业」 → 「[微信插件](https://work.weixin.qq.com/wework_admin/frame#profile/wxPlugin)」,拉到下边扫描二维码,关注以后即可收到推送的消息。 + +![](https://theseven.ftqq.com/20210208144808.png) + +PS:如果出现`接口请求正常,企业微信接受消息正常,个人微信无法收到消息`的情况: + +1. 进入「我的企业」 → 「[微信插件](https://work.weixin.qq.com/wework_admin/frame#profile/wxPlugin)」,拉到最下方,勾选 “允许成员在微信插件中接收和回复聊天消息” +![](https://img.ams1.imgbed.xyz/2021/06/01/HPIRU.jpg) + +2. 在企业微信客户端 「我」 → 「设置」 → 「新消息通知」中关闭 “仅在企业微信中接受消息” 限制条件 +![](https://img.ams1.imgbed.xyz/2021/06/01/HPKPX.jpg) + +#### 第五步,通过以下函数发送消息: + +PS:为使用方便,以下函数没有对 `access_token` 进行缓存。对于个人低频调用已经够用。带缓存的实现可查看 `index.php` 中的示例代码(依赖Redis实现)。 + +PHP版: + +```php +function send_to_wecom($text, $wecom_cid, $wecom_aid, $wecom_secret, $wecom_touid = '@all') +{ + $info = @json_decode(file_get_contents("https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=".urlencode($wecom_cid)."&corpsecret=".urlencode($wecom_secret)), true); + + if ($info && isset($info['access_token']) && strlen($info['access_token']) > 0) { + $access_token = $info['access_token']; + $url = 'https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token='.urlencode($access_token); + $data = new \stdClass(); + $data->touser = $wecom_touid; + $data->agentid = $wecom_aid; + $data->msgtype = "text"; + $data->text = ["content"=> $text]; + $data->duplicate_check_interval = 600; + + $data_json = json_encode($data); + $ch = curl_init(); + curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + @curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 5); + curl_setopt($ch, CURLOPT_POSTFIELDS, $data_json); + + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + + $response = curl_exec($ch); + return $response; + } + return false; +} + +``` + +使用实例: + +```php +$ret = send_to_wecom("推送测试\r\n测试换行", "企业ID③", "应用ID①", "应用secret②"); +print_r( $ret ); +``` + +PYTHON版: + +```python +import json,requests,base64 +def send_to_wecom(text,wecom_cid,wecom_aid,wecom_secret,wecom_touid='@all'): + get_token_url = f"https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={wecom_cid}&corpsecret={wecom_secret}" + response = requests.get(get_token_url).content + access_token = json.loads(response).get('access_token') + if access_token and len(access_token) > 0: + send_msg_url = f'https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={access_token}' + data = { + "touser":wecom_touid, + "agentid":wecom_aid, + "msgtype":"text", + "text":{ + "content":text + }, + "duplicate_check_interval":600 + } + response = requests.post(send_msg_url,data=json.dumps(data)).content + return response + else: + return False + +def send_to_wecom_image(base64_content,wecom_cid,wecom_aid,wecom_secret,wecom_touid='@all'): + get_token_url = f"https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={wecom_cid}&corpsecret={wecom_secret}" + response = requests.get(get_token_url).content + access_token = json.loads(response).get('access_token') + if access_token and len(access_token) > 0: + upload_url = f'https://qyapi.weixin.qq.com/cgi-bin/media/upload?access_token={access_token}&type=image' + upload_response = requests.post(upload_url, files={ + "picture": base64.b64decode(base64_content) + }).json() + if "media_id" in upload_response: + media_id = upload_response['media_id'] + else: + return False + + send_msg_url = f'https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={access_token}' + data = { + "touser":wecom_touid, + "agentid":wecom_aid, + "msgtype":"image", + "image":{ + "media_id": media_id + }, + "duplicate_check_interval":600 + } + response = requests.post(send_msg_url,data=json.dumps(data)).content + return response + else: + return False + +def send_to_wecom_markdown(text,wecom_cid,wecom_aid,wecom_secret,wecom_touid='@all'): + get_token_url = f"https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={wecom_cid}&corpsecret={wecom_secret}" + response = requests.get(get_token_url).content + access_token = json.loads(response).get('access_token') + if access_token and len(access_token) > 0: + send_msg_url = f'https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={access_token}' + data = { + "touser":wecom_touid, + "agentid":wecom_aid, + "msgtype":"markdown", + "markdown":{ + "content":text + }, + "duplicate_check_interval":600 + } + response = requests.post(send_msg_url,data=json.dumps(data)).content + return response + else: + return False +``` + +使用实例: + +```python +ret = send_to_wecom("推送测试\r\n测试换行", "企业ID③", "应用ID①", "应用secret②"); +print( ret ); +ret = send_to_wecom('文本中支持超链接', "企业ID③", "应用ID①", "应用secret②"); +print( ret ); +ret = send_to_wecom_image("此处填写图片Base64", "企业ID③", "应用ID①", "应用secret②"); +print( ret ); +ret = send_to_wecom_markdown("**Markdown 内容**", "企业ID③", "应用ID①", "应用secret②"); +print( ret ); +``` + +TypeScript 版: + +```typescript +import request from 'superagent' + +async function sendToWecom(body: { + text: string + wecomCId: string + wecomSecret: string + wecomAgentId: string + wecomTouid?: string +}): Promise<{ errcode: number; errmsg: string; invaliduser: string }> { + body.wecomTouid = body.wecomTouid ?? '@all' + const getTokenUrl = `https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=${body.wecomCId}&corpsecret=${body.wecomSecret}` + const getTokenRes = await request.get(getTokenUrl) + const accessToken = getTokenRes.body.access_token + if (accessToken?.length <= 0) { + throw new Error('获取 accessToken 失败') + } + const sendMsgUrl = `https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${accessToken}` + const sendMsgRes = await request.post(sendMsgUrl).send({ + touser: body.wecomTouid, + agentid: body.wecomAgentId, + msgtype: 'text', + text: { + content: body.text, + }, + duplicate_check_interval: 600, + }) + return sendMsgRes.body +} +``` + +使用实例: + +```typescript +sendToWecom({ + text: '推送测试\r\n测试换行', + wecomAgentId: '应用ID①', + wecomSecret: '应用secret②', + wecomCId: '企业ID③', +}) + .then((res) => { + console.log(res) + }) + .catch((err) => { + console.log(err) + }) +``` + +.NET Core 版: + +```C# +using System; +using RestSharp; +using Newtonsoft.Json; +namespace WeCom.Demo +{ + class WeCom + { + public string SendToWeCom( + string text,// 推送消息 + string weComCId,// 企业Id① + string weComSecret,// 应用secret② + string weComAId,// 应用ID③ + string weComTouId = "@all") + { + // 获取Token + string getTokenUrl = $"https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={weComCId}&corpsecret={weComSecret}"; + string token = JsonConvert + .DeserializeObject(new RestClient(getTokenUrl) + .Get(new RestRequest()).Content).access_token; + System.Console.WriteLine(token); + if (!String.IsNullOrWhiteSpace(token)) + { + var request = new RestRequest(); + var client = new RestClient($"https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={token}"); + var data = new + { + touser = weComTouId, + agentid = weComAId, + msgtype = "text", + text = new + { + content = text + }, + duplicate_check_interval = 600 + }; + string serJson = JsonConvert.SerializeObject(data); + System.Console.WriteLine(serJson); + request.Method = Method.POST; + request.AddHeader("Accept", "application/json"); + request.Parameters.Clear(); + request.AddParameter("application/json", serJson, ParameterType.RequestBody); + return client.Execute(request).Content; + } + return "-1"; + } +} + + +``` +使用实例: +```C# + static void Main(string[] args) + { // 测试 + Console.Write(new WeCom().SendToWeCom( + "msginfo", + "企业Id①" + , "应用secret②", + "应用ID③" + )); + } + + } +``` + +其他版本的函数可参照上边的逻辑自行编写,欢迎PR。 + +发送图片、卡片、文件或 Markdown 消息的高级用法见 [企业微信API](https://work.weixin.qq.com/api/doc/90000/90135/90236)。 + + + diff --git a/dotNetCore.cs b/dotNetCore.cs new file mode 100644 index 0000000..bd748ad --- /dev/null +++ b/dotNetCore.cs @@ -0,0 +1,58 @@ +using System; +using RestSharp; +using Newtonsoft.Json; +namespace WeCom.Demo +{ + class WeCom + { + public string SendToWeCom( + string text,// 推送消息 + string weComCId,// 企业Id① + string weComSecret,// 应用secret② + string weComAId,// 应用ID③ + string weComTouId = "@all") + { + // 获取Token + string getTokenUrl = $"https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={weComCId}&corpsecret={weComSecret}"; + string token = JsonConvert + .DeserializeObject(new RestClient(getTokenUrl) + .Get(new RestRequest()).Content).access_token; + System.Console.WriteLine(token); + if (!String.IsNullOrWhiteSpace(token)) + { + var request = new RestRequest(); + var client = new RestClient($"https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={token}"); + var data = new + { + touser = weComTouId, + agentid = weComAId, + msgtype = "text", + text = new + { + content = text + }, + duplicate_check_interval = 600 + }; + string serJson = JsonConvert.SerializeObject(data); + System.Console.WriteLine(serJson); + request.Method = Method.POST; + request.AddHeader("Accept", "application/json"); + request.Parameters.Clear(); + request.AddParameter("application/json", serJson, ParameterType.RequestBody); + return client.Execute(request).Content; + } + return "-1"; + } + static void Main(string[] args) + { // 测试 + Console.Write(new WeCom().SendToWeCom( + "msginfo", + "企业Id①" + , "应用secret②", + "应用ID③" + )); + } + + } +} + diff --git a/go-scf/.gitignore b/go-scf/.gitignore new file mode 100644 index 0000000..0d84dba --- /dev/null +++ b/go-scf/.gitignore @@ -0,0 +1,9 @@ +.vscode/ +main +msg_notice +*.exe +*.zip +*_test.go +*.zip +.DS_Store +config.yaml \ No newline at end of file diff --git a/go-scf/README.md b/go-scf/README.md new file mode 100644 index 0000000..600d863 --- /dev/null +++ b/go-scf/README.md @@ -0,0 +1,137 @@ +# 腾讯云云函数部署Server酱📣 + +本项目是对 [Wecom酱](https://github.com/easychen/wecomchan) 进行的扩展,可以通过企业微信 OpenAPI 向微信推送消息,实现微信消息提醒。 + +利用 [腾讯云云函数](https://cloud.tencent.com/product/scf) ServerLess 的能力,以极低的费用(按量付费,且有大量免费额度)来完成部署 + +优点: + +- 便宜:说是免费也不过分 +- 简单:不需要购买vps, 也不需要备案, 腾讯云速度有保障. +- 易搭建:一个可执行二进制文件,直接上传至腾讯云函数控制面板即可,虽然使用 Golang 编写,但是搭建无需 Golang 环境 +- Serverless:无服务器,函数调用完资源会释放 + +## 🖐️ 简单介绍 + +我们要实现的目标是把消息推送到微信上,此处借助了使用 企业微信,可以创建机器人,利用微信的 OpenAPI 来实现消息推送,本项目做了一个简单的封装。 + +欢迎PR代码。 + +> 老用户注意: +> +> 自 2.0 版本之后,不再需要 `config.yaml` 文件,配置改为从云函数的环境变量中读取,请直接下载 `main.zip` 上传至云函数并且设置环境变量即可。 + +## 👋 使用方法 + +### 1. 注册企业 & 创建机器人 & 获取相关配置信息 + +此处不再赘述,项目主页有完整的操作方法,见:https://github.com/riba2534/wecomchan + +### 2. 下载编译好的二进制文件 + +下载文件 [版本发布页面](https://github.com/riba2534/wecomchan/releases): + +- [main.zip](https://github.com/riba2534/wecomchan/releases/download/2.1/main.zip) :云函数可执行二进制文件,不用改动,等会直接上传即可。 + +### 3. 在腾讯云中创建云函数 & 配置环境变量 + +打开云函数控制台:https://console.cloud.tencent.com/scf/list + +点击新建: + +![image-20210705014652334](https://image-1252109614.cos.ap-beijing.myqcloud.com/img/image-20210705014652334.png) + +如图所示选择 + +1. 自定义创建,函数类型为 `事件函数` +2. 填 `wecomchan` +3. 运行环境选择 Go1 +4. 函数代码选择本地上传ZIP包,直接上传刚才下载的 `main.zip` +5. 在 `高级配置` 中配置环境变量,6 个环境变量,**缺一不可**,(后续想改环境变量,直接在创建好的函数中编辑即可) + +环境变量配置说明 + +| key | value | 备注 | +| :------------: | :----------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `FUNC_NAME` | 填 `wecomchan` | | +| `SEND_KEY` | 最终调用HTTP接口时校验是否是本人调用的密钥,随意设置,最终发起HTTP请求携带即可 | | +| `WECOM_CID` | 企业微信公司ID | | +| `WECOM_SECRET` | 企业微信应用Secret | | +| `WECOM_AID` | 企业微信应用ID | | +| `WECOM_TOUID` | `@all` | 此处指推送消息的默认发送对象,填 `@all`,则代表向该企业的全部成员推送消息(如果是个人用的话,一个企业中只有你自己,直接填 `@all` 即可),如果想指定具体发送的人,后面会说明怎么发。 | + +6. 在 `触发器配置` 中,新增 `API网关触发`,保持默认配置即可。 +7. 点击完成 + +![基础配置](https://image-1252109614.cos.ap-beijing.myqcloud.com/img/image-20210707204518173.png) + +![高级配置](https://image-1252109614.cos.ap-beijing.myqcloud.com/img/image-20210707204936310.png) + +![触发器配置](https://image-1252109614.cos.ap-beijing.myqcloud.com/img/image-20210707205811630.png) + +稍等一会,进入你创建的函数: + +![image-20210705015301810](https://image-1252109614.cos.ap-beijing.myqcloud.com/img/image-20210705015301810.png) + +图中所示的访问路径就是函数的请求路径,至此,所有的配置完成。 + +## 👌 发起HTTP请求测试是否成功 + +现已支持 `GET`、`POST` 方法进行请求。 + +> 当发送的文本中存在有换行符或其他字符时,请把 msg 参数进行 url 编码(使用 GET 方法注意,POST不需要) + +### 简单使用: + +在你刚才获得的路径之后拼几个GET参数,在后面加上:`?sendkey=你配置的sendkey&msg_type=text&msg=hello` + +![image-20210705015727720](https://image-1252109614.cos.ap-beijing.myqcloud.com/img/image-20210705015727720.png) + +可以看见返回 success 字样。 + +观察手机推送,也可以收到消息: + +![image-20210705015804023](https://image-1252109614.cos.ap-beijing.myqcloud.com/img/image-20210705015804023.png) + +之后,想怎么用就是你的事了,想给自己的微信推送,只需要给这个 URL 发一条 HTTP 请求即可。 + +### 给指定成员推送消息: + +如果你的需求是给企业微信中的指定成员发送消息而不是所有成员,则在 GET 请求中多加一个参数 `to_user`,值为 成员ID列表,如果想指定多个成员,则多个成员ID之间用 `|` 隔开。如请求:`https://xxxxx/wecomchan?sendkey=123456&msg_type=text&msg=测试消息&to_user=User1|User2` ,也能收到消息。 + +![image-20210707211125345](https://image-1252109614.cos.ap-beijing.myqcloud.com/img/image-20210707211125345.png) + +> 成员的 ID 在企业微信后台,`通讯录`,点开指定成员资料,有个 `账号` 字段,该字段即为该成员的ID. + +### 使用 `POST` 进行请求 + +大部分情况下,`GET` 请求已经可以很好的满足发送一些短消息的需求,但是当消息体过长时,云函数可能报参数过长错误,故在 `V2.1` 版本加入 `POST` 请求支持。 + +与 `GET` 请求不同的是,`POST` 请求不从 [Query string](https://en.wikipedia.org/wiki/Query_string) 获取参数,所有参数改为从 [HTTP message body](https://en.wikipedia.org/wiki/HTTP_message_body) 中获取,这里要求 Body 中必须是 `JSON` 格式,参数字段名称仍与 `GET` 请求的名称保持一致,且 `json` 的 `key` 和 `value` 必须是 `string` 类型,Body 格式例如: + +```json +{ + "sendkey": "123456", + "msg_type": "text", + "msg": "这是一条POST消息", + "to_user": "User1|User2" +} +``` + +### 参数说明: + +下表为请求的参数说明(`GET` 与 `POST` 字段名相同): + +| 参数名称 | 说明 | 是否可选 | +| ---------- | --------------------------------------------------------------------------------------------------------------- | -------- | +| `sendkey` | 校验是否是本人调用的密钥,随意设置,最终发起HTTP请求携带即可 | 必须 | +| `msg_type` | 消息类型,目前只有纯文本一种类型,值为 `text` | 必须 | +| `msg` | 消息内容,支持多行和UTF8字符,在程序中构建字符串时加上**换行符**即可,如果有特殊符号,记得使用 `urlencode` 编码 | 必须 | +| `to_user` | 如果需要给企业内指定成员发消息,可在此参数中指定成员。如果不传本参数,默认所有成员。 | 可选 | + +👇👇👇 + +--- + +如果发现bug,或者对本项目有任何建议,欢迎联系 `riba2534@qq.com` 或者直接提 [Issue](https://github.com/riba2534/wecomchan/issues). + diff --git a/go-scf/build.sh b/go-scf/build.sh new file mode 100755 index 0000000..d18a659 --- /dev/null +++ b/go-scf/build.sh @@ -0,0 +1,5 @@ +set -ex + +CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o main && upx -9 main + +zip main.zip main \ No newline at end of file diff --git a/go-scf/consts/consts.go b/go-scf/consts/consts.go new file mode 100644 index 0000000..2376f16 --- /dev/null +++ b/go-scf/consts/consts.go @@ -0,0 +1,18 @@ +package consts + +var ( + FUNC_NAME string + SEND_KEY string + WECOM_CID string + WECOM_SECRET string + WECOM_AID string + WECOM_TOUID string +) + +// 微信发消息API +const ( + // https://work.weixin.qq.com/api/doc/90000/90135/90236 + WeComMsgSendURL = "https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=%s" + // https://work.weixin.qq.com/api/doc/90000/90135/91039 + WeComAccessTokenURL = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=%s&corpsecret=%s" +) diff --git a/go-scf/dal/dal.go b/go-scf/dal/dal.go new file mode 100644 index 0000000..ff86f3e --- /dev/null +++ b/go-scf/dal/dal.go @@ -0,0 +1,50 @@ +package dal + +import ( + "fmt" + "io/ioutil" + "net/http" + "time" + + jsoniter "github.com/json-iterator/go" + "github.com/riba2534/wecomchan/go-scf/consts" + "github.com/riba2534/wecomchan/go-scf/model" +) + +var AccessToken string + +func loadAccessToken() { + client := http.Client{Timeout: 10 * time.Second} + req, _ := http.NewRequest("GET", fmt.Sprintf(consts.WeComAccessTokenURL, consts.WECOM_CID, consts.WECOM_SECRET), nil) + resp, err := client.Do(req) + if err != nil { + fmt.Println("getAccessToken err=", err) + } + defer resp.Body.Close() + if resp.StatusCode != 200 { + fmt.Println("getAccessToken statusCode is not 200") + } + respBodyBytes, _ := ioutil.ReadAll(resp.Body) + assesTokenResp := &model.AssesTokenResp{} + if err := jsoniter.Unmarshal(respBodyBytes, assesTokenResp); err != nil { + fmt.Println("getAccessToken json Unmarshal failed, err=", err) + panic(err) + } + if assesTokenResp.Errcode != 0 { + fmt.Println("getAccessToken assesTokenResp.Errcode != 0, err=", assesTokenResp.Errmsg) + panic(err) + } + AccessToken = assesTokenResp.AccessToken +} + +func Init() { + loadAccessToken() + fmt.Printf("[Init] accessToken load success, time=%s, token=%s\n", time.Now().Format("2006-01-02 15:04:05"), AccessToken) + go func() { + for { + time.Sleep(30 * time.Minute) + loadAccessToken() + fmt.Printf("[Goroutine] accessToken load success, time=%s, token=%s\n", time.Now().Format("2006-01-02 15:04:05"), AccessToken) + } + }() +} diff --git a/go-scf/go.mod b/go-scf/go.mod new file mode 100644 index 0000000..80bed7d --- /dev/null +++ b/go-scf/go.mod @@ -0,0 +1,8 @@ +module github.com/riba2534/wecomchan/go-scf + +go 1.16 + +require ( + github.com/json-iterator/go v1.1.11 + github.com/tencentyun/scf-go-lib v0.0.0-20200624065115-ba679e2ec9c9 +) diff --git a/go-scf/go.sum b/go-scf/go.sum new file mode 100644 index 0000000..e6e7262 --- /dev/null +++ b/go-scf/go.sum @@ -0,0 +1,17 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/tencentyun/scf-go-lib v0.0.0-20200624065115-ba679e2ec9c9 h1:JdeXp/XPi7lBmpQNSUxElMAvwppMlFSiamTtXYRFuUc= +github.com/tencentyun/scf-go-lib v0.0.0-20200624065115-ba679e2ec9c9/go.mod h1:K3DbqPpP2WE/9MWokWWzgFZcbgtMb9Wd5CYk9AAbEN8= diff --git a/go-scf/main.go b/go-scf/main.go new file mode 100644 index 0000000..5bfad7d --- /dev/null +++ b/go-scf/main.go @@ -0,0 +1,52 @@ +package main + +import ( + "context" + "fmt" + "strings" + + "github.com/riba2534/wecomchan/go-scf/consts" + "github.com/riba2534/wecomchan/go-scf/dal" + "github.com/riba2534/wecomchan/go-scf/service" + "github.com/riba2534/wecomchan/go-scf/utils" + "github.com/tencentyun/scf-go-lib/cloudfunction" + "github.com/tencentyun/scf-go-lib/events" +) + +func init() { + consts.FUNC_NAME = utils.GetEnvDefault("FUNC_NAME", "") + consts.SEND_KEY = utils.GetEnvDefault("SEND_KEY", "") + consts.WECOM_CID = utils.GetEnvDefault("WECOM_CID", "") + consts.WECOM_SECRET = utils.GetEnvDefault("WECOM_SECRET", "") + consts.WECOM_AID = utils.GetEnvDefault("WECOM_AID", "") + consts.WECOM_TOUID = utils.GetEnvDefault("WECOM_TOUID", "@all") + if consts.FUNC_NAME == "" || consts.SEND_KEY == "" || consts.WECOM_CID == "" || + consts.WECOM_SECRET == "" || consts.WECOM_AID == "" || consts.WECOM_TOUID == "" { + fmt.Printf("os.env load Fail, please check your os env.\nFUNC_NAME=%s\nSEND_KEY=%s\nWECOM_CID=%s\nWECOM_SECRET=%s\nWECOM_AID=%s\nWECOM_TOUID=%s\n", consts.FUNC_NAME, consts.SEND_KEY, consts.WECOM_CID, consts.WECOM_SECRET, consts.WECOM_AID, consts.WECOM_TOUID) + panic("os.env param error") + } + fmt.Println("os.env load success!") +} + +func HTTPHandler(ctx context.Context, event events.APIGatewayRequest) (events.APIGatewayResponse, error) { + path := event.Path + fmt.Println("req->", utils.MarshalToStringParam(event)) + var result interface{} + if strings.HasPrefix(path, "/"+consts.FUNC_NAME) { + result = service.WeComChanService(ctx, event) + } else { + // 匹配失败返回原始HTTP请求 + result = event + } + return events.APIGatewayResponse{ + IsBase64Encoded: false, + StatusCode: 200, + Headers: map[string]string{}, + Body: utils.MarshalToStringParam(result), + }, nil +} + +func main() { + dal.Init() + cloudfunction.Start(HTTPHandler) +} diff --git a/go-scf/model/model.go b/go-scf/model/model.go new file mode 100644 index 0000000..1a470b7 --- /dev/null +++ b/go-scf/model/model.go @@ -0,0 +1,27 @@ +package model + +type AssesTokenResp struct { + Errcode int `json:"errcode"` + Errmsg string `json:"errmsg"` + AccessToken string `json:"access_token"` + ExpiresIn int `json:"expires_in"` +} + +type MsgText struct { + Content string `json:"content"` +} + +// https://work.weixin.qq.com/api/doc/90002/90151/90854 +type WechatMsg struct { + ToUser string `json:"touser"` + AgentId string `json:"agentid"` + MsgType string `json:"msgtype"` + Text *MsgText `json:"text"` + DuplicateCheckInterval int `json:"duplicate_check_interval"` +} + +type PostResp struct { + Errcode int `json:"errcode"` + Errmsg string `json:"errmsg"` + Invaliduser string `json:"invaliduser"` +} diff --git a/go-scf/service/wecomchan.go b/go-scf/service/wecomchan.go new file mode 100644 index 0000000..6557dba --- /dev/null +++ b/go-scf/service/wecomchan.go @@ -0,0 +1,90 @@ +package service + +import ( + "bytes" + "context" + "errors" + "fmt" + "io/ioutil" + "net/http" + "time" + + jsoniter "github.com/json-iterator/go" + "github.com/riba2534/wecomchan/go-scf/consts" + "github.com/riba2534/wecomchan/go-scf/dal" + "github.com/riba2534/wecomchan/go-scf/model" + "github.com/riba2534/wecomchan/go-scf/utils" + "github.com/tencentyun/scf-go-lib/events" +) + +func WeComChanService(ctx context.Context, event events.APIGatewayRequest) map[string]interface{} { + sendKey := getQuery("sendkey", event) + msgType := getQuery("msg_type", event) + msg := getQuery("msg", event) + if msgType == "" || msg == "" { + return utils.MakeResp(-1, "param error") + } + if sendKey != consts.SEND_KEY { + return utils.MakeResp(-1, "sendkey error") + } + toUser := getQuery("to_user", event) + if toUser == "" { + toUser = consts.WECOM_TOUID + } + if err := postWechatMsg(dal.AccessToken, msg, msgType, toUser); err != nil { + return utils.MakeResp(0, err.Error()) + } + return utils.MakeResp(0, "success") +} + +func postWechatMsg(accessToken, msg, msgType, toUser string) error { + content := &model.WechatMsg{ + ToUser: toUser, + AgentId: consts.WECOM_AID, + MsgType: msgType, + DuplicateCheckInterval: 600, + Text: &model.MsgText{ + Content: msg, + }, + } + b, _ := jsoniter.Marshal(content) + client := http.Client{Timeout: 10 * time.Second} + req, _ := http.NewRequest("POST", fmt.Sprintf(consts.WeComMsgSendURL, accessToken), bytes.NewBuffer(b)) + req.Header.Set("Content-type", "application/json") + resp, err := client.Do(req) + if err != nil { + fmt.Println("[postWechatMsg] failed, err=", err) + return nil + } + defer resp.Body.Close() + if resp.StatusCode != 200 { + fmt.Println("postWechatMsg statusCode is not 200") + return errors.New("statusCode is not 200") + } + respBodyBytes, _ := ioutil.ReadAll(resp.Body) + postResp := &model.PostResp{} + if err := jsoniter.Unmarshal(respBodyBytes, postResp); err != nil { + fmt.Println("postWechatMsg json Unmarshal failed, err=", err) + return err + } + if postResp.Errcode != 0 { + fmt.Println("postWechatMsg postResp.Errcode != 0, err=", postResp.Errmsg) + return errors.New(postResp.Errmsg) + } + return nil +} + +func getQuery(key string, event events.APIGatewayRequest) string { + switch event.Method { + case "GET": + value := event.QueryString[key] + if len(value) > 0 && value[0] != "" { + return value[0] + } + return "" + case "POST": + return jsoniter.Get([]byte(event.Body), key).ToString() + default: + return "" + } +} diff --git a/go-scf/utils/utils.go b/go-scf/utils/utils.go new file mode 100644 index 0000000..d802415 --- /dev/null +++ b/go-scf/utils/utils.go @@ -0,0 +1,30 @@ +package utils + +import ( + "os" + + jsoniter "github.com/json-iterator/go" +) + +func MarshalToStringParam(param interface{}) string { + s, err := jsoniter.MarshalToString(param) + if err != nil { + return "{}" + } + return s +} + +func MakeResp(code int, msg string) map[string]interface{} { + return map[string]interface{}{ + "code": code, + "msg": msg, + } +} + +func GetEnvDefault(key, defVal string) string { + val, ex := os.LookupEnv(key) + if !ex { + return defVal + } + return val +} diff --git a/go-wecomchan/Dockerfile b/go-wecomchan/Dockerfile new file mode 100644 index 0000000..beb15eb --- /dev/null +++ b/go-wecomchan/Dockerfile @@ -0,0 +1,26 @@ +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 + +WORKDIR /go/src/app +COPY . . + +RUN apk update && apk upgrade && apk add --no-cache ca-certificates +RUN update-ca-certificates +RUN go build + +FROM scratch + +WORKDIR /root + +COPY --from=gobuilder /go/src/app/wecomchan . +COPY --from=gobuilder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ + +EXPOSE 8080 + +CMD ["./wecomchan"] diff --git a/go-wecomchan/Dockerfile.architecture b/go-wecomchan/Dockerfile.architecture new file mode 100644 index 0000000..514de5e --- /dev/null +++ b/go-wecomchan/Dockerfile.architecture @@ -0,0 +1,24 @@ +FROM --platform=$TARGETPLATFORM golang:1.16.5-alpine3.13 as gobuilder + +ENV GO111MODULE="on" +ENV GOPROXY="https://goproxy.cn,direct" +ENV CGO_ENABLED=0 + +WORKDIR /go/src/app +COPY . . + +RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories +RUN apk update && apk upgrade && apk add --no-cache ca-certificates +RUN update-ca-certificates +RUN go build + +FROM scratch + +WORKDIR /root + +COPY --from=gobuilder /go/src/app/wecomchan . +COPY --from=gobuilder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ + +EXPOSE 8080 + +CMD ["./wecomchan"] diff --git a/go-wecomchan/README.md b/go-wecomchan/README.md new file mode 100644 index 0000000..1a8b47f --- /dev/null +++ b/go-wecomchan/README.md @@ -0,0 +1,121 @@ +# go-wecomchan + +## what's new + +添加 Dockerfile.architecture 使用docker buildx支持构建多架构镜像。 + +关于docker buildx build 使用方式参考官方文档: + +[https://docs.docker.com/engine/reference/commandline/buildx_build/](https://docs.docker.com/engine/reference/commandline/buildx_build/) + +## 配置说明 + +直接使用和构建二进制文件使用需要golang环境,并且网络可以安装依赖。 +docker构建镜像使用,需要安装docker,不依赖golang以及网络。 + +## 修改默认值 + +修改的sendkey,企业微信公司ID 等默认值为你的企业中的相关信息,如不设置运行时和打包后都可通过环境变量传入。 + +```golang +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", "") +``` + +## 直接使用 + +如果没有添加默认值,需要先引入环境变量,以SENDKEY为例: + +`export SENDKEY=set_a_sendkey` +依次引入环境变量后,执行 +`go run .` + +## build命令构建二进制文件使用 + +1. 构建命令 +`go build` + +2. 启动 +`./wecomchan` + +## 构建docker镜像使用(推荐,不依赖golang,不依赖网络) + +新增打包好的镜像可以直接使用 + +- 推送文本or图片:`docker pull aozakiaoko/go-wecomchan` +Docker Hub 地址为:[https://hub.docker.com/r/aozakiaoko/go-wecomchan](https://hub.docker.com/r/aozakiaoko/go-wecomchan) + +已经更新latest镜像为 @fcbhank 的最新代码,并支持arm64设备。也可通过aozakiaoko/go-wecomchan:v2 获取最新镜像。 + +- 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 .` + +2. 修改默认值后启动镜像 +`docker run -dit -p 8080:8080 go-wecomchan` + +3. 通过环境变量启动镜像并启用redis + +```bash +docker run -dit -e SENDKEY=set_a_sendkey \ +-e WECOM_CID=企业微信公司ID \ +-e WECOM_SECRET=企业微信应用Secret \ +-e WECOM_AID=企业微信应用ID \ +-e WECOM_TOUID="@all" \ +-e REDIS_STAT=ON \ +-e REDIS_ADDR="localhost:6379" \ +-e REDIS_PASSWORD="" \ +# aozakiaoko/go-wecomchan 已经更新镜像为 @fcbhank 的最新代码,并支持arm64设备。 +# v2 fcbhank/go-wecomchan +-p 8080:8080 go-wecomchan +``` + +如不使用redis不要传入最后三个关于redis的环境变量(REDIS_STAT|REDIS_ADDR|REDIS_PASSWORD) + +4. 环境变量说明 + +|名称|描述| +|---|---| +|SENDKEY|发送时用来验证的key| +|WECOM_CID|企业微信公司ID| +|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_PASSWORD|redis的连接密码,如不启用redis缓存可不设置| + +## 使用docker-compose 部署 + +修改docker-compose.yml 文件内上述的环境变量,之后执行 + +`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 打包镜像(不依赖网络环境) +* [x] 通过环境变量传递企业微信id,secret等,镜像一次构建多次使用 +* [x] docker-compose redis + go-wecomchan 一键部署 \ No newline at end of file diff --git a/go-wecomchan/docker-compose.yml b/go-wecomchan/docker-compose.yml new file mode 100644 index 0000000..fa558f8 --- /dev/null +++ b/go-wecomchan/docker-compose.yml @@ -0,0 +1,37 @@ +version: '3' + +services: + go-wecomchan: + image: docker.io/aozakiaoko/go-wecomchan:latest + environment: + - SENDKEY=发送时用来验证的key + - WECOM_CID=企业微信公司ID + - WECOM_SECRET=企业微信应用Secret + - WECOM_AID=企业微信应用ID + - WECOM_TOUID=@all + - REDIS_STAT=ON + - REDIS_ADDR=redis:6379 + - REDIS_PASSWORD=redis的连接密码 + ports: + - 8080:8080 + networks: + - go-wecomchan + depends_on: + - redis + + redis: + image: docker.io/bitnami/redis:6.2 + environment: + - REDIS_PASSWORD=redis的连接密码 + - REDIS_DISABLE_COMMANDS=FLUSHDB,FLUSHALL + networks: + - go-wecomchan + volumes: + - 'redis_data:/bitnami/redis/data' + +volumes: + redis_data: + driver: local + +networks: + go-wecomchan: diff --git a/go-wecomchan/go.mod b/go-wecomchan/go.mod new file mode 100644 index 0000000..388cf93 --- /dev/null +++ b/go-wecomchan/go.mod @@ -0,0 +1,5 @@ +module go/wecomchan + +go 1.16 + +require github.com/go-redis/redis/v8 v8.10.0 diff --git a/go-wecomchan/go.sum b/go-wecomchan/go.sum new file mode 100644 index 0000000..97595ea --- /dev/null +++ b/go-wecomchan/go.sum @@ -0,0 +1,97 @@ +github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/go-redis/redis/v8 v8.10.0 h1:OZwrQKuZqdJ4QIM8wn8rnuz868Li91xA3J2DEq+TPGA= +github.com/go-redis/redis/v8 v8.10.0/go.mod h1:vXLTvigok0VtUX0znvbcEW1SOt4OA9CU1ZfnOtKOaiM= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.15.0 h1:1V1NfVQR87RtWAgp1lv9JZJ5Jap+XFGKPi00andXGi4= +github.com/onsi/ginkgo v1.15.0/go.mod h1:hF8qUzuuC8DJGygJH3726JnCZX4MYbRB8yFfISqnKUg= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.10.5 h1:7n6FEkpFmfCoo2t+YYqXH0evK+a9ICQz0xcAy9dYcaQ= +github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opentelemetry.io/otel v0.20.0 h1:eaP0Fqu7SXHwvjiqDq83zImeehOHX8doTvU9AwXON8g= +go.opentelemetry.io/otel v0.20.0/go.mod h1:Y3ugLH2oa81t5QO+Lty+zXf8zC9L26ax4Nzoxm/dooo= +go.opentelemetry.io/otel/metric v0.20.0 h1:4kzhXFP+btKm4jwxpjIqjs41A7MakRFUS86bqLHTIw8= +go.opentelemetry.io/otel/metric v0.20.0/go.mod h1:598I5tYlH1vzBjn+BTuhzTCSb/9debfNp6R3s7Pr1eU= +go.opentelemetry.io/otel/oteltest v0.20.0 h1:HiITxCawalo5vQzdHfKeZurV8x7ljcqAgiWzF6Vaeaw= +go.opentelemetry.io/otel/oteltest v0.20.0/go.mod h1:L7bgKf9ZB7qCwT9Up7i9/pn0PWIa9FqQ2IQ8LoxiGnw= +go.opentelemetry.io/otel/trace v0.20.0 h1:1DL6EXUdcg95gukhuRRvLDO/4X5THh/5dIV52lqtnbw= +go.opentelemetry.io/otel/trace v0.20.0/go.mod h1:6GjCW8zgDjwGHGa6GkyeB8+/5vjT16gUEi0Nf1iBdgw= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb h1:eBmm0M9fYhWpKZLjQUUKka/LtIxf46G4fxeEz5KJr9U= +golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091 h1:DMyOG0U+gKfu8JZzg2UQe9MeaC1X+xQWlAKcRnjxjCw= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/go-wecomchan/wecomchan.go b/go-wecomchan/wecomchan.go new file mode 100644 index 0000000..d808b7a --- /dev/null +++ b/go-wecomchan/wecomchan.go @@ -0,0 +1,302 @@ +package main + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io/ioutil" + "log" + "math" + "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 -------------------------------*/ + +const RedisTokenKey = "access_token" + +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[RedisTokenKey].(string) + + if RedisStat == "ON" { + log.Println("prepare to set redis key") + rdb := RedisClient() + // access_token有效时间为7200秒(2小时) + set, err := rdb.SetNX(ctx, RedisTokenKey, 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) +} + +// UploadMedia 上传临时素材并返回mediaId +func UploadMedia(msgType string, req *http.Request, accessToken string) (string, float64) { + // 企业微信图片上传不能大于2M + _ = req.ParseMultipartForm(2 << 20) + imgFile, imgHeader, err := req.FormFile("media") + log.Printf("文件大小==>%d字节", imgHeader.Size) + if err != nil { + log.Fatalln("图片文件出错==>", err) + // 自定义code无效的图片文件 + return "", 400 + } + 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 "", mediaResp["errcode"].(float64) + } else { + return mediaResp["media_id"].(string), float64(0) + } +} + +// 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 + } + return true +} + +// GetAccessToken 获取企业微信的access_token +func GetAccessToken() string { + accessToken := "" + if RedisStat == "ON" { + log.Println("尝试从redis获取token") + rdb := RedisClient() + value, err := rdb.Get(ctx, RedisTokenKey).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) + wecomChan := func(res http.ResponseWriter, req *http.Request) { + // 获取token + accessToken := GetAccessToken() + // 默认token有效 + tokenValid := true + + _ = 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为空 + mediaId := "" + 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() + } + } + + // 准备发送应用消息所需参数 + postData := InitJsonData(msgType) + postData.Text = Msg{ + Content: msgContent, + } + postData.Image = Pic{ + MediaId: mediaId, + } + + 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() + } + + res.Header().Set("Content-type", "application/json") + _, _ = res.Write([]byte(postStatus)) + } + http.HandleFunc("/wecomchan", wecomChan) + log.Fatal(http.ListenAndServe(":8080", nil)) +} diff --git a/index.php b/index.php new file mode 100644 index 0000000..a26b32d --- /dev/null +++ b/index.php @@ -0,0 +1,87 @@ +connect(REDIS_HOST, REDIS_PORT); + } + + return $GLOBALS['REDIS_INSTANCE']; +} + +function send_to_wecom($text, $wecom_cid, $wecom_secret, $wecom_aid, $wecom_touid = '@all') +{ + $access_token = false; + // 如果启用redis作为缓存 + if (REDIS_ON) { + $access_token = redis()->get(REDIS_KEY); + } + + if (!$access_token) { + $info = @json_decode(file_get_contents("https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=".urlencode($wecom_cid)."&corpsecret=".urlencode($wecom_secret)), true); + + if ($info && isset($info['access_token']) && strlen($info['access_token']) > 0) { + $access_token = $info['access_token']; + } + } + + if ($access_token) { + $url = 'https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token='.urlencode($access_token); + $data = new \stdClass(); + $data->touser = $wecom_touid; + $data->agentid = $wecom_aid; + $data->msgtype = "text"; + $data->text = ["content"=> $text]; + $data->duplicate_check_interval = 600; + + $data_json = json_encode($data); + $ch = curl_init(); + curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + @curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 5); + curl_setopt($ch, CURLOPT_POSTFIELDS, $data_json); + + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + + $response = curl_exec($ch); + if ($response !== false && REDIS_ON) { + redis()->set(REDIS_KEY, $access_token, ['nx', 'ex'=>REDIS_EXPIRED]); + } + return $response; + } + + + return false; +}