first commit

This commit is contained in:
wzj 2021-11-27 17:04:57 +08:00
commit 686071002a
25 changed files with 1568 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
demo.php
show.php
.DS_Store
.idea
go-wecomchan/wecomchan
go-wecomchan/wecomchan.exe

BIN
20210208142819.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

21
LICENSE Normal file
View File

@ -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.

13
ONLINE.md Normal file
View File

@ -0,0 +1,13 @@
# 在线服务搭建指南(PHP版)
## 安装条件
- PHP7.4+
- JSON &&CURL 模块
- 可访问外部网络的运行环境
## 安装说明
1. 用编辑器打开 `index.php`,按提示修改头部 define 的值( sendkey自己随意写其他参见企业微信配置文档
1. 将 `index.php` 上传运行环境
1. 通过 `http://指向运行环境的域名/?sendkey=你设定的sendkey&text=你要发送的内容` 即可发送内容

328
README.md Normal file
View File

@ -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('<a href="https://www.github.com/">文本中支持超链接</a>', "企业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<dynamic>(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)。

58
dotNetCore.cs Normal file
View File

@ -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<dynamic>(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③"
));
}
}
}

9
go-scf/.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
.vscode/
main
msg_notice
*.exe
*.zip
*_test.go
*.zip
.DS_Store
config.yaml

137
go-scf/README.md Normal file
View File

@ -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).

5
go-scf/build.sh Executable file
View File

@ -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

18
go-scf/consts/consts.go Normal file
View File

@ -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"
)

50
go-scf/dal/dal.go Normal file
View File

@ -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)
}
}()
}

8
go-scf/go.mod Normal file
View File

@ -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
)

17
go-scf/go.sum Normal file
View File

@ -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=

52
go-scf/main.go Normal file
View File

@ -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)
}

27
go-scf/model/model.go Normal file
View File

@ -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"`
}

View File

@ -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 ""
}
}

30
go-scf/utils/utils.go Normal file
View File

@ -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
}

26
go-wecomchan/Dockerfile Normal file
View File

@ -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"]

View File

@ -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"]

121
go-wecomchan/README.md Normal file
View File

@ -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] 通过环境变量传递企业微信idsecret等镜像一次构建多次使用
* [x] docker-compose redis + go-wecomchan 一键部署

View File

@ -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:

5
go-wecomchan/go.mod Normal file
View File

@ -0,0 +1,5 @@
module go/wecomchan
go 1.16
require github.com/go-redis/redis/v8 v8.10.0

97
go-wecomchan/go.sum Normal file
View File

@ -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=

302
go-wecomchan/wecomchan.go Normal file
View File

@ -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))
}

87
index.php Normal file
View File

@ -0,0 +1,87 @@
<?php
// config
// ======================================
define('SENDKEY', 'set_a_sendkey');
define('WECOM_CID', '企业微信公司ID');
define('WECOM_SECRET', '企业微信应用Secret');
define('WECOM_AID', '企业微信应用ID');
define('WECOM_TOUID', '@all');
// 以下配置需要有 redis 服务和 phpredis 扩展
define('REDIS_ON', false);
define('REDIS_HOST', '127.0.0.1');
define('REDIS_PORT', '6379');
define('REDIS_EXPIRED', '7000');
define('REDIS_KEY', 'wecom_access_token');
// code
// ======================================
if (strlen(@$_REQUEST['sendkey']) < 1
|| strlen(@$_REQUEST['text']) < 1 || @$_REQUEST['sendkey'] != SENDKEY
) {
die('bad params');
}
header("Content-Type: application/json; charset=UTF-8");
echo send_to_wecom(@$_REQUEST['text'], WECOM_CID, WECOM_SECRET, WECOM_AID, WECOM_TOUID);
function redis()
{
if (!isset($GLOBALS['REDIS_INSTANCE']) || !$GLOBALS['REDIS_INSTANCE']) {
$GLOBALS['REDIS_INSTANCE'] = new Redis();
$GLOBALS['REDIS_INSTANCE']->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;
}