├── .gitignore ├── 20210208142819.png ├── LICENSE ├── ONLINE.md ├── README.md ├── dotNetCore.cs ├── go-scf ├── .gitignore ├── README.md ├── build.sh ├── consts │ └── consts.go ├── dal │ └── dal.go ├── go.mod ├── go.sum ├── main.go ├── model │ └── model.go ├── service │ └── wecomchan.go └── utils │ └── utils.go ├── go-wecomchan ├── Dockerfile ├── Dockerfile.architecture ├── README.md ├── docker-compose.yml ├── go.mod ├── go.sum └── wecomchan.go ├── index.php ├── python-aliyunfc ├── README.md ├── main-code.zip └── pic │ ├── image-20220205142747826.png │ ├── image-20220205142906239.png │ ├── image-20220205143309699.png │ └── image-20220205144020332.png ├── python-baiduCFC ├── README.md ├── baidu-code.zip └── pic │ ├── image-20220517004653408.png │ ├── image-20220517004717978.png │ ├── image-20220517004919062.png │ ├── image-20220517005019692.png │ ├── image-20220517005724481.png │ ├── image-20220517005727886.png │ ├── image-20220517010037726.png │ ├── image-20220517011904665.png │ └── image-20220517013849304.png └── python-huaweiFG ├── README.md └── main.py /.gitignore: -------------------------------------------------------------------------------- 1 | demo.php 2 | show.php 3 | .DS_Store 4 | .idea 5 | go-wecomchan/wecomchan 6 | go-wecomchan/wecomchan.exe -------------------------------------------------------------------------------- /20210208142819.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easychen/wecomchan/e6df2c47d41d8a537751bf50fdead2ba4f61634d/20210208142819.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Easy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /ONLINE.md: -------------------------------------------------------------------------------- 1 | # 在线服务搭建指南(PHP版) 2 | 3 | ## 安装条件 4 | 5 | - PHP7.4+ 6 | - JSON &&CURL 模块 7 | - 可访问外部网络的运行环境 8 | 9 | ## 安装说明 10 | 11 | 1. 用编辑器打开 `index.php`,按提示修改头部 define 的值( sendkey自己随意写,其他参见企业微信配置文档 ) 12 | 1. 将 `index.php` 上传运行环境 13 | 1. 通过 `http://指向运行环境的域名/?sendkey=你设定的sendkey&text=你要发送的内容` 即可发送内容 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Wecom酱 2 | 3 | 通过企业微信向微信推送消息的解决方案。包括: 4 | 5 | 1. 配置说明(本页下方) 6 | 2. 推送函数(支持多种语言,见本页下方) 7 | 3. 自行搭建的在线服务源码 8 | 1. [PHP版搭建说明](ONLINE.md) 9 | 2. [Go版说明](go-wecomchan/README.md) 10 | 3. [Go适配华为函数工作流版本](https://github.com/Colo-Thor/wecomchan/releases/tag/2.2) 11 | 4. [腾讯云云函数搭建说明](go-scf/) ⚠️ 2022年5月起的最低月消费[已经取消了](https://cloud.tencent.com/document/product/583/104909) 12 | 5. [阿里云云函数搭建说明](python-aliyunfc/) 13 | 6. [百度智能云函数搭建说明](python-baiduCFC/) 14 | 7. [Python版华为函数工作流搭建说明](python-huaweiFG/) 15 | 16 | ## 🎈 本项目属于方糖推送生态。该生态包含项目如下: 17 | 18 | - [Server酱Turbo](https://sct.ftqq.com):支持企业微信、微信服务号、钉钉、飞书群机器人等多通道的在线服务,无需搭建直接使用,每天有免费额度 19 | - [Wecom酱](https://github.com/easychen/wecomchan):通过企业微信推送消息到微信的消息推送函数和在线服务方案,开源免费,可自己搭建。支持多语言。 20 | - [PushDeer](https://github.com/easychen/pushdeer):可自行搭建的、无需安装APP的开源推送方案。同时也提供安装APP的降级方案给低版本/没有快应用的系统。支持作为Server酱的通道进行推送,所有支持Server酱的软件和插件都能直接整合PushDeer。 21 | 22 | ## 企业微信应用消息配置说明 23 | 24 | 优点: 25 | 26 | 1. 一次配置,持续使用 27 | 1. 配置好以后,只需要微信就能收消息,不再需要安装企业微信客户端 28 | 29 | PS:消息接口无需认证即可使用,个人用微信就可以注册 30 | 31 | ### 具体操作 32 | 33 | #### 第一步,注册企业 34 | 35 | 用电脑打开[企业微信官网](https://work.weixin.qq.com/),注册一个企业 36 | 37 | #### 第二步,创建应用 38 | 39 | 注册成功后,点「管理企业」进入管理界面,选择「应用管理」 → 「自建」 → 「创建应用」 40 | 41 | ![](https://theseven.ftqq.com/20210208143228.png) 42 | 43 | 应用名称填入「Server酱」,应用logo到[这里](./20210208142819.png)下载,可见范围选择公司名。 44 | 45 | 46 | ![](https://theseven.ftqq.com/20210208143327.png) 47 | 48 | 创建完成后进入应用详情页,可以得到应用ID( `agentid` )①,应用Secret( `secret` )②。 49 | 50 | 注意:`secret`推送到手机端时,只能在`企业微信客户端`中查看。 51 | 52 | #### 第三步,添加可信IP 53 | 54 | > 2022年6月20日之后创建的应用,需要额外配置可信IP。 55 | 56 | 在「应用详情页」的最下方,开发者接口分类中,找到「企业可信IP」,点击「配置」,并填入服务器IP即可。 57 | 58 | 注意,如果你使用云函数等公用IP的云服务,可能需要在(云函数或其他服务的)设置界面中打开「固定公网IP」来获得一个独立的IP。否则有可能报「第三方服务IP」错误。 59 | 60 | #### 第四步,获取企业ID 61 | 62 | 进入「[我的企业](https://work.weixin.qq.com/wework_admin/frame#profile)」页面,拉到最下边,可以看到企业ID③,复制并填到上方。 63 | 64 | 推送UID直接填 `@all` ,推送给公司全员。 65 | 66 | #### 第五步,推送消息到微信 67 | 68 | 进入「我的企业」 → 「[微信插件](https://work.weixin.qq.com/wework_admin/frame#profile/wxPlugin)」,拉到下边扫描二维码,关注以后即可收到推送的消息。 69 | 70 | ![](https://theseven.ftqq.com/20210208144808.png) 71 | 72 | PS:如果出现`接口请求正常,企业微信接受消息正常,个人微信无法收到消息`的情况: 73 | 74 | 1. 进入「我的企业」 → 「[微信插件](https://work.weixin.qq.com/wework_admin/frame#profile/wxPlugin)」,拉到最下方,勾选 “允许成员在微信插件中接收和回复聊天消息” 75 | ![](https://img.ams1.imgbed.xyz/2021/06/01/HPIRU.jpg) 76 | 77 | 2. 在企业微信客户端 「我」 → 「设置」 → 「新消息通知」中关闭 “仅在企业微信中接受消息” 限制条件 78 | ![](https://img.ams1.imgbed.xyz/2021/06/01/HPKPX.jpg) 79 | 80 | #### 第六步,通过以下函数发送消息: 81 | 82 | PS:为使用方便,以下函数没有对 `access_token` 进行缓存。对于个人低频调用已经够用。带缓存的实现可查看 `index.php` 中的示例代码(依赖Redis实现)。 83 | 84 | PHP版: 85 | 86 | ```php 87 | function send_to_wecom($text, $wecom_cid, $wecom_aid, $wecom_secret, $wecom_touid = '@all') 88 | { 89 | $info = @json_decode(file_get_contents("https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=".urlencode($wecom_cid)."&corpsecret=".urlencode($wecom_secret)), true); 90 | 91 | if ($info && isset($info['access_token']) && strlen($info['access_token']) > 0) { 92 | $access_token = $info['access_token']; 93 | $url = 'https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token='.urlencode($access_token); 94 | $data = new \stdClass(); 95 | $data->touser = $wecom_touid; 96 | $data->agentid = $wecom_aid; 97 | $data->msgtype = "text"; 98 | $data->text = ["content"=> $text]; 99 | $data->duplicate_check_interval = 600; 100 | 101 | $data_json = json_encode($data); 102 | $ch = curl_init(); 103 | curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); 104 | curl_setopt($ch, CURLOPT_URL, $url); 105 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 106 | @curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); 107 | curl_setopt($ch, CURLOPT_POST, true); 108 | curl_setopt($ch, CURLOPT_TIMEOUT, 5); 109 | curl_setopt($ch, CURLOPT_POSTFIELDS, $data_json); 110 | 111 | curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); 112 | curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); 113 | 114 | $response = curl_exec($ch); 115 | return $response; 116 | } 117 | return false; 118 | } 119 | 120 | ``` 121 | 122 | 使用实例: 123 | 124 | ```php 125 | $ret = send_to_wecom("推送测试\r\n测试换行", "企业ID③", "应用ID①", "应用secret②"); 126 | print_r( $ret ); 127 | ``` 128 | 129 | PYTHON版: 130 | 131 | ```python 132 | import json,requests,base64 133 | def send_to_wecom(text,wecom_cid,wecom_aid,wecom_secret,wecom_touid='@all'): 134 | get_token_url = f"https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={wecom_cid}&corpsecret={wecom_secret}" 135 | response = requests.get(get_token_url).content 136 | access_token = json.loads(response).get('access_token') 137 | if access_token and len(access_token) > 0: 138 | send_msg_url = f'https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={access_token}' 139 | data = { 140 | "touser":wecom_touid, 141 | "agentid":wecom_aid, 142 | "msgtype":"text", 143 | "text":{ 144 | "content":text 145 | }, 146 | "duplicate_check_interval":600 147 | } 148 | response = requests.post(send_msg_url,data=json.dumps(data)).content 149 | return response 150 | else: 151 | return False 152 | 153 | def send_to_wecom_image(base64_content,wecom_cid,wecom_aid,wecom_secret,wecom_touid='@all'): 154 | get_token_url = f"https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={wecom_cid}&corpsecret={wecom_secret}" 155 | response = requests.get(get_token_url).content 156 | access_token = json.loads(response).get('access_token') 157 | if access_token and len(access_token) > 0: 158 | upload_url = f'https://qyapi.weixin.qq.com/cgi-bin/media/upload?access_token={access_token}&type=image' 159 | upload_response = requests.post(upload_url, files={ 160 | "picture": base64.b64decode(base64_content) 161 | }).json() 162 | if "media_id" in upload_response: 163 | media_id = upload_response['media_id'] 164 | else: 165 | return False 166 | 167 | send_msg_url = f'https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={access_token}' 168 | data = { 169 | "touser":wecom_touid, 170 | "agentid":wecom_aid, 171 | "msgtype":"image", 172 | "image":{ 173 | "media_id": media_id 174 | }, 175 | "duplicate_check_interval":600 176 | } 177 | response = requests.post(send_msg_url,data=json.dumps(data)).content 178 | return response 179 | else: 180 | return False 181 | 182 | def send_to_wecom_markdown(text,wecom_cid,wecom_aid,wecom_secret,wecom_touid='@all'): 183 | get_token_url = f"https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={wecom_cid}&corpsecret={wecom_secret}" 184 | response = requests.get(get_token_url).content 185 | access_token = json.loads(response).get('access_token') 186 | if access_token and len(access_token) > 0: 187 | send_msg_url = f'https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={access_token}' 188 | data = { 189 | "touser":wecom_touid, 190 | "agentid":wecom_aid, 191 | "msgtype":"markdown", 192 | "markdown":{ 193 | "content":text 194 | }, 195 | "duplicate_check_interval":600 196 | } 197 | response = requests.post(send_msg_url,data=json.dumps(data)).content 198 | return response 199 | else: 200 | return False 201 | ``` 202 | 203 | 使用实例: 204 | 205 | ```python 206 | ret = send_to_wecom("推送测试\r\n测试换行", "企业ID③", "应用ID①", "应用secret②"); 207 | print( ret ); 208 | ret = send_to_wecom('文本中支持超链接', "企业ID③", "应用ID①", "应用secret②"); 209 | print( ret ); 210 | ret = send_to_wecom_image("此处填写图片Base64", "企业ID③", "应用ID①", "应用secret②"); 211 | print( ret ); 212 | ret = send_to_wecom_markdown("**Markdown 内容**", "企业ID③", "应用ID①", "应用secret②"); 213 | print( ret ); 214 | ``` 215 | 216 | TypeScript 版: 217 | 218 | ```typescript 219 | import request from 'superagent' 220 | 221 | async function sendToWecom(body: { 222 | text: string 223 | wecomCId: string 224 | wecomSecret: string 225 | wecomAgentId: string 226 | wecomTouid?: string 227 | }): Promise<{ errcode: number; errmsg: string; invaliduser: string }> { 228 | body.wecomTouid = body.wecomTouid ?? '@all' 229 | const getTokenUrl = `https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=${body.wecomCId}&corpsecret=${body.wecomSecret}` 230 | const getTokenRes = await request.get(getTokenUrl) 231 | const accessToken = getTokenRes.body.access_token 232 | if (accessToken?.length <= 0) { 233 | throw new Error('获取 accessToken 失败') 234 | } 235 | const sendMsgUrl = `https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${accessToken}` 236 | const sendMsgRes = await request.post(sendMsgUrl).send({ 237 | touser: body.wecomTouid, 238 | agentid: body.wecomAgentId, 239 | msgtype: 'text', 240 | text: { 241 | content: body.text, 242 | }, 243 | duplicate_check_interval: 600, 244 | }) 245 | return sendMsgRes.body 246 | } 247 | ``` 248 | 249 | 使用实例: 250 | 251 | ```typescript 252 | sendToWecom({ 253 | text: '推送测试\r\n测试换行', 254 | wecomAgentId: '应用ID①', 255 | wecomSecret: '应用secret②', 256 | wecomCId: '企业ID③', 257 | }) 258 | .then((res) => { 259 | console.log(res) 260 | }) 261 | .catch((err) => { 262 | console.log(err) 263 | }) 264 | ``` 265 | 266 | .NET Core 版: 267 | 268 | ```C# 269 | using System; 270 | using RestSharp; 271 | using Newtonsoft.Json; 272 | namespace WeCom.Demo 273 | { 274 | class WeCom 275 | { 276 | public string SendToWeCom( 277 | string text,// 推送消息 278 | string weComCId,// 企业Id① 279 | string weComSecret,// 应用secret② 280 | string weComAId,// 应用ID③ 281 | string weComTouId = "@all") 282 | { 283 | // 获取Token 284 | string getTokenUrl = $"https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={weComCId}&corpsecret={weComSecret}"; 285 | string token = JsonConvert 286 | .DeserializeObject(new RestClient(getTokenUrl) 287 | .Get(new RestRequest()).Content).access_token; 288 | System.Console.WriteLine(token); 289 | if (!String.IsNullOrWhiteSpace(token)) 290 | { 291 | var request = new RestRequest(); 292 | var client = new RestClient($"https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={token}"); 293 | var data = new 294 | { 295 | touser = weComTouId, 296 | agentid = weComAId, 297 | msgtype = "text", 298 | text = new 299 | { 300 | content = text 301 | }, 302 | duplicate_check_interval = 600 303 | }; 304 | string serJson = JsonConvert.SerializeObject(data); 305 | System.Console.WriteLine(serJson); 306 | request.Method = Method.POST; 307 | request.AddHeader("Accept", "application/json"); 308 | request.Parameters.Clear(); 309 | request.AddParameter("application/json", serJson, ParameterType.RequestBody); 310 | return client.Execute(request).Content; 311 | } 312 | return "-1"; 313 | } 314 | } 315 | 316 | 317 | ``` 318 | 使用实例: 319 | ```C# 320 | static void Main(string[] args) 321 | { // 测试 322 | Console.Write(new WeCom().SendToWeCom( 323 | "msginfo", 324 | "企业Id①" 325 | , "应用secret②", 326 | "应用ID③" 327 | )); 328 | } 329 | 330 | } 331 | ``` 332 | 333 | - [纯Bash版本参考](https://gitee.com/Hemingway2003/pushservice/blob/master/wecom.sh) 334 | 335 | 其他版本的函数可参照上边的逻辑自行编写,欢迎PR。 336 | 337 | 发送图片、卡片、文件或 Markdown 消息的高级用法见 [企业微信API](https://work.weixin.qq.com/api/doc/90000/90135/90236)。 338 | 339 | 340 | 341 | -------------------------------------------------------------------------------- /dotNetCore.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using RestSharp; 3 | using Newtonsoft.Json; 4 | namespace WeCom.Demo 5 | { 6 | class WeCom 7 | { 8 | public string SendToWeCom( 9 | string text,// 推送消息 10 | string weComCId,// 企业Id① 11 | string weComSecret,// 应用secret② 12 | string weComAId,// 应用ID③ 13 | string weComTouId = "@all") 14 | { 15 | // 获取Token 16 | string getTokenUrl = $"https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={weComCId}&corpsecret={weComSecret}"; 17 | string token = JsonConvert 18 | .DeserializeObject(new RestClient(getTokenUrl) 19 | .Get(new RestRequest()).Content).access_token; 20 | System.Console.WriteLine(token); 21 | if (!String.IsNullOrWhiteSpace(token)) 22 | { 23 | var request = new RestRequest(); 24 | var client = new RestClient($"https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={token}"); 25 | var data = new 26 | { 27 | touser = weComTouId, 28 | agentid = weComAId, 29 | msgtype = "text", 30 | text = new 31 | { 32 | content = text 33 | }, 34 | duplicate_check_interval = 600 35 | }; 36 | string serJson = JsonConvert.SerializeObject(data); 37 | System.Console.WriteLine(serJson); 38 | request.Method = Method.POST; 39 | request.AddHeader("Accept", "application/json"); 40 | request.Parameters.Clear(); 41 | request.AddParameter("application/json", serJson, ParameterType.RequestBody); 42 | return client.Execute(request).Content; 43 | } 44 | return "-1"; 45 | } 46 | static void Main(string[] args) 47 | { // 测试 48 | Console.Write(new WeCom().SendToWeCom( 49 | "msginfo", 50 | "企业Id①" 51 | , "应用secret②", 52 | "应用ID③" 53 | )); 54 | } 55 | 56 | } 57 | } 58 | 59 | -------------------------------------------------------------------------------- /go-scf/.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | main 3 | msg_notice 4 | *.exe 5 | *.zip 6 | *_test.go 7 | *.zip 8 | .DS_Store 9 | config.yaml -------------------------------------------------------------------------------- /go-scf/README.md: -------------------------------------------------------------------------------- 1 | # 腾讯云云函数部署Server酱📣 2 | 3 | > 腾讯云函数2022年5月23日起的月最低消费[已经取消了](https://cloud.tencent.com/document/product/583/104909) 4 | 5 | 本项目是对 [Wecom酱](https://github.com/easychen/wecomchan) 进行的扩展,可以通过企业微信 OpenAPI 向微信推送消息,实现微信消息提醒。 6 | 7 | 利用 [腾讯云云函数](https://cloud.tencent.com/product/scf) ServerLess 的能力,以极低的费用(按量付费,且有大量免费额度)来完成部署 8 | 9 | 优点: 10 | 11 | - 便宜:说是免费也不过分 12 | - 简单:不需要购买vps, 也不需要备案, 腾讯云速度有保障. 13 | - 易搭建:一个可执行二进制文件,直接上传至腾讯云函数控制面板即可,虽然使用 Golang 编写,但是搭建无需 Golang 环境 14 | - Serverless:无服务器,函数调用完资源会释放 15 | 16 | ## 🖐️ 简单介绍 17 | 18 | 我们要实现的目标是把消息推送到微信上,此处借助了使用 企业微信,可以创建机器人,利用微信的 OpenAPI 来实现消息推送,本项目做了一个简单的封装。 19 | 20 | 欢迎PR代码。 21 | 22 | > 老用户注意: 23 | > 24 | > 自 2.0 版本之后,不再需要 `config.yaml` 文件,配置改为从云函数的环境变量中读取,请直接下载 `main.zip` 上传至云函数并且设置环境变量即可。 25 | 26 | ## 👋 使用方法 27 | 28 | ### 1. 注册企业 & 创建机器人 & 获取相关配置信息 29 | 30 | 此处不再赘述,项目主页有完整的操作方法,见:https://github.com/riba2534/wecomchan 31 | 32 | ### 2. 下载编译好的二进制文件 33 | 34 | 下载文件 [版本发布页面](https://github.com/riba2534/wecomchan/releases): 35 | 36 | - [main.zip](https://github.com/riba2534/wecomchan/releases/download/2.1/main.zip) :云函数可执行二进制文件,不用改动,等会直接上传即可。 37 | 38 | ### 3. 在腾讯云中创建云函数 & 配置环境变量 39 | 40 | 打开云函数控制台:https://console.cloud.tencent.com/scf/list 41 | 42 | 点击新建: 43 | 44 | ![image-20210705014652334](https://image-1252109614.cos.ap-beijing.myqcloud.com/img/image-20210705014652334.png) 45 | 46 | 如图所示选择 47 | 48 | 1. 自定义创建,函数类型为 `事件函数` 49 | 2. 填 `wecomchan` 50 | 3. 运行环境选择 Go1 51 | 4. 函数代码选择本地上传ZIP包,直接上传刚才下载的 `main.zip` 52 | 5. 在 `高级配置` 中配置环境变量,6 个环境变量,**缺一不可**,(后续想改环境变量,直接在创建好的函数中编辑即可) 53 | 54 | 环境变量配置说明 55 | 56 | | key | value | 备注 | 57 | | :------------: | :----------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 58 | | `FUNC_NAME` | 填 `wecomchan` | | 59 | | `SEND_KEY` | 最终调用HTTP接口时校验是否是本人调用的密钥,随意设置,最终发起HTTP请求携带即可 | | 60 | | `WECOM_CID` | 企业微信公司ID | | 61 | | `WECOM_SECRET` | 企业微信应用Secret | | 62 | | `WECOM_AID` | 企业微信应用ID | | 63 | | `WECOM_TOUID` | `@all` | 此处指推送消息的默认发送对象,填 `@all`,则代表向该企业的全部成员推送消息(如果是个人用的话,一个企业中只有你自己,直接填 `@all` 即可),如果想指定具体发送的人,后面会说明怎么发。 | 64 | 65 | 6. 在 `触发器配置` 中,新增 `API网关触发`,保持默认配置即可。 66 | 7. 点击完成 67 | 68 | ![基础配置](https://image-1252109614.cos.ap-beijing.myqcloud.com/img/image-20210707204518173.png) 69 | 70 | ![高级配置](https://image-1252109614.cos.ap-beijing.myqcloud.com/img/image-20210707204936310.png) 71 | 72 | ![触发器配置](https://image-1252109614.cos.ap-beijing.myqcloud.com/img/image-20210707205811630.png) 73 | 74 | 稍等一会,进入你创建的函数: 75 | 76 | ![image-20210705015301810](https://image-1252109614.cos.ap-beijing.myqcloud.com/img/image-20210705015301810.png) 77 | 78 | 图中所示的访问路径就是函数的请求路径,至此,所有的配置完成。 79 | 80 | ## 👌 发起HTTP请求测试是否成功 81 | 82 | 现已支持 `GET`、`POST` 方法进行请求。 83 | 84 | > 当发送的文本中存在有换行符或其他字符时,请把 msg 参数进行 url 编码(使用 GET 方法注意,POST不需要) 85 | 86 | ### 简单使用: 87 | 88 | 在你刚才获得的路径之后拼几个GET参数,在后面加上:`?sendkey=你配置的sendkey&msg_type=text&msg=hello` 89 | 90 | ![image-20210705015727720](https://image-1252109614.cos.ap-beijing.myqcloud.com/img/image-20210705015727720.png) 91 | 92 | 可以看见返回 success 字样。 93 | 94 | 观察手机推送,也可以收到消息: 95 | 96 | ![image-20210705015804023](https://image-1252109614.cos.ap-beijing.myqcloud.com/img/image-20210705015804023.png) 97 | 98 | 之后,想怎么用就是你的事了,想给自己的微信推送,只需要给这个 URL 发一条 HTTP 请求即可。 99 | 100 | ### 给指定成员推送消息: 101 | 102 | 如果你的需求是给企业微信中的指定成员发送消息而不是所有成员,则在 GET 请求中多加一个参数 `to_user`,值为 成员ID列表,如果想指定多个成员,则多个成员ID之间用 `|` 隔开。如请求:`https://xxxxx/wecomchan?sendkey=123456&msg_type=text&msg=测试消息&to_user=User1|User2` ,也能收到消息。 103 | 104 | ![image-20210707211125345](https://image-1252109614.cos.ap-beijing.myqcloud.com/img/image-20210707211125345.png) 105 | 106 | > 成员的 ID 在企业微信后台,`通讯录`,点开指定成员资料,有个 `账号` 字段,该字段即为该成员的ID. 107 | 108 | ### 使用 `POST` 进行请求 109 | 110 | 大部分情况下,`GET` 请求已经可以很好的满足发送一些短消息的需求,但是当消息体过长时,云函数可能报参数过长错误,故在 `V2.1` 版本加入 `POST` 请求支持。 111 | 112 | 与 `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 格式例如: 113 | 114 | ```json 115 | { 116 | "sendkey": "123456", 117 | "msg_type": "text", 118 | "msg": "这是一条POST消息", 119 | "to_user": "User1|User2" 120 | } 121 | ``` 122 | 123 | ### 参数说明: 124 | 125 | 下表为请求的参数说明(`GET` 与 `POST` 字段名相同): 126 | 127 | | 参数名称 | 说明 | 是否可选 | 128 | | ---------- | --------------------------------------------------------------------------------------------------------------- | -------- | 129 | | `sendkey` | 校验是否是本人调用的密钥,随意设置,最终发起HTTP请求携带即可 | 必须 | 130 | | `msg_type` | 消息类型,目前只有纯文本一种类型,值为 `text` | 必须 | 131 | | `msg` | 消息内容,支持多行和UTF8字符,在程序中构建字符串时加上**换行符**即可,如果有特殊符号,记得使用 `urlencode` 编码 | 必须 | 132 | | `to_user` | 如果需要给企业内指定成员发消息,可在此参数中指定成员。如果不传本参数,默认所有成员。 | 可选 | 133 | 134 | 👇👇👇 135 | 136 | --- 137 | 138 | 如果发现bug,或者对本项目有任何建议,欢迎联系 `riba2534@qq.com` 或者直接提 [Issue](https://github.com/riba2534/wecomchan/issues). 139 | 140 | -------------------------------------------------------------------------------- /go-scf/build.sh: -------------------------------------------------------------------------------- 1 | set -ex 2 | 3 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o main && upx -9 main 4 | 5 | zip main.zip main -------------------------------------------------------------------------------- /go-scf/consts/consts.go: -------------------------------------------------------------------------------- 1 | package consts 2 | 3 | var ( 4 | FUNC_NAME string 5 | SEND_KEY string 6 | WECOM_CID string 7 | WECOM_SECRET string 8 | WECOM_AID string 9 | WECOM_TOUID string 10 | ) 11 | 12 | // 微信发消息API 13 | const ( 14 | // https://work.weixin.qq.com/api/doc/90000/90135/90236 15 | WeComMsgSendURL = "https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=%s" 16 | // https://work.weixin.qq.com/api/doc/90000/90135/91039 17 | WeComAccessTokenURL = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=%s&corpsecret=%s" 18 | ) 19 | -------------------------------------------------------------------------------- /go-scf/dal/dal.go: -------------------------------------------------------------------------------- 1 | package dal 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "net/http" 7 | "time" 8 | 9 | jsoniter "github.com/json-iterator/go" 10 | "github.com/riba2534/wecomchan/go-scf/consts" 11 | "github.com/riba2534/wecomchan/go-scf/model" 12 | ) 13 | 14 | var AccessToken string 15 | 16 | func loadAccessToken() { 17 | client := http.Client{Timeout: 10 * time.Second} 18 | req, _ := http.NewRequest("GET", fmt.Sprintf(consts.WeComAccessTokenURL, consts.WECOM_CID, consts.WECOM_SECRET), nil) 19 | resp, err := client.Do(req) 20 | if err != nil { 21 | fmt.Println("getAccessToken err=", err) 22 | } 23 | defer resp.Body.Close() 24 | if resp.StatusCode != 200 { 25 | fmt.Println("getAccessToken statusCode is not 200") 26 | } 27 | respBodyBytes, _ := ioutil.ReadAll(resp.Body) 28 | assesTokenResp := &model.AssesTokenResp{} 29 | if err := jsoniter.Unmarshal(respBodyBytes, assesTokenResp); err != nil { 30 | fmt.Println("getAccessToken json Unmarshal failed, err=", err) 31 | panic(err) 32 | } 33 | if assesTokenResp.Errcode != 0 { 34 | fmt.Println("getAccessToken assesTokenResp.Errcode != 0, err=", assesTokenResp.Errmsg) 35 | panic(err) 36 | } 37 | AccessToken = assesTokenResp.AccessToken 38 | } 39 | 40 | func Init() { 41 | loadAccessToken() 42 | fmt.Printf("[Init] accessToken load success, time=%s, token=%s\n", time.Now().Format("2006-01-02 15:04:05"), AccessToken) 43 | go func() { 44 | for { 45 | time.Sleep(30 * time.Minute) 46 | loadAccessToken() 47 | fmt.Printf("[Goroutine] accessToken load success, time=%s, token=%s\n", time.Now().Format("2006-01-02 15:04:05"), AccessToken) 48 | } 49 | }() 50 | } 51 | -------------------------------------------------------------------------------- /go-scf/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/riba2534/wecomchan/go-scf 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/json-iterator/go v1.1.11 7 | github.com/tencentyun/scf-go-lib v0.0.0-20200624065115-ba679e2ec9c9 8 | ) 9 | -------------------------------------------------------------------------------- /go-scf/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 5 | github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ= 6 | github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 7 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= 8 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 9 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= 10 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 11 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 12 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 13 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 14 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 15 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 16 | github.com/tencentyun/scf-go-lib v0.0.0-20200624065115-ba679e2ec9c9 h1:JdeXp/XPi7lBmpQNSUxElMAvwppMlFSiamTtXYRFuUc= 17 | github.com/tencentyun/scf-go-lib v0.0.0-20200624065115-ba679e2ec9c9/go.mod h1:K3DbqPpP2WE/9MWokWWzgFZcbgtMb9Wd5CYk9AAbEN8= 18 | -------------------------------------------------------------------------------- /go-scf/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/riba2534/wecomchan/go-scf/consts" 9 | "github.com/riba2534/wecomchan/go-scf/dal" 10 | "github.com/riba2534/wecomchan/go-scf/service" 11 | "github.com/riba2534/wecomchan/go-scf/utils" 12 | "github.com/tencentyun/scf-go-lib/cloudfunction" 13 | "github.com/tencentyun/scf-go-lib/events" 14 | ) 15 | 16 | func init() { 17 | consts.FUNC_NAME = utils.GetEnvDefault("FUNC_NAME", "") 18 | consts.SEND_KEY = utils.GetEnvDefault("SEND_KEY", "") 19 | consts.WECOM_CID = utils.GetEnvDefault("WECOM_CID", "") 20 | consts.WECOM_SECRET = utils.GetEnvDefault("WECOM_SECRET", "") 21 | consts.WECOM_AID = utils.GetEnvDefault("WECOM_AID", "") 22 | consts.WECOM_TOUID = utils.GetEnvDefault("WECOM_TOUID", "@all") 23 | if consts.FUNC_NAME == "" || consts.SEND_KEY == "" || consts.WECOM_CID == "" || 24 | consts.WECOM_SECRET == "" || consts.WECOM_AID == "" || consts.WECOM_TOUID == "" { 25 | 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) 26 | panic("os.env param error") 27 | } 28 | fmt.Println("os.env load success!") 29 | } 30 | 31 | func HTTPHandler(ctx context.Context, event events.APIGatewayRequest) (events.APIGatewayResponse, error) { 32 | path := event.Path 33 | fmt.Println("req->", utils.MarshalToStringParam(event)) 34 | var result interface{} 35 | if strings.HasPrefix(path, "/"+consts.FUNC_NAME) { 36 | result = service.WeComChanService(ctx, event) 37 | } else { 38 | // 匹配失败返回原始HTTP请求 39 | result = event 40 | } 41 | return events.APIGatewayResponse{ 42 | IsBase64Encoded: false, 43 | StatusCode: 200, 44 | Headers: map[string]string{}, 45 | Body: utils.MarshalToStringParam(result), 46 | }, nil 47 | } 48 | 49 | func main() { 50 | dal.Init() 51 | cloudfunction.Start(HTTPHandler) 52 | } 53 | -------------------------------------------------------------------------------- /go-scf/model/model.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type AssesTokenResp struct { 4 | Errcode int `json:"errcode"` 5 | Errmsg string `json:"errmsg"` 6 | AccessToken string `json:"access_token"` 7 | ExpiresIn int `json:"expires_in"` 8 | } 9 | 10 | type MsgText struct { 11 | Content string `json:"content"` 12 | } 13 | 14 | // https://work.weixin.qq.com/api/doc/90002/90151/90854 15 | type WechatMsg struct { 16 | ToUser string `json:"touser"` 17 | AgentId string `json:"agentid"` 18 | MsgType string `json:"msgtype"` 19 | Text *MsgText `json:"text"` 20 | DuplicateCheckInterval int `json:"duplicate_check_interval"` 21 | } 22 | 23 | type PostResp struct { 24 | Errcode int `json:"errcode"` 25 | Errmsg string `json:"errmsg"` 26 | Invaliduser string `json:"invaliduser"` 27 | } 28 | -------------------------------------------------------------------------------- /go-scf/service/wecomchan.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "fmt" 8 | "io/ioutil" 9 | "net/http" 10 | "time" 11 | 12 | jsoniter "github.com/json-iterator/go" 13 | "github.com/riba2534/wecomchan/go-scf/consts" 14 | "github.com/riba2534/wecomchan/go-scf/dal" 15 | "github.com/riba2534/wecomchan/go-scf/model" 16 | "github.com/riba2534/wecomchan/go-scf/utils" 17 | "github.com/tencentyun/scf-go-lib/events" 18 | ) 19 | 20 | func WeComChanService(ctx context.Context, event events.APIGatewayRequest) map[string]interface{} { 21 | sendKey := getQuery("sendkey", event) 22 | msgType := getQuery("msg_type", event) 23 | msg := getQuery("msg", event) 24 | if msgType == "" || msg == "" { 25 | return utils.MakeResp(-1, "param error") 26 | } 27 | if sendKey != consts.SEND_KEY { 28 | return utils.MakeResp(-1, "sendkey error") 29 | } 30 | toUser := getQuery("to_user", event) 31 | if toUser == "" { 32 | toUser = consts.WECOM_TOUID 33 | } 34 | if err := postWechatMsg(dal.AccessToken, msg, msgType, toUser); err != nil { 35 | return utils.MakeResp(0, err.Error()) 36 | } 37 | return utils.MakeResp(0, "success") 38 | } 39 | 40 | func postWechatMsg(accessToken, msg, msgType, toUser string) error { 41 | content := &model.WechatMsg{ 42 | ToUser: toUser, 43 | AgentId: consts.WECOM_AID, 44 | MsgType: msgType, 45 | DuplicateCheckInterval: 600, 46 | Text: &model.MsgText{ 47 | Content: msg, 48 | }, 49 | } 50 | b, _ := jsoniter.Marshal(content) 51 | client := http.Client{Timeout: 10 * time.Second} 52 | req, _ := http.NewRequest("POST", fmt.Sprintf(consts.WeComMsgSendURL, accessToken), bytes.NewBuffer(b)) 53 | req.Header.Set("Content-type", "application/json") 54 | resp, err := client.Do(req) 55 | if err != nil { 56 | fmt.Println("[postWechatMsg] failed, err=", err) 57 | return nil 58 | } 59 | defer resp.Body.Close() 60 | if resp.StatusCode != 200 { 61 | fmt.Println("postWechatMsg statusCode is not 200") 62 | return errors.New("statusCode is not 200") 63 | } 64 | respBodyBytes, _ := ioutil.ReadAll(resp.Body) 65 | postResp := &model.PostResp{} 66 | if err := jsoniter.Unmarshal(respBodyBytes, postResp); err != nil { 67 | fmt.Println("postWechatMsg json Unmarshal failed, err=", err) 68 | return err 69 | } 70 | if postResp.Errcode != 0 { 71 | fmt.Println("postWechatMsg postResp.Errcode != 0, err=", postResp.Errmsg) 72 | return errors.New(postResp.Errmsg) 73 | } 74 | return nil 75 | } 76 | 77 | func getQuery(key string, event events.APIGatewayRequest) string { 78 | switch event.Method { 79 | case "GET": 80 | value := event.QueryString[key] 81 | if len(value) > 0 && value[0] != "" { 82 | return value[0] 83 | } 84 | return "" 85 | case "POST": 86 | return jsoniter.Get([]byte(event.Body), key).ToString() 87 | default: 88 | return "" 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /go-scf/utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "os" 5 | 6 | jsoniter "github.com/json-iterator/go" 7 | ) 8 | 9 | func MarshalToStringParam(param interface{}) string { 10 | s, err := jsoniter.MarshalToString(param) 11 | if err != nil { 12 | return "{}" 13 | } 14 | return s 15 | } 16 | 17 | func MakeResp(code int, msg string) map[string]interface{} { 18 | return map[string]interface{}{ 19 | "code": code, 20 | "msg": msg, 21 | } 22 | } 23 | 24 | func GetEnvDefault(key, defVal string) string { 25 | val, ex := os.LookupEnv(key) 26 | if !ex { 27 | return defVal 28 | } 29 | return val 30 | } 31 | -------------------------------------------------------------------------------- /go-wecomchan/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.16.5-alpine3.13 as gobuilder 2 | 3 | # 替换为国内源 4 | RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories 5 | 6 | ENV GO111MODULE="on" 7 | ENV GOPROXY="https://goproxy.cn,direct" 8 | ENV CGO_ENABLED=0 9 | 10 | WORKDIR /go/src/app 11 | COPY . . 12 | 13 | RUN apk update && apk upgrade && apk add --no-cache ca-certificates 14 | RUN update-ca-certificates 15 | RUN go build 16 | 17 | FROM scratch 18 | 19 | WORKDIR /root 20 | 21 | COPY --from=gobuilder /go/src/app/wecomchan . 22 | COPY --from=gobuilder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 23 | 24 | EXPOSE 8080 25 | 26 | CMD ["./wecomchan"] 27 | -------------------------------------------------------------------------------- /go-wecomchan/Dockerfile.architecture: -------------------------------------------------------------------------------- 1 | FROM --platform=$TARGETPLATFORM golang:1.16.5-alpine3.13 as gobuilder 2 | 3 | ENV GO111MODULE="on" 4 | ENV GOPROXY="https://goproxy.cn,direct" 5 | ENV CGO_ENABLED=0 6 | 7 | WORKDIR /go/src/app 8 | COPY . . 9 | 10 | RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories 11 | RUN apk update && apk upgrade && apk add --no-cache ca-certificates 12 | RUN update-ca-certificates 13 | RUN go build 14 | 15 | FROM scratch 16 | 17 | WORKDIR /root 18 | 19 | COPY --from=gobuilder /go/src/app/wecomchan . 20 | COPY --from=gobuilder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 21 | 22 | EXPOSE 8080 23 | 24 | CMD ["./wecomchan"] 25 | -------------------------------------------------------------------------------- /go-wecomchan/README.md: -------------------------------------------------------------------------------- 1 | # go-wecomchan 2 | 3 | ## what's new 4 | 5 | 添加 Dockerfile.architecture 使用docker buildx支持构建多架构镜像。 6 | 7 | 关于docker buildx build 使用方式参考官方文档: 8 | 9 | [https://docs.docker.com/engine/reference/commandline/buildx_build/](https://docs.docker.com/engine/reference/commandline/buildx_build/) 10 | 11 | ## 配置说明 12 | 13 | 直接使用和构建二进制文件使用需要golang环境,并且网络可以安装依赖。 14 | docker构建镜像使用,需要安装docker,不依赖golang以及网络。 15 | 16 | ## 修改默认值 17 | 18 | 修改的sendkey,企业微信公司ID 等默认值为你的企业中的相关信息,如不设置运行时和打包后都可通过环境变量传入。 19 | 20 | ```golang 21 | var Sendkey = GetEnvDefault("SENDKEY", "set_a_sendkey") 22 | var WecomCid = GetEnvDefault("WECOM_CID", "企业微信公司ID") 23 | var WecomSecret = GetEnvDefault("WECOM_SECRET", "企业微信应用Secret") 24 | var WecomAid = GetEnvDefault("WECOM_AID", "企业微信应用ID") 25 | var WecomToUid = GetEnvDefault("WECOM_TOUID", "@all") 26 | var RedisStat = GetEnvDefault("REDIS_STAT", "OFF") 27 | var RedisAddr = GetEnvDefault("REDIS_ADDR", "localhost:6379") 28 | var RedisPassword = GetEnvDefault("REDIS_PASSWORD", "") 29 | ``` 30 | 31 | ## 直接使用 32 | 33 | 如果没有添加默认值,需要先引入环境变量,以SENDKEY为例: 34 | 35 | `export SENDKEY=set_a_sendkey` 36 | 依次引入环境变量后,执行 37 | `go run .` 38 | 39 | ## build命令构建二进制文件使用 40 | 41 | 1. 构建命令 42 | `go build` 43 | 44 | 2. 启动 45 | `./wecomchan` 46 | 47 | ## 构建docker镜像使用(推荐,不依赖golang,不依赖网络) 48 | 49 | 新增打包好的镜像可以直接使用 50 | 51 | - 推送文本or图片:`docker pull aozakiaoko/go-wecomchan` 52 | Docker Hub 地址为:[https://hub.docker.com/r/aozakiaoko/go-wecomchan](https://hub.docker.com/r/aozakiaoko/go-wecomchan) 53 | 54 | 已经更新latest镜像为 @fcbhank 的最新代码,并支持arm64设备。也可通过aozakiaoko/go-wecomchan:v2 获取最新镜像。 55 | 56 | - v2_推送文本or图片:`docker pull fcbhank/go-wecomchan` 57 | Docker Hub 地址为:[https://hub.docker.com/r/fcbhank/go-wecomchan](https://hub.docker.com/r/fcbhank/go-wecomchan) 58 | 59 | 1. 构建镜像 60 | `docker build -t go-wecomchan .` 61 | 62 | 2. 修改默认值后启动镜像 63 | `docker run -dit -p 8080:8080 go-wecomchan` 64 | 65 | 3. 通过环境变量启动镜像并启用redis 66 | 67 | ```bash 68 | docker run -dit -e SENDKEY=set_a_sendkey \ 69 | -e WECOM_CID=企业微信公司ID \ 70 | -e WECOM_SECRET=企业微信应用Secret \ 71 | -e WECOM_AID=企业微信应用ID \ 72 | -e WECOM_TOUID="@all" \ 73 | -e REDIS_STAT=ON \ 74 | -e REDIS_ADDR="localhost:6379" \ 75 | -e REDIS_PASSWORD="" \ 76 | # aozakiaoko/go-wecomchan 已经更新镜像为 @fcbhank 的最新代码,并支持arm64设备。 77 | # v2 fcbhank/go-wecomchan 78 | -p 8080:8080 go-wecomchan 79 | ``` 80 | 81 | 如不使用redis不要传入最后三个关于redis的环境变量(REDIS_STAT|REDIS_ADDR|REDIS_PASSWORD) 82 | 83 | 4. 环境变量说明 84 | 85 | |名称|描述| 86 | |---|---| 87 | |SENDKEY|发送时用来验证的key| 88 | |WECOM_CID|企业微信公司ID| 89 | |WECOM_SECRET|企业微信应用Secret| 90 | |WECOM_AID|企业微信应用ID| 91 | |WECOM_TOUID|需要发送给的人,详见[企业微信官方文档](https://work.weixin.qq.com/api/doc/90000/90135/90236#%E6%96%87%E6%9C%AC%E6%B6%88%E6%81%AF)| 92 | |REDIS_STAT|是否启用redis换缓存token,ON-启用 OFF或空-不启用| 93 | |REDIS_ADDR|redis服务器地址,如不启用redis缓存可不设置| 94 | |REDIS_PASSWORD|redis的连接密码,如不启用redis缓存可不设置| 95 | 96 | ## 使用docker-compose 部署 97 | 98 | 修改docker-compose.yml 文件内上述的环境变量,之后执行 99 | 100 | `docker-compose up -d` 101 | 102 | ## 调用方式 103 | - v1_推送文本 104 | 访问 `http://localhost:8080/wecomchan?sendkey=你配置的sendkey&&msg=需要发送的消息&&msg_type=text` 105 | 106 | - v2_推送文本or图片 107 | 108 | ```bash 109 | # 推送文本消息 110 | curl --location --request GET 'http://localhost:8080/wecomchan?sendkey={你的sendkey}&msg={你的文本消息}&msg_type=text' 111 | 112 | # 推送图片消息 113 | curl --location --request POST 'http://localhost:8080/wecomchan?sendkey={你的sendkey}&msg_type=image' \ 114 | --form 'media=@"test.jpg"' 115 | ``` 116 | 117 | ## 后续预计添加 118 | 119 | * [x] Dockerfile 打包镜像(不依赖网络环境) 120 | * [x] 通过环境变量传递企业微信id,secret等,镜像一次构建多次使用 121 | * [x] docker-compose redis + go-wecomchan 一键部署 -------------------------------------------------------------------------------- /go-wecomchan/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | go-wecomchan: 5 | image: docker.io/aozakiaoko/go-wecomchan:latest 6 | environment: 7 | - SENDKEY=发送时用来验证的key 8 | - WECOM_CID=企业微信公司ID 9 | - WECOM_SECRET=企业微信应用Secret 10 | - WECOM_AID=企业微信应用ID 11 | - WECOM_TOUID=@all 12 | - REDIS_STAT=ON 13 | - REDIS_ADDR=redis:6379 14 | - REDIS_PASSWORD=redis的连接密码 15 | ports: 16 | - 8080:8080 17 | networks: 18 | - go-wecomchan 19 | depends_on: 20 | - redis 21 | 22 | redis: 23 | image: docker.io/bitnami/redis:6.2 24 | environment: 25 | - REDIS_PASSWORD=redis的连接密码 26 | - REDIS_DISABLE_COMMANDS=FLUSHDB,FLUSHALL 27 | networks: 28 | - go-wecomchan 29 | volumes: 30 | - 'redis_data:/bitnami/redis/data' 31 | 32 | volumes: 33 | redis_data: 34 | driver: local 35 | 36 | networks: 37 | go-wecomchan: 38 | -------------------------------------------------------------------------------- /go-wecomchan/go.mod: -------------------------------------------------------------------------------- 1 | module go/wecomchan 2 | 3 | go 1.16 4 | 5 | require github.com/go-redis/redis/v8 v8.10.0 6 | -------------------------------------------------------------------------------- /go-wecomchan/go.sum: -------------------------------------------------------------------------------- 1 | github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= 2 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 3 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 6 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 7 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 8 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 9 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 10 | github.com/go-redis/redis/v8 v8.10.0 h1:OZwrQKuZqdJ4QIM8wn8rnuz868Li91xA3J2DEq+TPGA= 11 | github.com/go-redis/redis/v8 v8.10.0/go.mod h1:vXLTvigok0VtUX0znvbcEW1SOt4OA9CU1ZfnOtKOaiM= 12 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 13 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 14 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 15 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 16 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 17 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 18 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 19 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 20 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 21 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 22 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 23 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 24 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 25 | github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= 26 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 27 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 28 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 29 | github.com/onsi/ginkgo v1.15.0 h1:1V1NfVQR87RtWAgp1lv9JZJ5Jap+XFGKPi00andXGi4= 30 | github.com/onsi/ginkgo v1.15.0/go.mod h1:hF8qUzuuC8DJGygJH3726JnCZX4MYbRB8yFfISqnKUg= 31 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 32 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 33 | github.com/onsi/gomega v1.10.5 h1:7n6FEkpFmfCoo2t+YYqXH0evK+a9ICQz0xcAy9dYcaQ= 34 | github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48= 35 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 36 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 37 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 38 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 39 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 40 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 41 | go.opentelemetry.io/otel v0.20.0 h1:eaP0Fqu7SXHwvjiqDq83zImeehOHX8doTvU9AwXON8g= 42 | go.opentelemetry.io/otel v0.20.0/go.mod h1:Y3ugLH2oa81t5QO+Lty+zXf8zC9L26ax4Nzoxm/dooo= 43 | go.opentelemetry.io/otel/metric v0.20.0 h1:4kzhXFP+btKm4jwxpjIqjs41A7MakRFUS86bqLHTIw8= 44 | go.opentelemetry.io/otel/metric v0.20.0/go.mod h1:598I5tYlH1vzBjn+BTuhzTCSb/9debfNp6R3s7Pr1eU= 45 | go.opentelemetry.io/otel/oteltest v0.20.0 h1:HiITxCawalo5vQzdHfKeZurV8x7ljcqAgiWzF6Vaeaw= 46 | go.opentelemetry.io/otel/oteltest v0.20.0/go.mod h1:L7bgKf9ZB7qCwT9Up7i9/pn0PWIa9FqQ2IQ8LoxiGnw= 47 | go.opentelemetry.io/otel/trace v0.20.0 h1:1DL6EXUdcg95gukhuRRvLDO/4X5THh/5dIV52lqtnbw= 48 | go.opentelemetry.io/otel/trace v0.20.0/go.mod h1:6GjCW8zgDjwGHGa6GkyeB8+/5vjT16gUEi0Nf1iBdgw= 49 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 50 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 51 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 52 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 53 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 54 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 55 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 56 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 57 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 58 | golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb h1:eBmm0M9fYhWpKZLjQUUKka/LtIxf46G4fxeEz5KJr9U= 59 | golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 60 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 61 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 62 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 63 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 64 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 65 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 66 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 67 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 68 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 69 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 70 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 71 | golang.org/x/sys v0.0.0-20210112080510-489259a85091 h1:DMyOG0U+gKfu8JZzg2UQe9MeaC1X+xQWlAKcRnjxjCw= 72 | golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 73 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 74 | golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= 75 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 76 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 77 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 78 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 79 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 80 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 81 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 82 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 83 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 84 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 85 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 86 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 87 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 88 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 89 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 90 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 91 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 92 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 93 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 94 | gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= 95 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 96 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 97 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 98 | -------------------------------------------------------------------------------- /go-wecomchan/wecomchan.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "io/ioutil" 9 | "log" 10 | "math" 11 | "mime/multipart" 12 | "net/http" 13 | "os" 14 | "reflect" 15 | "time" 16 | 17 | "github.com/go-redis/redis/v8" 18 | ) 19 | 20 | /*------------------------------- 环境变量配置 begin -------------------------------*/ 21 | 22 | var Sendkey = GetEnvDefault("SENDKEY", "set_a_sendkey") 23 | var WecomCid = GetEnvDefault("WECOM_CID", "企业微信公司ID") 24 | var WecomSecret = GetEnvDefault("WECOM_SECRET", "企业微信应用Secret") 25 | var WecomAid = GetEnvDefault("WECOM_AID", "企业微信应用ID") 26 | var WecomToUid = GetEnvDefault("WECOM_TOUID", "@all") 27 | var RedisStat = GetEnvDefault("REDIS_STAT", "OFF") 28 | var RedisAddr = GetEnvDefault("REDIS_ADDR", "localhost:6379") 29 | var RedisPassword = GetEnvDefault("REDIS_PASSWORD", "") 30 | var ctx = context.Background() 31 | 32 | /*------------------------------- 环境变量配置 end -------------------------------*/ 33 | 34 | /*------------------------------- 企业微信服务端API begin -------------------------------*/ 35 | 36 | var GetTokenApi = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=%s&corpsecret=%s" 37 | var SendMessageApi = "https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=%s" 38 | var UploadMediaApi = "https://qyapi.weixin.qq.com/cgi-bin/media/upload?access_token=%s&type=%s" 39 | 40 | /*------------------------------- 企业微信服务端API end -------------------------------*/ 41 | 42 | const RedisTokenKey = "access_token" 43 | 44 | type Msg struct { 45 | Content string `json:"content"` 46 | } 47 | type Pic struct { 48 | MediaId string `json:"media_id"` 49 | } 50 | type JsonData struct { 51 | ToUser string `json:"touser"` 52 | AgentId string `json:"agentid"` 53 | MsgType string `json:"msgtype"` 54 | DuplicateCheckInterval int `json:"duplicate_check_interval"` 55 | Text Msg `json:"text"` 56 | Image Pic `json:"image"` 57 | } 58 | 59 | // GetEnvDefault 获取配置信息,未获取到则取默认值 60 | func GetEnvDefault(key, defVal string) string { 61 | val, ex := os.LookupEnv(key) 62 | if !ex { 63 | return defVal 64 | } 65 | return val 66 | } 67 | 68 | // ParseJson 将json字符串解析为map 69 | func ParseJson(jsonStr string) map[string]interface{} { 70 | var wecomResponse map[string]interface{} 71 | if string(jsonStr) != "" { 72 | err := json.Unmarshal([]byte(string(jsonStr)), &wecomResponse) 73 | if err != nil { 74 | log.Println("生成json字符串错误") 75 | } 76 | } 77 | return wecomResponse 78 | } 79 | 80 | // GetRemoteToken 从企业微信服务端API获取access_token,存在redis服务则缓存 81 | func GetRemoteToken(corpId, appSecret string) string { 82 | getTokenUrl := fmt.Sprintf(GetTokenApi, corpId, appSecret) 83 | log.Println("getTokenUrl==>", getTokenUrl) 84 | resp, err := http.Get(getTokenUrl) 85 | if err != nil { 86 | log.Println(err) 87 | } 88 | defer resp.Body.Close() 89 | respData, err := ioutil.ReadAll(resp.Body) 90 | if err != nil { 91 | log.Println(err) 92 | } 93 | tokenResponse := ParseJson(string(respData)) 94 | log.Println("企业微信获取access_token接口返回==>", tokenResponse) 95 | accessToken := tokenResponse[RedisTokenKey].(string) 96 | 97 | if RedisStat == "ON" { 98 | log.Println("prepare to set redis key") 99 | rdb := RedisClient() 100 | // access_token有效时间为7200秒(2小时) 101 | set, err := rdb.SetNX(ctx, RedisTokenKey, accessToken, 7000*time.Second).Result() 102 | log.Println(set) 103 | if err != nil { 104 | log.Println(err) 105 | } 106 | } 107 | return accessToken 108 | } 109 | 110 | // RedisClient redis客户端 111 | func RedisClient() *redis.Client { 112 | rdb := redis.NewClient(&redis.Options{ 113 | Addr: RedisAddr, 114 | Password: RedisPassword, // no password set 115 | DB: 0, // use default DB 116 | }) 117 | return rdb 118 | } 119 | 120 | // PostMsg 推送消息 121 | func PostMsg(postData JsonData, postUrl string) string { 122 | postJson, _ := json.Marshal(postData) 123 | log.Println("postJson ", string(postJson)) 124 | log.Println("postUrl ", postUrl) 125 | msgReq, err := http.NewRequest("POST", postUrl, bytes.NewBuffer(postJson)) 126 | if err != nil { 127 | log.Println(err) 128 | } 129 | msgReq.Header.Set("Content-Type", "application/json") 130 | client := &http.Client{} 131 | resp, err := client.Do(msgReq) 132 | if err != nil { 133 | log.Fatalln("企业微信发送应用消息接口报错==>", err) 134 | } 135 | defer msgReq.Body.Close() 136 | body, _ := ioutil.ReadAll(resp.Body) 137 | mediaResp := ParseJson(string(body)) 138 | log.Println("企业微信发送应用消息接口返回==>", mediaResp) 139 | return string(body) 140 | } 141 | 142 | // UploadMedia 上传临时素材并返回mediaId 143 | func UploadMedia(msgType string, req *http.Request, accessToken string) (string, float64) { 144 | // 企业微信图片上传不能大于2M 145 | _ = req.ParseMultipartForm(2 << 20) 146 | imgFile, imgHeader, err := req.FormFile("media") 147 | log.Printf("文件大小==>%d字节", imgHeader.Size) 148 | if err != nil { 149 | log.Fatalln("图片文件出错==>", err) 150 | // 自定义code无效的图片文件 151 | return "", 400 152 | } 153 | buf := new(bytes.Buffer) 154 | writer := multipart.NewWriter(buf) 155 | if createFormFile, err := writer.CreateFormFile("media", imgHeader.Filename); err == nil { 156 | readAll, _ := ioutil.ReadAll(imgFile) 157 | createFormFile.Write(readAll) 158 | } 159 | writer.Close() 160 | 161 | uploadMediaUrl := fmt.Sprintf(UploadMediaApi, accessToken, msgType) 162 | log.Println("uploadMediaUrl==>", uploadMediaUrl) 163 | newRequest, _ := http.NewRequest("POST", uploadMediaUrl, buf) 164 | newRequest.Header.Set("Content-Type", writer.FormDataContentType()) 165 | log.Println("Content-Type ", writer.FormDataContentType()) 166 | client := &http.Client{} 167 | resp, err := client.Do(newRequest) 168 | respData, _ := ioutil.ReadAll(resp.Body) 169 | mediaResp := ParseJson(string(respData)) 170 | log.Println("企业微信上传临时素材接口返回==>", mediaResp) 171 | if err != nil { 172 | log.Fatalln("上传临时素材出错==>", err) 173 | return "", mediaResp["errcode"].(float64) 174 | } else { 175 | return mediaResp["media_id"].(string), float64(0) 176 | } 177 | } 178 | 179 | // ValidateToken 判断accessToken是否失效 180 | // true-未失效, false-失效需重新获取 181 | func ValidateToken(errcode interface{}) bool { 182 | codeTyp := reflect.TypeOf(errcode) 183 | log.Println("errcode的数据类型==>", codeTyp) 184 | if !codeTyp.Comparable() { 185 | log.Printf("type is not comparable: %v", codeTyp) 186 | return true 187 | } 188 | 189 | // 如果errcode为42001表明token已失效,则清空redis中的token缓存 190 | // 已知codeType为float64 191 | if math.Abs(errcode.(float64)-float64(42001)) < 1e-3 { 192 | if RedisStat == "ON" { 193 | log.Printf("token已失效,开始删除redis中的key==>%s", RedisTokenKey) 194 | rdb := RedisClient() 195 | rdb.Del(ctx, RedisTokenKey) 196 | log.Printf("删除redis中的key==>%s完毕", RedisTokenKey) 197 | } 198 | log.Println("现需重新获取token") 199 | return false 200 | } 201 | return true 202 | } 203 | 204 | // GetAccessToken 获取企业微信的access_token 205 | func GetAccessToken() string { 206 | accessToken := "" 207 | if RedisStat == "ON" { 208 | log.Println("尝试从redis获取token") 209 | rdb := RedisClient() 210 | value, err := rdb.Get(ctx, RedisTokenKey).Result() 211 | if err == redis.Nil { 212 | log.Println("access_token does not exist, need get it from remote API") 213 | } 214 | accessToken = value 215 | } 216 | if accessToken == "" { 217 | log.Println("get access_token from remote API") 218 | accessToken = GetRemoteToken(WecomCid, WecomSecret) 219 | } else { 220 | log.Println("get access_token from redis") 221 | } 222 | return accessToken 223 | } 224 | 225 | // InitJsonData 初始化Json公共部分数据 226 | func InitJsonData(msgType string) JsonData { 227 | return JsonData{ 228 | ToUser: WecomToUid, 229 | AgentId: WecomAid, 230 | MsgType: msgType, 231 | DuplicateCheckInterval: 600, 232 | } 233 | } 234 | 235 | // 主函数入口 236 | func main() { 237 | // 设置日志内容显示文件名和行号 238 | log.SetFlags(log.LstdFlags | log.Lshortfile) 239 | wecomChan := func(res http.ResponseWriter, req *http.Request) { 240 | // 获取token 241 | accessToken := GetAccessToken() 242 | // 默认token有效 243 | tokenValid := true 244 | 245 | _ = req.ParseForm() 246 | sendkey := req.FormValue("sendkey") 247 | if sendkey != Sendkey { 248 | log.Panicln("sendkey 错误,请检查") 249 | } 250 | msgContent := req.FormValue("msg") 251 | msgType := req.FormValue("msg_type") 252 | log.Println("mes_type=", msgType) 253 | // 默认mediaId为空 254 | mediaId := "" 255 | if msgType != "image" { 256 | log.Println("消息类型不是图片") 257 | } else { 258 | // token有效则跳出循环继续执行,否则重试3次 259 | for i := 0; i <= 3; i++ { 260 | var errcode float64 261 | mediaId, errcode = UploadMedia(msgType, req, accessToken) 262 | log.Printf("企业微信上传临时素材接口返回的media_id==>[%s], errcode==>[%f]\n", mediaId, errcode) 263 | tokenValid = ValidateToken(errcode) 264 | if tokenValid { 265 | break 266 | } 267 | 268 | accessToken = GetAccessToken() 269 | } 270 | } 271 | 272 | // 准备发送应用消息所需参数 273 | postData := InitJsonData(msgType) 274 | postData.Text = Msg{ 275 | Content: msgContent, 276 | } 277 | postData.Image = Pic{ 278 | MediaId: mediaId, 279 | } 280 | 281 | postStatus := "" 282 | for i := 0; i <= 3; i++ { 283 | sendMessageUrl := fmt.Sprintf(SendMessageApi, accessToken) 284 | postStatus = PostMsg(postData, sendMessageUrl) 285 | postResponse := ParseJson(postStatus) 286 | errcode := postResponse["errcode"] 287 | log.Println("发送应用消息接口返回errcode==>", errcode) 288 | tokenValid = ValidateToken(errcode) 289 | // token有效则跳出循环继续执行,否则重试3次 290 | if tokenValid { 291 | break 292 | } 293 | // 刷新token 294 | accessToken = GetAccessToken() 295 | } 296 | 297 | res.Header().Set("Content-type", "application/json") 298 | _, _ = res.Write([]byte(postStatus)) 299 | } 300 | http.HandleFunc("/wecomchan", wecomChan) 301 | log.Fatal(http.ListenAndServe(":8080", nil)) 302 | } 303 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | connect(REDIS_HOST, REDIS_PORT); 45 | $GLOBALS['REDIS_INSTANCE']->auth(REDIS_PASSWORD); 46 | } 47 | 48 | return $GLOBALS['REDIS_INSTANCE']; 49 | } 50 | 51 | function send_to_wecom($text, $wecom_cid, $wecom_secret, $wecom_aid, $wecom_touid = '@all') 52 | { 53 | $access_token = false; 54 | // 如果启用redis作为缓存 55 | if (REDIS_ON) { 56 | $access_token = redis()->get(REDIS_KEY); 57 | } 58 | 59 | if (!$access_token) { 60 | $info = @json_decode(file_get_contents("https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=".urlencode($wecom_cid)."&corpsecret=".urlencode($wecom_secret)), true); 61 | 62 | if ($info && isset($info['access_token']) && strlen($info['access_token']) > 0) { 63 | $access_token = $info['access_token']; 64 | } 65 | } 66 | 67 | if ($access_token) { 68 | $url = 'https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token='.urlencode($access_token); 69 | $data = new \stdClass(); 70 | $data->touser = $wecom_touid; 71 | $data->agentid = $wecom_aid; 72 | $data->msgtype = "text"; 73 | $data->text = ["content"=> $text]; 74 | $data->duplicate_check_interval = 600; 75 | 76 | $data_json = json_encode($data); 77 | $ch = curl_init(); 78 | curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); 79 | curl_setopt($ch, CURLOPT_URL, $url); 80 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 81 | @curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); 82 | curl_setopt($ch, CURLOPT_POST, true); 83 | curl_setopt($ch, CURLOPT_TIMEOUT, 5); 84 | curl_setopt($ch, CURLOPT_POSTFIELDS, $data_json); 85 | 86 | curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); 87 | curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); 88 | 89 | $response = curl_exec($ch); 90 | if ($response !== false && REDIS_ON) { 91 | redis()->set(REDIS_KEY, $access_token, ['nx', 'ex'=>REDIS_EXPIRED]); 92 | } 93 | return $response; 94 | } 95 | 96 | 97 | return false; 98 | } 99 | -------------------------------------------------------------------------------- /python-aliyunfc/README.md: -------------------------------------------------------------------------------- 1 | ## 阿里云函数部署 Wecom 酱 2 | 3 | 部署步骤: 4 | 5 | 1. 按照[首页](https://github.com/easychen/wecomchan)的说明配置企业微信, 准备好以下信息. 6 | 1. 企业 ID 7 | 2. 应用 secret 8 | 3. 应用 ID 9 | 4. 你自己设置的密码 (sendkey) 10 | 11 | 2. 打开[阿里云函数计算](https://fcnext.console.aliyun.com/overview), 创建服务. 12 | 13 | ![image-20220205142747826](pic/image-20220205142747826.png) 14 | 15 | 3. 服务名称自选, 点击确定. 16 | 17 | ![image-20220205142906239](pic/image-20220205142906239.png) 18 | 19 | 4. 创建函数, 函数名称自选, 运行环境 Python3, 内存规格 128MB, 其余保持默认. 20 | 21 | ![image-20220205143309699](pic/image-20220205143309699.png) 22 | 23 | 5. 选择`上传代码 - 上传 zip 包`, 上传[这个文件](main-code.zip), 使用网页代码编辑器修改 `index.py` 中的变量为第一步获取的变量, 完成后点击`部署代码`.![image-20220205144020332](pic/image-20220205144020332.png) 24 | 25 | 6. 完成! 26 | 27 | #### 使用方法 28 | 29 | 将以下内容以 json 格式 POST 到函数的公网访问地址即可. 30 | 31 | | 字段 | 说明 | 是否必须 | 32 | | ---- | ------------------------------------------------- | --------------- | 33 | | key | 设置的 sendkey | 是 | 34 | | type | text, image, markdown 或 file 其中之一 | 否, 默认为 text | 35 | | msg | 消息主体(需要推送的文本或图片/文件的 Base64 编码) | 是 | 36 | 37 | 例: 38 | 39 | ``` 40 | {"key":"123", "msg": "Hello, World!"} 41 | ``` 42 | 43 | ``` 44 | {"key":"123", "type": "markdown", "msg": "**Markdown Here!**"} 45 | ``` 46 | 47 | -------------------------------------------------------------------------------- /python-aliyunfc/main-code.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easychen/wecomchan/e6df2c47d41d8a537751bf50fdead2ba4f61634d/python-aliyunfc/main-code.zip -------------------------------------------------------------------------------- /python-aliyunfc/pic/image-20220205142747826.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easychen/wecomchan/e6df2c47d41d8a537751bf50fdead2ba4f61634d/python-aliyunfc/pic/image-20220205142747826.png -------------------------------------------------------------------------------- /python-aliyunfc/pic/image-20220205142906239.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easychen/wecomchan/e6df2c47d41d8a537751bf50fdead2ba4f61634d/python-aliyunfc/pic/image-20220205142906239.png -------------------------------------------------------------------------------- /python-aliyunfc/pic/image-20220205143309699.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easychen/wecomchan/e6df2c47d41d8a537751bf50fdead2ba4f61634d/python-aliyunfc/pic/image-20220205143309699.png -------------------------------------------------------------------------------- /python-aliyunfc/pic/image-20220205144020332.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easychen/wecomchan/e6df2c47d41d8a537751bf50fdead2ba4f61634d/python-aliyunfc/pic/image-20220205144020332.png -------------------------------------------------------------------------------- /python-baiduCFC/README.md: -------------------------------------------------------------------------------- 1 | ## 百度智能云·函数计算CFC 部署 Wecom 酱 2 | 3 | 部署步骤: 4 | 5 | 1. 按照[首页](https://github.com/easychen/wecomchan)的说明配置企业微信, 准备好以下信息. 6 | 1. 企业 ID 7 | 2. 应用 secret 8 | 3. 应用 ID 9 | 4. 你自己设置的密码 (sendkey) 10 | 11 | 2. 打开[百度智能云·函数计算CFC](https://console.bce.baidu.com/cfc/), 创建服务.(若没有账户请自行创建) 12 | 13 | ![image-20220517004653408](pic/image-20220517004653408.png) 14 | 15 | 3. 服务名称自选, 点击确定. 16 | 17 | ![image-20220517004717978](pic/image-20220517004717978.png) 18 | 19 | 4. 创建函数, 函数名称自选, 运行环境 Python3.6, 内存规格 128MB, 其余如图所示或默认. 20 | 21 | ![image-20220517004919062](pic/image-20220517004919062.png) 22 | 23 | ![image-20220517005019692](pic/image-20220517005019692.png) 24 | 25 | ![image-20220517005727886](pic/image-20220517005727886.png) 26 | 27 | ![image-20220517010037726](pic/image-20220517010037726.png) 28 | 29 | 5. 进入`代码编辑页`,选择`上传函数.ZIP包`, 上传[这个文件](baidu-code.zip), 上传成功后,使用`在线编辑`修改 `index.py` 中的几个变量为第一步获取的变量, 完成后点击`保存`. 30 | 31 | 需要修改的有第 `7,128,129,130`这四行。 32 | 33 | ![image-20220517011904665](pic/image-20220517011904665.png) 34 | 35 | 如果需要配置多个应用,可把132-142行的注释去掉,并修改为对应的变量。如果只有一个应用则忽略。 36 | ![image-20220517013849304](pic/image-20220517013849304.png) 37 | 38 | 6. 完成! 39 | 40 | #### 使用方法 41 | 42 | 将以下内容以 json 格式 POST 到函数的公网访问地址即可. 43 | 44 | | 字段 | 说明 | 是否必须 | 45 | | ---- | ------------------------------------------------- | --------------- | 46 | | key | 设置的 sendkey | 是 | 47 | | type | text, image, markdown 或 file 其中之一 | 否, 默认为 text | 48 | | msg | 消息主体(需要推送的文本或图片/文件的 Base64 编码) | 是 | 49 | | uid | 发送消息的用户id,格式为 `zhangsan\|lisi\|wangwu` | 否,默认为 `@all` | 50 | 51 | 例: 52 | 53 | ``` 54 | {"key":"123", "msg": "Hello, World!"} 55 | ``` 56 | 57 | ``` 58 | {"key":"123", "msg": "Hello, World!", "uid": "zhangsan"} 59 | ``` 60 | 61 | ``` 62 | {"key":"123", "type": "markdown", "msg": "**Markdown Here!**"} 63 | ``` 64 | 65 | ``` 66 | { 67 | "key": "123", 68 | "type": "text", 69 | "msg": "文本中支持超链接" 70 | } 71 | ``` -------------------------------------------------------------------------------- /python-baiduCFC/baidu-code.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easychen/wecomchan/e6df2c47d41d8a537751bf50fdead2ba4f61634d/python-baiduCFC/baidu-code.zip -------------------------------------------------------------------------------- /python-baiduCFC/pic/image-20220517004653408.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easychen/wecomchan/e6df2c47d41d8a537751bf50fdead2ba4f61634d/python-baiduCFC/pic/image-20220517004653408.png -------------------------------------------------------------------------------- /python-baiduCFC/pic/image-20220517004717978.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easychen/wecomchan/e6df2c47d41d8a537751bf50fdead2ba4f61634d/python-baiduCFC/pic/image-20220517004717978.png -------------------------------------------------------------------------------- /python-baiduCFC/pic/image-20220517004919062.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easychen/wecomchan/e6df2c47d41d8a537751bf50fdead2ba4f61634d/python-baiduCFC/pic/image-20220517004919062.png -------------------------------------------------------------------------------- /python-baiduCFC/pic/image-20220517005019692.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easychen/wecomchan/e6df2c47d41d8a537751bf50fdead2ba4f61634d/python-baiduCFC/pic/image-20220517005019692.png -------------------------------------------------------------------------------- /python-baiduCFC/pic/image-20220517005724481.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easychen/wecomchan/e6df2c47d41d8a537751bf50fdead2ba4f61634d/python-baiduCFC/pic/image-20220517005724481.png -------------------------------------------------------------------------------- /python-baiduCFC/pic/image-20220517005727886.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easychen/wecomchan/e6df2c47d41d8a537751bf50fdead2ba4f61634d/python-baiduCFC/pic/image-20220517005727886.png -------------------------------------------------------------------------------- /python-baiduCFC/pic/image-20220517010037726.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easychen/wecomchan/e6df2c47d41d8a537751bf50fdead2ba4f61634d/python-baiduCFC/pic/image-20220517010037726.png -------------------------------------------------------------------------------- /python-baiduCFC/pic/image-20220517011904665.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easychen/wecomchan/e6df2c47d41d8a537751bf50fdead2ba4f61634d/python-baiduCFC/pic/image-20220517011904665.png -------------------------------------------------------------------------------- /python-baiduCFC/pic/image-20220517013849304.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easychen/wecomchan/e6df2c47d41d8a537751bf50fdead2ba4f61634d/python-baiduCFC/pic/image-20220517013849304.png -------------------------------------------------------------------------------- /python-huaweiFG/README.md: -------------------------------------------------------------------------------- 1 | 到[华为云函数](https://console.huaweicloud.com/functiongraph)创建python3.9的项目, 名称写WecomPush, 然后复制main.py的内容到华为云函数index.py里, 修改代码中(8, 128-131)对应参数, 之后在触发器里创建APIG网关, 安全认证选None, 仅支持使用params的方式给参数, 不支持通过body, 示例: https://push.example.com/?key=100&msg=hello, 我写的菜, text类型可以推送, 其他的估计不行, 可以pull. 2 | -------------------------------------------------------------------------------- /python-huaweiFG/main.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import logging 3 | import json 4 | import requests 5 | import base64 6 | 7 | # 修改为公司ID 8 | WECOM_ID = "修改为自己的公司ID" 9 | 10 | 11 | def send_to_wecom(text, wecom_cid, wecom_aid, wecom_secret, wecom_touid): 12 | get_token_url = f"https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={wecom_cid}&corpsecret={wecom_secret}" 13 | response = requests.get(get_token_url).content 14 | access_token = json.loads(response).get('access_token') 15 | if access_token and len(access_token) > 0: 16 | send_msg_url = f'https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={access_token}' 17 | data = { 18 | "touser": wecom_touid, 19 | "agentid": wecom_aid, 20 | "msgtype": "text", 21 | "text": { 22 | "content": text 23 | }, 24 | "duplicate_check_interval": 600 25 | } 26 | response = requests.post(send_msg_url, data=json.dumps(data)).content 27 | return hook_return(str(response)) 28 | else: 29 | return hook_return(None) 30 | 31 | 32 | def send_to_wecom_markdown(text, wecom_cid, wecom_aid, wecom_secret, wecom_touid): 33 | get_token_url = f"https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={wecom_cid}&corpsecret={wecom_secret}" 34 | response = requests.get(get_token_url).content 35 | access_token = json.loads(response).get('access_token') 36 | if access_token and len(access_token) > 0: 37 | send_msg_url = f'https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={access_token}' 38 | data = { 39 | "touser": wecom_touid, 40 | "agentid": wecom_aid, 41 | "msgtype": "markdown", 42 | "markdown": { 43 | "content": text 44 | }, 45 | "duplicate_check_interval": 600 46 | } 47 | response = requests.post(send_msg_url, data=json.dumps(data)).content 48 | return hook_return(str(response)) 49 | else: 50 | return hook_return(None) 51 | 52 | 53 | def send_to_wecom_pic(base64_content, wecom_cid, wecom_aid, wecom_secret, wecom_touid): 54 | get_token_url = f"https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={wecom_cid}&corpsecret={wecom_secret}" 55 | response = requests.get(get_token_url).content 56 | access_token = json.loads(response).get('access_token') 57 | if access_token and len(access_token) > 0: 58 | upload_url = f'https://qyapi.weixin.qq.com/cgi-bin/media/upload?access_token={access_token}&type=image' 59 | upload_response = requests.post(upload_url, files={ 60 | "picture": base64.b64decode(base64_content) 61 | }).json() 62 | 63 | logging.info('upload response: ' + str(upload_response)) 64 | 65 | media_id = upload_response['media_id'] 66 | 67 | send_msg_url = f'https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={access_token}' 68 | data = { 69 | "touser": wecom_touid, 70 | "agentid": wecom_aid, 71 | "msgtype": "image", 72 | "image": { 73 | "media_id": media_id 74 | }, 75 | "duplicate_check_interval": 600 76 | } 77 | response = requests.post(send_msg_url, data=json.dumps(data)).content 78 | return hook_return(str(response)) 79 | else: 80 | return hook_return(None) 81 | 82 | 83 | def send_to_wecom_file(base64_content, file_name, wecom_cid, wecom_aid, wecom_secret, wecom_touid): 84 | get_token_url = f"https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={wecom_cid}&corpsecret={wecom_secret}" 85 | response = requests.get(get_token_url).content 86 | access_token = json.loads(response).get('access_token') 87 | if access_token and len(access_token) > 0: 88 | upload_url = f'https://qyapi.weixin.qq.com/cgi-bin/media/upload?access_token={access_token}&type=file' 89 | upload_response = requests.post(upload_url + "&debug=1", files={ 90 | "media": (file_name, base64.b64decode(base64_content)) # 此处上传中文文件名文件旧版本 urllib 有 bug. 91 | }).json() 92 | 93 | logging.info('upload response: ' + str(upload_response)) 94 | 95 | media_id = upload_response['media_id'] 96 | 97 | send_msg_url = f'https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={access_token}' 98 | data = { 99 | "touser": wecom_touid, 100 | "agentid": wecom_aid, 101 | "msgtype": "file", 102 | "file": { 103 | "media_id": media_id 104 | }, 105 | "duplicate_check_interval": 600 106 | } 107 | response = requests.post(send_msg_url, data=json.dumps(data)).content 108 | return hook_return(str(response)) 109 | else: 110 | return hook_return(None) 111 | 112 | 113 | def handler(event, context): 114 | request_body = json.dumps(event.get('queryStringParameters', '')) 115 | 116 | print(request_body) 117 | # get path info 118 | try: 119 | path_info = event.get('path') 120 | except ValueError: 121 | path_info = "ERROR" 122 | 123 | logging.info("request body: " + request_body) 124 | logging.info("path_info: " + str(path_info)) 125 | 126 | # path_info 修改为触发器对应的值,上面设定的 / 这里也写 / 127 | # 应用1 128 | if path_info == "/WecomPush": 129 | send_key = '修改为应用1的' 130 | wecom_agentid = "修改为应用1的" 131 | wecom_secret = "修改为应用1的" 132 | 133 | # # 应用2 134 | # elif path_info == "/app2": 135 | # send_key = "修改为应用2的" 136 | # wecom_agentid = "修改为应用2的" 137 | # wecom_secret = "修改为应用2的" 138 | # # 应用3 139 | # elif path_info == "/app3": 140 | # send_key = "修改为应用3的" 141 | # wecom_agentid = "修改为应用3的" 142 | # wecom_secret = "修改为应用3的" 143 | # # 如果有多个应用,模拟上面添加多个elif即可。path_info可自定义,但需要添加和path_info一致的HTTP触发器 144 | 145 | else: 146 | response = '{"code": -6, "msg": "invalid path info"}' 147 | return hook_return(str(response)) 148 | # input_json = None 149 | try: 150 | input_json = json.loads(request_body) 151 | if input_json['key'] != send_key: 152 | status = '403 Forbidden' 153 | response_headers = [('Content-type', 'text/json')] 154 | response = '{"code": -2, "msg": "invalid send key"}' 155 | return hook_return(str(response)) 156 | except Exception as e: 157 | logging.exception(e) 158 | status = '403 Forbidden' 159 | response_headers = [('Content-type', 'text/json')] 160 | response = '{"code": -1, "msg": "invalid json input"}' 161 | return hook_return(str(response)) 162 | # 获取发送的用户 163 | wecom_touid = input_json.get('uid', '@all') 164 | 165 | logging.info("wecom_touid: " + str(wecom_touid)) 166 | 167 | code = 0 168 | msg = "ok" 169 | status = '200 OK' 170 | 171 | try: 172 | if 'type' not in input_json or input_json['type'] == 'text': 173 | result = send_to_wecom(input_json['msg'], WECOM_ID, wecom_agentid, wecom_secret, wecom_touid) 174 | elif input_json['type'] == 'image': 175 | result = send_to_wecom_pic(input_json['msg'], WECOM_ID, wecom_agentid, wecom_secret, wecom_touid) 176 | elif input_json['type'] == 'markdown': 177 | result = send_to_wecom_markdown(input_json['msg'], WECOM_ID, wecom_agentid, wecom_secret, wecom_touid) 178 | elif input_json['type'] == 'file': 179 | if 'filename' in input_json: 180 | result = send_to_wecom_file(input_json['msg'], input_json['filename'], WECOM_ID, wecom_agentid, 181 | wecom_secret, wecom_touid) 182 | else: 183 | result = send_to_wecom_file(input_json['msg'], "Wepush推送", WECOM_ID, wecom_agentid, wecom_secret, 184 | wecom_touid) 185 | msg = "filename not found. using default." 186 | else: 187 | code = -5 188 | msg = "invalid msg type. type should be text(default), image, markdown or file." 189 | status = "500 Internal Server Error" 190 | result = "" 191 | 192 | logging.info('wechat api response: ' + str(result)) 193 | if result is None: 194 | status = "500 Internal Server Error" 195 | code = -4 196 | msg = "wechat api error: wrong config?" 197 | except Exception as e: 198 | status = "500 Internal Server Error" 199 | code = -3 200 | msg = "unexpected error: " + str(e) 201 | logging.exception(e) 202 | 203 | response_headers = [('Content-type', 'text/json')] 204 | response = json.dumps({"code": code, "msg": msg}) 205 | return hook_return(str(response)) 206 | 207 | 208 | def hook_return(string): 209 | return { 210 | "statusCode": 200, 211 | "isBase64Encoded": False, 212 | "body": string, 213 | "headers": { 214 | "Content-Type": "application/json" 215 | } 216 | } 217 | --------------------------------------------------------------------------------