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/),注册一个企业
+
+#### 第二步,创建应用
+
+注册成功后,点「管理企业」进入管理界面,选择「应用管理」 → 「自建」 → 「创建应用」
+
+
+
+应用名称填入「Server酱」,应用logo到[这里](./20210208142819.png)下载,可见范围选择公司名。
+
+
+
+
+创建完成后进入应用详情页,可以得到应用ID( `agentid` )①,应用Secret( `secret` )②。
+
+注意:`secret`推送到手机端时,只能在`企业微信客户端`中查看。
+
+
+
+#### 第三步,获取企业ID
+
+进入「[我的企业](https://work.weixin.qq.com/wework_admin/frame#profile)」页面,拉到最下边,可以看到企业ID③,复制并填到上方。
+
+推送UID直接填 `@all` ,推送给公司全员。
+
+#### 第四步,推送消息到微信
+
+进入「我的企业」 → 「[微信插件](https://work.weixin.qq.com/wework_admin/frame#profile/wxPlugin)」,拉到下边扫描二维码,关注以后即可收到推送的消息。
+
+
+
+PS:如果出现`接口请求正常,企业微信接受消息正常,个人微信无法收到消息`的情况:
+
+1. 进入「我的企业」 → 「[微信插件](https://work.weixin.qq.com/wework_admin/frame#profile/wxPlugin)」,拉到最下方,勾选 “允许成员在微信插件中接收和回复聊天消息”
+
+
+2. 在企业微信客户端 「我」 → 「设置」 → 「新消息通知」中关闭 “仅在企业微信中接受消息” 限制条件
+
+
+#### 第五步,通过以下函数发送消息:
+
+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
+
+点击新建:
+
+
+
+如图所示选择
+
+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. 点击完成
+
+
+
+
+
+
+
+稍等一会,进入你创建的函数:
+
+
+
+图中所示的访问路径就是函数的请求路径,至此,所有的配置完成。
+
+## 👌 发起HTTP请求测试是否成功
+
+现已支持 `GET`、`POST` 方法进行请求。
+
+> 当发送的文本中存在有换行符或其他字符时,请把 msg 参数进行 url 编码(使用 GET 方法注意,POST不需要)
+
+### 简单使用:
+
+在你刚才获得的路径之后拼几个GET参数,在后面加上:`?sendkey=你配置的sendkey&msg_type=text&msg=hello`
+
+
+
+可以看见返回 success 字样。
+
+观察手机推送,也可以收到消息:
+
+
+
+之后,想怎么用就是你的事了,想给自己的微信推送,只需要给这个 URL 发一条 HTTP 请求即可。
+
+### 给指定成员推送消息:
+
+如果你的需求是给企业微信中的指定成员发送消息而不是所有成员,则在 GET 请求中多加一个参数 `to_user`,值为 成员ID列表,如果想指定多个成员,则多个成员ID之间用 `|` 隔开。如请求:`https://xxxxx/wecomchan?sendkey=123456&msg_type=text&msg=测试消息&to_user=User1|User2` ,也能收到消息。
+
+
+
+> 成员的 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;
+}