first commit
This commit is contained in:
commit
686071002a
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
demo.php
|
||||
show.php
|
||||
.DS_Store
|
||||
.idea
|
||||
go-wecomchan/wecomchan
|
||||
go-wecomchan/wecomchan.exe
|
||||
BIN
20210208142819.png
Normal file
BIN
20210208142819.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
21
LICENSE
Normal file
21
LICENSE
Normal 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
13
ONLINE.md
Normal 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
328
README.md
Normal 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/),注册一个企业
|
||||
|
||||
#### 第二步,创建应用
|
||||
|
||||
注册成功后,点「管理企业」进入管理界面,选择「应用管理」 → 「自建」 → 「创建应用」
|
||||
|
||||

|
||||
|
||||
应用名称填入「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('<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
58
dotNetCore.cs
Normal 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
9
go-scf/.gitignore
vendored
Normal 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
137
go-scf/README.md
Normal 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
|
||||
|
||||
点击新建:
|
||||
|
||||

|
||||
|
||||
如图所示选择
|
||||
|
||||
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).
|
||||
|
||||
5
go-scf/build.sh
Executable file
5
go-scf/build.sh
Executable 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
18
go-scf/consts/consts.go
Normal 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
50
go-scf/dal/dal.go
Normal 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
8
go-scf/go.mod
Normal 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
17
go-scf/go.sum
Normal 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
52
go-scf/main.go
Normal 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
27
go-scf/model/model.go
Normal 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"`
|
||||
}
|
||||
90
go-scf/service/wecomchan.go
Normal file
90
go-scf/service/wecomchan.go
Normal 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
30
go-scf/utils/utils.go
Normal 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
26
go-wecomchan/Dockerfile
Normal 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"]
|
||||
24
go-wecomchan/Dockerfile.architecture
Normal file
24
go-wecomchan/Dockerfile.architecture
Normal 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
121
go-wecomchan/README.md
Normal 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] 通过环境变量传递企业微信id,secret等,镜像一次构建多次使用
|
||||
* [x] docker-compose redis + go-wecomchan 一键部署
|
||||
37
go-wecomchan/docker-compose.yml
Normal file
37
go-wecomchan/docker-compose.yml
Normal 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
5
go-wecomchan/go.mod
Normal 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
97
go-wecomchan/go.sum
Normal 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
302
go-wecomchan/wecomchan.go
Normal 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
87
index.php
Normal 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;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user