├── .eslintignore ├── .eslintrc ├── .gitignore ├── .travis.yml ├── MIT-License ├── Makefile ├── README.md ├── figures ├── api.graffle ├── wechat.graffle └── wechat.png ├── index.js ├── lib ├── api_card.js ├── api_common.js ├── api_custom_service.js ├── api_datacube.js ├── api_device.js ├── api_feedback.js ├── api_group.js ├── api_ip.js ├── api_js.js ├── api_mass_send.js ├── api_material.js ├── api_media.js ├── api_menu.js ├── api_message.js ├── api_miniprogram_login.js ├── api_payment.js ├── api_poi.js ├── api_qrcode.js ├── api_semantic.js ├── api_shakearound.js ├── api_shop_common.js ├── api_shop_express.js ├── api_shop_goods.js ├── api_shop_group.js ├── api_shop_order.js ├── api_shop_shelf.js ├── api_shop_stock.js ├── api_subscribe_message.js ├── api_template.js ├── api_url.js ├── api_user.js ├── api_wxacode.js └── util.js ├── package.json └── test ├── api_common.test.js ├── api_user.test.js ├── config.js ├── fixture ├── conditional_menu.json ├── image.jpg ├── invalid.json ├── menu.json ├── movie.mp4 ├── pic.jpg └── test.mp3 └── util.test.js /.eslintignore: -------------------------------------------------------------------------------- 1 | *.debug.js 2 | *.min.js 3 | node_modules/* 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "indent": [ 4 | 2, 5 | 2 6 | ], 7 | "quotes": [ 8 | 2, 9 | "single" 10 | ], 11 | "linebreak-style": [ 12 | 2, 13 | "unix" 14 | ], 15 | "semi": [2, "always"], 16 | "strict": [2, "global"], 17 | "curly": 2, 18 | "eqeqeq": 2, 19 | "no-eval": 2, 20 | "guard-for-in": 2, 21 | "no-caller": 2, 22 | "no-else-return": 2, 23 | "no-eq-null": 2, 24 | "no-extend-native": 2, 25 | "no-extra-bind": 2, 26 | "no-floating-decimal": 2, 27 | "no-implied-eval": 2, 28 | "no-labels": 2, 29 | "no-with": 2, 30 | "no-loop-func": 1, 31 | "no-native-reassign": 2, 32 | "no-redeclare": [2, {"builtinGlobals": true}], 33 | "no-delete-var": 2, 34 | "no-shadow-restricted-names": 2, 35 | "no-undef-init": 2, 36 | "no-use-before-define": 2, 37 | "no-unused-vars": [2, {"args": "none"}], 38 | "no-undef": 2, 39 | "callback-return": [2, ["callback", "cb", "next"]], 40 | "global-require": 0, 41 | "no-console": 0, 42 | "require-yield": 0 43 | }, 44 | "env": { 45 | "es6": true, 46 | "node": true, 47 | "browser": true 48 | }, 49 | "globals": { 50 | "describe": true, 51 | "it": true, 52 | "before": true, 53 | "after": true 54 | }, 55 | "parserOptions": { 56 | "ecmaVersion": 8, 57 | "sourceType": "script", 58 | "ecmaFeatures": { 59 | "jsx": true 60 | } 61 | }, 62 | "extends": "eslint:recommended" 63 | } 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | example 3 | .DS_Store 4 | .idea 5 | coverage 6 | .nyc_output 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "14" 4 | - "12" 5 | - "10" 6 | - "8" 7 | script: make test-coveralls 8 | -------------------------------------------------------------------------------- /MIT-License: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Jackson Tian 2 | http://weibo.com/shyvo 3 | 4 | The MIT License 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining 7 | a copy of this software and associated documentation files (the 8 | "Software"), to deal in the Software without restriction, including 9 | without limitation the rights to use, copy, modify, merge, publish, 10 | distribute, sublicense, and/or sell copies of the Software, and to 11 | permit persons to whom the Software is furnished to do so, subject to 12 | the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TESTS = test/*.js 2 | REPORTER = spec 3 | TIMEOUT = 20000 4 | MOCHA = ./node_modules/mocha/bin/_mocha 5 | PATH := ./node_modules/.bin:$(PATH) 6 | 7 | lint: 8 | @eslint --fix lib index.js test 9 | 10 | test: lint 11 | @mocha -t $(TIMEOUT) -R spec $(TESTS) 12 | 13 | test-cov: 14 | @nyc --reporter=html --reporter=text mocha -t $(TIMEOUT) -R spec $(TESTS) 15 | 16 | test-coveralls: 17 | @nyc mocha -t $(TIMEOUT) -R spec $(TESTS) 18 | @echo TRAVIS_JOB_ID $(TRAVIS_JOB_ID) 19 | @nyc report --reporter=text-lcov | coveralls 20 | 21 | .PHONY: test 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Wechat API(ES6版) 2 | =========== 3 | 微信公共平台API。 4 | 5 | ## 模块状态 6 | - [![NPM version](https://badge.fury.io/js/co-wechat-api.png)](http://badge.fury.io/js/co-wechat-api) 7 | - [![Build Status](https://travis-ci.org/node-webot/co-wechat-api.png?branch=master)](https://travis-ci.org/node-webot/co-wechat-api) 8 | - [![Dependencies Status](https://david-dm.org/node-webot/co-wechat-api.png)](https://david-dm.org/node-webot/co-wechat-api) 9 | - [![Coverage Status](https://coveralls.io/repos/node-webot/co-wechat-api/badge.png)](https://coveralls.io/r/node-webot/co-wechat-api) 10 | 11 | ## 功能列表 12 | - 发送客服消息(文本、图片、语音、视频、音乐、图文、小程序卡片) 13 | - 菜单操作(查询、创建、删除、个性化菜单) 14 | - 二维码(创建临时、永久二维码,查看二维码URL) 15 | - 分组操作(查询、创建、修改、移动用户到分组) 16 | - 用户信息(查询用户基本信息、获取关注者列表) 17 | - 媒体文件(上传、获取) 18 | - 群发消息(文本、图片、语音、视频、图文) 19 | - 客服记录(查询客服记录,查看客服、查看在线客服) 20 | - 群发消息 21 | - 公众号支付(发货通知、订单查询) 22 | - 微信小店(商品管理、库存管理、邮费模板管理、分组管理、货架管理、订单管理、功能接口) 23 | - 模版消息 24 | - 网址缩短 25 | - 语义查询 26 | - 数据分析 27 | - JSSDK服务端支持 28 | - 素材管理 29 | - 摇一摇周边 30 | - 小程序订阅消息(暂仅支持发送) 31 | 32 | 详细参见[API文档](http://doxmate.cool/node-webot/co-wechat-api/api.html) 33 | 34 | 企业版本请前往: 35 | 36 | ## Installation 37 | 38 | ```sh 39 | $ npm install co-wechat-api 40 | ``` 41 | 42 | ## Usage 43 | 44 | ```js 45 | var WechatAPI = require('co-wechat-api'); 46 | 47 | async function() { 48 | var api = new WechatAPI(appid, appsecret); 49 | var result = await api.updateRemark('open_id', 'remarked'); 50 | } 51 | ``` 52 | 53 | ### 多进程 54 | 当多进程时,token需要全局维护,以下为保存token的接口: 55 | 56 | ```js 57 | var api = new API('appid', 'secret', async function () { 58 | // 传入一个获取全局token的方法 59 | var txt = await fs.readFile('access_token.txt', 'utf8'); 60 | return JSON.parse(txt); 61 | }, async function (token) { 62 | // 请将token存储到全局,跨进程、跨机器级别的全局,比如写到数据库、redis等 63 | // 这样才能在cluster模式及多机情况下使用,以下为写入到文件的示例 64 | await fs.writeFile('access_token.txt', JSON.stringify(token)); 65 | }); 66 | ``` 67 | 68 | ## Show cases 69 | ### Node.js API自动回复 70 | 71 | ![Node.js API自动回复机器人](http://nodeapi.diveintonode.org/assets/qrcode.jpg) 72 | 73 | 欢迎关注。 74 | 75 | 代码: 76 | 77 | 你可以在[CloudFoundry](http://www.cloudfoundry.com/)、[appfog](https://www.appfog.com/)、[BAE](http://developer.baidu.com/wiki/index.php?title=docs/cplat/rt/node.js)等搭建自己的机器人。 78 | 79 | ## 详细API 80 | 原始API文档请参见:[消息接口指南](http://mp.weixin.qq.com/wiki/index.php?title=消息接口指南)。 81 | ## License 82 | The MIT license. 83 | 84 | ## 交流群 85 | QQ群:157964097,使用疑问,开发,贡献代码请加群。 86 | 87 | ## 感谢 88 | 感谢以下贡献者: 89 | 90 | ``` 91 | 92 | project : co-wechat-api 93 | repo age : 2 years, 6 months 94 | active : 37 days 95 | commits : 109 96 | files : 50 97 | authors : 98 | 75 Jackson Tian 68.8% 99 | 7 肥鼠 6.4% 100 | 6 magicxie 5.5% 101 | 3 马剑 2.8% 102 | 2 TimZhang 1.8% 103 | 2 Ziyi Yan 1.8% 104 | 2 ken 1.8% 105 | 2 Lei 1.8% 106 | 2 pillarhou 1.8% 107 | 2 sunwf 1.8% 108 | 1 Jichao Wu 0.9% 109 | 1 HelloYou 0.9% 110 | 1 swfbarhr 0.9% 111 | 1 ladjzero 0.9% 112 | 1 三点 0.9% 113 | 1 mukaiu 0.9% 114 | 115 | ``` 116 | 117 | ## 捐赠 118 | 如果您觉得Wechat对您有帮助,欢迎请作者一杯咖啡 119 | 120 | ![捐赠wechat](https://cloud.githubusercontent.com/assets/327019/2941591/2b9e5e58-d9a7-11e3-9e80-c25aba0a48a1.png) 121 | 122 | -------------------------------------------------------------------------------- /figures/wechat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/node-webot/co-wechat-api/42c6e31b15b0c7d05a0231fe5ad17f139121200e/figures/wechat.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const API = require('./lib/api_common'); 4 | // 菜单接口 5 | API.mixin(require('./lib/api_menu')); 6 | // 分组管理 7 | API.mixin(require('./lib/api_group')); 8 | // 用户信息 9 | API.mixin(require('./lib/api_user')); 10 | // 二维码 11 | API.mixin(require('./lib/api_qrcode')); 12 | // 媒体管理(上传、下载) 13 | API.mixin(require('./lib/api_media')); 14 | // 永久素材管理接口 15 | API.mixin(require('./lib/api_material')); 16 | // 客服消息 17 | API.mixin(require('./lib/api_message')); 18 | // 模板消息 19 | API.mixin(require('./lib/api_template')); 20 | // 获取客服聊天记录 21 | API.mixin(require('./lib/api_custom_service')); 22 | // 高级群发接口 23 | API.mixin(require('./lib/api_mass_send')); 24 | // 微信小店商品管理接口 25 | API.mixin(require('./lib/api_shop_goods')); 26 | // 微信小店库存管理接口 27 | API.mixin(require('./lib/api_shop_stock')); 28 | // 微信小店邮费模版管理接口 29 | API.mixin(require('./lib/api_shop_express')); 30 | // 微信小店分组管理接口 31 | API.mixin(require('./lib/api_shop_group')); 32 | // 微信小店货架管理接口 33 | API.mixin(require('./lib/api_shop_shelf')); 34 | // 微信小店订单管理接口 35 | API.mixin(require('./lib/api_shop_order')); 36 | // 微信小店功能管理接口 37 | API.mixin(require('./lib/api_shop_common')); 38 | // 支付接口 39 | API.mixin(require('./lib/api_payment')); 40 | // 用户维权系统接口 41 | API.mixin(require('./lib/api_feedback')); 42 | // 短网址接口 43 | API.mixin(require('./lib/api_url')); 44 | // 语义查询接口 45 | API.mixin(require('./lib/api_semantic')); 46 | // 获取微信服务器IP地址 47 | API.mixin(require('./lib/api_ip')); 48 | // 图文消息数据分析接口 49 | API.mixin(require('./lib/api_datacube')); 50 | // js sdk接口 51 | API.mixin(require('./lib/api_js')); 52 | // 卡券接口 53 | API.mixin(require('./lib/api_card')); 54 | // 设备接口 55 | API.mixin(require('./lib/api_device')); 56 | // 摇一摇周边接口 57 | API.mixin(require('./lib/api_shakearound')); 58 | // 门店管理接口 59 | API.mixin(require('./lib/api_poi')); 60 | // 小程序二维码接口 61 | API.mixin(require('./lib/api_wxacode')); 62 | // 小程序登录 63 | API.mixin(require('./lib/api_miniprogram_login')); 64 | // 小程序订阅消息接口 65 | API.mixin(require('./lib/api_subscribe_message')); 66 | 67 | module.exports = API; 68 | -------------------------------------------------------------------------------- /lib/api_card.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | const { promisify } = require('util'); 6 | const { stat } = require('fs'); 7 | const statAsync = promisify(stat); 8 | const formstream = require('formstream'); 9 | 10 | const { postJSON } = require('./util'); 11 | 12 | /** 13 | * 上传Logo 14 | * Examples: 15 | * ``` 16 | * api.uploadLogo('filepath'); 17 | * ``` 18 | * 19 | * Result: 20 | * ``` 21 | * { 22 | * "errcode":0, 23 | * "errmsg":"ok", 24 | * "url":"http://mmbiz.qpic.cn/mmbiz/iaL1LJM1mF9aRKPZJkmG8xXhiaHqkKSVMMWeN3hLut7X7hicFNjakmxibMLGWpXrEXB33367o7zHN0CwngnQY7zb7g/0" 25 | * } 26 | * ``` * @name uploadLogo 27 | * @param {String} filepath 文件路径 28 | */ 29 | exports.uploadLogo = async function (filepath) { 30 | var stat = await statAsync(filepath); 31 | const { accessToken } = await this.ensureAccessToken(); 32 | var form = formstream(); 33 | form.file('buffer', filepath, path.basename(filepath), stat.size); 34 | var url = this.fileServerPrefix + 'media/uploadimg?access_token=' + accessToken; 35 | var opts = { 36 | dataType: 'json', 37 | method: 'POST', 38 | timeout: 60000, // 60秒超时 39 | headers: form.headers(), 40 | stream: form 41 | }; 42 | return this.request(url, opts); 43 | }; 44 | 45 | /** 46 | * @name addLocations 47 | * @param {Array} locations 位置 48 | */ 49 | exports.addLocations = async function (locations) { 50 | var data = { 51 | location_list: locations 52 | }; 53 | const { accessToken } = await this.ensureAccessToken(); 54 | var url = 'https://api.weixin.qq.com/card/location/batchadd?access_token=' + accessToken; 55 | return this.request(url, postJSON(data)); 56 | }; 57 | 58 | exports.getLocations = async function (offset, count) { 59 | var data = { 60 | offset: offset, 61 | count: count 62 | }; 63 | const { accessToken } = await this.ensureAccessToken(); 64 | var url = 'https://api.weixin.qq.com/card/location/batchget?access_token=' + accessToken; 65 | return this.request(url, postJSON(data)); 66 | }; 67 | 68 | exports.getColors = async function () { 69 | const { accessToken } = await this.ensureAccessToken(); 70 | var url = 'https://api.weixin.qq.com/card/getcolors?access_token=' + accessToken; 71 | return this.request(url, {dataType: 'json'}); 72 | }; 73 | 74 | exports.createCard = async function (card) { 75 | const { accessToken } = await this.ensureAccessToken(); 76 | var url = 'https://api.weixin.qq.com/card/create?access_token=' + accessToken; 77 | var data = {card: card}; 78 | return this.request(url, postJSON(data)); 79 | }; 80 | 81 | exports.getRedirectUrl = function (url, encryptCode, cardId) { 82 | // TODO 83 | }; 84 | 85 | exports.createQRCode = async function (card) { 86 | const { accessToken } = await this.ensureAccessToken(); 87 | var url = 'https://api.weixin.qq.com/card/qrcode/create?access_token=' + accessToken; 88 | var data = { 89 | action_name: 'QR_CARD', 90 | action_info: { 91 | card: card 92 | } 93 | }; 94 | return this.request(url, postJSON(data)); 95 | }; 96 | 97 | exports.consumeCode = async function (code, cardId) { 98 | const { accessToken } = await this.ensureAccessToken(); 99 | var url = 'https://api.weixin.qq.com/card/code/consume?access_token=' + accessToken; 100 | var data = { 101 | code: code, 102 | card_id: cardId 103 | }; 104 | return this.request(url, postJSON(data)); 105 | }; 106 | 107 | exports.decryptCode = async function (encryptCode) { 108 | const { accessToken } = await this.ensureAccessToken(); 109 | var url = 'https://api.weixin.qq.com/card/code/decrypt?access_token=' + accessToken; 110 | var data = { 111 | encrypt_code: encryptCode 112 | }; 113 | return this.request(url, postJSON(data)); 114 | }; 115 | 116 | exports.deleteCard = async function (cardId) { 117 | const { accessToken } = await this.ensureAccessToken(); 118 | var url = 'https://api.weixin.qq.com/card/delete?access_token=' + accessToken; 119 | var data = { 120 | card_id: cardId 121 | }; 122 | return this.request(url, postJSON(data)); 123 | }; 124 | 125 | exports.getCode = async function (code, cardId) { 126 | const { accessToken } = await this.ensureAccessToken(); 127 | var url = 'https://api.weixin.qq.com/card/code/get?access_token=' + accessToken; 128 | var data = {code: code}; 129 | if (cardId) { 130 | data.card_id = cardId; 131 | } 132 | return this.request(url, postJSON(data)); 133 | }; 134 | 135 | exports.getCards = async function (offset, count, status_list) { 136 | const { accessToken } = await this.ensureAccessToken(); 137 | var url = 'https://api.weixin.qq.com/card/batchget?access_token=' + accessToken; 138 | var data = { 139 | offset: offset, 140 | count: count 141 | }; 142 | if (status_list) { 143 | data.status_list = status_list; 144 | } 145 | return this.request(url, postJSON(data)); 146 | }; 147 | 148 | exports.getCard = async function (cardId) { 149 | const { accessToken } = await this.ensureAccessToken(); 150 | var url = 'https://api.weixin.qq.com/card/get?access_token=' + accessToken; 151 | var data = { 152 | card_id: cardId 153 | }; 154 | return this.request(url, postJSON(data)); 155 | }; 156 | /** 157 | * 获取用户已领取的卡券 158 | * 详细细节 https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1451025272&token=&lang=zh_CN 159 | * Examples: 160 | * ``` 161 | * api.getCardList('openid', 'card_id'); 162 | * ``` 163 | * 164 | * @param {String} openid 用户的openid 165 | * @param {String} cardId 卡券的card_id 166 | */ 167 | exports.getCardList = async function (openid, cardId) { 168 | const { accessToken } = await this.ensureAccessToken(); 169 | // { 170 | // "openid":"openid", 171 | // "card_id":"cardId" 172 | // } 173 | var prefix = 'https://api.weixin.qq.com/'; 174 | var url = prefix + 'card/user/getcardlist?access_token=' + accessToken; 175 | var data = { 176 | 'openid': openid, 177 | 'card_id': cardId 178 | }; 179 | return this.request(url, postJSON(data)); 180 | }; 181 | 182 | exports.updateCode = async function (code, cardId, newcode) { 183 | const { accessToken } = await this.ensureAccessToken(); 184 | var url = 'https://api.weixin.qq.com/card/code/update?access_token=' + accessToken; 185 | var data = { 186 | code: code, 187 | card_id: cardId, 188 | newcode: newcode 189 | }; 190 | return this.request(url, postJSON(data)); 191 | }; 192 | 193 | exports.unavailableCode = async function (code, cardId) { 194 | const { accessToken } = await this.ensureAccessToken(); 195 | var url = 'https://api.weixin.qq.com/card/code/unavailable?access_token=' + accessToken; 196 | var data = { 197 | code: code 198 | }; 199 | if (cardId) { 200 | data.card_id = cardId; 201 | } 202 | return this.request(url, postJSON(data)); 203 | }; 204 | 205 | exports.updateCard = async function (cardId, cardInfo) { 206 | const { accessToken } = await this.ensureAccessToken(); 207 | var url = 'https://api.weixin.qq.com/card/update?access_token=' + accessToken; 208 | var data = { 209 | card_id: cardId, 210 | member_card: cardInfo 211 | }; 212 | return this.request(url, postJSON(data)); 213 | }; 214 | 215 | exports.updateCardStock = async function (cardId, num) { 216 | const { accessToken } = await this.ensureAccessToken(); 217 | var url = 'https://api.weixin.qq.com/card/modifystock?access_token=' + accessToken; 218 | var data = { 219 | card_id: cardId 220 | }; 221 | if (num > 0) { 222 | data.increase_stock_value = Math.abs(num); 223 | } else { 224 | data.reduce_stock_value = Math.abs(num); 225 | } 226 | return this.request(url, postJSON(data)); 227 | }; 228 | 229 | exports.activateMembercard = async function (info) { 230 | const { accessToken } = await this.ensureAccessToken(); 231 | var url = 'https://api.weixin.qq.com/card/membercard/activate?access_token=' + accessToken; 232 | return this.request(url, postJSON(info)); 233 | }; 234 | 235 | exports.getActivateMembercardUrl = async function (info) { 236 | const { accessToken } = await this.ensureAccessToken(); 237 | var url = 'https://api.weixin.qq.com/card/membercard/activate/geturl?access_token=' + accessToken; 238 | return this.request(url, postJSON(info)); 239 | }; 240 | 241 | 242 | exports.updateMembercard = async function (info) { 243 | const { accessToken } = await this.ensureAccessToken(); 244 | var url = 'https://api.weixin.qq.com/card/membercard/updateuser?access_token=' + accessToken; 245 | return this.request(url, postJSON(info)); 246 | }; 247 | 248 | exports.getActivateTempinfo = async function (activate_ticket) { 249 | const { accessToken } = await this.ensureAccessToken(); 250 | var url = 'https://api.weixin.qq.com/card/membercard/activatetempinfo/get?access_token=' + accessToken; 251 | return this.request(url, postJSON({activate_ticket})); 252 | }; 253 | 254 | exports.activateUserForm = async function (data) { 255 | const { accessToken } = await this.ensureAccessToken(); 256 | var url = 'https://api.weixin.qq.com/card/membercard/activateuserform/set?access_token=' + accessToken; 257 | return this.request(url, postJSON(data)); 258 | }; 259 | 260 | exports.updateMovieTicket = async function (info) { 261 | const { accessToken } = await this.ensureAccessToken(); 262 | var url = 'https://api.weixin.qq.com/card/movieticket/updateuser?access_token=' + accessToken; 263 | return this.request(url, postJSON(info)); 264 | }; 265 | 266 | exports.checkInBoardingPass = async function (info) { 267 | const { accessToken } = await this.ensureAccessToken(); 268 | var url = 'https://api.weixin.qq.com/card/boardingpass/checkin?access_token=' + accessToken; 269 | return this.request(url, postJSON(info)); 270 | }; 271 | 272 | exports.updateLuckyMonkeyBalance = async function (code, cardId, balance) { 273 | const { accessToken } = await this.ensureAccessToken(); 274 | var url = 'https://api.weixin.qq.com/card/luckymonkey/updateuserbalance?access_token=' + accessToken; 275 | var data = { 276 | 'code': code, 277 | 'card_id': cardId, 278 | 'balance': balance 279 | }; 280 | return this.request(url, postJSON(data)); 281 | }; 282 | 283 | exports.updateMeetingTicket = async function (info) { 284 | const { accessToken } = await this.ensureAccessToken(); 285 | var url = 'https://api.weixin.qq.com/card/meetingticket/updateuser?access_token=' + accessToken; 286 | return this.request(url, postJSON(info)); 287 | }; 288 | 289 | exports.setTestWhitelist = async function (info) { 290 | const { accessToken } = await this.ensureAccessToken(); 291 | var url = 'https://api.weixin.qq.com/card/testwhitelist/set?access_token=' + accessToken; 292 | return this.request(url, postJSON(info)); 293 | }; 294 | -------------------------------------------------------------------------------- /lib/api_common.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // 本文件用于wechat API,基础文件,主要用于Token的处理和mixin机制 4 | const httpx = require('httpx'); 5 | const liburl = require('url'); 6 | const JSONbig = require('json-bigint'); 7 | const { 8 | replaceJSONCtlChars 9 | } = require('./util'); 10 | 11 | class AccessToken { 12 | constructor(accessToken, expireTime) { 13 | this.accessToken = accessToken; 14 | this.expireTime = expireTime; 15 | } 16 | 17 | /*! 18 | * 检查AccessToken是否有效,检查规则为当前时间和过期时间进行对比 19 | * Examples: 20 | * ``` 21 | * token.isValid(); 22 | * ``` 23 | */ 24 | isValid() { 25 | return !!this.accessToken && Date.now() < this.expireTime; 26 | } 27 | } 28 | 29 | class API { 30 | /** 31 | * 根据 appid 和 appsecret 创建API的构造函数 32 | * 如需跨进程跨机器进行操作Wechat API(依赖access token),access token需要进行全局维护 33 | * 使用策略如下: 34 | * 1. 调用用户传入的获取 token 的异步方法,获得 token 之后使用 35 | * 2. 使用appid/appsecret获取 token 。并调用用户传入的保存 token 方法保存 36 | * Tips: 37 | * - 如果跨机器运行wechat模块,需要注意同步机器之间的系统时间。 38 | * Examples: 39 | * ``` 40 | * var API = require('wechat-api'); 41 | * var api = new API('appid', 'secret'); 42 | * ``` 43 | * 以上即可满足单进程使用。 44 | * 当多进程时,token 需要全局维护,以下为保存 token 的接口。 45 | * ``` 46 | * var api = new API('appid', 'secret', async function () { 47 | * // 传入一个获取全局 token 的方法 48 | * var txt = await fs.readFile('access_token.txt', 'utf8'); 49 | * return JSON.parse(txt); 50 | * }, async function (token) { 51 | * // 请将 token 存储到全局,跨进程、跨机器级别的全局,比如写到数据库、redis等 52 | * // 这样才能在cluster模式及多机情况下使用,以下为写入到文件的示例 53 | * await fs.writeFile('access_token.txt', JSON.stringify(token)); 54 | * }); 55 | * ``` 56 | * @param {String} appid 在公众平台上申请得到的appid 57 | * @param {String} appsecret 在公众平台上申请得到的app secret 58 | * @param {AsyncFunction} getToken 可选的。获取全局token对象的方法,多进程模式部署时需在意 59 | * @param {AsyncFunction} saveToken 可选的。保存全局token对象的方法,多进程模式部署时需在意 60 | */ 61 | constructor(appid, appsecret, getToken, saveToken, tokenFromCustom) { 62 | this.appid = appid; 63 | this.appsecret = appsecret; 64 | this.getToken = getToken || async function () { 65 | return this.store; 66 | }; 67 | this.saveToken = saveToken || async function (token) { 68 | this.store = token; 69 | if (process.env.NODE_ENV === 'production') { 70 | console.warn('Don\'t save token in memory, when cluster or multi-computer!'); 71 | } 72 | }; 73 | this.prefix = 'https://api.weixin.qq.com/cgi-bin/'; 74 | this.snsPrefix = 'https://api.weixin.qq.com/sns/'; 75 | this.mpPrefix = 'https://mp.weixin.qq.com/cgi-bin/'; 76 | this.fileServerPrefix = 'http://file.api.weixin.qq.com/cgi-bin/'; 77 | this.payPrefix = 'https://api.weixin.qq.com/pay/'; 78 | this.merchantPrefix = 'https://api.weixin.qq.com/merchant/'; 79 | this.customservicePrefix = 'https://api.weixin.qq.com/customservice/'; 80 | this.wxaPrefix = 'https://api.weixin.qq.com/wxa/'; 81 | this.defaults = {}; 82 | this.tokenFromCustom = tokenFromCustom; 83 | // set default js ticket handle 84 | this.registerTicketHandle(); 85 | } 86 | 87 | /** 88 | * 用于设置urllib的默认options * Examples: 89 | * ``` 90 | * api.setOpts({timeout: 15000}); 91 | * ``` 92 | * @param {Object} opts 默认选项 93 | */ 94 | setOpts(opts) { 95 | this.defaults = opts; 96 | } 97 | 98 | /** 99 | * 设置urllib的hook 100 | */ 101 | async request(url, opts, retry) { 102 | if (typeof retry === 'undefined') { 103 | retry = 3; 104 | } 105 | 106 | var options = {}; 107 | Object.assign(options, this.defaults); 108 | opts || (opts = {}); 109 | var keys = Object.keys(opts); 110 | for (var i = 0; i < keys.length; i++) { 111 | var key = keys[i]; 112 | if (key !== 'headers') { 113 | options[key] = opts[key]; 114 | } else { 115 | if (opts.headers) { 116 | options.headers = options.headers || {}; 117 | Object.assign(options.headers, opts.headers); 118 | } 119 | } 120 | } 121 | 122 | var res = await httpx.request(url, options); 123 | if (res.statusCode < 200 || res.statusCode > 204) { 124 | var err = new Error(`url: ${url}, status code: ${res.statusCode}`); 125 | err.name = 'WeChatAPIError'; 126 | throw err; 127 | } 128 | 129 | var buffer = await httpx.read(res); 130 | var contentType = res.headers['content-type'] || ''; 131 | if (contentType.includes('application/json') || contentType.includes('text/plain')) { 132 | var data; 133 | var origin = buffer.toString(); 134 | try { 135 | data = JSONbig.parse(replaceJSONCtlChars(origin)); 136 | } catch (ex) { 137 | if (contentType.includes('text/plain')) { 138 | return origin; 139 | } 140 | 141 | let err = new Error('JSON.parse error. buffer is ' + origin); 142 | err.name = 'WeChatAPIError'; 143 | throw err; 144 | } 145 | 146 | if (data && data.errcode) { 147 | let err = new Error(data.errmsg); 148 | err.name = 'WeChatAPIError'; 149 | err.code = data.errcode; 150 | 151 | if ((err.code === 40001 || err.code === 42001) && retry > 0 && !this.tokenFromCustom) { 152 | // 销毁已过期的token 153 | await this.saveToken(null); 154 | let token = await this.getAccessToken(); 155 | let urlobj = liburl.parse(url, true); 156 | 157 | if (urlobj.query && urlobj.query.access_token) { 158 | urlobj.query.access_token = token.accessToken; 159 | delete urlobj.search; 160 | } 161 | 162 | return this.request(liburl.format(urlobj), opts, retry - 1); 163 | } 164 | 165 | throw err; 166 | } 167 | 168 | return data; 169 | } 170 | 171 | return buffer; 172 | } 173 | 174 | /*! 175 | * 根据创建API时传入的appid和appsecret获取access token 176 | * 进行后续所有API调用时,需要先获取access token 177 | * 详细请看: * 应用开发者无需直接调用本API。 * Examples: 178 | * ``` 179 | * var token = await api.getAccessToken(); 180 | * ``` 181 | * - `err`, 获取access token出现异常时的异常对象 182 | * - `result`, 成功时得到的响应结果 * Result: 183 | * ``` 184 | * {"access_token": "ACCESS_TOKEN","expires_in": 7200} 185 | * ``` 186 | */ 187 | async getAccessToken() { 188 | var url = this.prefix + 'token?grant_type=client_credential&appid=' + this.appid + '&secret=' + this.appsecret; 189 | var data = await this.request(url); 190 | 191 | // 过期时间,因网络延迟等,将实际过期时间提前10秒,以防止临界点 192 | var expireTime = Date.now() + (data.expires_in - 10) * 1000; 193 | var token = new AccessToken(data.access_token, expireTime); 194 | await this.saveToken(token); 195 | return token; 196 | } 197 | 198 | /*! 199 | * 需要access token的接口调用如果采用preRequest进行封装后,就可以直接调用。 200 | * 无需依赖 getAccessToken 为前置调用。 201 | * 应用开发者无需直接调用此API。 202 | * Examples: 203 | * ``` 204 | * await api.ensureAccessToken(); 205 | * ``` 206 | */ 207 | async ensureAccessToken() { 208 | // 调用用户传入的获取token的异步方法,获得token之后使用(并缓存它)。 209 | var token = await this.getToken(); 210 | var accessToken; 211 | if (token && (accessToken = new AccessToken(token.accessToken, token.expireTime)).isValid()) { 212 | return accessToken; 213 | } else if (this.tokenFromCustom) { 214 | let err = new Error('accessToken Error'); 215 | err.name = 'WeChatAPIError'; 216 | err.code = 40001; 217 | throw err; 218 | } 219 | return this.getAccessToken(); 220 | } 221 | } 222 | 223 | /** 224 | * 用于支持对象合并。将对象合并到API.prototype上,使得能够支持扩展 225 | * Examples: 226 | * ``` 227 | * // 媒体管理(上传、下载) 228 | * API.mixin(require('./lib/api_media')); 229 | * ``` 230 | * @param {Object} obj 要合并的对象 231 | */ 232 | API.mixin = function (obj) { 233 | for (var key in obj) { 234 | if (API.prototype.hasOwnProperty(key)) { 235 | throw new Error('Don\'t allow override existed prototype method. method: '+ key); 236 | } 237 | API.prototype[key] = obj[key]; 238 | } 239 | }; 240 | 241 | API.AccessToken = AccessToken; 242 | 243 | module.exports = API; 244 | -------------------------------------------------------------------------------- /lib/api_custom_service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | const { promisify } = require('util'); 6 | const { stat } = require('fs'); 7 | const statAsync = promisify(stat); 8 | 9 | const formstream = require('formstream'); 10 | 11 | const { postJSON } = require('./util'); 12 | 13 | /** 14 | * 获取客服聊天记录 15 | * 详细请看:http://mp.weixin.qq.com/wiki/19/7c129ec71ddfa60923ea9334557e8b23.html 16 | * Opts: 17 | * ``` 18 | * { 19 | * "starttime" : 123456789, 20 | * "endtime" : 987654321, 21 | * "openid": "OPENID", // 非必须 22 | * "pagesize" : 10, 23 | * "pageindex" : 1, 24 | * } 25 | * ``` 26 | * Examples: 27 | * ``` 28 | * var result = await api.getRecords(opts); 29 | * ``` 30 | * Result: 31 | * ``` 32 | * { 33 | * "recordlist": [ 34 | * { 35 | * "worker": " test1", 36 | * "openid": "oDF3iY9WMaswOPWjCIp_f3Bnpljk", 37 | * "opercode": 2002, 38 | * "time": 1400563710, 39 | * "text": " 您好,客服test1为您服务。" 40 | * }, 41 | * { 42 | * "worker": " test1", 43 | * "openid": "oDF3iY9WMaswOPWjCIp_f3Bnpljk", 44 | * "opercode": 2003, 45 | * "time": 1400563731, 46 | * "text": " 你好,有什么事情? " 47 | * }, 48 | * ] 49 | * } 50 | * ``` 51 | * @param {Object} opts 查询条件 52 | */ 53 | exports.getRecords = async function (opts) { 54 | const { accessToken } = await this.ensureAccessToken(); 55 | // https://api.weixin.qq.com/customservice/msgrecord/getrecord?access_token=ACCESS_TOKEN 56 | var url = this.customservicePrefix + 'msgrecord/getrecord?access_token=' + accessToken; 57 | return this.request(url, postJSON(opts)); 58 | }; 59 | 60 | /** 61 | * 获取客服基本信息 62 | * 详细请看:http://dkf.qq.com/document-3_1.html 63 | * Examples: 64 | * ``` 65 | * var result = await api.getCustomServiceList(); 66 | * ``` 67 | * Result: 68 | * ``` 69 | * { 70 | * "kf_list": [ 71 | * { 72 | * "kf_account": "test1@test", 73 | * "kf_nick": "ntest1", 74 | * "kf_id": "1001" 75 | * }, 76 | * { 77 | * "kf_account": "test2@test", 78 | * "kf_nick": "ntest2", 79 | * "kf_id": "1002" 80 | * }, 81 | * { 82 | * "kf_account": "test3@test", 83 | * "kf_nick": "ntest3", 84 | * "kf_id": "1003" 85 | * } 86 | * ] 87 | * } 88 | * ``` 89 | */ 90 | exports.getCustomServiceList = async function () { 91 | const { accessToken } = await this.ensureAccessToken(); 92 | // https://api.weixin.qq.com/cgi-bin/customservice/getkflist?access_token= ACCESS_TOKEN 93 | var url = this.prefix + 'customservice/getkflist?access_token=' + accessToken; 94 | return this.request(url, {dataType: 'json'}); 95 | }; 96 | 97 | /** 98 | * 获取在线客服接待信息 99 | * 详细请看:http://dkf.qq.com/document-3_2.html * Examples: 100 | * ``` 101 | * var result = await api.getOnlineCustomServiceList(); 102 | * ``` 103 | * Result: 104 | * ``` 105 | * { 106 | * "kf_online_list": [ 107 | * { 108 | * "kf_account": "test1@test", 109 | * "status": 1, 110 | * "kf_id": "1001", 111 | * "auto_accept": 0, 112 | * "accepted_case": 1 113 | * }, 114 | * { 115 | * "kf_account": "test2@test", 116 | * "status": 1, 117 | * "kf_id": "1002", 118 | * "auto_accept": 0, 119 | * "accepted_case": 2 120 | * } 121 | * ] 122 | * } 123 | * ``` 124 | */ 125 | exports.getOnlineCustomServiceList = async function () { 126 | const { accessToken } = await this.ensureAccessToken(); 127 | // https://api.weixin.qq.com/cgi-bin/customservice/getonlinekflist?access_token= ACCESS_TOKEN 128 | var url = this.prefix + 'customservice/getonlinekflist?access_token=' + accessToken; 129 | return this.request(url, {dataType: 'json'}); 130 | }; 131 | 132 | /** 133 | * 添加客服账号 134 | * 详细请看:http://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1458044813&token=&lang=zh_CN * Examples: 135 | * ``` 136 | * var result = await api.addKfAccount('test@test', 'nickname', 'password'); 137 | * ``` 138 | * Result: 139 | * ``` 140 | * { 141 | * "errcode" : 0, 142 | * "errmsg" : "ok", 143 | * } 144 | * ``` 145 | * @param {String} account 账号名字,格式为:前缀@公共号名字 146 | * @param {String} nick 昵称 147 | */ 148 | exports.addKfAccount = async function (account, nick) { 149 | const { accessToken } = await this.ensureAccessToken(); 150 | // https://api.weixin.qq.com/customservice/kfaccount/add?access_token=ACCESS_TOKEN 151 | var prefix = 'https://api.weixin.qq.com/'; 152 | var url = prefix + 'customservice/kfaccount/add?access_token=' + accessToken; 153 | var data = { 154 | 'kf_account': account, 155 | 'nickname': nick 156 | }; 157 | 158 | return this.request(url, postJSON(data)); 159 | }; 160 | 161 | /** 162 | * 邀请绑定客服帐号 163 | * 详细请看:https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1458044813&token=&lang=zh_CN 164 | * Examples: 165 | * ``` 166 | * var result = await api.inviteworker('test@test', 'invite_wx'); 167 | * ``` 168 | * Result: 169 | * ``` 170 | * { 171 | * "errcode" : 0, 172 | * "errmsg" : "ok", 173 | * } 174 | * ``` 175 | * @param {String} account 账号名字,格式为:前缀@公共号名字 176 | * @param {String} wx 邀请绑定的个人微信账号 177 | */ 178 | exports.inviteworker = async function (account, wx) { 179 | const { accessToken } = await this.ensureAccessToken(); 180 | // https://api.weixin.qq.com/customservice/kfaccount/inviteworker?access_token=ACCESS_TOKEN 181 | var prefix = 'https://api.weixin.qq.com/'; 182 | var url = prefix + 'customservice/kfaccount/inviteworker?access_token=' + accessToken; 183 | var data = { 184 | 'kf_account': account, 185 | 'invite_wx': wx 186 | }; 187 | 188 | return this.request(url, postJSON(data)); 189 | }; 190 | 191 | /** 192 | * 设置客服账号 193 | * 详细请看:http://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1458044813&token=&lang=zh_CN * Examples: 194 | * ``` 195 | * api.updateKfAccount('test@test', 'nickname', 'password'); 196 | * ``` 197 | * Result: 198 | * ``` 199 | * { 200 | * "errcode" : 0, 201 | * "errmsg" : "ok", 202 | * } 203 | * ``` 204 | * @param {String} account 账号名字,格式为:前缀@公共号名字 205 | * @param {String} nick 昵称 206 | */ 207 | exports.updateKfAccount = async function (account, nick) { 208 | const { accessToken } = await this.ensureAccessToken(); 209 | // https://api.weixin.qq.com/customservice/kfaccount/add?access_token=ACCESS_TOKEN 210 | var prefix = 'https://api.weixin.qq.com/'; 211 | var url = prefix + 'customservice/kfaccount/update?access_token=' + accessToken; 212 | var data = { 213 | 'kf_account': account, 214 | 'nickname': nick 215 | }; 216 | 217 | return this.request(url, postJSON(data)); 218 | }; 219 | 220 | /** 221 | * 删除客服账号 222 | * 详细请看:http://mp.weixin.qq.com/wiki/9/6fff6f191ef92c126b043ada035cc935.html * Examples: 223 | * ``` 224 | * api.deleteKfAccount('test@test'); 225 | * ``` 226 | * Result: 227 | * ``` 228 | * { 229 | * "errcode" : 0, 230 | * "errmsg" : "ok", 231 | * } 232 | * ``` 233 | * @param {String} account 账号名字,格式为:前缀@公共号名字 234 | */ 235 | exports.deleteKfAccount = async function (account) { 236 | const { accessToken } = await this.ensureAccessToken(); 237 | // https://api.weixin.qq.com/customservice/kfaccount/del?access_token=ACCESS_TOKEN 238 | var prefix = 'https://api.weixin.qq.com/'; 239 | var url = prefix + 'customservice/kfaccount/del?access_token=' + accessToken + '&kf_account=' + account; 240 | 241 | return this.request(url, {dataType: 'json'}); 242 | }; 243 | 244 | /** 245 | * 设置客服头像 246 | * 详细请看:http://mp.weixin.qq.com/wiki/9/6fff6f191ef92c126b043ada035cc935.html * Examples: 247 | * ``` 248 | * api.setKfAccountAvatar('test@test', '/path/to/avatar.png'); 249 | * ``` 250 | * Result: 251 | * ``` 252 | * { 253 | * "errcode" : 0, 254 | * "errmsg" : "ok", 255 | * } 256 | * ``` 257 | * @param {String} account 账号名字,格式为:前缀@公共号名字 258 | * @param {String} filepath 头像路径 259 | */ 260 | exports.setKfAccountAvatar = async function (account, filepath) { 261 | const { accessToken } = await this.ensureAccessToken(); 262 | // http://api.weixin.qq.com/customservice/kfaccount/uploadheadimg?access_token=ACCESS_TOKEN&kf_account=KFACCOUNT 263 | var stat = await statAsync(filepath); 264 | var form = formstream(); 265 | form.file('media', filepath, path.basename(filepath), stat.size); 266 | var prefix = 'https://api.weixin.qq.com/'; 267 | var url = prefix + 'customservice/kfaccount/uploadheadimg?access_token=' + accessToken + '&kf_account=' + account; 268 | var opts = { 269 | dataType: 'json', 270 | method: 'POST', 271 | timeout: 60000, // 60秒超时 272 | headers: form.headers(), 273 | data: form 274 | }; 275 | return this.request(url, opts); 276 | }; 277 | 278 | /** 279 | * 创建客服会话 280 | * 详细请看:http://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1458044820&token=&lang=zh_CN * Examples: 281 | * ``` 282 | * api.createKfSession('test@test', 'OPENID'); 283 | * ``` 284 | * Result: 285 | * ``` 286 | * { 287 | * "errcode" : 0, 288 | * "errmsg" : "ok", 289 | * } 290 | * ``` 291 | * @param {String} account 账号名字,格式为:前缀@公共号名字 292 | * @param {String} openid openid 293 | */ 294 | exports.createKfSession = async function(account, openid) { 295 | const { accessToken } = await this.ensureAccessToken(); 296 | // https://api.weixin.qq.com/customservice/kfsession/create?access_token=ACCESS_TOKEN 297 | var prefix = 'https://api.weixin.qq.com/'; 298 | var url = prefix + 'customservice/kfsession/create?access_token=' + accessToken; 299 | var data = { 300 | kf_account: account, 301 | openid: openid 302 | }; 303 | 304 | return this.request(url, postJSON(data)); 305 | }; 306 | -------------------------------------------------------------------------------- /lib/api_datacube.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { postJSON } = require('./util'); 4 | 5 | var methods = [ 6 | // 用户分析数据接口 7 | 'getUserSummary', // 获取用户增减数据 8 | 'getUserCumulate', // 获取累计用户数据 9 | // 图文分析数据接口 10 | 'getArticleSummary', // 获取图文群发每日数据 11 | 'getArticleTotal', // 获取图文群发总数据 12 | 'getUserRead', // 获取图文统计数据 13 | 'getUserReadHour', // 获取图文统计分时数据 14 | 'getUserShare', // 获取图文分享转发数据 15 | 'getUserShareHour', // 获取图文分享转发分时数据 16 | // 消息分析数据接口 17 | 'getUpstreamMsg', //获取消息发送概况数据 18 | 'getUpstreamMsgHour', // 获取消息分送分时数据 19 | 'getUpstreamMsgWeek', // 获取消息发送周数据 20 | 'getUpstreamMsgMonth', // 获取消息发送月数据 21 | 'getUpstreamMsgDist', // 获取消息发送分布数据 22 | 'getUpstreamMsgDistWeek', // 获取消息发送分布周数据 23 | 'getUpstreamMsgDistMonth', // 获取消息发送分布月数据 24 | // 接口分析数据接口 25 | 'getInterfaceSummary', // 获取接口分析数据 26 | 'getInterfaceSummaryHour' // 获取接口分析分时数据 27 | ]; 28 | 29 | /** 30 | * 公众平台官网数据统计模块 31 | * 详情请见: 32 | * Examples: 33 | * ``` 34 | * // 用户分析数据接口 35 | * var result = await api.getUserSummary(startDate, endDate); // 获取用户增减数据 36 | * var result = await api.getUserCumulate(startDate, endDate); // 获取累计用户数据 37 | * // 图文分析数据接口 38 | * var result = await api.getArticleSummary(startDate, endDate); // 获取图文群发每日数据 39 | * var result = await api.getArticleTotal(startDate, endDate); // 获取图文群发总数据 40 | * var result = await api.getUserRead(startDate, endDate); // 获取图文统计数据 41 | * var result = await api.getUserReadHour(startDate, endDate); // 获取图文统计分时数据 42 | * var result = await api.getUserShare(startDate, endDate); // 获取图文分享转发数据 43 | * var result = await api.getUserShareHour(startDate, endDate); // 获取图文分享转发分时数据 44 | * // 消息分析数据接口 45 | * var result = await api.getUpstreamMsg(startDate, endDate); // 获取消息发送概况数据 46 | * var result = await api.getUpstreamMsgHour(startDate, endDate); // 获取消息分送分时数据 47 | * var result = await api.getUpstreamMsgWeek(startDate, endDate); // 获取消息发送周数据 48 | * var result = await api.getUpstreamMsgMonth(startDate, endDate); // 获取消息发送月数据 49 | * var result = await api.getUpstreamMsgDist(startDate, endDate); // 获取消息发送分布数据 50 | * var result = await api.getUpstreamMsgDistWeek(startDate, endDate); // 获取消息发送分布周数据 51 | * var result = await api.getUpstreamMsgDistMonth(startDate, endDate); // 获取消息发送分布月数据 52 | * // 接口分析数据接口 53 | * var result = await api.getInterfaceSummary(startDate, endDate); // 获取接口分析数据 54 | * var result = await api.getInterfaceSummaryHour(startDate, endDate); // 获取接口分析分时数据 55 | * ``` 56 | * 57 | * Result: 58 | * ``` 59 | * { 60 | * "list":[...] // 详细请参见 61 | * } 62 | * ``` 63 | * @param {String} startDate 起始日期,格式为2014-12-08 64 | * @param {String} endDate 结束日期,格式为2014-12-08 65 | */ 66 | methods.forEach(function (method) { 67 | exports[method] = async function (begin, end) { 68 | const { accessToken } = await this.ensureAccessToken(); 69 | var data = { 70 | begin_date: begin, 71 | end_date: end 72 | }; 73 | var url = 'https://api.weixin.qq.com/datacube/' + method.toLowerCase() + '?access_token=' + accessToken; 74 | return this.request(url, postJSON(data)); 75 | }; 76 | }); 77 | -------------------------------------------------------------------------------- /lib/api_device.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { postJSON } = require('./util'); 4 | 5 | exports.transferMessage = async function (deviceType, deviceId, openid, content) { 6 | const { accessToken } = await this.ensureAccessToken(); 7 | // https://api.weixin.qq.com/device/transmsg?access_token=ACCESS_TOKEN 8 | var url = 'https://api.weixin.qq.com/device/transmsg?access_token=' + accessToken; 9 | var info = { 10 | 'device_type': deviceType, 11 | 'device_id': deviceId, 12 | 'open_id': openid, 13 | 'content': new Buffer(content).toString('base64') 14 | }; 15 | return this.request(url, postJSON(info)); 16 | }; 17 | 18 | exports.transferStatus = async function (deviceType, deviceId, openid, status) { 19 | const { accessToken } = await this.ensureAccessToken(); 20 | // https://api.weixin.qq.com/device/transmsg?access_token=ACCESS_TOKEN 21 | var url = 'https://api.weixin.qq.com/device/transmsg?access_token=' + accessToken; 22 | var info = { 23 | 'device_type': deviceType, 24 | 'device_id': deviceId, 25 | 'open_id': openid, 26 | 'msg_type': '2', 27 | 'device_status': status 28 | }; 29 | return this.request(url, postJSON(info)); 30 | }; 31 | 32 | exports.createDeviceQRCode = async function (deviceIds) { 33 | const { accessToken } = await this.ensureAccessToken(); 34 | // https://api.weixin.qq.com/device/create_qrcode?access_token=ACCESS_TOKEN 35 | var url = 'https://api.weixin.qq.com/device/create_qrcode?access_token=' + accessToken; 36 | var info = { 37 | 'device_num': deviceIds.length, 38 | 'device_id_list': deviceIds 39 | }; 40 | return this.request(url, postJSON(info)); 41 | }; 42 | 43 | exports.authorizeDevices = async function (devices, optype) { 44 | const { accessToken } = await this.ensureAccessToken(); 45 | // https://api.weixin.qq.com/device/authorize_device?access_token=ACCESS_TOKEN 46 | var url = 'https://api.weixin.qq.com/device/authorize_device?access_token=' + accessToken; 47 | var data = { 48 | 'device_num': devices.length, 49 | 'device_list': devices, 50 | 'op_type': optype 51 | }; 52 | return this.request(url, postJSON(data)); 53 | }; 54 | 55 | exports.getDeviceQRCode = async function (devices, optype) { 56 | const { accessToken } = await this.ensureAccessToken(); 57 | // https://api.weixin.qq.com/device/getqrcode?access_token=ACCESS_TOKEN 58 | var url = 'https://api.weixin.qq.com/device/getqrcode?access_token=' + accessToken; 59 | return this.request(url, {dataType: 'json'}); 60 | }; 61 | 62 | exports.bindDevice = async function (deviceId, openid, ticket) { 63 | const { accessToken } = await this.ensureAccessToken(); 64 | // https://api.weixin.qq.com/device/bind?access_token=ACCESS_TOKEN 65 | var url = 'https://api.weixin.qq.com/device/bind?access_token=' + accessToken; 66 | var data = { 67 | ticket: ticket, 68 | device_id: deviceId, 69 | openid: openid 70 | }; 71 | return this.request(url, postJSON(data)); 72 | }; 73 | 74 | exports.unbindDevice = async function (deviceId, openid, ticket) { 75 | const { accessToken } = await this.ensureAccessToken(); 76 | // https://api.weixin.qq.com/device/unbind?access_token=ACCESS_TOKEN 77 | var url = 'https://api.weixin.qq.com/device/unbind?access_token=' + accessToken; 78 | var data = { 79 | ticket: ticket, 80 | device_id: deviceId, 81 | openid: openid 82 | }; 83 | return this.request(url, postJSON(data)); 84 | }; 85 | exports.compelBindDevice = async function (deviceId, openid) { 86 | const { accessToken } = await this.ensureAccessToken(); 87 | // https://api.weixin.qq.com/device/compel_bind?access_token=ACCESS_TOKEN 88 | var url = 'https://api.weixin.qq.com/device/compel_bind?access_token=' + accessToken; 89 | var data = { 90 | device_id: deviceId, 91 | openid: openid 92 | }; 93 | return this.request(url, postJSON(data)); 94 | }; 95 | 96 | exports.compelUnbindDevice = async function (deviceId, openid) { 97 | const { accessToken } = await this.ensureAccessToken(); 98 | // https://api.weixin.qq.com/device/compel_unbind?access_token=ACCESS_TOKEN 99 | var url = 'https://api.weixin.qq.com/device/compel_unbind?access_token=' + accessToken; 100 | var data = { 101 | device_id: deviceId, 102 | openid: openid 103 | }; 104 | return this.request(url, postJSON(data)); 105 | }; 106 | 107 | exports.getDeviceStatus = async function (deviceId) { 108 | const { accessToken } = await this.ensureAccessToken(); 109 | // https://api.weixin.qq.com/device/get_stat?access_token=ACCESS_TOKEN&device_id=DEVICE_ID 110 | var url = 'https://api.weixin.qq.com/device/get_stat?access_token=' + accessToken + '&device_id=' + deviceId; 111 | return this.request(url, {dataType: 'json'}); 112 | }; 113 | 114 | exports.verifyDeviceQRCode = async function (ticket) { 115 | const { accessToken } = await this.ensureAccessToken(); 116 | // https://api.weixin.qq.com/device/verify_qrcode?access_token=ACCESS_TOKEN 117 | var url = 'https://api.weixin.qq.com/device/verify_qrcode?access_token=' + accessToken; 118 | var data = { 119 | ticket: ticket, 120 | }; 121 | return this.request(url, postJSON(data)); 122 | }; 123 | 124 | exports.getOpenID = async function (deviceId, deviceType) { 125 | const { accessToken } = await this.ensureAccessToken(); 126 | // https://api.weixin.qq.com/device/get_openid?access_token=ACCESS_TOKEN&device_type=DEVICE_TYPE&device_id=DEVICE_ID 127 | var url = 'https://api.weixin.qq.com/device/get_openid?access_token=' + accessToken + '&device_id=' + deviceId + '&device_type=' + deviceType; 128 | return this.request(url, {dataType: 'json'}); 129 | }; 130 | 131 | exports.getBindDevice = async function (openid) { 132 | const { accessToken } = await this.ensureAccessToken(); 133 | // https://api.weixin.qq.com/device/get_bind_device?access_token=ACCESS_TOKEN&openid=OPENID 134 | var url = 'https://api.weixin.qq.com/device/get_bind_device?access_token=' + accessToken + '&openid=' + openid; 135 | return this.request(url, {dataType: 'json'}); 136 | }; 137 | -------------------------------------------------------------------------------- /lib/api_feedback.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * 标记客户的投诉处理状态 5 | * Examples: 6 | * ``` 7 | * api.updateFeedback(openid, feedbackId); 8 | * ``` 9 | * 10 | * Result: 11 | * ``` 12 | * { 13 | * "errcode": 0, 14 | * "errmsg": "success" 15 | * } 16 | * ``` 17 | * @param {String} openid 用户ID 18 | * @param {String} feedbackId 投诉ID 19 | */ 20 | exports.updateFeedback = async function (openid, feedbackId) { 21 | const { accessToken } = await this.ensureAccessToken(); 22 | var feedbackUrl = 'https://api.weixin.qq.com/payfeedback/update'; 23 | // https://api.weixin.qq.com/payfeedback/update?access_token=xxxxx&openid=XXXX&feedbackid=xxxx 24 | var data = { 25 | 'access_token': accessToken, 26 | 'openid': openid, 27 | 'feedbackid': feedbackId 28 | }; 29 | var opts = { 30 | dataType: 'json', 31 | type: 'GET', 32 | data: data, 33 | headers: { 34 | 'Content-Type': 'application/json' 35 | } 36 | }; 37 | this.request(feedbackUrl, opts); 38 | }; 39 | -------------------------------------------------------------------------------- /lib/api_group.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { postJSON } = require('./util'); 4 | 5 | /** 6 | * 获取分组列表 7 | * 详情请见: 8 | * Examples: 9 | * ``` 10 | * api.getGroups(); 11 | * ``` 12 | 13 | * Result: 14 | * ``` 15 | * { 16 | * "groups": [ 17 | * {"id": 0, "name": "未分组", "count": 72596}, 18 | * {"id": 1, "name": "黑名单", "count": 36} 19 | * ] 20 | * } 21 | * ``` 22 | */ 23 | exports.getGroups = async function () { 24 | const { accessToken } = await this.ensureAccessToken(); 25 | // https://api.weixin.qq.com/cgi-bin/groups/get?access_token=ACCESS_TOKEN 26 | var url = this.prefix + 'groups/get?access_token=' + accessToken; 27 | return this.request(url, {dataType: 'json'}); 28 | }; 29 | 30 | /** 31 | * 查询用户在哪个分组 32 | * 详情请见: 33 | * Examples: 34 | * ``` 35 | * api.getWhichGroup(openid); 36 | * ``` 37 | 38 | * Result: 39 | * ``` 40 | * { 41 | * "groupid": 102 42 | * } 43 | * ``` 44 | * @param {String} openid Open ID 45 | */ 46 | exports.getWhichGroup = async function (openid) { 47 | const { accessToken } = await this.ensureAccessToken(); 48 | // https://api.weixin.qq.com/cgi-bin/groups/getid?access_token=ACCESS_TOKEN 49 | var url = this.prefix + 'groups/getid?access_token=' + accessToken; 50 | var data = { 51 | 'openid': openid 52 | }; 53 | return this.request(url, postJSON(data)); 54 | }; 55 | 56 | /** 57 | * 创建分组 58 | * 详情请见: 59 | * Examples: 60 | * ``` 61 | * api.createGroup('groupname'); 62 | * ``` 63 | 64 | * Result: 65 | * ``` 66 | * {"group": {"id": 107, "name": "test"}} 67 | * ``` 68 | * @param {String} name 分组名字 69 | */ 70 | exports.createGroup = async function (name) { 71 | const { accessToken } = await this.ensureAccessToken(); 72 | // https://api.weixin.qq.com/cgi-bin/groups/create?access_token=ACCESS_TOKEN 73 | // POST数据格式:json 74 | // POST数据例子:{"group":{"name":"test"}} 75 | var url = this.prefix + 'groups/create?access_token=' + accessToken; 76 | var data = { 77 | 'group': {'name': name} 78 | }; 79 | return this.request(url, postJSON(data)); 80 | }; 81 | 82 | /** 83 | * 更新分组名字 84 | * 详情请见: 85 | * Examples: 86 | * ``` 87 | * api.updateGroup(107, 'new groupname'); 88 | * ``` 89 | 90 | * Result: 91 | * ``` 92 | * {"errcode": 0, "errmsg": "ok"} 93 | * ``` 94 | * @param {Number} id 分组ID 95 | * @param {String} name 新的分组名字 96 | */ 97 | exports.updateGroup = async function (id, name) { 98 | const { accessToken } = await this.ensureAccessToken(); 99 | // http请求方式: POST(请使用https协议) 100 | // https://api.weixin.qq.com/cgi-bin/groups/update?access_token=ACCESS_TOKEN 101 | // POST数据格式:json 102 | // POST数据例子:{"group":{"id":108,"name":"test2_modify2"}} 103 | var url = this.prefix + 'groups/update?access_token=' + accessToken; 104 | var data = { 105 | 'group': {'id': id, 'name': name} 106 | }; 107 | return this.request(url, postJSON(data)); 108 | }; 109 | 110 | /** 111 | * 移动用户进分组 112 | * 详情请见: 113 | * Examples: 114 | * ``` 115 | * api.moveUserToGroup(openid, groupId); 116 | * ``` 117 | 118 | * Result: 119 | * ``` 120 | * {"errcode": 0, "errmsg": "ok"} 121 | * ``` 122 | * @param {String} openid 用户的openid 123 | * @param {Number} groupId 分组ID 124 | */ 125 | exports.moveUserToGroup = async function (openid, groupId) { 126 | const { accessToken } = await this.ensureAccessToken(); 127 | // http请求方式: POST(请使用https协议) 128 | // https://api.weixin.qq.com/cgi-bin/groups/members/update?access_token=ACCESS_TOKEN 129 | // POST数据格式:json 130 | // POST数据例子:{"openid":"oDF3iYx0ro3_7jD4HFRDfrjdCM58","to_groupid":108} 131 | var url = this.prefix + 'groups/members/update?access_token=' + accessToken; 132 | var data = { 133 | 'openid': openid, 134 | 'to_groupid': groupId 135 | }; 136 | return this.request(url, postJSON(data)); 137 | }; 138 | 139 | /** 140 | * 批量移动用户分组 141 | * 详情请见: 142 | * Examples: 143 | * ``` 144 | * api.moveUsersToGroup(openids, groupId); 145 | * ``` 146 | 147 | * Result: 148 | * ``` 149 | * {"errcode": 0, "errmsg": "ok"} 150 | * ``` 151 | * @param {String} openids 用户的openid数组 152 | * @param {Number} groupId 分组ID 153 | */ 154 | exports.moveUsersToGroup = async function (openids, groupId) { 155 | const { accessToken } = await this.ensureAccessToken(); 156 | // http请求方式: POST(请使用https协议) 157 | // https://api.weixin.qq.com/cgi-bin/groups/members/batchupdate?access_token=ACCESS_TOKEN 158 | // POST数据格式:json 159 | // POST数据例子:{"openid_list":["oDF3iYx0ro3_7jD4HFRDfrjdCM58","oDF3iY9FGSSRHom3B-0w5j4jlEyY"],"to_groupid":108} 160 | var url = this.prefix + 'groups/members/batchupdate?access_token=' + accessToken; 161 | var data = { 162 | 'openid_list': openids, 163 | 'to_groupid': groupId 164 | }; 165 | return this.request(url, postJSON(data)); 166 | }; 167 | /** 168 | * 删除分组 169 | * 详情请见: 170 | * Examples: 171 | * ``` 172 | * api.removeGroup(groupId); 173 | * ``` 174 | 175 | * Result: 176 | * ``` 177 | * {"errcode": 0, "errmsg": "ok"} 178 | * ``` 179 | * @param {Number} groupId 分组ID 180 | */ 181 | exports.removeGroup = async function (groupId) { 182 | const { accessToken } = await this.ensureAccessToken(); 183 | var url = this.prefix + 'groups/delete?access_token=' + accessToken; 184 | var data = { 185 | 'group': { id: groupId} 186 | }; 187 | return this.request(url, postJSON(data)); 188 | }; 189 | -------------------------------------------------------------------------------- /lib/api_ip.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * 获取微信服务器IP地址 5 | * 详情请见: 6 | * Examples: 7 | * ``` 8 | * api.getIp(); 9 | * ``` 10 | 11 | * Result: 12 | * ``` 13 | * { 14 | * "ip_list":["127.0.0.1","127.0.0.1"] 15 | * } 16 | * ``` 17 | */ 18 | exports.getIp = async function () { 19 | const { accessToken } = await this.ensureAccessToken(); 20 | // https://api.weixin.qq.com/cgi-bin/getcallbackip?access_token=ACCESS_TOKEN 21 | var url = this.prefix + 'getcallbackip?access_token=' + accessToken; 22 | return this.request(url, {dataType: 'json'}); 23 | }; 24 | -------------------------------------------------------------------------------- /lib/api_js.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const crypto = require('crypto'); 4 | 5 | class Ticket { 6 | constructor(ticket, expireTime) { 7 | this.ticket = ticket; 8 | this.expireTime = expireTime; 9 | } 10 | 11 | isValid() { 12 | return !!this.ticket && (new Date().getTime()) < this.expireTime; 13 | } 14 | } 15 | 16 | /** 17 | * 多台服务器负载均衡时,ticketToken需要外部存储共享。 18 | * 需要调用此registerTicketHandle来设置获取和保存的自定义方法。 * Examples: 19 | * ``` 20 | * api.registerTicketHandle(getTicketToken, saveTicketToken); 21 | * // getTicketToken 22 | * function getTicketToken(type) { 23 | * settingModel.getItem(type, {key: 'weixin_ticketToken'}, function (err, setting) { 24 | * if (err) return callback(err); 25 | * callback(null, setting.value); 26 | * }); 27 | * } 28 | * // saveTicketToken 29 | * function saveTicketToken(type, _ticketToken) { 30 | * settingModel.setItem(type, {key:'weixin_ticketToken', value: ticketToken}, function (err) { 31 | * if (err) return callback(err); 32 | * callback(null); 33 | * }); 34 | * } 35 | * ``` 36 | * @param {Function} getTicketToken 获取外部ticketToken的函数 37 | * @param {Function} saveTicketToken 存储外部ticketToken的函数 38 | */ 39 | exports.registerTicketHandle = function (getTicketToken, saveTicketToken) { 40 | if (!getTicketToken && !saveTicketToken) { 41 | this.ticketStore = {}; 42 | } 43 | this.getTicketToken = getTicketToken || async function (type) { 44 | type = type || 'jsapi'; 45 | return this.ticketStore[type]; 46 | }; 47 | 48 | this.saveTicketToken = saveTicketToken || async function (type, ticketToken) { 49 | // 向下兼容 50 | if (arguments.length === 1) { 51 | ticketToken = type; 52 | type = 'jsapi'; 53 | } 54 | 55 | this.ticketStore[type] = ticketToken; 56 | if (process.env.NODE_ENV === 'production') { 57 | console.warn('Dont save ticket in memory, when cluster or multi-computer!'); 58 | } 59 | }; 60 | }; 61 | 62 | /** 63 | * 获取js sdk所需的有效js ticket 64 | * - `err`, 异常对象 65 | * - `result`, 正常获取时的数据 * Result: 66 | * - `errcode`, 0为成功 67 | * - `errmsg`, 成功为'ok',错误则为详细错误信息 68 | * - `ticket`, js sdk有效票据,如:bxLdikRXVbTPdHSM05e5u5sUoXNKd8-41ZO3MhKoyN5OfkWITDGgnr2fwJ0m9E8NYzWKVZvdVtaUgWvsdshFKA 69 | * - `expires_in`, 有效期7200秒,开发者必须在自己的服务全局缓存jsapi_ticket */ 70 | exports.getTicket = async function (type) { 71 | const { accessToken } = await this.ensureAccessToken(); 72 | type = type || 'jsapi'; 73 | 74 | var url = this.prefix + 'ticket/getticket?access_token=' + accessToken + '&type=' + type; 75 | var data = await this.request(url, {dataType: 'json'}); 76 | 77 | // 过期时间,因网络延迟等,将实际过期时间提前10秒,以防止临界点 78 | var expireTime = Date.now() + (data.expires_in - 10) * 1000; 79 | var ticket = new Ticket(data.ticket, expireTime); 80 | await this.saveTicketToken(type, ticket); 81 | return ticket; 82 | }; 83 | 84 | /*! 85 | * 生成随机字符串 */ 86 | var createNonceStr = function () { 87 | return Math.random().toString(36).substr(2, 15); 88 | }; 89 | 90 | /*! 91 | * 生成时间戳 */ 92 | var createTimestamp = function () { 93 | return '' + Math.floor(Date.now() / 1000); 94 | }; 95 | 96 | /*! 97 | * 排序查询字符串 */ 98 | var raw = function (args) { 99 | var keys = Object.keys(args); 100 | keys = keys.sort(); 101 | var newArgs = {}; 102 | for (let i = 0; i < keys.length; i++) { 103 | let key = keys[i]; 104 | newArgs[key.toLowerCase()] = args[key]; 105 | } 106 | 107 | var string = ''; 108 | var newKeys = Object.keys(newArgs); 109 | for (let i = 0; i < newKeys.length; i++) { 110 | let k = newKeys[i]; 111 | string += '&' + k + '=' + newArgs[k]; 112 | } 113 | return string.substr(1); 114 | }; 115 | 116 | /*! 117 | * 签名算法 * @param {String} nonceStr 生成签名的随机串 118 | * @param {String} jsapi_ticket 用于签名的jsapi_ticket 119 | * @param {String} timestamp 时间戳 120 | * @param {String} url 用于签名的url,注意必须与调用JSAPI时的页面URL完全一致 */ 121 | var sign = function (nonceStr, jsapi_ticket, timestamp, url) { 122 | var ret = { 123 | jsapi_ticket: jsapi_ticket, 124 | nonceStr: nonceStr, 125 | timestamp: timestamp, 126 | url: url 127 | }; 128 | var string = raw(ret); 129 | var shasum = crypto.createHash('sha1'); 130 | shasum.update(string); 131 | return shasum.digest('hex'); 132 | }; 133 | 134 | /*! 135 | * 卡券card_ext里的签名算法 * @name signCardExt 136 | * @param {String} api_ticket 用于签名的临时票据,获取方式见2.获取api_ticket。 137 | * @param {String} card_id 生成卡券时获得的card_id 138 | * @param {String} timestamp 时间戳,商户生成从1970 年1 月1 日是微信卡券接口文档00:00:00 至今的秒数,即当前的时间,且最终需要转换为字符串形式;由商户生成后传入。 139 | * @param {String} code 指定的卡券code 码,只能被领一次。use_custom_code 字段为true 的卡券必须填写,非自定义code 不必填写。 140 | * @param {String} openid 指定领取者的openid,只有该用户能领取。bind_openid 字段为true 的卡券必须填写,非自定义code 不必填写。 141 | * @param {String} balance 红包余额,以分为单位。红包类型(LUCKY_MONEY)必填、其他卡券类型不必填。 */ 142 | var signCardExt = function(api_ticket, card_id, timestamp, code, openid, balance) { 143 | var values = [api_ticket, card_id, timestamp, code || '', openid || '', balance || '']; 144 | values.sort(); 145 | 146 | var string = values.join(''); 147 | var shasum = crypto.createHash('sha1'); 148 | shasum.update(string); 149 | return shasum.digest('hex'); 150 | }; 151 | 152 | exports.ensureTicket = async function (type) { 153 | var cache = await this.getTicketToken(type); 154 | 155 | var ticket; 156 | // 有ticket并且ticket有效直接调用 157 | if (cache) { 158 | ticket = new Ticket(cache.ticket, cache.expireTime); 159 | } 160 | 161 | // 没有ticket或者无效 162 | if (!ticket || !ticket.isValid()) { 163 | // 从微信端获取ticket 164 | ticket = await this.getTicket(type); 165 | } 166 | return ticket; 167 | }; 168 | 169 | /** 170 | * 获取微信JS SDK Config的所需参数 * Examples: 171 | * ``` 172 | * var param = { 173 | * debug: false, 174 | * jsApiList: ['onMenuShareTimeline', 'onMenuShareAppMessage'], 175 | * url: 'http://www.xxx.com', 176 | * openTagList: [] // 可选,需要使用的开放标签列表,例如['wx-open-launch-app'] 177 | * }; 178 | * api.getJsConfig(param); 179 | * ``` 180 | * - `result`, 调用正常时得到的js sdk config所需参数 181 | * @param {Object} param 参数 182 | */ 183 | exports.getJsConfig = async function (param) { 184 | var ticket = await this.ensureTicket('jsapi'); 185 | var nonceStr = createNonceStr(); 186 | var jsAPITicket = ticket.ticket; 187 | var timestamp = createTimestamp(); 188 | var signature = sign(nonceStr, jsAPITicket, timestamp, param.url); 189 | 190 | return { 191 | debug: param.debug, 192 | appId: this.appid, 193 | timestamp: timestamp, 194 | nonceStr: nonceStr, 195 | signature: signature, 196 | jsApiList: param.jsApiList, 197 | openTagList: param.openTagList || [] 198 | }; 199 | }; 200 | 201 | /** 202 | * 获取微信JS SDK Config的所需参数 203 | * Examples: 204 | * ``` 205 | * var param = { 206 | * card_id: 'p-hXXXXXXX', 207 | * code: '1234', 208 | * openid: '111111', 209 | * balance: 100 210 | * }; 211 | * api.getCardExt(param); 212 | * ``` 213 | * - `result`, 调用正常时得到的card_ext对象,包含所需参数 214 | * @name getCardExt 215 | * @param {Object} param 参数 216 | */ 217 | exports.getCardExt = async function (param) { 218 | var apiTicket = await this.ensureTicket('wx_card'); 219 | var timestamp = createTimestamp(); 220 | var signature = signCardExt(apiTicket.ticket, param.card_id, timestamp, param.code, param.openid, param.balance); 221 | var result = { 222 | timestamp: timestamp, 223 | signature: signature 224 | }; 225 | 226 | result.code = param.code || ''; 227 | result.openid = param.openid || ''; 228 | 229 | if (param.balance) { 230 | result.balance = param.balance; 231 | } 232 | return result; 233 | }; 234 | 235 | /** 236 | * 获取最新的js api ticket 237 | * Examples: 238 | * ``` 239 | * api.getLatestTicket(); 240 | * ``` 241 | * - `err`, 获取js api ticket出现异常时的异常对象 242 | * - `ticket`, 获取的ticket 243 | */ 244 | exports.getLatestTicket = async function () { 245 | return this.ensureTicket('jsapi'); 246 | }; 247 | -------------------------------------------------------------------------------- /lib/api_mass_send.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { postJSON } = require('./util'); 4 | 5 | /** 6 | * 上传多媒体文件,分别有图片(image)、语音(voice)、视频(video)和缩略图(thumb) 7 | * 详情请见: 8 | * Examples: 9 | * ``` 10 | * api.uploadNews(news); 11 | * ``` 12 | * News: 13 | * ``` 14 | * { 15 | * "articles": [ 16 | * { 17 | * "thumb_media_id":"qI6_Ze_6PtV7svjolgs-rN6stStuHIjs9_DidOHaj0Q-mwvBelOXCFZiq2OsIU-p", 18 | * "author":"xxx", 19 | * "title":"Happy Day", 20 | * "content_source_url":"www.qq.com", 21 | * "content":"content", 22 | * "digest":"digest", 23 | * "show_cover_pic":"1" 24 | * }, 25 | * { 26 | * "thumb_media_id":"qI6_Ze_6PtV7svjolgs-rN6stStuHIjs9_DidOHaj0Q-mwvBelOXCFZiq2OsIU-p", 27 | * "author":"xxx", 28 | * "title":"Happy Day", 29 | * "content_source_url":"www.qq.com", 30 | * "content":"content", 31 | * "digest":"digest", 32 | * "show_cover_pic":"0" 33 | * } 34 | * ] 35 | * } 36 | * ``` 37 | * Result: 38 | * ``` 39 | * { 40 | * "type":"news", 41 | * "media_id":"CsEf3ldqkAYJAU6EJeIkStVDSvffUJ54vqbThMgplD-VJXXof6ctX5fI6-aYyUiQ", 42 | * "created_at":1391857799 43 | * } 44 | * ``` * @param {Object} news 图文消息对象 */ 45 | exports.uploadNews = async function (news) { 46 | const { accessToken } = await this.ensureAccessToken(); 47 | // https://api.weixin.qq.com/cgi-bin/media/uploadnews?access_token=ACCESS_TOKEN 48 | var url = this.prefix + 'media/uploadnews?access_token=' + accessToken; 49 | return this.request(url, postJSON(news)); 50 | }; 51 | 52 | /** 53 | * 将通过上传下载多媒体文件得到的视频media_id变成视频素材 54 | * 详情请见: 55 | * Examples: 56 | * ``` 57 | * api.uploadMPVideo(opts); 58 | * ``` 59 | * Opts: 60 | * ``` 61 | * { 62 | * "media_id": "rF4UdIMfYK3efUfyoddYRMU50zMiRmmt_l0kszupYh_SzrcW5Gaheq05p_lHuOTQ", 63 | * "title": "TITLE", 64 | * "description": "Description" 65 | * } 66 | * ``` 67 | * Result: 68 | * ``` 69 | * { 70 | * "type":"video", 71 | * "media_id":"IhdaAQXuvJtGzwwc0abfXnzeezfO0NgPK6AQYShD8RQYMTtfzbLdBIQkQziv2XJc", 72 | * "created_at":1391857799 73 | * } 74 | * ``` * @param {Object} opts 待上传为素材的视频 */ 75 | exports.uploadMPVideo = async function (opts) { 76 | const { accessToken } = await this.ensureAccessToken(); 77 | // https://file.api.weixin.qq.com/cgi-bin/media/uploadvideo?access_token=ACCESS_TOKEN 78 | var url = this.fileServerPrefix + 'media/uploadvideo?access_token=' + accessToken; 79 | return this.request(url, postJSON(opts)); 80 | }; 81 | 82 | /** 83 | * 群发消息,分别有图文(news)、文本(text)、语音(voice)、图片(image)和视频(video) 84 | * 详情请见: 85 | * Examples: 86 | * ``` 87 | * api.massSend(opts, receivers); 88 | * ``` 89 | * opts: 90 | * ``` 91 | * { 92 | * "image":{ 93 | * "media_id":"123dsdajkasd231jhksad" 94 | * }, 95 | * "msgtype":"image" 96 | * "send_ignore_reprint":0 97 | * } 98 | * ``` 99 | 100 | * Result: 101 | * ``` 102 | * { 103 | * "errcode":0, 104 | * "errmsg":"send job submission success", 105 | * "msg_id":34182 106 | * } 107 | * ``` 108 | * @param {Object} opts 待发送的数据 109 | * @param {String|Array} receivers 接收人。一个标签,或者openid列表 110 | * @param {String|Array} clientMsgId 开发者侧群发msgid,长度限制64字节,如不填,则后台默认以群发范围和群发内容的摘要值做为clientmsgid 111 | * @param {Int} sendIgnoreReprint 图文消息被判定为转载时,是否继续群发。 1为继续群发(转载),0为停止群发。 该参数默认为0。 112 | */ 113 | exports.massSend = async function (opts, receivers, clientMsgId, sendIgnoreReprint) { 114 | const { accessToken } = await this.ensureAccessToken(); 115 | var url; 116 | if (sendIgnoreReprint !== undefined) { 117 | opts.send_ignore_reprint = sendIgnoreReprint; 118 | } 119 | if (clientMsgId !== undefined) { 120 | opts.clientmsgid = clientMsgId; 121 | } 122 | if (Array.isArray(receivers)) { 123 | opts.touser = receivers; 124 | url = this.prefix + 'message/mass/send?access_token=' + accessToken; 125 | } else { 126 | if (typeof receivers === 'boolean') { 127 | opts.filter = { 128 | 'is_to_all': receivers 129 | }; 130 | } else { 131 | opts.filter = { 132 | 'tag_id': receivers 133 | }; 134 | } 135 | url = this.prefix + 'message/mass/sendall?access_token=' + accessToken; 136 | } 137 | // https://api.weixin.qq.com/cgi-bin/message/mass/sendall?access_token=ACCESS_TOKEN 138 | return this.request(url, postJSON(opts)); 139 | }; 140 | 141 | /** 142 | * 群发图文(news)消息 143 | * 详情请见: 144 | * Examples: 145 | * ``` 146 | * api.massSendNews(mediaId, receivers); 147 | * ``` 148 | * Result: 149 | * ``` 150 | * { 151 | * "errcode":0, 152 | * "errmsg":"send job submission success", 153 | * "msg_id":34182 154 | * } 155 | * ``` 156 | * @param {String} mediaId 图文消息的media id 157 | * @param {String|Array|Boolean} receivers 接收人。一个组,或者openid列表, 或者true(群发给所有人) 158 | * @param {String|Array} clientMsgId 开发者侧群发msgid,长度限制64字节,如不填,则后台默认以群发范围和群发内容的摘要值做为clientmsgid 159 | * @param {Int} sendIgnoreReprint 图文消息被判定为转载时,是否继续群发。 1为继续群发(转载),0为停止群发。 该参数默认为0。 160 | * */ 161 | exports.massSendNews = async function (mediaId, receivers, clientMsgId, sendIgnoreReprint) { 162 | var opts = { 163 | 'mpnews': { 164 | 'media_id': mediaId 165 | }, 166 | 'msgtype': 'mpnews' 167 | }; 168 | return this.massSend(opts, receivers, clientMsgId, sendIgnoreReprint); 169 | }; 170 | 171 | /** 172 | * 群发文字(text)消息 173 | * 详情请见: 174 | * Examples: 175 | * ``` 176 | * api.massSendText(content, receivers); 177 | * ``` 178 | * Result: 179 | * ``` 180 | * { 181 | * "errcode":0, 182 | * "errmsg":"send job submission success", 183 | * "msg_id":34182 184 | * } 185 | * ``` 186 | * @param {String} content 文字消息内容 187 | * @param {String|Array} clientMsgId 开发者侧群发msgid,长度限制64字节,如不填,则后台默认以群发范围和群发内容的摘要值做为clientmsgid 188 | * @param {String|Array} receivers 接收人。一个组,或者openid列表 */ 189 | exports.massSendText = async function (content, receivers, clientMsgId) { 190 | var opts = { 191 | 'text': { 192 | 'content': content 193 | }, 194 | 'msgtype': 'text' 195 | }; 196 | return this.massSend(opts, receivers, clientMsgId); 197 | }; 198 | 199 | /** 200 | * 群发声音(voice)消息 201 | * 详情请见: 202 | * Examples: 203 | * ``` 204 | * api.massSendVoice(media_id, receivers); 205 | * ``` 206 | * Result: 207 | * ``` 208 | * { 209 | * "errcode":0, 210 | * "errmsg":"send job submission success", 211 | * "msg_id":34182 212 | * } 213 | * ``` 214 | * @param {String} mediaId 声音media id 215 | * @param {String|Array} clientMsgId 开发者侧群发msgid,长度限制64字节,如不填,则后台默认以群发范围和群发内容的摘要值做为clientmsgid 216 | * @param {String|Array} receivers 接收人。一个组,或者openid列表 */ 217 | exports.massSendVoice = async function (mediaId, receivers, clientMsgId) { 218 | var opts = { 219 | 'voice': { 220 | 'media_id': mediaId 221 | }, 222 | 'msgtype': 'voice' 223 | }; 224 | return this.massSend(opts, receivers, clientMsgId); 225 | }; 226 | 227 | /** 228 | * 群发图片(image)消息 229 | * 详情请见: 230 | * Examples: 231 | * ``` 232 | * api.massSendImage(media_id, receivers); 233 | * ``` 234 | * Result: 235 | * ``` 236 | * { 237 | * "errcode":0, 238 | * "errmsg":"send job submission success", 239 | * "msg_id":34182 240 | * } 241 | * ``` 242 | * @param {String} mediaId 图片media id 243 | * @param {String|Array} clientMsgId 开发者侧群发msgid,长度限制64字节,如不填,则后台默认以群发范围和群发内容的摘要值做为clientmsgid 244 | * @param {String|Array} receivers 接收人。一个组,或者openid列表 */ 245 | exports.massSendImage = async function (mediaId, receivers, clientMsgId) { 246 | var opts = { 247 | 'image': { 248 | 'media_id': mediaId 249 | }, 250 | 'msgtype': 'image' 251 | }; 252 | return this.massSend(opts, receivers, clientMsgId); 253 | }; 254 | 255 | /** 256 | * 群发视频(video)消息 257 | * 详情请见: 258 | * Examples: 259 | * ``` 260 | * api.massSendVideo(mediaId, receivers); 261 | * ``` 262 | 263 | * Result: 264 | * ``` 265 | * { 266 | * "errcode":0, 267 | * "errmsg":"send job submission success", 268 | * "msg_id":34182 269 | * } 270 | * ``` 271 | * @param {String} mediaId 视频media id 272 | * @param {String|Array} clientMsgId 开发者侧群发msgid,长度限制64字节,如不填,则后台默认以群发范围和群发内容的摘要值做为clientmsgid 273 | * @param {String|Array} receivers 接收人。一个组,或者openid列表 274 | */ 275 | exports.massSendVideo = async function (mediaId, receivers, clientMsgId) { 276 | var opts = { 277 | 'mpvideo': { 278 | 'media_id': mediaId 279 | }, 280 | 'msgtype': 'mpvideo' 281 | }; 282 | return this.massSend(opts, receivers, clientMsgId); 283 | }; 284 | 285 | /** 286 | * 群发视频(video)消息,直接通过上传文件得到的media id进行群发(自动生成素材) 287 | * 详情请见: 288 | * Examples: 289 | * ``` 290 | * api.massSendMPVideo(data, receivers); 291 | * ``` 292 | * Data: 293 | * ``` 294 | * { 295 | * "media_id": "rF4UdIMfYK3efUfyoddYRMU50zMiRmmt_l0kszupYh_SzrcW5Gaheq05p_lHuOTQ", 296 | * "title": "TITLE", 297 | * "description": "Description" 298 | * } 299 | * ``` 300 | * Result: 301 | * ``` 302 | * { 303 | * "errcode":0, 304 | * "errmsg":"send job submission success", 305 | * "msg_id":34182 306 | * } 307 | * ``` 308 | * @param {Object} data 视频数据 309 | * @param {String|Array} clientMsgId 开发者侧群发msgid,长度限制64字节,如不填,则后台默认以群发范围和群发内容的摘要值做为clientmsgid 310 | * @param {String|Array} receivers 接收人。一个组,或者openid列表 */ 311 | exports.massSendMPVideo = async function (data, receivers, clientMsgId) { 312 | // 自动帮转视频的media_id 313 | var result = await this.uploadMPVideo(data); 314 | return this.massSendVideo(result.media_id, receivers, clientMsgId); 315 | }; 316 | 317 | /** 318 | * 删除群发消息 319 | * 详情请见: 320 | * Examples: 321 | * ``` 322 | * api.deleteMass(message_id); 323 | * ``` 324 | 325 | * Result: 326 | * ``` 327 | * { 328 | * "errcode":0, 329 | * "errmsg":"ok" 330 | * } 331 | * ``` * @param {String} messageId 待删除群发的消息id 332 | */ 333 | exports.deleteMass = async function (messageId) { 334 | const { accessToken } = await this.ensureAccessToken(); 335 | var opts = { 336 | msg_id: messageId 337 | }; 338 | var url = this.prefix + 'message/mass/delete?access_token=' + accessToken; 339 | return this.request(url, postJSON(opts)); 340 | }; 341 | 342 | /** 343 | * 预览接口,预览图文消息 344 | * 详情请见: 345 | * Examples: 346 | * ``` 347 | * api.previewNews(openid, mediaId); 348 | * ``` 349 | 350 | * Result: 351 | * ``` 352 | * { 353 | * "errcode":0, 354 | * "errmsg":"send job submission success", 355 | * "msg_id": 34182 356 | * } 357 | * ``` * @param {String} openid 用户openid 358 | * @param {String} mediaId 图文消息mediaId 359 | */ 360 | exports.previewNews = async function (openid, mediaId) { 361 | const { accessToken } = await this.ensureAccessToken(); 362 | var opts = { 363 | 'touser': openid, 364 | 'mpnews': { 365 | 'media_id': mediaId 366 | }, 367 | 'msgtype': 'mpnews' 368 | }; 369 | var url = this.prefix + 'message/mass/preview?access_token=' + accessToken; 370 | return this.request(url, postJSON(opts)); 371 | }; 372 | 373 | /** 374 | * 预览接口,预览文本消息 375 | * 详情请见: 376 | * Examples: 377 | * ``` 378 | * api.previewText(openid, content); 379 | * ``` 380 | * Result: 381 | * ``` 382 | * { 383 | * "errcode":0, 384 | * "errmsg":"send job submission success", 385 | * "msg_id": 34182 386 | * } 387 | * ``` 388 | * @param {String} openid 用户openid 389 | * @param {String} content 文本消息 390 | */ 391 | exports.previewText = async function (openid, content) { 392 | const { accessToken } = await this.ensureAccessToken(); 393 | var opts = { 394 | 'touser': openid, 395 | 'text': { 396 | 'content': content 397 | }, 398 | 'msgtype': 'text' 399 | }; 400 | var url = this.prefix + 'message/mass/preview?access_token=' + accessToken; 401 | return this.request(url, postJSON(opts)); 402 | }; 403 | 404 | /** 405 | * 预览接口,预览语音消息 406 | * 详情请见: 407 | * Examples: 408 | * ``` 409 | * api.previewVoice(openid, mediaId); 410 | * ``` 411 | * Result: 412 | * ``` 413 | * { 414 | * "errcode":0, 415 | * "errmsg":"send job submission success", 416 | * "msg_id": 34182 417 | * } 418 | * ``` * @param {String} openid 用户openid 419 | * @param {String} mediaId 语音mediaId */ 420 | exports.previewVoice = async function (openid, mediaId) { 421 | const { accessToken } = await this.ensureAccessToken(); 422 | var opts = { 423 | 'touser': openid, 424 | 'voice': { 425 | 'media_id': mediaId 426 | }, 427 | 'msgtype': 'voice' 428 | }; 429 | var url = this.prefix + 'message/mass/preview?access_token=' + accessToken; 430 | return this.request(url, postJSON(opts)); 431 | }; 432 | 433 | /** 434 | * 预览接口,预览图片消息 435 | * 详情请见: 436 | * Examples: 437 | * ``` 438 | * api.previewImage(openid, mediaId); 439 | * ``` 440 | 441 | * Result: 442 | * ``` 443 | * { 444 | * "errcode":0, 445 | * "errmsg":"send job submission success", 446 | * "msg_id": 34182 447 | * } 448 | * ``` * @param {String} openid 用户openid 449 | * @param {String} mediaId 图片mediaId 450 | */ 451 | exports.previewImage = async function (openid, mediaId) { 452 | const { accessToken } = await this.ensureAccessToken(); 453 | var opts = { 454 | 'touser': openid, 455 | 'image': { 456 | 'media_id': mediaId 457 | }, 458 | 'msgtype': 'image' 459 | }; 460 | var url = this.prefix + 'message/mass/preview?access_token=' + accessToken; 461 | return this.request(url, postJSON(opts)); 462 | }; 463 | 464 | /** 465 | * 预览接口,预览视频消息 466 | * 详情请见: 467 | * Examples: 468 | * ``` 469 | * api.previewVideo(openid, mediaId); 470 | * ``` 471 | * Result: 472 | * ``` 473 | * { 474 | * "errcode":0, 475 | * "errmsg":"send job submission success", 476 | * "msg_id": 34182 477 | * } 478 | * ``` * @param {String} openid 用户openid 479 | * @param {String} mediaId 视频mediaId */ 480 | exports.previewVideo = async function (openid, mediaId) { 481 | const { accessToken } = await this.ensureAccessToken(); 482 | var opts = { 483 | 'touser': openid, 484 | 'mpvideo': { 485 | 'media_id': mediaId 486 | }, 487 | 'msgtype': 'mpvideo' 488 | }; 489 | var url = this.prefix + 'message/mass/preview?access_token=' + accessToken; 490 | return this.request(url, postJSON(opts)); 491 | }; 492 | 493 | /** 494 | * 查询群发消息状态 495 | * 详情请见: 496 | * Examples: 497 | * ``` 498 | * api.getMassMessageStatus(messageId); 499 | * ``` 500 | * Result: 501 | * ``` 502 | * { 503 | * "msg_id":201053012, 504 | * "msg_status":"SEND_SUCCESS" 505 | * } 506 | * ``` * @param {String} messageId 消息ID */ 507 | exports.getMassMessageStatus = async function (messageId) { 508 | const { accessToken } = await this.ensureAccessToken(); 509 | var opts = { 510 | 'msg_id': messageId 511 | }; 512 | var url = this.prefix + 'message/mass/get?access_token=' + accessToken; 513 | return this.request(url, postJSON(opts)); 514 | }; 515 | -------------------------------------------------------------------------------- /lib/api_material.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | const { promisify } = require('util'); 6 | const { stat } = require('fs'); 7 | const statAsync = promisify(stat); 8 | 9 | const formstream = require('formstream'); 10 | 11 | const { postJSON } = require('./util'); 12 | 13 | /** 14 | * 上传永久素材,分别有图片(image)、语音(voice)、和缩略图(thumb) 15 | * 详情请见: 16 | * Examples: 17 | * ``` 18 | * api.uploadMaterial('filepath', type); 19 | * ``` 20 | 21 | * Result: 22 | * ``` 23 | * {"type":"TYPE","media_id":"MEDIA_ID","created_at":123456789} 24 | * ``` 25 | * Shortcut: * - `exports.uploadImageMaterial(filepath);` 26 | * - `exports.uploadVoiceMaterial(filepath);` 27 | * - `exports.uploadThumbMaterial(filepath);` * @param {String} filepath 文件路径 28 | * @param {String} type 媒体类型,可用值有image、voice、video、thumb 29 | */ 30 | exports.uploadMaterial = async function (filepath, type) { 31 | const { accessToken } = await this.ensureAccessToken(); 32 | var stat = await statAsync(filepath); 33 | var form = formstream(); 34 | form.file('media', filepath, path.basename(filepath), stat.size); 35 | var url = this.prefix + 'material/add_material?access_token=' + accessToken + '&type=' + type; 36 | var opts = { 37 | dataType: 'json', 38 | method: 'POST', 39 | timeout: 60000, // 60秒超时 40 | headers: form.headers(), 41 | data: form 42 | }; 43 | return this.request(url, opts); 44 | }; 45 | 46 | ['image', 'voice', 'thumb'].forEach(function (type) { 47 | var method = 'upload' + type[0].toUpperCase() + type.substring(1) + 'Material'; 48 | exports[method] = async function (filepath) { 49 | return this.uploadMaterial(filepath, type); 50 | }; 51 | }); 52 | 53 | /** 54 | * 上传永久素材,视频(video) 55 | * 详情请见: 56 | * Examples: 57 | * ``` 58 | * var description = { 59 | * "title":VIDEO_TITLE, 60 | * "introduction":INTRODUCTION 61 | * }; 62 | * api.uploadVideoMaterial('filepath', description); 63 | * ``` 64 | * 65 | * Result: 66 | * ``` 67 | * {"media_id":"MEDIA_ID"} 68 | * ``` 69 | * @param {String} filepath 视频文件路径 70 | * @param {Object} description 描述 71 | */ 72 | exports.uploadVideoMaterial = async function (filepath, description) { 73 | const { accessToken } = await this.ensureAccessToken(); 74 | var stat = await statAsync(filepath); 75 | var form = formstream(); 76 | form.file('media', filepath, path.basename(filepath), stat.size); 77 | form.field('description', JSON.stringify(description)); 78 | var url = this.prefix + 'material/add_material?access_token=' + accessToken + '&type=video'; 79 | var opts = { 80 | dataType: 'json', 81 | method: 'POST', 82 | timeout: 60000, // 60秒超时 83 | headers: form.headers(), 84 | data: form 85 | }; 86 | return this.request(url, opts); 87 | }; 88 | 89 | /** 90 | * 新增永久图文素材 * News: 91 | * ``` 92 | * { 93 | * "articles": [ 94 | * { 95 | * "title": TITLE, 96 | * "thumb_media_id": THUMB_MEDIA_ID, 97 | * "author": AUTHOR, 98 | * "digest": DIGEST, 99 | * "show_cover_pic": SHOW_COVER_PIC(0 / 1), 100 | * "content": CONTENT, 101 | * "content_source_url": CONTENT_SOURCE_URL 102 | * }, 103 | * //若新增的是多图文素材,则此处应还有几段articles结构 104 | * ] 105 | * } 106 | * ``` 107 | * Examples: 108 | * ``` 109 | * api.uploadNewsMaterial(news); 110 | * ``` 111 | * 112 | * Result: 113 | * ``` 114 | * {"errcode":0,"errmsg":"ok"} 115 | * ``` 116 | * @param {Object} news 图文对象 117 | */ 118 | exports.uploadNewsMaterial = async function (news) { 119 | const { accessToken } = await this.ensureAccessToken(); 120 | var url = this.prefix + 'material/add_news?access_token=' + accessToken; 121 | return this.request(url, postJSON(news)); 122 | }; 123 | /** 124 | * 更新永久图文素材 125 | * News: 126 | * ``` 127 | * { 128 | * "media_id":MEDIA_ID, 129 | * "index":INDEX, 130 | * "articles": [ 131 | * { 132 | * "title": TITLE, 133 | * "thumb_media_id": THUMB_MEDIA_ID, 134 | * "author": AUTHOR, 135 | * "digest": DIGEST, 136 | * "show_cover_pic": SHOW_COVER_PIC(0 / 1), 137 | * "content": CONTENT, 138 | * "content_source_url": CONTENT_SOURCE_URL 139 | * }, 140 | * //若新增的是多图文素材,则此处应还有几段articles结构 141 | * ] 142 | * } 143 | * ``` 144 | * Examples: 145 | * ``` 146 | * api.updateNewsMaterial(news); 147 | * ``` 148 | * Result: 149 | * ``` 150 | * {"errcode":0,"errmsg":"ok"} 151 | * ``` 152 | * @param {Object} news 图文对象 153 | */ 154 | exports.updateNewsMaterial = async function (news) { 155 | const { accessToken } = await this.ensureAccessToken(); 156 | var url = this.prefix + 'material/update_news?access_token=' + accessToken; 157 | return this.request(url, postJSON(news)); 158 | }; 159 | /** 160 | * 根据媒体ID获取永久素材 161 | * 详情请见: 162 | * Examples: 163 | * ``` 164 | * api.getMaterial('media_id'); 165 | * ``` 166 | * 167 | * - `result`, 调用正常时得到的文件Buffer对象 168 | * - `res`, HTTP响应对象 * @param {String} mediaId 媒体文件的ID 169 | */ 170 | exports.getMaterial = async function (mediaId) { 171 | const { accessToken } = await this.ensureAccessToken(); 172 | var url = this.prefix + 'material/get_material?access_token=' + accessToken; 173 | var opts = { 174 | method: 'POST', 175 | data: JSON.stringify({'media_id': mediaId}), 176 | headers: { 177 | 'Content-Type': 'application/json' 178 | }, 179 | timeout : 60000 // 60秒超时 180 | }; 181 | return this.request(url, opts); 182 | }; 183 | 184 | /** 185 | * 删除永久素材 186 | * 详情请见: 187 | * Examples: 188 | * ``` 189 | * api.removeMaterial('media_id'); 190 | * ``` 191 | * 192 | * - `result`, 调用正常时得到的文件Buffer对象 193 | * - `res`, HTTP响应对象 * @param {String} mediaId 媒体文件的ID 194 | */ 195 | exports.removeMaterial = async function (mediaId) { 196 | const { accessToken } = await this.ensureAccessToken(); 197 | var url = this.prefix + 'material/del_material?access_token=' + accessToken; 198 | return this.request(url, postJSON({'media_id': mediaId})); 199 | }; 200 | 201 | /** 202 | * 获取素材总数 203 | * 详情请见: 204 | * Examples: 205 | * ``` 206 | * api.getMaterialCount(); 207 | * ``` 208 | * 209 | * - `result`, 调用正常时得到的文件Buffer对象 210 | * - `res`, HTTP响应对象 * Result: 211 | * ``` 212 | * { 213 | * "voice_count":COUNT, 214 | * "video_count":COUNT, 215 | * "image_count":COUNT, 216 | * "news_count":COUNT 217 | * } 218 | * ``` 219 | */ 220 | exports.getMaterialCount = async function () { 221 | const { accessToken } = await this.ensureAccessToken(); 222 | var url = this.prefix + 'material/get_materialcount?access_token=' + accessToken; 223 | return this.request(url, {dataType: 'json'}); 224 | }; 225 | 226 | /** 227 | * 获取永久素材列表 228 | * 详情请见: 229 | * Examples: 230 | * ``` 231 | * api.getMaterials(type, offset, count); 232 | * ``` 233 | * 234 | * - `result`, 调用正常时得到的文件Buffer对象 235 | * - `res`, HTTP响应对象 * Result: 236 | * ``` 237 | * { 238 | * "total_count": TOTAL_COUNT, 239 | * "item_count": ITEM_COUNT, 240 | * "item": [{ 241 | * "media_id": MEDIA_ID, 242 | * "name": NAME, 243 | * "update_time": UPDATE_TIME 244 | * }, 245 | * //可能会有多个素材 246 | * ] 247 | * } 248 | * ``` 249 | * @param {String} type 素材的类型,图片(image)、视频(video)、语音 (voice)、图文(news) 250 | * @param {Number} offset 从全部素材的该偏移位置开始返回,0表示从第一个素材 返回 251 | * @param {Number} count 返回素材的数量,取值在1到20之间 252 | */ 253 | exports.getMaterials = async function (type, offset, count) { 254 | const { accessToken } = await this.ensureAccessToken(); 255 | var url = this.prefix + 'material/batchget_material?access_token=' + accessToken; 256 | var data = { 257 | type: type, 258 | offset: offset, 259 | count: count 260 | }; 261 | return this.request(url, postJSON(data)); 262 | }; 263 | -------------------------------------------------------------------------------- /lib/api_media.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const util = require('util'); 4 | const path = require('path'); 5 | 6 | const { stat } = require('fs'); 7 | 8 | const statAsync = util.promisify(stat); 9 | 10 | const formstream = require('formstream'); 11 | 12 | /** 13 | * 新增临时素材,分别有图片(image)、语音(voice)、视频(video)和缩略图(thumb) 14 | * 详情请见: 15 | * Examples: 16 | * ``` 17 | * api.uploadMedia('filepath', type); 18 | * ``` 19 | * 20 | * Result: 21 | * ``` 22 | * {"type":"TYPE","media_id":"MEDIA_ID","created_at":123456789} 23 | * ``` 24 | * Shortcut: 25 | * - `exports.uploadImageMedia(filepath);` 26 | * - `exports.uploadVoiceMedia(filepath);` 27 | * - `exports.uploadVideoMedia(filepath);` 28 | * - `exports.uploadThumbMedia(filepath);` 29 | * 30 | * 不再推荐使用: 31 | * 32 | * - `exports.uploadImage(filepath);` 33 | * - `exports.uploadVoice(filepath);` 34 | * - `exports.uploadVideo(filepath);` 35 | * - `exports.uploadThumb(filepath);` 36 | * 37 | * @param {String|Buffer} filepath 文件路径/文件Buffer数据 38 | * @param {String} type 媒体类型,可用值有image、voice、video、thumb 39 | * @param {String} filename 文件名 40 | * @param {String} mime 文件类型,filepath为Buffer数据时才需要传 41 | */ 42 | exports.uploadMedia = async function (filepath, type, filename, mime) { 43 | const { accessToken } = await this.ensureAccessToken(); 44 | var form = formstream(); 45 | if (Buffer.isBuffer(filepath)) { 46 | form.buffer('media', filepath, filename, mime); 47 | } else if (typeof filepath === 'string') { 48 | var stat = await statAsync(filepath); 49 | form.file('media', filepath, filename || path.basename(filepath), stat.size); 50 | } 51 | var url = this.prefix + 'media/upload?access_token=' + accessToken + '&type=' + type; 52 | var opts = { 53 | method: 'POST', 54 | timeout: 60000, // 60秒超时 55 | headers: form.headers(), 56 | data: form 57 | }; 58 | opts.headers.Accept = 'application/json'; 59 | return this.request(url, opts); 60 | }; 61 | 62 | ['image', 'voice', 'video', 'thumb'].forEach(function (type) { 63 | var method = 'upload' + type[0].toUpperCase() + type.substring(1); 64 | var newMethod = method + 'Media'; 65 | exports[method] = util.deprecate(async function (filepath) { 66 | return this.uploadMedia(filepath, type); 67 | }, `${method}: Use ${newMethod} instead`); 68 | 69 | exports[newMethod] = async function (filepath) { 70 | return this.uploadMedia(filepath, type); 71 | }; 72 | }); 73 | 74 | /** 75 | * 获取临时素材 76 | * 详情请见: 77 | * Examples: 78 | * ``` 79 | * api.getMedia('media_id'); 80 | * ``` 81 | * - `result`, 调用正常时得到的文件Buffer对象 82 | * - `res`, HTTP响应对象 83 | * @param {String} mediaId 媒体文件的ID 84 | */ 85 | exports.getMedia = async function (mediaId) { 86 | const { accessToken } = await this.ensureAccessToken(); 87 | var url = this.prefix + 'media/get?access_token=' + accessToken + '&media_id=' + mediaId; 88 | var opts = { 89 | timeout: 60000 // 60秒超时 90 | }; 91 | return this.request(url, opts); 92 | }; 93 | /** 94 | * 上传图文消息内的图片获取URL 95 | * 详情请见: 96 | * Examples: 97 | * ``` 98 | * api.uploadImage('filepath'); 99 | * ``` 100 | * Result: 101 | * ``` 102 | * {"url": "http://mmbiz.qpic.cn/mmbiz/gLO17UPS6FS2xsypf378iaNhWacZ1G1UplZYWEYfwvuU6Ont96b1roYsCNFwaRrSaKTPCUdBK9DgEHicsKwWCBRQ/0"} 103 | * ``` 104 | * @param {String} filepath 图片文件路径 105 | */ 106 | exports.uploadImage = async function (filepath) { 107 | const { accessToken } = await this.ensureAccessToken(); 108 | var stat = await statAsync(filepath); 109 | var form = formstream(); 110 | form.file('media', filepath, path.basename(filepath), stat.size); 111 | var url = this.prefix + 'media/uploadimg?access_token=' + accessToken; 112 | var opts = { 113 | method: 'POST', 114 | timeout: 60000, // 60秒超时 115 | headers: form.headers(), 116 | data: form 117 | }; 118 | opts.headers.Accept = 'application/json'; 119 | return this.request(url, opts); 120 | }; 121 | -------------------------------------------------------------------------------- /lib/api_menu.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { postJSON } = require('./util'); 4 | 5 | /** 6 | * 创建自定义菜单 7 | * 详细请看:http://mp.weixin.qq.com/wiki/index.php?title=自定义菜单创建接口 * Menu: 8 | * ``` 9 | * { 10 | * "button":[ 11 | * { 12 | * "type":"click", 13 | * "name":"今日歌曲", 14 | * "key":"V1001_TODAY_MUSIC" 15 | * }, 16 | * { 17 | * "name":"菜单", 18 | * "sub_button":[ 19 | * { 20 | * "type":"view", 21 | * "name":"搜索", 22 | * "url":"http://www.soso.com/" 23 | * }, 24 | * { 25 | * "type":"click", 26 | * "name":"赞一下我们", 27 | * "key":"V1001_GOOD" 28 | * }] 29 | * }] 30 | * } 31 | * ] 32 | * } 33 | * ``` 34 | * Examples: 35 | * ``` 36 | * var result = await api.createMenu(menu); 37 | * ``` 38 | * Result: 39 | * ``` 40 | * {"errcode":0,"errmsg":"ok"} 41 | * ``` 42 | * @param {Object} menu 菜单对象 */ 43 | exports.createMenu = async function (menu) { 44 | const { accessToken } = await this.ensureAccessToken(); 45 | var url = this.prefix + 'menu/create?access_token=' + accessToken; 46 | return this.request(url, postJSON(menu)); 47 | }; 48 | 49 | /** 50 | * 获取菜单 51 | * 详细请看: * Examples: 52 | * ``` 53 | * var result = await api.getMenu(); 54 | * ``` 55 | * Result: 56 | * ``` 57 | * // 结果示例 58 | * { 59 | * "menu": { 60 | * "button":[ 61 | * {"type":"click","name":"今日歌曲","key":"V1001_TODAY_MUSIC","sub_button":[]}, 62 | * {"type":"click","name":"歌手简介","key":"V1001_TODAY_SINGER","sub_button":[]}, 63 | * {"name":"菜单","sub_button":[ 64 | * {"type":"view","name":"搜索","url":"http://www.soso.com/","sub_button":[]}, 65 | * {"type":"view","name":"视频","url":"http://v.qq.com/","sub_button":[]}, 66 | * {"type":"click","name":"赞一下我们","key":"V1001_GOOD","sub_button":[]}] 67 | * } 68 | * ] 69 | * } 70 | * } 71 | * ``` 72 | */ 73 | exports.getMenu = async function () { 74 | const { accessToken } = await this.ensureAccessToken(); 75 | var url = this.prefix + 'menu/get?access_token=' + accessToken; 76 | return this.request(url, {dataType: 'json'}); 77 | }; 78 | 79 | /** 80 | * 删除自定义菜单 81 | * 详细请看: 82 | * Examples: 83 | * ``` 84 | * var result = await api.removeMenu(); 85 | * ``` 86 | * Result: 87 | * ``` 88 | * {"errcode":0,"errmsg":"ok"} 89 | * ``` 90 | */ 91 | exports.removeMenu = async function () { 92 | const { accessToken } = await this.ensureAccessToken(); 93 | var url = this.prefix + 'menu/delete?access_token=' + accessToken; 94 | return this.request(url, {dataType: 'json'}); 95 | }; 96 | 97 | /** 98 | * 获取自定义菜单配置 99 | * 详细请看: 100 | * Examples: 101 | * ``` 102 | * var result = await api.getMenuConfig(); 103 | * ``` 104 | * Result: 105 | * ``` 106 | * {"errcode":0,"errmsg":"ok"} 107 | * ``` 108 | */ 109 | exports.getMenuConfig = async function () { 110 | const { accessToken } = await this.ensureAccessToken(); 111 | var url = this.prefix + 'get_current_selfmenu_info?access_token=' + accessToken; 112 | return this.request(url, {dataType: 'json'}); 113 | }; 114 | 115 | /** 116 | * 创建个性化自定义菜单 117 | * 详细请看:http://mp.weixin.qq.com/wiki/0/c48ccd12b69ae023159b4bfaa7c39c20.html * Menu: 118 | * ``` 119 | * { 120 | * "button":[ 121 | * { 122 | * "type":"click", 123 | * "name":"今日歌曲", 124 | * "key":"V1001_TODAY_MUSIC" 125 | * }, 126 | * { 127 | * "name":"菜单", 128 | * "sub_button":[ 129 | * { 130 | * "type":"view", 131 | * "name":"搜索", 132 | * "url":"http://www.soso.com/" 133 | * }, 134 | * { 135 | * "type":"view", 136 | * "name":"视频", 137 | * "url":"http://v.qq.com/" 138 | * }, 139 | * { 140 | * "type":"click", 141 | * "name":"赞一下我们", 142 | * "key":"V1001_GOOD" 143 | * }] 144 | * }], 145 | * "matchrule":{ 146 | * "group_id":"2", 147 | * "sex":"1", 148 | * "country":"中国", 149 | * "province":"广东", 150 | * "city":"广州", 151 | * "client_platform_type":"2" 152 | * } 153 | * } 154 | * ``` 155 | * Examples: 156 | * ``` 157 | * var result = await api.addConditionalMenu(menu); 158 | * ``` 159 | * Result: 160 | * ``` 161 | * {"errcode":0,"errmsg":"ok"} 162 | * ``` 163 | * @param {Object} menu 菜单对象 164 | */ 165 | exports.addConditionalMenu = async function (menu) { 166 | // https://api.weixin.qq.com/cgi-bin/menu/addconditional?access_token=ACCESS_TOKEN 167 | const { accessToken } = await this.ensureAccessToken(); 168 | var url = this.prefix + 'menu/addconditional?access_token=' + accessToken; 169 | return this.request(url, postJSON(menu)); 170 | }; 171 | 172 | /** 173 | * 删除个性化自定义菜单 174 | * 详细请看:http://mp.weixin.qq.com/wiki/0/c48ccd12b69ae023159b4bfaa7c39c20.html * Menu: 175 | * ``` 176 | * { 177 | * "menuid":"208379533" 178 | * } 179 | * ``` 180 | * Examples: 181 | * ``` 182 | * var result = await api.delConditionalMenu(menuid); 183 | * ``` 184 | * Result: 185 | * ``` 186 | * {"errcode":0,"errmsg":"ok"} 187 | * ``` 188 | * @param {String} menuid 菜单id 189 | */ 190 | exports.delConditionalMenu = async function (menuid) { 191 | // https://api.weixin.qq.com/cgi-bin/menu/delconditional?access_token=ACCESS_TOKEN 192 | const { accessToken } = await this.ensureAccessToken(); 193 | var url = this.prefix + 'menu/delconditional?access_token=' + accessToken; 194 | var data = { 195 | menuid: menuid 196 | }; 197 | return this.request(url, postJSON(data)); 198 | }; 199 | 200 | /** 201 | * 测试个性化自定义菜单 202 | * 详细请看:http://mp.weixin.qq.com/wiki/0/c48ccd12b69ae023159b4bfaa7c39c20.html * Menu: 203 | * ``` 204 | * { 205 | * "user_id":"nickma" 206 | * } 207 | * ``` 208 | * Examples: 209 | * ``` 210 | * var result = await api.tryConditionalMenu(user_id); 211 | * ``` 212 | * Result: 213 | * ``` 214 | * { 215 | * "button": [ 216 | * { 217 | * "type": "view", 218 | * "name": "tx", 219 | * "url": "http://www.qq.com/", 220 | * "sub_button": [ ] 221 | * }, 222 | * { 223 | * "type": "view", 224 | * "name": "tx", 225 | * "url": "http://www.qq.com/", 226 | * "sub_button": [ ] 227 | * }, 228 | * { 229 | * "type": "view", 230 | * "name": "tx", 231 | * "url": "http://www.qq.com/", 232 | * "sub_button": [ ] 233 | * } 234 | * ] 235 | * } 236 | * ``` 237 | * @param {String} user_id user_id可以是粉丝的OpenID,也可以是粉丝的微信号。 238 | */ 239 | exports.tryConditionalMenu = async function (user_id) { 240 | // https://api.weixin.qq.com/cgi-bin/menu/trymatch?access_token=ACCESS_TOKEN 241 | const { accessToken } = await this.ensureAccessToken(); 242 | var url = this.prefix + 'menu/trymatch?access_token=' + accessToken; 243 | var data = { 244 | user_id: user_id 245 | }; 246 | return this.request(url, postJSON(data)); 247 | }; 248 | -------------------------------------------------------------------------------- /lib/api_message.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { postJSON } = require('./util'); 4 | 5 | /** 6 | * 客服消息,发送文字消息 7 | * 详细细节 http://mp.weixin.qq.com/wiki/index.php?title=发送客服消息 8 | * Examples: 9 | * ``` 10 | * api.sendText('openid', 'Hello world'); 11 | * ``` 12 | 13 | * @param {String} openid 用户的openid 14 | * @param {String} text 发送的消息内容 15 | */ 16 | exports.sendText = async function (openid, text) { 17 | const { accessToken } = await this.ensureAccessToken(); 18 | // { 19 | // "touser":"OPENID", 20 | // "msgtype":"text", 21 | // "text": { 22 | // "content":"Hello World" 23 | // } 24 | // } 25 | var url = this.prefix + 'message/custom/send?access_token=' + accessToken; 26 | var data = { 27 | 'touser': openid, 28 | 'msgtype': 'text', 29 | 'text': { 30 | 'content': text 31 | } 32 | }; 33 | return this.request(url, postJSON(data)); 34 | }; 35 | 36 | /** 37 | * 客服消息,发送图片消息 38 | * 详细细节 http://mp.weixin.qq.com/wiki/index.php?title=发送客服消息 39 | * Examples: 40 | * ``` 41 | * api.sendImage('openid', 'media_id'); 42 | * ``` 43 | 44 | * @param {String} openid 用户的openid 45 | * @param {String} mediaId 媒体文件的ID,参见uploadMedia方法 46 | */ 47 | exports.sendImage = async function (openid, mediaId) { 48 | const { accessToken } = await this.ensureAccessToken(); 49 | // { 50 | // "touser":"OPENID", 51 | // "msgtype":"image", 52 | // "image": { 53 | // "media_id":"MEDIA_ID" 54 | // } 55 | // } 56 | var url = this.prefix + 'message/custom/send?access_token=' + accessToken; 57 | var data = { 58 | 'touser': openid, 59 | 'msgtype':'image', 60 | 'image': { 61 | 'media_id': mediaId 62 | } 63 | }; 64 | return this.request(url, postJSON(data)); 65 | }; 66 | /** 67 | * 客服消息,发送卡券 68 | * 详细细节 http://mp.weixin.qq.com/wiki/index.php?title=发送客服消息 69 | * Examples: 70 | * ``` 71 | * api.sendCard('openid', 'card_id'); 72 | * ``` 73 | 74 | * @param {String} openid 用户的openid 75 | * @param {String} card_id 卡券的ID 76 | */ 77 | exports.sendCard = async function (openid, cardid) { 78 | const { accessToken } = await this.ensureAccessToken(); 79 | // { 80 | // "touser":"OPENID", 81 | // "msgtype":"wxcard", 82 | // "wxcard": { 83 | // "card_id":"MEDIA_ID" 84 | // } 85 | // } 86 | var url = this.prefix + 'message/custom/send?access_token=' + accessToken; 87 | var data = { 88 | 'touser': openid, 89 | 'msgtype': 'wxcard', 90 | 'wxcard': { 91 | 'card_id': cardid 92 | } 93 | }; 94 | return this.request(url, postJSON(data)); 95 | }; 96 | /** 97 | * 客服消息,发送语音消息 98 | * 详细细节 http://mp.weixin.qq.com/wiki/index.php?title=发送客服消息 99 | * Examples: 100 | * ``` 101 | * api.sendVoice('openid', 'media_id'); 102 | * ``` 103 | 104 | * @param {String} openid 用户的openid 105 | * @param {String} mediaId 媒体文件的ID 106 | */ 107 | exports.sendVoice = async function (openid, mediaId) { 108 | const { accessToken } = await this.ensureAccessToken(); 109 | // { 110 | // "touser":"OPENID", 111 | // "msgtype":"voice", 112 | // "voice": { 113 | // "media_id":"MEDIA_ID" 114 | // } 115 | // } 116 | var url = this.prefix + 'message/custom/send?access_token=' + accessToken; 117 | var data = { 118 | 'touser': openid, 119 | 'msgtype': 'voice', 120 | 'voice': { 121 | 'media_id': mediaId 122 | } 123 | }; 124 | return this.request(url, postJSON(data)); 125 | }; 126 | 127 | /** 128 | * 客服消息,发送视频消息 129 | * 详细细节 http://mp.weixin.qq.com/wiki/index.php?title=发送客服消息 130 | * Examples: 131 | * ``` 132 | * api.sendVideo('openid', 'media_id', 'thumb_media_id'); 133 | * ``` 134 | 135 | * @param {String} openid 用户的openid 136 | * @param {String} mediaId 媒体文件的ID 137 | * @param {String} thumbMediaId 缩略图文件的ID 138 | */ 139 | exports.sendVideo = async function (openid, mediaId, thumbMediaId) { 140 | const { accessToken } = await this.ensureAccessToken(); 141 | // { 142 | // "touser":"OPENID", 143 | // "msgtype":"video", 144 | // "image": { 145 | // "media_id":"MEDIA_ID" 146 | // "thumb_media_id":"THUMB_MEDIA_ID" 147 | // } 148 | // } 149 | var url = this.prefix + 'message/custom/send?access_token=' + accessToken; 150 | var data = { 151 | 'touser': openid, 152 | 'msgtype':'video', 153 | 'video': { 154 | 'media_id': mediaId, 155 | 'thumb_media_id': thumbMediaId 156 | } 157 | }; 158 | return this.request(url, postJSON(data)); 159 | }; 160 | 161 | /** 162 | * 客服消息,发送音乐消息 163 | * 详细细节 http://mp.weixin.qq.com/wiki/index.php?title=发送客服消息 164 | * Examples: 165 | * ``` 166 | * var music = { 167 | * title: '音乐标题', // 可选 168 | * description: '描述内容', // 可选 169 | * musicurl: 'http://url.cn/xxx', 音乐文件地址 170 | * hqmusicurl: "HQ_MUSIC_URL", 171 | * thumb_media_id: "THUMB_MEDIA_ID" 172 | * }; 173 | * api.sendMusic('openid', music); 174 | * ``` 175 | 176 | * @param {String} openid 用户的openid 177 | * @param {Object} music 音乐文件 178 | */ 179 | exports.sendMusic = async function (openid, music) { 180 | const { accessToken } = await this.ensureAccessToken(); 181 | // { 182 | // "touser":"OPENID", 183 | // "msgtype":"music", 184 | // "music": { 185 | // "title":"MUSIC_TITLE", // 可选 186 | // "description":"MUSIC_DESCRIPTION", // 可选 187 | // "musicurl":"MUSIC_URL", 188 | // "hqmusicurl":"HQ_MUSIC_URL", 189 | // "thumb_media_id":"THUMB_MEDIA_ID" 190 | // } 191 | // } 192 | var url = this.prefix + 'message/custom/send?access_token=' + accessToken; 193 | var data = { 194 | 'touser': openid, 195 | 'msgtype':'music', 196 | 'music': music 197 | }; 198 | return this.request(url, postJSON(data)); 199 | }; 200 | 201 | /** 202 | * 客服消息,发送图文消息 203 | * 详细细节 http://mp.weixin.qq.com/wiki/index.php?title=发送客服消息 204 | * Examples: 205 | * ``` 206 | * var articles = [ 207 | * { 208 | * "title":"Happy Day", 209 | * "description":"Is Really A Happy Day", 210 | * "url":"URL", 211 | * "picurl":"PIC_URL" 212 | * }, 213 | * { 214 | * "title":"Happy Day", 215 | * "description":"Is Really A Happy Day", 216 | * "url":"URL", 217 | * "picurl":"PIC_URL" 218 | * }]; 219 | * api.sendNews('openid', articles); 220 | * ``` 221 | 222 | * @param {String} openid 用户的openid 223 | * @param {Array} articles 图文列表 224 | */ 225 | exports.sendNews = async function (openid, articles) { 226 | const { accessToken } = await this.ensureAccessToken(); 227 | // { 228 | // "touser":"OPENID", 229 | // "msgtype":"news", 230 | // "news":{ 231 | // "articles": [ 232 | // { 233 | // "title":"Happy Day", 234 | // "description":"Is Really A Happy Day", 235 | // "url":"URL", 236 | // "picurl":"PIC_URL" 237 | // }, 238 | // { 239 | // "title":"Happy Day", 240 | // "description":"Is Really A Happy Day", 241 | // "url":"URL", 242 | // "picurl":"PIC_URL" 243 | // }] 244 | // } 245 | // } 246 | var url = this.prefix + 'message/custom/send?access_token=' + accessToken; 247 | var data = { 248 | 'touser': openid, 249 | 'msgtype':'news', 250 | 'news': { 251 | 'articles': articles 252 | } 253 | }; 254 | return this.request(url, postJSON(data)); 255 | }; 256 | 257 | /** 258 | * 客服消息,发送图文消息(点击跳转到图文消息页面) 259 | * 详细细节 https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140547 260 | * Examples: 261 | * ``` 262 | * api.sendMpNews('openid', 'mediaId'); 263 | * ``` 264 | 265 | * @param {String} openid 用户的openid 266 | * @param {String} mediaId 图文消息媒体文件的ID 267 | */ 268 | exports.sendMpNews = async function (openid, mediaId) { 269 | const { accessToken } = await this.ensureAccessToken(); 270 | var url = this.prefix + 'message/custom/send?access_token=' + accessToken; 271 | var data = { 272 | 'touser': openid, 273 | 'msgtype':'mpnews', 274 | 'mpnews': { 275 | 'media_id': mediaId 276 | } 277 | }; 278 | return this.request(url, postJSON(data)); 279 | }; 280 | 281 | /** 282 | * 客服消息,发送小程序卡片(要求小程序与公众号已关联) 283 | * 详细细节 https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140547 284 | * Examples: 285 | * ``` 286 | * var miniprogram = { 287 | * title: '小程序标题', // 必填 288 | * appid: '小程序appid', // 必填 289 | * pagepath: 'pagepath', // 打开后小程序的地址,可以带query 290 | * thumb_media_id: "THUMB_MEDIA_ID" 291 | * }; 292 | * api.sendMiniProgram('openid', miniprogram); 293 | * ``` 294 | 295 | * @param {String} openid 用户的openid 296 | * @param {Object} miniprogram 小程序信息 297 | */ 298 | exports.sendMiniProgram = async function (openid, miniprogram) { 299 | const { accessToken } = await this.ensureAccessToken(); 300 | var url = this.prefix + 'message/custom/send?access_token=' + accessToken; 301 | var data = { 302 | 'touser': openid, 303 | 'msgtype': 'miniprogrampage', 304 | 'miniprogrampage': miniprogram 305 | }; 306 | return this.request(url, postJSON(data)); 307 | }; 308 | 309 | /** 310 | * 获取自动回复规则 311 | * 详细请看: 312 | * Examples: 313 | * ``` 314 | * var result = await api.getAutoreply(); 315 | * ``` 316 | * Result: 317 | * ``` 318 | * { 319 | * "is_add_friend_reply_open": 1, 320 | * "is_autoreply_open": 1, 321 | * "add_friend_autoreply_info": { 322 | * "type": "text", 323 | * "content": "Thanks for your attention!" 324 | * }, 325 | * "message_default_autoreply_info": { 326 | * "type": "text", 327 | * "content": "Hello, this is autoreply!" 328 | * }, 329 | * "keyword_autoreply_info": { 330 | * "list": [ 331 | * { 332 | * "rule_name": "autoreply-news", 333 | * "create_time": 1423028166, 334 | * "reply_mode": "reply_all", 335 | * "keyword_list_info": [ 336 | * { 337 | * "type": "text", 338 | * "match_mode": "contain", 339 | * "content": "news测试"//此处content即为关键词内容 340 | * } 341 | * ], 342 | * "reply_list_info": [ 343 | * { 344 | * "type": "news", 345 | * "news_info": { 346 | * "list": [ 347 | * { 348 | * "title": "it's news", 349 | * "author": "jim", 350 | * "digest": "it's digest", 351 | * "show_cover": 1, 352 | * "cover_url": "http://mmbiz.qpic.cn/mmbiz/GE7et87vE9vicuCibqXsX9GPPLuEtBfXfKbE8sWdt2DDcL0dMfQWJWTVn1N8DxI0gcRmrtqBOuwQHeuPKmFLK0ZQ/0", 353 | * "content_url": "http://mp.weixin.qq.com/s?__biz=MjM5ODUwNTM3Ng==&mid=203929886&idx=1&sn=628f964cf0c6d84c026881b6959aea8b#rd", 354 | * "source_url": "http://www.url.com" 355 | * } 356 | * ] 357 | * } 358 | * }, 359 | * { 360 | * .... 361 | * } 362 | * ] 363 | * }, 364 | * { 365 | * "rule_name": "autoreply-voice", 366 | * "create_time": 1423027971, 367 | * "reply_mode": "random_one", 368 | * "keyword_list_info": [ 369 | * { 370 | * "type": "text", 371 | * "match_mode": "contain", 372 | * "content": "voice测试" 373 | * } 374 | * ], 375 | * "reply_list_info": [ 376 | * { 377 | * "type": "voice", 378 | * "content": "NESsxgHEvAcg3egJTtYj4uG1PTL6iPhratdWKDLAXYErhN6oEEfMdVyblWtBY5vp" 379 | * } 380 | * ] 381 | * }, 382 | * ... 383 | * ] 384 | * } 385 | * } 386 | * ``` 387 | */ 388 | exports.getAutoreply = async function () { 389 | const { accessToken } = await this.ensureAccessToken(); 390 | var url = this.prefix + 'get_current_autoreply_info?access_token=' + accessToken; 391 | return this.request(url, {dataType: 'json'}); 392 | }; 393 | -------------------------------------------------------------------------------- /lib/api_miniprogram_login.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const url = require('url'); 3 | 4 | /** 5 | * 小程序登录凭证校验 6 | * 详细细节 https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/login/auth.code2Session.html 7 | * Examples: 8 | * ``` 9 | * api.code2Session('jd745fgdfg'); 10 | * ``` 11 | 12 | * @param {String} jsCode 小程序登录时获取的 code 13 | */ 14 | exports.code2Session = async function (jsCode) { 15 | // https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code 16 | var urlObj = new url.URL(this.snsPrefix + 'jscode2session'); 17 | var params = urlObj.searchParams; 18 | params.append('appid', this.appid); 19 | params.append('secret', this.appsecret); 20 | params.append('js_code', jsCode); 21 | params.append('grant_type', 'authorization_code'); 22 | 23 | return this.request(urlObj.href); 24 | }; 25 | -------------------------------------------------------------------------------- /lib/api_payment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { postJSON } = require('./util'); 4 | 5 | /** 6 | * 微信公众号支付: 发货通知 7 | * 详情请见: 接口文档订单发货通知 * Data: 8 | * ``` 9 | * { 10 | * "appid" : "wwwwb4f85f3a797777", 11 | * "openid" : "oX99MDgNcgwnz3zFN3DNmo8uwa-w", 12 | * "transid" : "111112222233333", 13 | * "out_trade_no" : "555666uuu", 14 | * "deliver_timestamp" : "1369745073", 15 | * "deliver_status" : "1", 16 | * "deliver_msg" : "ok", 17 | * "app_signature" : "53cca9d47b883bd4a5c85a9300df3da0cb48565c", 18 | * "sign_method" : "sha1" 19 | * } 20 | * ``` 21 | * Examples: 22 | * ``` 23 | * api.deliverNotify(data); 24 | * ``` 25 | 26 | * Result: 27 | * ``` 28 | * {"errcode":0, "errmsg":"ok"} 29 | * ``` * @param {Object} package package对象 30 | */ 31 | exports.deliverNotify = async function (data) { 32 | const { accessToken } = await this.ensureAccessToken(); 33 | var url = this.payPrefix + 'delivernotify?access_token=' + accessToken; 34 | return this.request(url, postJSON(data)); 35 | }; 36 | 37 | /** 38 | * 微信公众号支付: 订单查询 39 | * 详情请见: 接口文档订单查询部分 * Package: 40 | * ``` 41 | * { 42 | * "appid" : "wwwwb4f85f3a797777", 43 | * "package" : "out_trade_no=11122&partner=1900090055&sign=4e8d0df3da0c3d0df38f", 44 | * "timestamp" : "1369745073", 45 | * "app_signature" : "53cca9d47b883bd4a5c85a9300df3da0cb48565c", 46 | * "sign_method" : "sha1" 47 | * } 48 | * ``` 49 | * Examples: 50 | * ``` 51 | * api.orderQuery(query); 52 | * ``` 53 | 54 | * Result: 55 | * ``` 56 | * { 57 | * "errcode":0, 58 | * "errmsg":"ok", 59 | * "order_info": { 60 | * "ret_code":0, 61 | * "ret_msg":"", 62 | * "input_charset":"GBK", 63 | * "trade_state":"0", 64 | * "trade_mode":"1", 65 | * "partner":"1900000109", 66 | * "bank_type":"CMB_FP", 67 | * "bank_billno":"207029722724", 68 | * "total_fee":"1", 69 | * "fee_type":"1", 70 | * "transaction_id":"1900000109201307020305773741", 71 | * "out_trade_no":"2986872580246457300", 72 | * "is_split":"false", 73 | * "is_refund":"false", 74 | * "attach":"", 75 | * "time_end":"20130702175943", 76 | * "transport_fee":"0", 77 | * "product_fee":"1", 78 | * "discount":"0", 79 | * "rmb_total_fee":"" 80 | * } 81 | * } 82 | * ``` * @param {Object} query query对象 83 | */ 84 | exports.orderQuery = async function (query) { 85 | const { accessToken } = await this.ensureAccessToken(); 86 | var url = this.payPrefix + 'orderquery?access_token=' + accessToken; 87 | return this.request(url, postJSON(query)); 88 | }; 89 | -------------------------------------------------------------------------------- /lib/api_poi.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // 微信门店接口, 当前版本基于《POI 门店管理接口文档 V2.2.3》 4 | // 文档请参考(内置于微信卡券接口文档):http://mp.weixin.qq.com/wiki/9/d8a5f3b102915f30516d79b44fe665ed.html 5 | const { postJSON } = require('./util'); 6 | 7 | /** 8 | * 创建门店 * Tips: 9 | * - 创建门店接口调用成功后不会实时返回poi_id。 10 | * - 成功创建后,门店信息会经过审核,审核通过后方可使用并获取poi_id。 11 | * - 图片photo_url必须为上传图片接口(api.uploadLogo,参见卡券接口)生成的url。 12 | * - 门店类目categories请参考微信公众号后台的门店管理部分。 * Poi: 13 | * ``` 14 | * { 15 | * "sid": "5794560", 16 | * "business_name": "肯打鸡", 17 | * "branch_name": "东方路店", 18 | * "province": "上海市", 19 | * "city": "上海市", 20 | * "district": "浦东新区", 21 | * "address": "东方路88号", 22 | * "telephone": "021-5794560", 23 | * "categories": ["美食,快餐小吃"], 24 | * "offset_type": 1, 25 | * "longitude": 125.5794560, 26 | * "latitude": 45.5794560, 27 | * "photo_list": [{ 28 | * "photo_url": "https://5794560.qq.com/1" 29 | * }, { 30 | * "photo_url": "https://5794560.qq.com/2" 31 | * }], 32 | * "recommend": "脉娜鸡腿堡套餐,脉乐鸡,全家捅", 33 | * "special": "免费WIFE,外卖服务", 34 | * "introduction": "肯打鸡是全球大型跨国连锁餐厅,2015年创立于米国,在世界上大约拥有3 亿间分店,主要售卖肯打鸡等垃圾食品", 35 | * "open_time": "10:00-18:00", 36 | * "avg_price": 88 37 | * } 38 | * ``` 39 | * Examples: 40 | * ``` 41 | * api.addPoi(poi); 42 | * ``` 43 | 44 | * Result: 45 | * ``` 46 | * {"errcode":0,"errmsg":"ok"} 47 | * ``` 48 | * @name addPoi 49 | * @param {Object} poi 门店对象 50 | */ 51 | exports.addPoi = async function (poi) { 52 | const { accessToken } = await this.ensureAccessToken(); 53 | var data = { 54 | business: { 55 | base_info: poi 56 | } 57 | }; 58 | var url = this.prefix + 'poi/addpoi?access_token=' + accessToken; 59 | return this.request(url, postJSON(data)); 60 | }; 61 | 62 | /** 63 | * 获取门店信息 * Examples: 64 | * ``` 65 | * api.getPoi(POI_ID); 66 | * ``` 67 | 68 | * Result: 69 | * ``` 70 | * { 71 | * "sid": "5794560", 72 | * "business_name": "肯打鸡", 73 | * "branch_name": "东方路店", 74 | * "province": "上海市", 75 | * "city": "上海市", 76 | * "district": "浦东新区", 77 | * "address": "东方路88号", 78 | * "telephone": "021-5794560", 79 | * "categories": ["美食,快餐小吃"], 80 | * "offset_type": 1, 81 | * "longitude": 125.5794560, 82 | * "latitude": 45.5794560, 83 | * "photo_list": [{ 84 | * "photo_url": "https://5794560.qq.com/1" 85 | * }, { 86 | * "photo_url": "https://5794560.qq.com/2" 87 | * }], 88 | * "recommend": "脉娜鸡腿堡套餐,脉乐鸡,全家捅", 89 | * "special": "免费WIFE,外卖服务", 90 | * "introduction": "肯打鸡是全球大型跨国连锁餐厅,2015年创立于米国,在世界上大约拥有3 亿间分店,主要售卖肯打鸡等垃圾食品", 91 | * "open_time": "10:00-18:00", 92 | * "avg_price": 88, 93 | * "available_state": 3, 94 | * "update_status": 0 95 | * } 96 | * ``` 97 | * @name getPoi 98 | * @param {Number} poiId 门店ID 99 | */ 100 | exports.getPoi = async function (poiId) { 101 | const { accessToken } = await this.ensureAccessToken(); 102 | var url = this.prefix + 'poi/getpoi?access_token=' + accessToken; 103 | var data = { 104 | poi_id: poiId 105 | }; 106 | return this.request(url, postJSON(data)); 107 | }; 108 | 109 | /** 110 | * 获取门店列表 111 | * Examples: 112 | * ``` 113 | * api.getPois(0, 20); 114 | * ``` 115 | 116 | * Result: 117 | * ``` 118 | * { 119 | * "errcode": 0, 120 | * "errmsg": "ok" 121 | * "business_list": [{ 122 | * "base_info": { 123 | * "sid": "100", 124 | * "poi_id": "5794560", 125 | * "business_name": "肯打鸡", 126 | * "branch_name": "东方路店", 127 | * "address": "东方路88号", 128 | * "available_state": 3 129 | * } 130 | * }, { 131 | * "base_info": { 132 | * "sid": "101", 133 | * "business_name": "肯打鸡", 134 | * "branch_name": "西方路店", 135 | * "address": "西方路88号", 136 | * "available_state": 4 137 | * } 138 | * }], 139 | * "total_count": "2", 140 | * } 141 | * ``` 142 | * @name getPois 143 | * @param {Number} begin 开始位置,0即为从第一条开始查询 144 | * @param {Number} limit 返回数据条数,最大允许50,默认为20 145 | */ 146 | exports.getPois = async function (begin, limit) { 147 | const { accessToken } = await this.ensureAccessToken(); 148 | var url = this.prefix + 'poi/getpoilist?access_token=' + accessToken; 149 | var data = { 150 | begin: begin, 151 | limit: limit 152 | }; 153 | return this.request(url, postJSON(data)); 154 | }; 155 | 156 | /** 157 | * 删除门店 158 | * Tips: 159 | * - 待审核门店不允许删除 * Examples: 160 | * ``` 161 | * api.delPoi(POI_ID); 162 | * ``` 163 | * @name delPoi 164 | * @param {Number} poiId 门店ID 165 | */ 166 | exports.delPoi = async function (poiId) { 167 | const { accessToken } = await this.ensureAccessToken(); 168 | var url = this.prefix + 'poi/delpoi?access_token=' + accessToken; 169 | var data = { 170 | poi_id: poiId 171 | }; 172 | return this.request(url, postJSON(data)); 173 | }; 174 | 175 | /** 176 | * 修改门店服务信息 * Tips: * - 待审核门店不允许修改 * Poi: 177 | * ``` 178 | * { 179 | * "poi_id": "5794560", 180 | * "telephone": "021-5794560", 181 | * "photo_list": [{ 182 | * "photo_url": "https://5794560.qq.com/1" 183 | * }, { 184 | * "photo_url": "https://5794560.qq.com/2" 185 | * }], 186 | * "recommend": "脉娜鸡腿堡套餐,脉乐鸡,全家捅", 187 | * "special": "免费WIFE,外卖服务", 188 | * "introduction": "肯打鸡是全球大型跨国连锁餐厅,2015年创立于米国,在世界上大约拥有3 亿间分店,主要售卖肯打鸡等垃圾食品", 189 | * "open_time": "10:00-18:00", 190 | * "avg_price": 88 191 | * } 192 | * ``` 193 | * 特别注意,以上7个字段,若有填写内容则为覆盖更新,若无内容则视为不修改,维持原有内容。 194 | * photo_list字段为全列表覆盖,若需要增加图片,需将之前图片同样放入list中,在其后增加新增图片。 * Examples: 195 | * ``` 196 | * api.updatePoi(poi); 197 | * ``` 198 | 199 | * Result: 200 | * ``` 201 | * {"errcode":0,"errmsg":"ok"} 202 | * ``` 203 | * @name updatePoi 204 | * @param {Object} poi 门店对象 205 | */ 206 | exports.updatePoi = async function (poi) { 207 | const { accessToken } = await this.ensureAccessToken(); 208 | var data = { 209 | business: { 210 | base_info: poi 211 | } 212 | }; 213 | var url = this.prefix + 'poi/updatepoi?access_token=' + accessToken; 214 | return this.request(url, postJSON(data)); 215 | }; 216 | -------------------------------------------------------------------------------- /lib/api_qrcode.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { postJSON } = require('./util'); 4 | 5 | /** 6 | * 创建临时二维码 7 | * 详细请看: 8 | * Examples: 9 | * ``` 10 | * api.createTmpQRCode(10000, 1800); 11 | * ``` 12 | * 13 | * Result: 14 | * ``` 15 | * { 16 | * "ticket":"gQG28DoAAAAAAAAAASxodHRwOi8vd2VpeGluLnFxLmNvbS9xL0FuWC1DNmZuVEhvMVp4NDNMRnNRAAIEesLvUQMECAcAAA==", 17 | * "expire_seconds":1800 18 | * } 19 | * ``` 20 | * @param {Number} sceneId 场景ID 21 | * @param {Number} expire 过期时间,单位秒。最大不超过1800 22 | */ 23 | exports.createTmpQRCode = async function (sceneId, expire) { 24 | const { accessToken } = await this.ensureAccessToken(); 25 | var url = this.prefix + 'qrcode/create?access_token=' + accessToken; 26 | var data = { 27 | 'expire_seconds': expire, 28 | 'action_name': 'QR_SCENE', 29 | 'action_info': {'scene': {'scene_id': sceneId}} 30 | }; 31 | // 字符串 32 | if (typeof sceneId === 'string') { 33 | data.action_name = 'QR_STR_SCENE'; 34 | data.action_info.scene = {'scene_str': sceneId}; 35 | } 36 | return this.request(url, postJSON(data)); 37 | }; 38 | 39 | /** 40 | * 创建永久二维码 41 | * 详细请看: 42 | * Examples: 43 | * ``` 44 | * api.createLimitQRCode(100); 45 | * ``` 46 | * 47 | * Result: 48 | * ``` 49 | * { 50 | * "ticket":"gQG28DoAAAAAAAAAASxodHRwOi8vd2VpeGluLnFxLmNvbS9xL0FuWC1DNmZuVEhvMVp4NDNMRnNRAAIEesLvUQMECAcAAA==" 51 | * } 52 | * ``` 53 | * @param {Number} sceneId 场景ID。ID不能大于100000 54 | */ 55 | exports.createLimitQRCode = async function (sceneId) { 56 | const { accessToken } = await this.ensureAccessToken(); 57 | var url = this.prefix + 'qrcode/create?access_token=' + accessToken; 58 | var data = { 59 | 'action_name': 'QR_LIMIT_SCENE', 60 | 'action_info': {'scene': {'scene_id': sceneId}} 61 | }; 62 | // 字符串 63 | if (typeof sceneId === 'string') { 64 | data.action_name = 'QR_LIMIT_STR_SCENE'; 65 | data.action_info.scene = {'scene_str': sceneId}; 66 | } 67 | return this.request(url, postJSON(data)); 68 | }; 69 | 70 | /** 71 | * 生成显示二维码的链接。微信扫描后,可立即进入场景 72 | * Examples: 73 | * ``` 74 | * api.showQRCodeURL(ticket); 75 | * // => https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=TICKET 76 | * ``` 77 | * @param {String} ticket 二维码Ticket 78 | * @return {String} 显示二维码的URL地址,通过img标签可以显示出来 */ 79 | exports.showQRCodeURL = function(ticket) { 80 | return this.mpPrefix + 'showqrcode?ticket=' + ticket; 81 | }; 82 | -------------------------------------------------------------------------------- /lib/api_semantic.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { postJSON } = require('./util'); 4 | 5 | /** 6 | * 发送语义理解请求 7 | * 详细请看:http://mp.weixin.qq.com/wiki/index.php?title=%E8%AF%AD%E4%B9%89%E7%90%86%E8%A7%A3 * Opts: 8 | * ``` 9 | * { 10 | * "query":"查一下明天从北京到上海的南航机票", 11 | * "city":"北京", 12 | * "category": "flight,hotel" 13 | * } 14 | * ``` 15 | * Examples: 16 | * ``` 17 | * api.semantic(uid, opts); 18 | * ``` 19 | 20 | * Result: 21 | * ``` 22 | * { 23 | * "errcode":0, 24 | * "query":"查一下明天从北京到上海的南航机票", 25 | * "type":"flight", 26 | * "semantic":{ 27 | * "details":{ 28 | * "start_loc":{ 29 | * "type":"LOC_CITY", 30 | * "city":"北京市", 31 | * "city_simple":"北京", 32 | * "loc_ori":"北京" 33 | * }, 34 | * "end_loc": { 35 | * "type":"LOC_CITY", 36 | * "city":"上海市", 37 | * "city_simple":"上海", 38 | * "loc_ori":"上海" 39 | * }, 40 | * "start_date": { 41 | * "type":"DT_ORI", 42 | * "date":"2014-03-05", 43 | * "date_ori":"明天" 44 | * }, 45 | * "airline":"中国南方航空公司" 46 | * }, 47 | * "intent":"SEARCH" 48 | * } 49 | * ``` 50 | * @param {String} openid 用户ID 51 | * @param {Object} opts 查询条件 52 | */ 53 | exports.semantic = async function (uid, opts) { 54 | const { accessToken } = await this.ensureAccessToken(); 55 | // https://api.weixin.qq.com/semantic/semproxy/search?access_token=YOUR_ACCESS_TOKEN 56 | var url = 'https://api.weixin.qq.com/semantic/semproxy/search?access_token=' + accessToken; 57 | opts.appid = this.appid; 58 | opts.uid = uid; 59 | return this.request(url, postJSON(opts)); 60 | }; 61 | -------------------------------------------------------------------------------- /lib/api_shakearound.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const { promisify } = require('util'); 5 | const { stat } = require('fs'); 6 | const statAsync = promisify(stat); 7 | 8 | const formstream = require('formstream'); 9 | 10 | const { postJSON } = require('./util'); 11 | 12 | /** 13 | * 申请开通功能 14 | * 接口说明: 15 | * 申请开通摇一摇周边功能。成功提交申请请求后,工作人员会在三个工作日内完成审核。若审核不通过,可以重新提交申请请求。 16 | * 若是审核中,请耐心等待工作人员审核,在审核中状态不能再提交申请请求。 17 | * 详情请参见: * Options: 18 | * ``` 19 | * { 20 | * "name": "zhang_san", 21 | * "phone_number": "13512345678", 22 | * "email": "weixin123@qq.com", 23 | * "industry_id": "0118", 24 | * "qualification_cert_urls": [ 25 | * "http://shp.qpic.cn/wx_shake_bus/0/1428565236d03d864b7f43db9ce34df5f720509d0e/0", 26 | * "http://shp.qpic.cn/wx_shake_bus/0/1428565236d03d864b7f43db9ce34df5f720509d0e/0" 27 | * ], 28 | * "apply_reason": "test" 29 | * } 30 | * ``` * Examples: 31 | * ``` 32 | * api.registerShakeAccount(options); 33 | * ``` 34 | * Result: 35 | * ``` 36 | * { 37 | * "data" : { }, 38 | * "errcode": 0, 39 | * "errmsg": "success." 40 | * } 41 | * ``` * @name registerShakeAccount 42 | * @param {Object} options 请求参数 43 | */ 44 | exports.registerShakeAccount = async function (options) { 45 | const { accessToken } = await this.ensureAccessToken(); 46 | var url = 'https://api.weixin.qq.com/shakearound/account/register?access_token=' + accessToken; 47 | return this.request(url, postJSON(options)); 48 | }; 49 | 50 | /** 51 | * 查询审核状态 52 | * 接口说明: 53 | * 查询已经提交的开通摇一摇周边功能申请的审核状态。在申请提交后,工作人员会在三个工作日内完成审核。 54 | * 详情请参见:http://mp.weixin.qq.com/wiki/13/025f1d471dc999928340161c631c6635.html 55 | * Examples: 56 | * ``` 57 | * api.checkShakeAccountStatus(); 58 | * ``` 59 | 60 | * Result: 61 | * ``` 62 | * { 63 | * "data" : { 64 | * "apply_time": 1432026025, 65 | * "audit_comment": "test", 66 | * "audit_status": 1, //审核状态。0:审核未通过、1:审核中、2:审核已通过;审核会在三个工作日内完成 67 | * "audit_time": 0 68 | * }, 69 | * "errcode": 0, 70 | * "errmsg": "success." 71 | * } 72 | * ``` * @name checkShakeAccountStatus 73 | */ 74 | exports.checkShakeAccountStatus = async function () { 75 | const { accessToken } = await this.ensureAccessToken(); 76 | var url = 'https://api.weixin.qq.com/shakearound/account/auditstatus?access_token=' + accessToken; 77 | return this.request(url, {dataType: 'json'}); 78 | }; 79 | 80 | /** 81 | * 设备管理: 申请设备ID。 82 | * 接口说明: 83 | * 申请配置设备所需的UUID、Major、Minor。若激活率小于50%,不能新增设备。单次新增设备超过500个, 84 | * 需走人工审核流程。审核通过后,可用返回的批次ID用“查询设备列表”接口拉取本次申请的设备ID。 85 | * 详情请参见: * Options: 86 | * ``` 87 | * { 88 | * "quantity":3, 89 | * "apply_reason":"测试", 90 | * "comment":"测试专用", 91 | * "poi_id":1234 92 | * } 93 | * ``` * Examples: 94 | * ``` 95 | * api.applyBeacons(options); 96 | * ``` 97 | * Result: 98 | * ``` 99 | * { 100 | * "data" : { ... }, 101 | * "errcode": 0, 102 | * "errmsg": "success." 103 | * } 104 | * ``` * @name applyBeacons 105 | * @param {Object} options 请求参数 106 | */ 107 | exports.applyBeacons = async function (options) { 108 | const { accessToken } = await this.ensureAccessToken(); 109 | var url = 'https://api.weixin.qq.com/shakearound/device/applyid?access_token=' + accessToken; 110 | return this.request(url, postJSON(options)); 111 | }; 112 | 113 | /** 114 | * 设备管理: 编辑设备的备注信息。 115 | * 接口说明: 116 | * 可用设备ID或完整的UUID、Major、Minor指定设备,二者选其一。 117 | * 详情请参见:http://mp.weixin.qq.com/wiki/15/b9e012f917e3484b7ed02771156411f3.html 118 | * Options: 119 | * ``` 120 | * { 121 | * "device_identifier": { 122 | * // 设备编号,若填了UUID、major、minor,则可不填设备编号,若二者都填,则以设备编号为优先 123 | * "device_id": 10011, 124 | * "uuid": "FDA50693-A4E2-4FB1-AFCF-C6EB07647825", 125 | * "major": 1002, 126 | * "minor": 1223 127 | * }, 128 | * "comment": "test" 129 | * } 130 | * ``` 131 | * Examples: 132 | * ``` 133 | * api.updateBeacon(options); 134 | * ``` 135 | 136 | * Result: 137 | * ``` 138 | * { 139 | * "data" : { 140 | * }, 141 | * "errcode": 0, 142 | * "errmsg": "success." 143 | * } 144 | * ``` * @name updateBeacon 145 | * @param {Object} options 请求参数 146 | */ 147 | exports.updateBeacon = async function (options) { 148 | const { accessToken } = await this.ensureAccessToken(); 149 | var url = 'https://api.weixin.qq.com/shakearound/device/update?access_token=' + accessToken; 150 | return this.request(url, postJSON(options)); 151 | }; 152 | 153 | /** 154 | * 设备管理: 配置设备与门店的关联关系。 155 | * 接口说明: 156 | * 修改设备关联的门店ID、设备的备注信息。可用设备ID或完整的UUID、Major、Minor指定设备,二者选其一。 157 | * 详情请参见:http://mp.weixin.qq.com/wiki/15/b9e012f917e3484b7ed02771156411f3.html 158 | * Options: 159 | * ``` 160 | * { 161 | * "device_identifier": { 162 | * "device_id": 10011, 163 | * "uuid": "FDA50693-A4E2-4FB1-AFCF-C6EB07647825", 164 | * "major": 1002, 165 | * "minor": 1223 166 | * }, 167 | * "poi_id": 1231 168 | * } 169 | * ``` 170 | * Examples: 171 | * ``` 172 | * api.bindBeaconLocation(options); 173 | * ``` 174 | 175 | * Result: 176 | * ``` 177 | * { 178 | * "data" : { 179 | * }, 180 | * "errcode": 0, 181 | * "errmsg": "success." 182 | * } 183 | * ``` * @name bindBeaconLocation 184 | * @param {Object} options 请求参数 185 | */ 186 | exports.bindBeaconLocation = async function (options) { 187 | const { accessToken } = await this.ensureAccessToken(); 188 | var url = 'https://api.weixin.qq.com/shakearound/device/bindlocation?access_token=' + accessToken; 189 | return this.request(url, postJSON(options)); 190 | }; 191 | 192 | /** 193 | * 设备管理: 查询设备列表 194 | * 接口说明: 195 | * 查询已有的设备ID、UUID、Major、Minor、激活状态、备注信息、关联门店、关联页面等信息。 196 | * 可指定设备ID或完整的UUID、Major、Minor查询,也可批量拉取设备信息列表。 197 | * 详情请参见:http://mp.weixin.qq.com/wiki/15/b9e012f917e3484b7ed02771156411f3.html 198 | * Options: 199 | * 1) 查询指定设备时: 200 | * ``` 201 | * { 202 | * "device_identifier": [ 203 | * { 204 | * "device_id":10011, 205 | * "uuid":"FDA50693-A4E2-4FB1-AFCF-C6EB07647825", 206 | * "major":1002, 207 | * "minor":1223 208 | * } 209 | * ] 210 | * } 211 | * ``` 212 | * 2) 需要分页查询或者指定范围内的设备时: 213 | * ``` 214 | * { 215 | * "begin": 0, 216 | * "count": 3 217 | * } 218 | * ``` 219 | * 3) 当需要根据批次ID查询时: 220 | * ``` 221 | * { 222 | * "apply_id": 1231, 223 | * "begin": 0, 224 | * "count": 3 225 | * } 226 | * ``` 227 | * Examples: 228 | * ``` 229 | * api.getBeacons(options); 230 | * ``` 231 | * Result: 232 | * ``` 233 | * { 234 | * "data": { 235 | * "devices": [ 236 | * { 237 | * "comment": "", 238 | * "device_id": 10097, 239 | * "major": 10001, 240 | * "minor": 12102, 241 | * "page_ids": "15369", 242 | * "status": 1, //激活状态,0:未激活,1:已激活(但不活跃),2:活跃 243 | * "poi_id": 0, 244 | * "uuid": "FDA50693-A4E2-4FB1-AFCF-C6EB07647825" 245 | * }, 246 | * { 247 | * "comment": "", 248 | * "device_id": 10098, 249 | * "major": 10001, 250 | * "minor": 12103, 251 | * "page_ids": "15368", 252 | * "status": 1, 253 | * "poi_id": 0, 254 | * "uuid": "FDA50693-A4E2-4FB1-AFCF-C6EB07647825" 255 | * } 256 | * ], 257 | * "total_count": 151 258 | * }, 259 | * "errcode": 0, 260 | * "errmsg": "success." 261 | * } 262 | * ``` * @name getBeacons 263 | * @param {Object} options 请求参数 264 | */ 265 | exports.getBeacons = async function (options) { 266 | const { accessToken } = await this.ensureAccessToken(); 267 | var url = 'https://api.weixin.qq.com/shakearound/device/search?access_token=' + accessToken; 268 | return this.request(url, postJSON(options)); 269 | }; 270 | 271 | /** 272 | * 页面管理: 新增页面 273 | * 接口说明: 274 | * 新增摇一摇出来的页面信息,包括在摇一摇页面出现的主标题、副标题、图片和点击进去的超链接。 275 | * 其中,图片必须为用素材管理接口(uploadPageIcon函数)上传至微信侧服务器后返回的链接。 276 | * 详情请参见:http://mp.weixin.qq.com/wiki/5/6626199ea8757c752046d8e46cf13251.html 277 | * Page: 278 | * ``` 279 | * { 280 | * "title":"主标题", 281 | * "description":"副标题", 282 | * "page_url":" https://zb.weixin.qq.com", 283 | * "comment":"数据示例", 284 | * "icon_url":"http://shp.qpic.cn/wx_shake_bus/0/14288351768a23d76e7636b56440172120529e8252/120" 285 | * //调用uploadPageIcon函数获取到该URL 286 | * } 287 | * ``` 288 | * Examples: 289 | * ``` 290 | * api.createPage(page); 291 | * ``` 292 | * Result: 293 | * ``` 294 | * { 295 | * "data" : { 296 | * "page_id": 28840 297 | * }, 298 | * "errcode": 0, 299 | * "errmsg": "success." 300 | * } 301 | * ``` * @name createPage 302 | * @param {Object} page 请求参数 303 | */ 304 | exports.createPage = async function (page) { 305 | const { accessToken } = await this.ensureAccessToken(); 306 | var url = 'https://api.weixin.qq.com/shakearound/page/add?access_token=' + accessToken; 307 | return this.request(url, postJSON(page)); 308 | }; 309 | 310 | /** 311 | * 页面管理: 编辑页面信息 312 | * 接口说明: 313 | * 编辑摇一摇出来的页面信息,包括在摇一摇页面出现的主标题、副标题、图片和点击进去的超链接。 314 | * 详情请参见:http://mp.weixin.qq.com/wiki/5/6626199ea8757c752046d8e46cf13251.html 315 | * Page: 316 | * ``` 317 | * { 318 | * "page_id":12306, 319 | * "title":"主标题", 320 | * "description":"副标题", 321 | * "page_url":" https://zb.weixin.qq.com", 322 | * "comment":"数据示例", 323 | * "icon_url":"http://shp.qpic.cn/wx_shake_bus/0/14288351768a23d76e7636b56440172120529e8252/120" 324 | * //调用uploadPageIcon函数获取到该URL 325 | * } 326 | * ``` 327 | * Examples: 328 | * ``` 329 | * api.updatePage(page); 330 | * ``` 331 | * Result: 332 | * ``` 333 | * { 334 | * "data" : { 335 | * "page_id": 28840 336 | * }, 337 | * "errcode": 0, 338 | * "errmsg": "success." 339 | * } 340 | * ``` * @name updatePage 341 | * @param {Object} page 请求参数 342 | */ 343 | exports.updatePage = async function (page) { 344 | const { accessToken } = await this.ensureAccessToken(); 345 | var url = 'https://api.weixin.qq.com/shakearound/page/update?access_token=' + accessToken; 346 | return this.request(url, postJSON(page)); 347 | }; 348 | 349 | /** 350 | * 页面管理: 删除页面 351 | * 接口说明: 352 | * 删除已有的页面,包括在摇一摇页面出现的主标题、副标题、图片和点击进去的超链接。 353 | * 只有页面与设备没有关联关系时,才可被删除。 354 | * 详情请参见:http://mp.weixin.qq.com/wiki/5/6626199ea8757c752046d8e46cf13251.html 355 | * Page_ids: 356 | * ``` 357 | * { 358 | * "page_ids":[12345,23456,34567] 359 | * } 360 | * ``` 361 | * Examples: 362 | * ``` 363 | * api.deletePages(options); 364 | * ``` 365 | 366 | * Result: 367 | * ``` 368 | * { 369 | * "data" : { 370 | * }, 371 | * "errcode": 0, 372 | * "errmsg": "success." 373 | * } 374 | * ``` 375 | * @name deletePages 376 | * @param {Object} page_ids 请求参数 377 | */ 378 | exports.deletePages = async function (page_ids) { 379 | const { accessToken } = await this.ensureAccessToken(); 380 | var data = {page_ids: page_ids}; 381 | var url = 'https://api.weixin.qq.com/shakearound/page/delete?access_token=' + accessToken; 382 | return this.request(url, postJSON(data)); 383 | }; 384 | 385 | /** 386 | * 页面管理: 查询页面列表 387 | * 接口说明: 388 | * 查询已有的页面,包括在摇一摇页面出现的主标题、副标题、图片和点击进去的超链接。提供两种查询方式,可指定页面ID查询,也可批量拉取页面列表。 389 | * 详情请参见:http://mp.weixin.qq.com/wiki/5/6626199ea8757c752046d8e46cf13251.html 390 | * Options: 391 | * 1) 需要查询指定页面时: 392 | * ``` 393 | * { 394 | * "page_ids":[12345, 23456, 34567] 395 | * } 396 | * ``` 397 | * 2) 需要分页查询或者指定范围内的页面时: 398 | * ``` 399 | * { 400 | * "begin": 0, 401 | * "count": 3 402 | * } 403 | * ``` * Examples: 404 | * ``` 405 | * api.getBeacons(options); 406 | * ``` 407 | 408 | * Result: 409 | * ``` 410 | * { 411 | * "data": { 412 | * "pages": [ 413 | * { 414 | * "comment": "just for test", 415 | * "description": "test", 416 | * "icon_url": "https://www.baidu.com/img/bd_logo1", 417 | * "page_id": 28840, 418 | * "page_url": "http://xw.qq.com/testapi1", 419 | * "title": "测试1" 420 | * }, 421 | * { 422 | * "comment": "just for test", 423 | * "description": "test", 424 | * "icon_url": "https://www.baidu.com/img/bd_logo1", 425 | * "page_id": 28842, 426 | * "page_url": "http://xw.qq.com/testapi2", 427 | * "title": "测试2" 428 | * } 429 | * ], 430 | * "total_count": 2 431 | * }, 432 | * "errcode": 0, 433 | * "errmsg": "success." 434 | * } 435 | * ``` * @name getPages 436 | * @param {Object} options 请求参数 437 | */ 438 | exports.getPages = async function (options) { 439 | const { accessToken } = await this.ensureAccessToken(); 440 | var url = 'https://api.weixin.qq.com/shakearound/page/search?access_token=' + accessToken; 441 | return this.request(url, postJSON(options)); 442 | }; 443 | 444 | /** 445 | * 上传图片素材 446 | * 接口说明: 447 | * 上传在摇一摇页面展示的图片素材,素材保存在微信侧服务器上。 448 | * 格式限定为:jpg,jpeg,png,gif,图片大小建议120px*120 px,限制不超过200 px *200 px,图片需为正方形。 449 | * 详情请参见:http://mp.weixin.qq.com/wiki/5/e997428269ff189d8f9a4b9e177be2d9.html 450 | * Examples: 451 | * ``` 452 | * api.uploadPageIcon('filepath'); 453 | * ``` 454 | 455 | * Result: 456 | * ``` 457 | * { 458 | * "data" : { 459 | * "pic_url": "http://shp.qpic.cn/wechat_shakearound_pic/0/1428377032e9dd2797018cad79186e03e8c5aec8dc/120" 460 | * }, 461 | * "errcode": 0, 462 | * "errmsg": "success." 463 | * } 464 | * ``` * @name uploadPageIcon 465 | * @param {String} filepath 文件路径 466 | */ 467 | exports.uploadPageIcon = async function (filepath) { 468 | const { accessToken } = await this.ensureAccessToken(); 469 | var stat = await statAsync(filepath); 470 | var form = formstream(); 471 | form.file('media', filepath, path.basename(filepath), stat.size); 472 | var url = 'https://api.weixin.qq.com/shakearound/material/add?access_token=' + accessToken; 473 | var opts = { 474 | dataType: 'json', 475 | method: 'POST', 476 | timeout: 60000, // 60秒超时 477 | headers: form.headers(), 478 | data: form 479 | }; 480 | return this.request(url, opts); 481 | }; 482 | 483 | /** 484 | * 配置设备与页面的关联关系 485 | * 接口说明: 486 | * 配置设备与页面的关联关系。支持建立或解除关联关系,也支持新增页面或覆盖页面等操作。 487 | * 配置完成后,在此设备的信号范围内,即可摇出关联的页面信息。若设备配置多个页面,则随机出现页面信息。 488 | * 详情请参见:http://mp.weixin.qq.com/wiki/6/c449687e71510db19564f2d2d526b6ea.html 489 | * Options: 490 | * ``` 491 | * { 492 | * "device_identifier": { 493 | * // 设备编号,若填了UUID、major、minor,则可不填设备编号,若二者都填,则以设备编号为优先 494 | * "device_id":10011, 495 | * "uuid":"FDA50693-A4E2-4FB1-AFCF-C6EB07647825", 496 | * "major":1002, 497 | * "minor":1223 498 | * }, 499 | * "page_ids":[12345, 23456, 334567] 500 | * } 501 | * ``` 502 | * Examples: 503 | * ``` 504 | * api.bindBeaconWithPages(options); 505 | * ``` 506 | 507 | * Result: 508 | * ``` 509 | * { 510 | * "data" : { 511 | * }, 512 | * "errcode": 0, 513 | * "errmsg": "success." 514 | * } 515 | * ``` * @name bindBeaconWithPages 516 | * @param {Object} options 请求参数 517 | */ 518 | exports.bindBeaconWithPages = async function (options) { 519 | const { accessToken } = await this.ensureAccessToken(); 520 | var url = 'https://api.weixin.qq.com/shakearound/device/bindpage?access_token=' + accessToken; 521 | return this.request(url, postJSON(options)); 522 | }; 523 | 524 | /** 525 | * 查询设备与页面的关联关系 526 | * 接口说明: 527 | * 查询设备与页面的关联关系。提供两种查询方式,可指定页面ID分页查询该页面所关联的所有的设备信息; 528 | * 也可根据设备ID或完整的UUID、Major、Minor查询该设备所关联的所有页面信息。 529 | * 详情请参见:http://mp.weixin.qq.com/wiki/6/c449687e71510db19564f2d2d526b6ea.html 530 | * Options: 531 | * 1) 当查询指定设备所关联的页面时: 532 | * ``` 533 | * { 534 | * "type": 1, 535 | * "device_identifier": { 536 | * // 设备编号,若填了UUID、major、minor,则可不填设备编号,若二者都填,则以设备编号为优先 537 | * "device_id":10011, 538 | * "uuid":"FDA50693-A4E2-4FB1-AFCF-C6EB07647825", 539 | * "major":1002, 540 | * "minor":1223 541 | * } 542 | * } 543 | * ``` 544 | * 2) 当查询页面所关联的设备时: 545 | * { 546 | * "type": 2, 547 | * "page_id": 11101, 548 | * "begin": 0, 549 | * "count": 3 550 | * } 551 | * Examples: 552 | * ``` 553 | * api.searchBeaconPageRelation(options); 554 | * ``` 555 | 556 | * Result: 557 | * ``` 558 | * { 559 | * "data": { 560 | * "relations": [ 561 | * { 562 | * "device_id": 797994, 563 | * "major": 10001, 564 | * "minor": 10023, 565 | * "page_id": 50054, 566 | * "uuid": "FDA50693-A4E2-4FB1-AFCF-C6EB07647825" 567 | * }, 568 | * { 569 | * "device_id": 797994, 570 | * "major": 10001, 571 | * "minor": 10023, 572 | * "page_id": 50055, 573 | * "uuid": "FDA50693-A4E2-4FB1-AFCF-C6EB07647825" 574 | * } 575 | * ], 576 | * "total_count": 2 577 | * }, 578 | * "errcode": 0, 579 | * "errmsg": "success." 580 | * } 581 | * ``` * @name searchBeaconPageRelation 582 | * @param {Object} options 请求参数 583 | */ 584 | exports.searchBeaconPageRelation = async function (options) { 585 | const { accessToken } = await this.ensureAccessToken(); 586 | var url = 'https://api.weixin.qq.com/shakearound/relation/search?access_token=' + accessToken; 587 | return this.request(url, postJSON(options)); 588 | }; 589 | 590 | /** 591 | * 获取摇周边的设备及用户信息 592 | * 接口说明: 593 | * 获取设备信息,包括UUID、major、minor,以及距离、openID等信息。 594 | * 详情请参见:http://mp.weixin.qq.com/wiki/3/34904a5db3d0ec7bb5306335b8da1faf.html 595 | * Ticket: 596 | * ``` 597 | * { 598 | * "ticket":”6ab3d8465166598a5f4e8c1b44f44645” 599 | * } 600 | * ``` 601 | * Examples: 602 | * ``` 603 | * api.getShakeInfo(ticket); 604 | * ``` 605 | 606 | * Result: 607 | * ``` 608 | * { 609 | * "data" : { 610 | * }, 611 | * "errcode": 0, 612 | * "errmsg": "success." 613 | * } 614 | * ``` * @name getShakeInfo 615 | * @param {Object} ticket 摇周边业务的ticket,可在摇到的URL中得到,ticket生效时间为30分钟 616 | */ 617 | exports.getShakeInfo = async function (ticket) { 618 | const { accessToken } = await this.ensureAccessToken(); 619 | var data = { 620 | ticket: ticket 621 | }; 622 | 623 | var url = 'https://api.weixin.qq.com/shakearound/user/getshakeinfo?access_token=' + accessToken; 624 | return this.request(url, postJSON(data)); 625 | }; 626 | 627 | /** 628 | * 数据统计: 以设备为维度的数据统计接口 629 | * 接口说明: 630 | * 查询单个设备进行摇周边操作的人数、次数,点击摇周边消息的人数、次数;查询的最长时间跨度为30天。 631 | * 详情请参见:http://mp.weixin.qq.com/wiki/0/8a24bcacad40fe7ee98d1573cb8a6764.html 632 | * Options: 633 | * ``` 634 | * { 635 | * "device_identifier": { 636 | * "device_id":10011, //设备编号,若填了UUID、major、minor,则可不填设备编号,若二者都填,则以设备编号为优先 637 | * "uuid":"FDA50693-A4E2-4FB1-AFCF-C6EB07647825", //UUID、major、minor,三个信息需填写完整,若填了设备编号,则可不填此信息。 638 | * "major":1002, 639 | * "minor":1223 640 | * }, 641 | * "begin_date": 12313123311, 642 | * "end_date": 123123131231 643 | * } 644 | * ``` 645 | * Examples: 646 | * ``` 647 | * api.getDeviceStatistics(options); 648 | * ``` 649 | 650 | * Result: 651 | * ``` 652 | * { 653 | * "data" : { 654 | * { 655 | * "click_pv": 0, 656 | * "click_uv": 0, 657 | * "ftime": 1425052800, 658 | * "shake_pv": 0, 659 | * "shake_uv": 0 660 | * }, 661 | * { 662 | * "click_pv": 0, 663 | * "click_uv": 0, 664 | * "ftime": 1425139200, 665 | * "shake_pv": 0, 666 | * "shake_uv": 0 667 | * } 668 | * }, 669 | * "errcode": 0, 670 | * "errmsg": "success." 671 | * } 672 | * ``` 673 | * @name getDeviceStatistics 674 | * @param {Object} options 请求参数 675 | */ 676 | exports.getDeviceStatistics = async function (options) { 677 | const { accessToken } = await this.ensureAccessToken(); 678 | var url = 'https://api.weixin.qq.com/shakearound/statistics/device?access_token=' + accessToken; 679 | return this.request(url, postJSON(options)); 680 | }; 681 | 682 | /** 683 | * 数据统计: 以页面为维度的数据统计接口 684 | * 接口说明: 685 | * 查询单个页面通过摇周边摇出来的人数、次数,点击摇周边页面的人数、次数;查询的最长时间跨度为30天。 686 | * 详情请参见:http://mp.weixin.qq.com/wiki/0/8a24bcacad40fe7ee98d1573cb8a6764.html 687 | * Options: 688 | * ``` 689 | * { 690 | * "page_id": 12345, 691 | * "begin_date": 12313123311, 692 | * "end_date": 123123131231 693 | * } 694 | * ``` 695 | * Examples: 696 | * ``` 697 | * api.getPageStatistics(options); 698 | * ``` 699 | 700 | * Result: 701 | * ``` 702 | * { 703 | * "data" : { 704 | * { 705 | * "click_pv": 0, 706 | * "click_uv": 0, 707 | * "ftime": 1425052800, 708 | * "shake_pv": 0, 709 | * "shake_uv": 0 710 | * }, 711 | * { 712 | * "click_pv": 0, 713 | * "click_uv": 0, 714 | * "ftime": 1425139200, 715 | * "shake_pv": 0, 716 | * "shake_uv": 0 717 | * } 718 | * }, 719 | * "errcode": 0, 720 | * "errmsg": "success." 721 | * } 722 | * ``` * @name getPageStatistics 723 | * @param {Object} options 请求参数 724 | */ 725 | exports.getPageStatistics = async function (options) { 726 | const { accessToken } = await this.ensureAccessToken(); 727 | var url = 'https://api.weixin.qq.com/shakearound/statistics/page?access_token=' + accessToken; 728 | return this.request(url, postJSON(options)); 729 | }; 730 | -------------------------------------------------------------------------------- /lib/api_shop_common.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | 6 | /** 7 | * 上传图片 8 | * 详细请看: 9 | * Examples: 10 | * ``` 11 | * api.uploadPicture('/path/to/your/img.jpg'); 12 | * ``` 13 | * 14 | * Result: 15 | * ``` 16 | * { 17 | * "errcode": 0, 18 | * "errmsg": "success" 19 | * "image_url": "http://mmbiz.qpic.cn/mmbiz/4whpV1VZl2ibl4JWwwnW3icSJGqecVtRiaPxwWEIr99eYYL6AAAp1YBo12CpQTXFH6InyQWXITLvU4CU7kic4PcoXA/0" 20 | * } 21 | * ``` 22 | * @param {String} filepath 文件路径 23 | */ 24 | exports.uploadPicture = async function (filepath) { 25 | const { accessToken } = await this.ensureAccessToken(); 26 | var basename = path.basename(filepath); 27 | var url = this.merchantPrefix + 'common/upload_img?access_token=' + 28 | accessToken + '&filename=' + basename; 29 | var reader = fs.createReadStream(filepath); 30 | var opts = { 31 | dataType: 'json', 32 | method: 'POST', 33 | data: reader 34 | }; 35 | 36 | return this.request(url, opts); 37 | }; 38 | -------------------------------------------------------------------------------- /lib/api_shop_express.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { postJSON } = require('./util'); 4 | 5 | /** 6 | * 增加邮费模板 7 | * 详细请看: 8 | * Examples: 9 | * ``` 10 | * api.addExpress(express); 11 | * ``` 12 | * Express: 13 | * ``` 14 | * { 15 | * "delivery_template": { 16 | * "Name": "testexpress", 17 | * "Assumer": 0, 18 | * "Valuation": 0, 19 | * "TopFee": [ 20 | * { 21 | * "Type": 10000027, 22 | * "Normal": { 23 | * "StartStandards": 1, 24 | * "StartFees": 2, 25 | * "AddStandards": 3, 26 | * "AddFees": 1 27 | * }, 28 | * "Custom": [ 29 | * { 30 | * "StartStandards": 1, 31 | * "StartFees": 100, 32 | * "AddStandards": 1, 33 | * "AddFees": 3, 34 | * "DestCountry": "中国", 35 | * "DestProvince": "广东省", 36 | * "DestCity": "广州市" 37 | * } 38 | * ] 39 | * }, 40 | * { 41 | * "Type": 10000028, 42 | * "Normal": { 43 | * "StartStandards": 1, 44 | * "StartFees": 3, 45 | * "AddStandards": 3, 46 | * "AddFees": 2 47 | * }, 48 | * "Custom": [ 49 | * { 50 | * "StartStandards": 1, 51 | * "StartFees": 10, 52 | * "AddStandards": 1, 53 | * "AddFees": 30, 54 | * "DestCountry": "中国", 55 | * "DestProvince": "广东省", 56 | * "DestCity": "广州市" 57 | * } 58 | * ] 59 | * }, 60 | * { 61 | * "Type": 10000029, 62 | * "Normal": { 63 | * "StartStandards": 1, 64 | * "StartFees": 4, 65 | * "AddStandards": 3, 66 | * "AddFees": 3 67 | * }, 68 | * "Custom": [ 69 | * { 70 | * "StartStandards": 1, 71 | * "StartFees": 8, 72 | * "AddStandards": 2, 73 | * "AddFees": 11, 74 | * "DestCountry": "中国", 75 | * "DestProvince": "广东省", 76 | * "DestCity": "广州市" 77 | * } 78 | * ] 79 | * } 80 | * ] 81 | * } 82 | * } 83 | * ``` 84 | 85 | * Result: 86 | * ``` 87 | * { 88 | * "errcode": 0, 89 | * "errmsg": "success" 90 | * "template_id": 123456 91 | * } 92 | * ``` 93 | * @param {Object} express 邮费模版 94 | */ 95 | exports.addExpressTemplate = async function (express) { 96 | const { accessToken } = await this.ensureAccessToken(); 97 | var url = this.merchantPrefix + 'express/add?access_token=' + accessToken; 98 | return this.request(url, postJSON(express)); 99 | }; 100 | 101 | /** 102 | * 修改邮费模板 103 | * 详细请看: 104 | * Examples: 105 | * ``` 106 | * api.deleteExpressTemplate(templateId); 107 | * ``` 108 | 109 | * Result: 110 | * ``` 111 | * { 112 | * "errcode": 0, 113 | * "errmsg": "success" 114 | * } 115 | * ``` 116 | * @param {Number} templateId 邮费模版ID 117 | */ 118 | exports.deleteExpressTemplate = async function (templateId) { 119 | const { accessToken } = await this.ensureAccessToken(); 120 | var data = { 121 | template_id: templateId 122 | }; 123 | var url = this.merchantPrefix + 'express/del?access_token=' + accessToken; 124 | return this.request(url, postJSON(data)); 125 | }; 126 | 127 | /** 128 | * 修改邮费模板 129 | * 详细请看: 130 | * Examples: 131 | * ``` 132 | * api.updateExpressTemplate(template); 133 | * ``` 134 | 135 | * Express: 136 | * ``` 137 | * { 138 | * "template_id": 123456, 139 | * "delivery_template": ... 140 | * } 141 | * ``` 142 | * Result: 143 | * ``` 144 | * { 145 | * "errcode": 0, 146 | * "errmsg": "success" 147 | * } 148 | * ``` 149 | * @param {Object} template 邮费模版 150 | */ 151 | exports.updateExpressTemplate = async function (template) { 152 | const { accessToken } = await this.ensureAccessToken(); 153 | var url = this.merchantPrefix + 'express/del?access_token=' + accessToken; 154 | return this.request(url, postJSON(template)); 155 | }; 156 | 157 | /** 158 | * 获取指定ID的邮费模板 159 | * 详细请看: 160 | * Examples: 161 | * ``` 162 | * api.getExpressTemplateById(templateId); 163 | * ``` 164 | 165 | * Result: 166 | * ``` 167 | * { 168 | * "errcode": 0, 169 | * "errmsg": "success", 170 | * "template_info": { 171 | * "Id": 103312916, 172 | * "Name": "testexpress", 173 | * "Assumer": 0, 174 | * "Valuation": 0, 175 | * "TopFee": [ 176 | * { 177 | * "Type": 10000027, 178 | * "Normal": { 179 | * "StartStandards": 1, 180 | * "StartFees": 2, 181 | * "AddStandards": 3, 182 | * "AddFees": 1 183 | * }, 184 | * "Custom": [ 185 | * { 186 | * "StartStandards": 1, 187 | * "StartFees": 1000, 188 | * "AddStandards": 1, 189 | * "AddFees": 3, 190 | * "DestCountry": "中国", 191 | * "DestProvince": "广东省", 192 | * "DestCity": "广州市" 193 | * } 194 | * ] 195 | * }, 196 | * ... 197 | * ] 198 | * } 199 | * } 200 | * ``` 201 | * @param {Number} templateId 邮费模版Id 202 | */ 203 | exports.getExpressTemplateById = async function (templateId) { 204 | const { accessToken } = await this.ensureAccessToken(); 205 | var data = { 206 | template_id: templateId 207 | }; 208 | var url = this.merchantPrefix + 'express/getbyid?access_token=' + accessToken; 209 | return this.request(url, postJSON(data)); 210 | }; 211 | 212 | /** 213 | * 获取所有邮费模板的未封装版本 214 | * 详细请看: 215 | * Examples: 216 | * ``` 217 | * api.getAllExpressTemplates(); 218 | * ``` 219 | 220 | * Result: 221 | * ``` 222 | * { 223 | * "errcode": 0, 224 | * "errmsg": "success", 225 | * "templates_info": [ 226 | * { 227 | * "Id": 103312916, 228 | * "Name": "testexpress1", 229 | * "Assumer": 0, 230 | * "Valuation": 0, 231 | * "TopFee": [...], 232 | * }, 233 | * { 234 | * "Id": 103312917, 235 | * "Name": "testexpress2", 236 | * "Assumer": 0, 237 | * "Valuation": 2, 238 | * "TopFee": [...], 239 | * }, 240 | * { 241 | * "Id": 103312918, 242 | * "Name": "testexpress3", 243 | * "Assumer": 0, 244 | * "Valuation": 1, 245 | * "TopFee": [...], 246 | * } 247 | * ] 248 | * } 249 | * ``` 250 | */ 251 | exports.getAllExpressTemplates = async function () { 252 | const { accessToken } = await this.ensureAccessToken(); 253 | var url = this.merchantPrefix + 'express/getall?access_token=' + accessToken; 254 | return this.request(url, {dataType: 'json'}); 255 | }; 256 | -------------------------------------------------------------------------------- /lib/api_shop_goods.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { postJSON } = require('./util'); 4 | 5 | /** 6 | * 增加商品 7 | * 详细请看: 8 | * Examples: 9 | * ``` 10 | * api.createGoods(goods); 11 | * ``` 12 | * Goods: 13 | * ``` 14 | * { 15 | * "product_base":{ 16 | * "category_id":[ 17 | * "537074298" 18 | * ], 19 | * "property":[ 20 | * {"id":"1075741879","vid":"1079749967"}, 21 | * {"id":"1075754127","vid":"1079795198"}, 22 | * {"id":"1075777334","vid":"1079837440"} 23 | * ], 24 | * "name":"testaddproduct", 25 | * "sku_info":[ 26 | * { 27 | * "id":"1075741873", 28 | * "vid":["1079742386","1079742363"] 29 | * } 30 | * ], 31 | * "main_img": "http://mmbiz.qpic.cn/mmbiz/4whpV1VZl2iccsvYbHvnphkyGtnvjD3ulEKogfsiaua49pvLfUS8Ym0GSYjViaLic0FD3vN0V8PILcibEGb2fPfEOmw/0", 32 | * "img":[ 33 | * "http://mmbiz.qpic.cn/mmbiz/4whpV1VZl2iccsvYbHvnphkyGtnvjD3ulEKogfsiaua49pvLfUS8Ym0GSYjViaLic0FD3vN0V8PILcibEGb2fPfEOmw/0" 34 | * ], 35 | * "detail":[ 36 | * {"text":"testfirst"}, 37 | * {"img": 4whpV1VZl2iccsvYbHvnphkyGtnvjD3ul1UcLcwxrFdwTKYhH9Q5YZoCfX4Ncx655ZK6ibnlibCCErbKQtReySaVA/0"}, 38 | * {"text":"testagain"} 39 | * ], 40 | * "buy_limit":10 41 | * }, 42 | * "sku_list":[ 43 | * { 44 | * "sku_id":"1075741873:1079742386", 45 | * "price":30, 46 | * "icon_url": "http://mmbiz.qpic.cn/mmbiz/4whpV1VZl28bJj62XgfHPibY3ORKicN1oJ4CcoIr4BMbfA8LqyyjzOZzqrOGz3f5K Wq1QGP3fo6TOTSYD3TBQjuw/0", 47 | * "product_code":"testing", 48 | * "ori_price":9000000, 49 | * "quantity":800 50 | * }, 51 | * { 52 | * "sku_id":"1075741873:1079742363", 53 | * "price":30, 54 | * "icon_url": "http://mmbiz.qpic.cn/mmbiz/4whpV1VZl28bJj62XgfHPibY3ORKicN1oJ4CcoIr4BMbfA8LqyyjzOZzqrOGz3f5K Wq1QGP3fo6TOTSYD3TBQjuw/0", 55 | * "product_code":"testingtesting", 56 | * "ori_price":9000000, 57 | * "quantity":800 58 | * } 59 | * ], 60 | * "attrext":{ 61 | * "location":{ 62 | * "country":"中国", 63 | * "province":"广东省", 64 | * "city":"广州市", 65 | * "address":"T.I.T创意园" 66 | * }, 67 | * "isPostFree":0, 68 | * "isHasReceipt":1, 69 | * "isUnderGuaranty":0, 70 | * "isSupportReplace":0 71 | * }, 72 | * "delivery_info":{ 73 | * "delivery_type":0, 74 | * "template_id":0, 75 | * "express":[ 76 | * {"id":10000027,"price":100}, 77 | * {"id":10000028,"price":100}, 78 | * {"id":10000029,"price":100} 79 | * ] 80 | * } 81 | * } 82 | * ``` 83 | * Result: 84 | * ``` 85 | * { 86 | * "errcode": 0, 87 | * "errmsg": "success", 88 | * "product_id": "pDF3iYwktviE3BzU3BKiSWWi9Nkw" 89 | * } 90 | * ``` 91 | * @param {Object} goods 商品信息 92 | */ 93 | exports.createGoods = async function (goods) { 94 | const { accessToken } = await this.ensureAccessToken(); 95 | var url = this.merchantPrefix + 'create?access_token=' + accessToken; 96 | return this.request(url, postJSON(goods)); 97 | }; 98 | 99 | /** 100 | * 删除商品 101 | * 详细请看: 102 | * Examples: 103 | * ``` 104 | * api.deleteGoods(productId); 105 | * ``` 106 | 107 | * Result: 108 | * ``` 109 | * { 110 | * "errcode": 0, 111 | * "errmsg": "success", 112 | * } 113 | * ``` 114 | * @param {String} productId 商品Id 115 | */ 116 | exports.deleteGoods = async function (productId) { 117 | const { accessToken } = await this.ensureAccessToken(); 118 | var data = { 119 | 'product_id': productId 120 | }; 121 | var url = this.merchantPrefix + 'del?access_token=' + accessToken; 122 | return this.request(url, postJSON(data)); 123 | }; 124 | 125 | /** 126 | * 修改商品 127 | * 详细请看: 128 | * Examples: 129 | * ``` 130 | * api.updateGoods(goods); 131 | * ``` 132 | * Goods: 133 | * ``` 134 | * { 135 | * "product_id":"pDF3iY6Kr_BV_CXaiYysoGqJhppQ", 136 | * "product_base":{ 137 | * "category_id":[ 138 | * "537074298" 139 | * ], 140 | * "property":[ 141 | * {"id":"1075741879","vid":"1079749967"}, 142 | * {"id":"1075754127","vid":"1079795198"}, 143 | * {"id":"1075777334","vid":"1079837440"} 144 | * ], 145 | * "name":"testaddproduct", 146 | * "sku_info":[ 147 | * { 148 | * "id":"1075741873", 149 | * "vid":["1079742386","1079742363"] 150 | * } 151 | * ], 152 | * "main_img": "http://mmbiz.qpic.cn/mmbiz/4whpV1VZl2iccsvYbHvnphkyGtnvjD3ulEKogfsiaua49pvLfUS8Ym0GSYjViaLic0FD3vN0V8PILcibEGb2fPfEOmw/0", 153 | * "img":[ 154 | * "http://mmbiz.qpic.cn/mmbiz/4whpV1VZl2iccsvYbHvnphkyGtnvjD3ulEKogfsiaua49pvLfUS8Ym0GSYjViaLic0FD3vN0V8PILcibEGb2fPfEOmw/0" 155 | * ], 156 | * "detail":[ 157 | * {"text":"testfirst"}, 158 | * {"img": 4whpV1VZl2iccsvYbHvnphkyGtnvjD3ul1UcLcwxrFdwTKYhH9Q5YZoCfX4Ncx655ZK6ibnlibCCErbKQtReySaVA/0"}, 159 | * {"text":"testagain"} 160 | * ], 161 | * "buy_limit":10 162 | * }, 163 | * "sku_list":[ 164 | * { 165 | * "sku_id":"1075741873:1079742386", 166 | * "price":30, 167 | * "icon_url": "http://mmbiz.qpic.cn/mmbiz/4whpV1VZl28bJj62XgfHPibY3ORKicN1oJ4CcoIr4BMbfA8LqyyjzOZzqrOGz3f5K Wq1QGP3fo6TOTSYD3TBQjuw/0", 168 | * "product_code":"testing", 169 | * "ori_price":9000000, 170 | * "quantity":800 171 | * }, 172 | * { 173 | * "sku_id":"1075741873:1079742363", 174 | * "price":30, 175 | * "icon_url": "http://mmbiz.qpic.cn/mmbiz/4whpV1VZl28bJj62XgfHPibY3ORKicN1oJ4CcoIr4BMbfA8LqyyjzOZzqrOGz3f5K Wq1QGP3fo6TOTSYD3TBQjuw/0", 176 | * "product_code":"testingtesting", 177 | * "ori_price":9000000, 178 | * "quantity":800 179 | * } 180 | * ], 181 | * "attrext":{ 182 | * "location":{ 183 | * "country":"中国", 184 | * "province":"广东省", 185 | * "city":"广州市", 186 | * "address":"T.I.T创意园" 187 | * }, 188 | * "isPostFree":0, 189 | * "isHasReceipt":1, 190 | * "isUnderGuaranty":0, 191 | * "isSupportReplace":0 192 | * }, 193 | * "delivery_info":{ 194 | * "delivery_type":0, 195 | * "template_id":0, 196 | * "express":[ 197 | * {"id":10000027,"price":100}, 198 | * {"id":10000028,"price":100}, 199 | * {"id":10000029,"price":100} 200 | * ] 201 | * } 202 | * } 203 | * ``` 204 | 205 | * Result: 206 | * ``` 207 | * { 208 | * "errcode": 0, 209 | * "errmsg": "success" 210 | * } 211 | * ``` 212 | * @param {Object} goods 商品信息 213 | */ 214 | exports.updateGoods = async function (goods) { 215 | const { accessToken } = await this.ensureAccessToken(); 216 | var url = this.merchantPrefix + 'update?access_token=' + accessToken; 217 | return this.request(url, postJSON(goods)); 218 | }; 219 | 220 | /** 221 | * 查询商品 222 | * 详细请看: 223 | * Examples: 224 | * ``` 225 | * api.getGoods(productId); 226 | * ``` 227 | * Result: 228 | * ``` 229 | * { 230 | * "errcode": 0, 231 | * "errmsg": "success", 232 | * "product_info":{ 233 | * "product_id":"pDF3iY6Kr_BV_CXaiYysoGqJhppQ", 234 | * "product_base":{ 235 | * "name":"testaddproduct", 236 | * "category_id":[537074298], 237 | * "img":[ 238 | * "http://mmbiz.qpic.cn/mmbiz/4whpV1VZl2iccsvYbHvnphkyGtnvjD3ulEKogfsiaua49pvLfUS8Ym0GSYjViaLic0FD3vN0V8PILcibEGb2fPfEOmw/0" 239 | * ], 240 | * "property":[ 241 | * {"id":"品牌","vid":"Fujifilm/富⼠士"}, 242 | * {"id":"屏幕尺⼨寸","vid":"1.8英⼨寸"}, 243 | * {"id":"防抖性能","vid":"CCD防抖"} 244 | * ], 245 | * "sku_info":[ 246 | * { 247 | * "id":"1075741873", 248 | * "vid":[ 249 | * "1079742386", 250 | * "1079742363" 251 | * ] 252 | * } 253 | * ], 254 | * "buy_limit":10, 255 | * "main_img": 4whpV1VZl2iccsvYbHvnphkyGtnvjD3ulEKogfsiaua49pvLfUS8Ym0GSYjViaLic 0FD3vN0V8PILcibEGb2fPfEOmw/0", 256 | * "detail_html": "
\"\"

test

\"\"

test again

" 257 | * }, 258 | * "sku_list":[ 259 | * { 260 | * "sku_id":"1075741873:1079742386", 261 | * "price":30, 262 | * "icon_url": "http://mmbiz.qpic.cn/mmbiz/4whpV1VZl2iccsvYbHvnphkyGtnvjD3ulEKogfsiaua49pvLfUS8Ym0GSYjViaLic0FD3vN0V8PILcibEGb2fPfEOmw/0", 263 | * "quantity":800, 264 | * "product_code":"testing", 265 | * "ori_price":9000000 266 | * }, 267 | * { 268 | * "sku_id":"1075741873:1079742363", 269 | * "price":30, 270 | * "icon_url": "http://mmbiz.qpic.cn/mmbiz/4whpV1VZl28bJj62XgfHPibY3ORKicN1oJ4CcoIr4BMbfA8LqyyjzOZzqrOGz3f5KWq1QGP3fo6TOTSYD3TBQjuw/0", 271 | * "quantity":800, 272 | * "product_code":"testingtesting", 273 | * "ori_price":9000000 274 | * } 275 | * ], 276 | * "attrext":{ 277 | * "isPostFree":0, 278 | * "isHasReceipt":1, 279 | * "isUnderGuaranty":0, 280 | * "isSupportReplace":0, 281 | * "location":{ 282 | * "country":"中国", 283 | * "province":"广东省", 284 | * "city":"⼲州市", 285 | * "address":"T.I.T创意园" 286 | * } 287 | * }, 288 | * "delivery_info":{ 289 | * "delivery_type":1, 290 | * "template_id":103312920 291 | * } 292 | * } 293 | * } 294 | * ``` 295 | * @param {String} productId 商品Id 296 | */ 297 | exports.getGoods = async function (productId) { 298 | const { accessToken } = await this.ensureAccessToken(); 299 | var url = this.merchantPrefix + 'get?product_id=' + productId + '&access_token=' + accessToken; 300 | return this.request(url, {dataType: 'json'}); 301 | }; 302 | 303 | /** 304 | * 获取指定状态的所有商品 305 | * 详细请看: 306 | * Examples: 307 | * ``` 308 | * api.deleteGoods(productId); 309 | * ``` 310 | * Result: 311 | * ``` 312 | * { 313 | * "errcode": 0, 314 | * "errmsg": "success", 315 | * "products_info": [ 316 | * { 317 | * "product_base": ..., 318 | * "sku_list": ..., 319 | * "attrext": ..., 320 | * "delivery_info": ..., 321 | * "product_id": "pDF3iY-mql6CncpbVajaB_obC321", 322 | * "status": 1 323 | * } 324 | * ] 325 | * } 326 | * ``` 327 | * @param {Number} status 状态码。(0-全部, 1-上架, 2-下架) 328 | */ 329 | exports.getGoodsByStatus = async function (status) { 330 | const { accessToken } = await this.ensureAccessToken(); 331 | var data = {status: status}; 332 | var url = this.merchantPrefix + 'getbystatus?access_token=' + accessToken; 333 | return this.request(url, postJSON(data)); 334 | }; 335 | 336 | /** 337 | * 商品上下架 338 | * 详细请看: 339 | * Examples: 340 | * ``` 341 | * api.updateGoodsStatus(productId, status); 342 | * ``` 343 | 344 | * Result: 345 | * ``` 346 | * { 347 | * "errcode": 0, 348 | * "errmsg": "success" 349 | * } 350 | * ``` 351 | * @param {String} productId 商品Id 352 | * @param {Number} status 状态码。(0-全部, 1-上架, 2-下架) 353 | */ 354 | exports.updateGoodsStatus = async function (productId, status) { 355 | const { accessToken } = await this.ensureAccessToken(); 356 | var data = { 357 | product_id: productId, 358 | status: status 359 | }; 360 | var url = this.merchantPrefix + 'modproductstatus?access_token=' + accessToken; 361 | return this.request(url, postJSON(data)); 362 | }; 363 | 364 | /** 365 | * 获取指定分类的所有子分类 366 | * 详细请看: 367 | * Examples: 368 | * ``` 369 | * api.getSubCats(catId); 370 | * ``` 371 | 372 | * Result: 373 | * ``` 374 | * { 375 | * "errcode": 0, 376 | * "errmsg": "success" 377 | * "cate_list": [! 378 | * {"id": "537074292","name": "数码相机"}, 379 | * {"id": "537074293","name": "家⽤用摄像机"}, 380 | * {"id": "537074298",! "name": "单反相机"} 381 | * ] 382 | * } 383 | * ``` 384 | * @param {Number} catId 大分类ID 385 | */ 386 | exports.getSubCats = async function (catId) { 387 | const { accessToken } = await this.ensureAccessToken(); 388 | var data = { 389 | cate_id: catId 390 | }; 391 | var url = this.merchantPrefix + 'category/getsub?access_token=' + accessToken; 392 | return this.request(url, postJSON(data)); 393 | }; 394 | 395 | /** 396 | * 获取指定子分类的所有SKU 397 | * 详细请看: 398 | * Examples: 399 | * ``` 400 | * api.getSKUs(catId); 401 | * ``` 402 | 403 | * Result: 404 | * ``` 405 | * { 406 | * "errcode": 0, 407 | * "errmsg": "success" 408 | * "sku_table": [ 409 | * { 410 | * "id": "1075741873", 411 | * "name": "颜⾊色", 412 | * "value_list": [ 413 | * {"id": "1079742375", "name": "撞⾊色"}, 414 | * {"id": "1079742376","name": "桔⾊色"} 415 | * ] 416 | * } 417 | * ] 418 | * } 419 | * ``` 420 | * @param {Number} catId 大分类ID 421 | */ 422 | exports.getSKUs = async function (catId) { 423 | const { accessToken } = await this.ensureAccessToken(); 424 | var data = { 425 | cate_id: catId 426 | }; 427 | var url = this.merchantPrefix + 'category/getsku?access_token=' + accessToken; 428 | return this.request(url, postJSON(data)); 429 | }; 430 | 431 | /** 432 | * 获取指定分类的所有属性 433 | * 详细请看: 434 | * Examples: 435 | * ``` 436 | * api.getProperties(catId); 437 | * ``` 438 | 439 | * Result: 440 | * ``` 441 | * { 442 | * "errcode": 0, 443 | * "errmsg": "success" 444 | * "properties": [ 445 | * { 446 | * "id": "1075741879", 447 | * "name": "品牌", 448 | * "property_value": [ 449 | * {"id": "200050867","name": "VIC&"}, 450 | * {"id": "200050868","name": "Kate&"}, 451 | * {"id": "200050971","name": "M&"}, 452 | * {"id": "200050972","name": "Black&"} 453 | * ] 454 | * }, 455 | * { 456 | * "id": "123456789", 457 | * "name": "颜⾊色", 458 | * "property_value": ... 459 | * } 460 | * ] 461 | * } 462 | * ``` 463 | * @param {Number} catId 分类ID 464 | */ 465 | exports.getProperties = async function (catId) { 466 | const { accessToken } = await this.ensureAccessToken(); 467 | var data = { 468 | cate_id: catId 469 | }; 470 | var url = this.merchantPrefix + 'category/getproperty?access_token=' + accessToken; 471 | return this.request(url, postJSON(data)); 472 | }; 473 | 474 | -------------------------------------------------------------------------------- /lib/api_shop_group.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // 商品分组管理接口 4 | const { postJSON } = require('./util'); 5 | 6 | /** 7 | * 创建商品分组 8 | * 详细请看: 9 | * Examples: 10 | * ``` 11 | * api.createGoodsGroup(groupName, productList); 12 | * ``` 13 | 14 | * Result: 15 | * ``` 16 | * { 17 | * "errcode": 0, 18 | * "errmsg": "success", 19 | * "group_id": 19 20 | * } 21 | * ``` 22 | * @param {String} groupName 分组名 23 | * @param {Array} productList 该组商品列表 24 | * @param {Function} callback 回调函数 25 | */ 26 | exports.createGoodsGroup = async function (groupName, productList) { 27 | const { accessToken } = await this.ensureAccessToken(); 28 | var data = { 29 | 'group_detail': { 30 | 'group_name': groupName, 31 | 'product_list': productList && productList.length ? productList: [] 32 | } 33 | }; 34 | var url = this.merchantPrefix + 'group/add?access_token=' + accessToken; 35 | return this.request(url, postJSON(data)); 36 | }; 37 | 38 | /** 39 | * 删除商品分组 40 | * 详细请看: 41 | * Examples: 42 | * ``` 43 | * api.deleteGoodsGroup(groupId); 44 | * ``` 45 | 46 | * Result: 47 | * ``` 48 | * { 49 | * "errcode": 0, 50 | * "errmsg": "success" 51 | * } 52 | * ``` 53 | * @param {String} groupId 分组ID 54 | */ 55 | exports.deleteGoodsGroup = async function (groupId) { 56 | const { accessToken } = await this.ensureAccessToken(); 57 | var data = { 58 | 'group_id': groupId 59 | }; 60 | var url = this.merchantPrefix + 'group/del?access_token=' + accessToken; 61 | return this.request(url, postJSON(data)); 62 | }; 63 | 64 | /** 65 | * 修改商品分组属性 66 | * 详细请看: 67 | * Examples: 68 | * ``` 69 | * api.updateGoodsGroup(groupId, groupName); 70 | * ``` 71 | 72 | * Result: 73 | * ``` 74 | * { 75 | * "errcode": 0, 76 | * "errmsg": "success" 77 | * } 78 | * ``` 79 | * @param {String} groupId 分组ID 80 | * @param {String} groupName 分组名 81 | */ 82 | exports.updateGoodsGroup = async function (groupId, groupName) { 83 | const { accessToken } = await this.ensureAccessToken(); 84 | var data = { 85 | 'group_id': groupId, 86 | 'group_name': groupName 87 | }; 88 | var url = this.merchantPrefix + 'group/propertymod?access_token=' + accessToken; 89 | return this.request(url, postJSON(data)); 90 | }; 91 | 92 | /** 93 | * 修改商品分组内的商品 94 | * 详细请看: 95 | * Examples: 96 | * ``` 97 | * api.updateGoodsForGroup(groupId, addProductList, delProductList); 98 | * ``` 99 | * Result: 100 | * ``` 101 | * { 102 | * "errcode": 0, 103 | * "errmsg": "success" 104 | * } 105 | * ``` 106 | * @param {Object} groupId 分组ID 107 | * @param {Array} addProductList 待添加的商品数组 108 | * @param {Array} delProductList 待删除的商品数组 109 | * @param {Function} callback 回调函数 110 | */ 111 | exports.updateGoodsForGroup = async function (groupId, addProductList, delProductList) { 112 | const { accessToken } = await this.ensureAccessToken(); 113 | var data = { 114 | 'group_id': groupId, 115 | 'product': [] 116 | }; 117 | 118 | if (addProductList && addProductList.length) { 119 | addProductList.forEach(function (val) { 120 | data.product.push({ 121 | 'product_id': val, 122 | 'mod_action': 1 123 | }); 124 | }); 125 | } 126 | 127 | if (delProductList && delProductList.length) { 128 | delProductList.forEach(function (val) { 129 | data.product.push({ 130 | 'product_id': val, 131 | 'mod_action': 0 132 | }); 133 | }); 134 | } 135 | 136 | var url = this.merchantPrefix + 'group/productmod?access_token=' + accessToken; 137 | return this.request(url, postJSON(data)); 138 | }; 139 | 140 | /** 141 | * 获取所有商品分组 142 | * 详细请看: 143 | * Examples: 144 | * ``` 145 | * api.getAllGroups(); 146 | * ``` 147 | 148 | * Result: 149 | * ``` 150 | * { 151 | * "errcode": 0, 152 | * "errmsg": "success" 153 | * "groups_detail": [ 154 | * { 155 | * "group_id": 200077549, 156 | * "group_name": "新品上架" 157 | * },{ 158 | * "group_id": 200079772, 159 | * "group_name": "全球热卖" 160 | * } 161 | * ] 162 | * } 163 | * ``` 164 | */ 165 | exports.getAllGroups = async function () { 166 | const { accessToken } = await this.ensureAccessToken(); 167 | var url = this.merchantPrefix + 'group/getall?access_token=' + accessToken; 168 | return this.request(url, {dataType: 'json'}); 169 | }; 170 | 171 | /** 172 | * 根据ID获取商品分组 173 | * 详细请看: 174 | * Examples: 175 | * ``` 176 | * api.getGroupById(groupId); 177 | * ``` 178 | 179 | * Result: 180 | * ``` 181 | * { 182 | * "errcode": 0, 183 | * "errmsg": "success" 184 | * "group_detail": { 185 | * "group_id": 200077549, 186 | * "group_name": "新品上架", 187 | * "product_list": [ 188 | * "pDF3iYzZoY-Budrzt8O6IxrwIJAA", 189 | * "pDF3iY3pnWSGJcO2MpS2Nxy3HWx8", 190 | * "pDF3iY33jNt0Dj3M3UqiGlUxGrio" 191 | * ] 192 | * } 193 | * } 194 | * ``` 195 | * @param {String} groupId 分组ID 196 | */ 197 | exports.getGroupById = async function (groupId) { 198 | const { accessToken } = await this.ensureAccessToken(); 199 | var data = { 200 | 'group_id': groupId 201 | }; 202 | var url = this.merchantPrefix + 'group/getbyid?access_token=' + accessToken; 203 | return this.request(url, postJSON(data)); 204 | }; 205 | -------------------------------------------------------------------------------- /lib/api_shop_order.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { postJSON } = require('./util'); 4 | 5 | /** 6 | * 根据订单Id获取订单详情 7 | * 详细请看: 8 | * Examples: 9 | * ``` 10 | * api.getOrderById(orderId); 11 | * ``` 12 | * Result: 13 | * ``` 14 | * { 15 | * "errcode": 0, 16 | * "errmsg": "success", 17 | * "order": { 18 | * "order_id": "7197417460812533543", 19 | * "order_status": 6, 20 | * "order_total_price": 6, 21 | * "order_create_time": 1394635817, 22 | * "order_express_price": 5, 23 | * "buyer_openid": "oDF3iY17NsDAW4UP2qzJXPsz1S9Q", 24 | * "buyer_nick": "likeacat", 25 | * "receiver_name": "张小猫", 26 | * "receiver_province": "广东省", 27 | * "receiver_city": "广州市", 28 | * "receiver_address": "华景路一号南方通信大厦5楼", 29 | * "receiver_mobile": "123456789", 30 | * "receiver_phone": "123456789", 31 | * "product_id": "pDF3iYx7KDQVGzB7kDg6Tge5OKFo", 32 | * "product_name": "安莉芳E-BRA专柜女士舒适内衣蕾丝3/4薄杯聚拢上托性感文胸KB0716", 33 | * "product_price": 1, 34 | * "product_sku": "10000983:10000995;10001007:10001010", 35 | * "product_count": 1, 36 | * "product_img": "http://img2.paipaiimg.com/00000000/item-52B87243-63CCF66C00000000040100003565C1EA.0.300x300.jpg", 37 | * "delivery_id": "1900659372473", 38 | * "delivery_company": "059Yunda", 39 | * "trans_id": "1900000109201404103172199813" 40 | * } 41 | * } 42 | * ``` 43 | * @param {String} orderId 订单Id 44 | */ 45 | exports.getOrderById = async function (orderId) { 46 | const { accessToken } = await this.ensureAccessToken(); 47 | var data = { 48 | 'order_id': orderId 49 | }; 50 | var url = this.merchantPrefix + 'order/getbyid?access_token=' + accessToken; 51 | return this.request(url, postJSON(data)); 52 | }; 53 | 54 | /** 55 | * 根据订单状态/创建时间获取订单详情 56 | * 详细请看: 57 | * Examples: 58 | * ``` 59 | * api.getOrdersByStatus([status,] [beginTime,] [endTime,]); 60 | * ``` 61 | * Usage: 62 | * 当只传入callback参数时,查询所有状态,所有时间的订单 63 | * 当传入一个参数,参数为Number类型,查询指定状态,所有时间的订单 64 | * 当传入一个参数,参数为Date类型,查询所有状态,指定订单创建起始时间的订单(待测试) 65 | * 当传入二个参数,第一参数为订单状态码,第二参数为订单创建起始时间 66 | * 当传入三个参数,第一参数为订单状态码,第二参数为订单创建起始时间,第三参数为订单创建终止时间 67 | * Result: 68 | * ``` 69 | * { 70 | * "errcode": 0, 71 | * "errmsg": "success", 72 | * "order_list": [ 73 | * { 74 | * "order_id": "7197417460812533543", 75 | * "order_status": 6, 76 | * "order_total_price": 6, 77 | * "order_create_time": 1394635817, 78 | * "order_express_price": 5, 79 | * "buyer_openid": "oDF3iY17NsDAW4UP2qzJXPsz1S9Q", 80 | * "buyer_nick": "likeacat", 81 | * "receiver_name": "张小猫", 82 | * "receiver_province": "广东省", 83 | * "receiver_city": "广州市", 84 | * "receiver_address": "华景路一号南方通信大厦5楼", 85 | * "receiver_mobile": "123456", 86 | * "receiver_phone": "123456", 87 | * "product_id": "pDF3iYx7KDQVGzB7kDg6Tge5OKFo", 88 | * "product_name": "安莉芳E-BRA专柜女士舒适内衣蕾丝3/4薄杯聚拢上托性感文胸KB0716", 89 | * "product_price": 1, 90 | * "product_sku": "10000983:10000995;10001007:10001010", 91 | * "product_count": 1, 92 | * "product_img": "http://mmbiz.qpic.cn/mmbiz/4whpV1VZl2icND8WwMThBEcehjhDv2icY4GrDSG5RLM3B2qd9kOicWGVJcsAhvXfibhWRNoGOvCfMC33G9z5yQr2Qw/0", 93 | * "delivery_id": "1900659372473", 94 | * "delivery_company": "059Yunda", 95 | * "trans_id": "1900000109201404103172199813" 96 | * }, 97 | * { 98 | * "order_id": "7197417460812533569", 99 | * "order_status": 8, 100 | * "order_total_price": 1, 101 | * "order_create_time": 1394636235, 102 | * "order_express_price": 0, 103 | * "buyer_openid": "oDF3iY17NsDAW4UP2qzJXPsz1S9Q", 104 | * "buyer_nick": "likeacat", 105 | * "receiver_name": "张小猫", 106 | * "receiver_province": "广东省", 107 | * "receiver_city": "广州市", 108 | * "receiver_address": "华景路一号南方通信大厦5楼", 109 | * "receiver_mobile": "123456", 110 | * "receiver_phone": "123456", 111 | * "product_id": "pDF3iYx7KDQVGzB7kDg6Tge5OKFo", 112 | * "product_name": "项坠333", 113 | * "product_price": 1, 114 | * "product_sku": "1075741873:1079742377", 115 | * "product_count": 1, 116 | * "product_img": "http://mmbiz.qpic.cn/mmbiz/4whpV1VZl2icND8WwMThBEcehjhDv2icY4GrDSG5RLM3B2qd9kOicWGVJcsAhvXfibhWRNoGOvCfMC33G9z5yQr2Qw/0", 117 | * "delivery_id": "1900659372473", 118 | * "delivery_company": "059Yunda", 119 | * "trans_id": "1900000109201404103172199813" 120 | * } 121 | * ] 122 | * } 123 | * ``` 124 | * @param {Number} status 状态码。(无此参数-全部状态, 2-待发货, 3-已发货, 5-已完成, 8-维权中) 125 | * @param {Date} beginTime 订单创建时间起始时间。(无此参数则不按照时间做筛选) 126 | * @param {Date} endTime 订单创建时间终止时间。(无此参数则不按照时间做筛选) 127 | */ 128 | exports.getOrdersByStatus = async function (status, beginTime, endTime) { 129 | const { accessToken } = await this.ensureAccessToken(); 130 | var data = {}; 131 | if (arguments.length === 1) { 132 | if (typeof status === 'number') { 133 | // (status) 134 | data.status = status; 135 | } else if (status instanceof Date) { 136 | data.begintime = Math.round(status.getTime() / 1000); 137 | data.endtime = Math.round(new Date().getTime() / 1000); 138 | } else { 139 | throw new Error('first parameter must be Number or Date'); 140 | } 141 | } else if (arguments.length === 2) { 142 | if (typeof status === 'number' && beginTime instanceof Date) { 143 | data.status = status; 144 | data.begintime = Math.round(beginTime.getTime() / 1000); 145 | data.endtime = Math.round(new Date().getTime() / 1000); 146 | } else { 147 | throw new Error('first parameter must be Number and second parameter must be Date'); 148 | } 149 | } else if (arguments.length === 3) { 150 | data.status = status; 151 | data.begintime = Math.round(beginTime.getTime() / 1000); 152 | data.endtime = Math.round(endTime.getTime() / 1000); 153 | } 154 | var url = this.merchantPrefix + 'order/getbyfilter?access_token=' + accessToken; 155 | return this.request(url, postJSON(data)); 156 | }; 157 | 158 | /** 159 | * 设置订单发货信息 160 | * 详细请看: 161 | * Examples: 162 | * ``` 163 | * api.setExpressForOrder(orderId, deliveryCompany, deliveryTrackNo, isOthers); 164 | * ``` 165 | * Result: 166 | * ``` 167 | * { 168 | * "errcode": 0, 169 | * "errmsg": "success" 170 | * } 171 | * ``` 172 | * @param {String} orderId 订单Id 173 | * @param {String} deliveryCompany 物流公司 (物流公司Id请参考微信小店API手册) 174 | * @param {String} deliveryTrackNo 运单Id 175 | * @param {Number} isOthers 是否为6.4.5表之外的其它物流公司(0-否,1-是,无该字段默认为不是其它物流公司) 176 | */ 177 | exports.setExpressForOrder = async function (orderId, deliveryCompany, deliveryTrackNo, isOthers) { 178 | const { accessToken } = await this.ensureAccessToken(); 179 | var data = { 180 | 'order_id': orderId, 181 | 'delivery_company': deliveryCompany, 182 | 'delivery_track_no': deliveryTrackNo 183 | }; 184 | if (typeof isOthers === 'undefined') { 185 | data.is_others = 0; 186 | } else { 187 | data.is_others = isOthers ? 1 : 0; 188 | } 189 | var url = this.merchantPrefix + 'order/setdelivery?access_token=' + accessToken; 190 | return this.request(url, postJSON(data)); 191 | }; 192 | 193 | /** 194 | * 设置订单发货信息-不需要物流配送 195 | * 适用于不需要实体物流配送的虚拟商品,完成本操作后订单即完成。 196 | * 详细请看: 197 | * Examples: 198 | * ``` 199 | * api.setNoDeliveryForOrder(orderId); 200 | * ``` 201 | * Result: 202 | * ``` 203 | * { 204 | * "errcode": 0, 205 | * "errmsg": "success" 206 | * } 207 | * ``` 208 | * @param {String} orderId 订单Id 209 | */ 210 | exports.setNoDeliveryForOrder = async function (orderId) { 211 | const { accessToken } = await this.ensureAccessToken(); 212 | var data = { 213 | 'order_id': orderId, 214 | 'need_delivery': 0 215 | }; 216 | var url = this.merchantPrefix + 'order/setdelivery?access_token=' + accessToken; 217 | return this.request(url, postJSON(data)); 218 | }; 219 | 220 | /** 221 | * 关闭订单 222 | * 详细请看: 223 | * Examples: 224 | * ``` 225 | * api.closeOrder(orderId); 226 | * ``` 227 | 228 | * Result: 229 | * ``` 230 | * { 231 | * "errcode": 0, 232 | * "errmsg": "success" 233 | * } 234 | * ``` 235 | * @param {String} orderId 订单Id 236 | */ 237 | exports.closeOrder = async function (orderId) { 238 | const { accessToken } = await this.ensureAccessToken(); 239 | var data = { 240 | 'order_id': orderId 241 | }; 242 | var url = this.merchantPrefix + 'order/close?access_token=' + accessToken; 243 | return this.request(url, postJSON(data)); 244 | }; 245 | -------------------------------------------------------------------------------- /lib/api_shop_shelf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { postJSON } = require('./util'); 4 | 5 | /** 6 | * 增加货架 7 | * 详细请看: 8 | * Examples: 9 | * ``` 10 | * api.createShelf(shelf); 11 | * ``` 12 | * Shelf: 13 | * ``` 14 | * { 15 | * "shelf_data": { 16 | * "module_infos": [ 17 | * { 18 | * "group_info": { 19 | * "filter": { 20 | * "count": 2 21 | * }, 22 | * "group_id": 50 23 | * }, 24 | * "eid": 1 25 | * }, 26 | * { 27 | * "group_infos": { 28 | * "groups": [ 29 | * { 30 | * "group_id": 49 31 | * }, 32 | * { 33 | * "group_id": 50 34 | * }, 35 | * { 36 | * "group_id": 51 37 | * } 38 | * ] 39 | * }, 40 | * "eid": 2 41 | * }, 42 | * { 43 | * "group_info": { 44 | * "group_id": 52, 45 | * "img": "http://mmbiz.qpic.cn/mmbiz/4whpV1VZl29nqqObBwFwnIX3licVPnFV5Jm64z4I0TTicv0TjN7Vl9bykUUibYKIOjicAwIt6Oy0Y6a1Rjp5Tos8tg/0" 46 | * }, 47 | * "eid": 3 48 | * }, 49 | * { 50 | * "group_infos": { 51 | * "groups": [ 52 | * { 53 | * "group_id": 49, 54 | * "img": "http://mmbiz.qpic.cn/mmbiz/4whpV1VZl29nqqObBwFwnIX3licVPnFV5uUQx7TLx4tB9qZfbe3JmqR4NkkEmpb5LUWoXF1ek9nga0IkeSSFZ8g/0" 55 | * }, 56 | * { 57 | * "group_id": 50, 58 | * "img": "http://mmbiz.qpic.cn/mmbiz/4whpV1VZl29nqqObBwFwnIX3licVPnFV5G1kdy3ViblHrR54gbCmbiaMnl5HpLGm5JFeENyO9FEZAy6mPypEpLibLA/0" 59 | * }, 60 | * { 61 | * "group_id": 52, 62 | * "img": "http://mmbiz.qpic.cn/mmbiz/4whpV1VZl29nqqObBwFwnIX3licVPnFV5uUQx7TLx4tB9qZfbe3JmqR4NkkEmpb5LUWoXF1ek9nga0IkeSSFZ8g/0" 63 | * } 64 | * ] 65 | * }, 66 | * "eid": 4 67 | * }, 68 | * { 69 | * "group_infos": { 70 | * "groups": [ 71 | * { 72 | * "group_id": 43 73 | * }, 74 | * { 75 | * "group_id": 44 76 | * }, 77 | * { 78 | * "group_id": 45 79 | * }, 80 | * { 81 | * "group_id": 46 82 | * } 83 | * ], 84 | * "img_background": "http://mmbiz.qpic.cn/mmbiz/4whpV1VZl29nqqObBwFwnIX3licVPnFV5uUQx7TLx4tB9qZfbe3JmqR4NkkEmpb5LUWoXF1ek9nga0IkeSSFZ8g/0" 85 | * }, 86 | * "eid": 5 87 | * } 88 | * ] 89 | * }, 90 | * "shelf_banner": "http://mmbiz.qpic.cn/mmbiz/4whpV1VZl2ibrWQn8zWFUh1YznsMV0XEiavFfLzDWYyvQOBBszXlMaiabGWzz5B2KhNn2IDemHa3iarmCyribYlZYyw/0", 91 | * "shelf_name": "测试货架" 92 | * } 93 | * ``` 94 | * Result: 95 | * ``` 96 | * { 97 | * "errcode": 0, 98 | * "errmsg": "success", 99 | * "shelf_id": 12 100 | * } 101 | * ``` 102 | * @param {Object} shelf 货架信息 103 | */ 104 | exports.createShelf = async function (shelf) { 105 | const { accessToken } = await this.ensureAccessToken(); 106 | var url = this.merchantPrefix + 'shelf/add?access_token=' + accessToken; 107 | return this.request(url, postJSON(shelf)); 108 | }; 109 | 110 | /** 111 | * 删除货架 112 | * 详细请看: 113 | * Examples: 114 | * ``` 115 | * api.deleteShelf(shelfId); 116 | * ``` 117 | 118 | * Result: 119 | * ``` 120 | * { 121 | * "errcode": 0, 122 | * "errmsg": "success" 123 | * } 124 | * ``` 125 | * @param {String} shelfId 货架Id 126 | */ 127 | exports.deleteShelf = async function (shelfId) { 128 | const { accessToken } = await this.ensureAccessToken(); 129 | var data = { 130 | 'shelf_id': shelfId 131 | }; 132 | var url = this.merchantPrefix + 'shelf/del?access_token=' + accessToken; 133 | return this.request(url, postJSON(data)); 134 | }; 135 | 136 | /** 137 | * 修改货架 138 | * 详细请看: 139 | * Examples: 140 | * ``` 141 | * api.updateShelf(shelf); 142 | * ``` 143 | * Shelf: 144 | * ``` 145 | * { 146 | * "shelf_id": 12345, 147 | * "shelf_data": ..., 148 | * "shelf_banner": "http://mmbiz.qpic.cn/mmbiz/ 4whpV1VZl2ibrWQn8zWFUh1YznsMV0XEiavFfLzDWYyvQOBBszXlMaiabGWzz5B2K hNn2IDemHa3iarmCyribYlZYyw/0", 149 | * "shelf_name": "货架名称" 150 | * } 151 | * ``` 152 | 153 | * Result: 154 | * ``` 155 | * { 156 | * "errcode": 0, 157 | * "errmsg": "success" 158 | * } 159 | * ``` 160 | * @param {Object} shelf 货架信息 161 | */ 162 | exports.updateShelf = async function (shelf) { 163 | const { accessToken } = await this.ensureAccessToken(); 164 | var url = this.merchantPrefix + 'shelf/mod?access_token=' + accessToken; 165 | return this.request(url, postJSON(shelf)); 166 | }; 167 | 168 | /** 169 | * 获取所有货架 170 | * 详细请看: 171 | * Examples: 172 | * ``` 173 | * api.getAllShelf(); 174 | * ``` 175 | 176 | * Result: 177 | * ``` 178 | * { 179 | * "errcode": 0, 180 | * "errmsg": "success", 181 | * "shelves": [ 182 | * { 183 | * "shelf_info": { 184 | * "module_infos": [ 185 | * { 186 | * "group_infos": { 187 | * "groups": [ 188 | * { 189 | * "group_id": 200080093 190 | * }, 191 | * { 192 | * "group_id": 200080118 193 | * }, 194 | * { 195 | * "group_id": 200080119 196 | * }, 197 | * { 198 | * "group_id": 200080135 199 | * } 200 | * ], 201 | * "img_background": "http://mmbiz.qpic.cn/mmbiz/4whpV1VZl294FzPwnf9dAcaN7ButStztAZyy2yHY8pW6sTQKicIhAy5F0a2CqmrvDBjMFLtc2aEhAQ7uHsPow9A/0" 202 | * }, 203 | * "eid": 5 204 | * } 205 | * ] 206 | * }, 207 | * "shelf_banner": "http://mmbiz.qpic.cn/mmbiz/4whpV1VZl294FzPwnf9dAcaN7ButStztAZyy2yHY8pW6sTQKicIhAy5F0a2CqmrvDBjMFLtc2aEhAQ7uHsPow9A/0", 208 | * "shelf_name": "新新人类", 209 | * "shelf_id": 22 210 | * }, 211 | * { 212 | * "shelf_info": { 213 | * "module_infos": [ 214 | * { 215 | * "group_info": { 216 | * "group_id": 200080119, 217 | * "filter": { 218 | * "count": 4 219 | * } 220 | * }, 221 | * "eid": 1 222 | * } 223 | * ] 224 | * }, 225 | * "shelf_banner": "http://mmbiz.qpic.cn/mmbiz/4whpV1VZl294FzPwnf9dAcaN7ButStztAZyy2yHY8pW6sTQKicIhAy5F0a2CqmrvDBjMFLtc2aEhAQ7uHsPow9A/0", 226 | * "shelf_name": "店铺", 227 | * "shelf_id": 23 228 | * } 229 | * ] 230 | * } 231 | * ``` 232 | */ 233 | exports.getAllShelves = async function () { 234 | const { accessToken } = await this.ensureAccessToken(); 235 | var url = this.merchantPrefix + 'shelf/getall?access_token=' + accessToken; 236 | return this.request(url, {dataType: 'json'}); 237 | }; 238 | 239 | /** 240 | * 根据货架ID获取货架信息 241 | * 详细请看: 242 | * Examples: 243 | * ``` 244 | * api.getShelfById(shelfId); 245 | * ``` 246 | 247 | * Result: 248 | * ``` 249 | * { 250 | * "errcode": 0, 251 | * "errmsg": "success", 252 | * "shelf_info": { 253 | * "module_infos": [...] 254 | * }, 255 | * "shelf_banner": "http://mmbiz.qpic.cn/mmbiz/4whpV1VZl2ibp2DgDXiaic6WdflMpNdInS8qUia2BztlPu1gPlCDLZXEjia2qBdjoLiaCGUno9zbs1UyoqnaTJJGeEew/0", 256 | * "shelf_name": "新建货架", 257 | * "shelf_id": 97 258 | * } 259 | * ``` 260 | * @param {String} shelfId 货架Id 261 | */ 262 | exports.getShelfById = async function (shelfId) { 263 | const { accessToken } = await this.ensureAccessToken(); 264 | var data = { 265 | 'shelf_id': shelfId 266 | }; 267 | var url = this.merchantPrefix + 'shelf/getbyid?access_token=' + accessToken; 268 | return this.request(url, postJSON(data)); 269 | }; 270 | -------------------------------------------------------------------------------- /lib/api_shop_stock.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // 库存管理接口 4 | const { postJSON } = require('./util'); 5 | 6 | /** 7 | * 增加库存 8 | * 详细请看: 9 | * Examples: 10 | * ``` 11 | * api.updateStock(10, productId, sku); // 增加10件库存 12 | * api.updateStock(-10, productId, sku); // 减少10件库存 13 | * ``` 14 | 15 | * Result: 16 | * ``` 17 | * { 18 | * "errcode": 0, 19 | * "errmsg": "success" 20 | * } 21 | * ``` 22 | * @param {Number} number 增加或者删除的数量 23 | * @param {String} productId 商品ID 24 | * @param {String} sku SKU信息 25 | */ 26 | exports.updateStock = async function (number, productId, sku) { 27 | const { accessToken } = await this.ensureAccessToken(); 28 | var url; 29 | if (number > 0) { 30 | url = this.merchantPrefix + 'stock/add?access_token=' + accessToken; 31 | } else { 32 | url = this.merchantPrefix + 'stock/reduce?access_token=' + accessToken; 33 | } 34 | var data = { 35 | 'product_id': productId, 36 | 'sku_info': sku, 37 | 'quantity': Math.abs(number) 38 | }; 39 | return this.request(url, postJSON(data)); 40 | }; 41 | -------------------------------------------------------------------------------- /lib/api_subscribe_message.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { postJSON, getData } = require('./util'); 4 | 5 | 6 | /** 7 | * 发送订阅消息 8 | * Examples: 9 | * ``` 10 | * var templateId: '模板id'; 11 | * var page: ''; 12 | * var data = { 13 | character_string1: { value: '223' }, 14 | thing5: { value: '测试商品' }, 15 | date3: { value: '2020年1月1日' }, 16 | number6: { value: '2342375986' }, 17 | character_string9: { value: 'sf686539' }, 18 | }, 19 | * api.sendSubscribeMessage('openid', templateId, page, data); 20 | * ``` 21 | * @param {String} openid 用户的openid 22 | * @param {String} templateId 模板ID 23 | * @param {String} page 点击模板卡片后的跳转页面,仅限本小程序内的页面。支持带参数,(示例index?foo=bar)。该字段不填则模板无跳转。 24 | * @param {Object} data 渲染模板的数据 25 | */ 26 | exports.sendSubscribeMessage = async function (openid, templateId, page, data, ) { 27 | const { accessToken } = await this.ensureAccessToken(); 28 | var apiUrl = this.prefix + 'message/subscribe/send?access_token=' + accessToken; 29 | var template = { 30 | touser: openid, 31 | template_id: templateId, 32 | page: page, 33 | data: data 34 | }; 35 | return this.request(apiUrl, postJSON(template)); 36 | }; 37 | 38 | -------------------------------------------------------------------------------- /lib/api_template.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { postJSON, getData } = require('./util'); 4 | 5 | /** 6 | * 设置所属行业 7 | * Examples: 8 | * ``` 9 | * var industryIds = { 10 | * "industry_id1":'1', 11 | * "industry_id2":"4" 12 | * }; 13 | * api.setIndustry(industryIds); 14 | * ``` 15 | * @param {Object} industryIds 公众号模板消息所属行业编号 16 | */ 17 | exports.setIndustry = async function (industryIds) { 18 | const { accessToken } = await this.ensureAccessToken(); 19 | var apiUrl = this.prefix + 'template/api_set_industry?access_token=' + accessToken; 20 | return this.request(apiUrl, postJSON(industryIds)); 21 | }; 22 | 23 | /** 24 | * 获取设置的行业信息 25 | * Examples: 26 | * ``` 27 | * api.getIndustry(callback); 28 | * ``` 29 | * Callback: 30 | * 31 | * - `err`, 调用失败时得到的异常 32 | * - `result`, 调用正常时得到的对象 33 | * 34 | * Result: 35 | * ``` 36 | * // 结果示例 37 | * { 38 | * "primary_industry":{"first_class":"运输与仓储","second_class":"快递"}, 39 | * "secondary_industry":{"first_class":"IT科技","second_class":"互联网|电子商务"} 40 | * } 41 | * ``` 42 | */ 43 | exports.getIndustry = async function(){ 44 | const { accessToken } = await this.ensureAccessToken(); 45 | var apiUrl = this.prefix + 'template/get_industry?access_token=' + accessToken; 46 | return this.request(apiUrl, getData()); 47 | }; 48 | 49 | /** 50 | * 获得模板ID 51 | * Examples: 52 | * ``` 53 | * var templateIdShort = 'TM00015'; 54 | * api.addTemplate(templateIdShort); 55 | * ``` 56 | * @param {String} templateIdShort 模板库中模板的编号,有“TM**”和“OPENTMTM**”等形式 57 | */ 58 | exports.addTemplate = async function (templateIdShort) { 59 | const { accessToken } = await this.ensureAccessToken(); 60 | var apiUrl = this.prefix + 'template/api_add_template?access_token=' + accessToken; 61 | var templateId = { 62 | template_id_short: templateIdShort 63 | }; 64 | return this.request(apiUrl, postJSON(templateId)); 65 | }; 66 | 67 | /** 68 | * 获取模板列表 69 | * Examples: 70 | * ``` 71 | * api.getAllPrivateTemplate(callback); 72 | * ``` 73 | * Callback: 74 | * 75 | * - `err`, 调用失败时得到的异常 76 | * - `result`, 调用正常时得到的对象 77 | * 78 | * Result: 79 | * ``` 80 | * // 结果示例 81 | * { 82 | * "template_list": [{ 83 | * "template_id": "iPk5sOIt5X_flOVKn5GrTFpncEYTojx6ddbt8WYoV5s", 84 | * "title": "领取奖金提醒", 85 | * "primary_industry": "IT科技", 86 | * "deputy_industry": "互联网|电子商务", 87 | * "content": "{ {result.DATA} }\n\n领奖金额:{ {withdrawMoney.DATA} }\n领奖 时间:{ {withdrawTime.DATA} }\n银行信息:{ {cardInfo.DATA} }\n到账时间: { {arrivedTime.DATA} }\n{ {remark.DATA} }", 88 | * "example": "您已提交领奖申请\n\n领奖金额:xxxx元\n领奖时间:2013-10-10 12:22:22\n银行信息:xx银行(尾号xxxx)\n到账时间:预计xxxxxxx\n\n预计将于xxxx到达您的银行卡" 89 | * }] 90 | * } 91 | * ``` 92 | */ 93 | exports.getAllPrivateTemplate = async function(callback){ 94 | // https://api.weixin.qq.com/cgi-bin/template/get_all_private_template?access_token=ACCESS_TOKEN 95 | const { accessToken } = await this.ensureAccessToken(); 96 | var apiUrl = this.prefix + 'template/get_all_private_template?access_token=' + accessToken; 97 | return this.request(apiUrl, getData()); 98 | }; 99 | 100 | /** 101 | * 发送模板消息 102 | * Examples: 103 | * ``` 104 | * var templateId: '模板id'; 105 | * // URL置空,则在发送后,点击模板消息会进入一个空白页面(ios), 或无法点击(android) 106 | * var url: 'http://weixin.qq.com/download'; 107 | * var topcolor = '#FF0000'; // 顶部颜色 108 | * var data = { 109 | * user:{ 110 | * "value":'黄先生', 111 | * "color":"#173177" 112 | * } 113 | * }; 114 | * api.sendTemplate('openid', templateId, url, topColor, data); 115 | * ``` 116 | * @param {String} openid 用户的openid 117 | * @param {String} templateId 模板ID 118 | * @param {String} url URL置空,则在发送后,点击模板消息会进入一个空白页面(ios),或无法点击(android) 119 | * @param {String} topColor 字体颜色 120 | * @param {Object} data 渲染模板的数据 121 | * @param {Object} miniprogram 跳转小程序所需数据 {appid, pagepath} 122 | */ 123 | exports.sendTemplate = async function (openid, templateId, url, topColor, data, miniprogram) { 124 | const { accessToken } = await this.ensureAccessToken(); 125 | var apiUrl = this.prefix + 'message/template/send?access_token=' + accessToken; 126 | var template = { 127 | touser: openid, 128 | template_id: templateId, 129 | url: url, 130 | miniprogram: miniprogram, 131 | color: topColor, 132 | data: data 133 | }; 134 | return this.request(apiUrl, postJSON(template)); 135 | }; 136 | 137 | /** 138 | * 删除模板 139 | * Examples: 140 | * ``` 141 | * var templateId = ”Dyvp3-Ff0cnail_CDSzk1fIc6-9lOkxsQE7exTJbwUE” 142 | * api.delPrivateTemplate(templateId, callback); 143 | * ``` 144 | * Callback: 145 | * 146 | * - `err`, 调用失败时得到的异常 147 | * - `result`, 调用正常时得到的对象 148 | * 149 | * @param {String} templateId 公众帐号下模板消息ID 150 | */ 151 | exports.delPrivateTemplate = async function(templateId){ 152 | const { accessToken } = await this.ensureAccessToken(); 153 | var apiUrl = this.prefix + 'template/del_private_template?access_token=' + accessToken; 154 | var templateIdData = { 155 | template_id: templateId 156 | }; 157 | return this.request(apiUrl, postJSON(templateIdData)); 158 | }; 159 | 160 | /** 161 | * 发送模板消息支持小程序 162 | * Examples: 163 | * ``` 164 | * var templateId = '模板id'; 165 | * var page = 'index?foo=bar'; // 小程序页面路径 166 | * var formId = '提交表单id'; 167 | * var color = '#FF0000'; // 字体颜色 168 | * var data = { 169 | * keyword1: { 170 | * "value":'黄先生', 171 | * "color":"#173177" 172 | * } 173 | * var emphasisKeyword = 'keyword1.DATA' 174 | * }; 175 | * api.sendMiniProgramTemplate('openid', templateId, page, formId, data, color, emphasisKeyword); 176 | * ``` 177 | * @param {String} openid 接收者(用户)的 openid 178 | * @param {String} templateId 所需下发的模板消息的id 179 | * @param {String} page 点击模板卡片后的跳转页面,仅限本小程序内的页面。支持带参数,(示例index?foo=bar)。该字段不填则模板无跳转 180 | * @param {String} formId 表单提交场景下,为 submit 事件带上的 formId;支付场景下,为本次支付的 prepay_id 181 | * @param {Object} data 模板内容,不填则下发空模板 182 | * @param {String} color 模板内容字体的颜色,不填默认黑色 【废弃】 183 | * @param {String} emphasisKeyword 模板需要放大的关键词,不填则默认无放大 184 | */ 185 | exports.sendMiniProgramTemplate = async function (openid, templateId, page, formId, data, color, emphasisKeyword) { 186 | const { accessToken } = await this.ensureAccessToken(); 187 | var apiUrl = this.prefix + 'message/wxopen/template/send?access_token=' + accessToken; 188 | var template = { 189 | touser: openid, 190 | template_id: templateId, 191 | page, 192 | form_id: formId, 193 | data: data, 194 | color: color, 195 | emphasis_keyword: emphasisKeyword 196 | }; 197 | return this.request(apiUrl, postJSON(template)); 198 | }; 199 | 200 | /** 201 | * 通过API推送订阅模板消息给到授权微信用户 202 | * Examples: 203 | * ``` 204 | * var templateId = '模板id'; 205 | * var url = '点击消息跳转的链接,需要有ICP备案'; 206 | * var scene = '订阅场景值'; 207 | * var miniprogram = { 208 | * appid:'', 209 | * pagepath:'', 210 | * } 211 | * var data = { 212 | * keyword1: { 213 | * "value":'黄先生', 214 | * "color":"#173177" 215 | * } 216 | * }; 217 | * var title = '消息标题,15字以内' 218 | * api.sendSubscribe('openid', templateId, url, miniprogram, scene, title, data); 219 | * ``` 220 | * 221 | * @param {String} openid 接收者(用户)的 openid 222 | * @param {String} templateId 所需下发的模板消息的id 223 | * @param {String} url 点击消息跳转的链接,需要有ICP备案 224 | * @param {Object} miniprogram 跳小程序所需数据,不需跳小程序可不用传该数据 225 | * @param {String} scene 订阅场景值 226 | * @param {String} title 消息标题,15字以内 227 | * @param {Object} data 消息正文,value为消息内容文本(200字以内),没有固定格式,可用\n换行,color为整段消息内容的字体颜色(目前仅支持整段消息为一种颜色) 228 | */ 229 | exports.sendSubscribe = async function (openid, templateId, url, miniprogram, scene, title, data) { 230 | const { accessToken } = await this.ensureAccessToken(); 231 | var apiUrl = this.prefix + 'message/template/subscribe?access_token=' + accessToken; 232 | var template = { 233 | touser: openid, 234 | template_id: templateId, 235 | url, 236 | miniprogram, 237 | scene, 238 | title, 239 | data, 240 | }; 241 | return this.request(apiUrl, postJSON(template)); 242 | } 243 | -------------------------------------------------------------------------------- /lib/api_url.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { postJSON } = require('./util'); 4 | 5 | /** 6 | * 短网址服务 7 | * 详细细节 http://mp.weixin.qq.com/wiki/index.php?title=长链接转短链接接口 8 | * Examples: 9 | * ``` 10 | * api.shorturl('http://mp.weixin.com'); 11 | * ``` 12 | 13 | * @param {String} longUrl 需要转换的长链接,支持http://、https://、weixin://wxpay格式的url 14 | */ 15 | exports.shorturl = async function (longUrl) { 16 | const { accessToken } = await this.ensureAccessToken(); 17 | // https://api.weixin.qq.com/cgi-bin/shorturl?access_token=ACCESS_TOKEN 18 | var url = this.prefix + 'shorturl?access_token=' + accessToken; 19 | var data = { 20 | 'action': 'long2short', 21 | 'long_url': longUrl 22 | }; 23 | return this.request(url, postJSON(data)); 24 | }; 25 | -------------------------------------------------------------------------------- /lib/api_user.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { postJSON } = require('./util'); 4 | 5 | /** 6 | * 获取用户基本信息。可以设置lang,其中zh_CN 简体,zh_TW 繁体,en 英语。默认为en 7 | * 详情请见: 8 | * Examples: 9 | * ``` 10 | * api.getUser(openid); 11 | * api.getUser({openid: 'openid', lang: 'en'}); 12 | * ``` 13 | * 14 | * Result: 15 | * ``` 16 | * { 17 | * "subscribe": 1, 18 | * "openid": "o6_bmjrPTlm6_2sgVt7hMZOPfL2M", 19 | * "nickname": "Band", 20 | * "sex": 1, 21 | * "language": "zh_CN", 22 | * "city": "广州", 23 | * "province": "广东", 24 | * "country": "中国", 25 | * "headimgurl": "http://wx.qlogo.cn/mmopen/g3MonUZtNHkdmzicIlibx6iaFqAc56vxLSUfpb6n5WKSYVY0ChQKkiaJSgQ1dZuTOgvLLrhJbERQQ4eMsv84eavHiaiceqxibJxCfHe/0", 26 | * "subscribe_time": 1382694957 27 | * } 28 | * ``` 29 | * @param {String|Object} options 用户的openid。或者配置选项,包含openid和lang两个属性。 30 | */ 31 | exports.getUser = async function (options) { 32 | const { accessToken } = await this.ensureAccessToken(); 33 | if (typeof options !== 'object') { 34 | options = { 35 | openid: options, 36 | lang: 'en' 37 | }; 38 | } 39 | // https://api.weixin.qq.com/cgi-bin/user/info?access_token=ACCESS_TOKEN&openid=OPENID 40 | var url = this.prefix + 'user/info?openid=' + options.openid + '&lang=' + options.lang + '&access_token=' + accessToken; 41 | return this.request(url, {dataType: 'json'}); 42 | }; 43 | 44 | /** 45 | * 批量获取用户基本信息 46 | * Example: 47 | * ``` 48 | * api.batchGetUsers(['openid1', 'openid2']) 49 | * api.batchGetUsers(['openid1', 'openid2'], 'en') 50 | * ``` 51 | 52 | * Result: 53 | * ``` 54 | * { 55 | * "user_info_list": [{ 56 | * "subscribe": 1, 57 | * "openid": "otvxTs4dckWG7imySrJd6jSi0CWE", 58 | * "nickname": "iWithery", 59 | * "sex": 1, 60 | * "language": "zh_CN", 61 | * "city": "Jieyang", 62 | * "province": "Guangdong", 63 | * "country": "China", 64 | * "headimgurl": "http://wx.qlogo.cn/mmopen/xbIQx1GRqdvyqkMMhEaGOX802l1CyqMJNgUzKP8MeAeHFicRDSnZH7FY4XB7p8XHXIf6uJA2SCunTPicGKezDC4saKISzRj3nz/0", 65 | * "subscribe_time": 1434093047, 66 | * "unionid": "oR5GjjgEhCMJFyzaVZdrxZ2zRRF4", 67 | * "remark": "", 68 | * "groupid": 0 69 | * }, { 70 | * "subscribe": 0, 71 | * "openid": "otvxTs_JZ6SEiP0imdhpi50fuSZg", 72 | * "unionid": "oR5GjjjrbqBZbrnPwwmSxFukE41U", 73 | * }] 74 | * } 75 | * ``` 76 | * @param {Array} openids 用户的openid数组。 77 | * @param {String} lang 语言(zh_CN, zh_TW, en),默认简体中文(zh_CN) 78 | */ 79 | exports.batchGetUsers = async function (openids, lang = 'zh_CN') { 80 | const { accessToken } = await this.ensureAccessToken(); 81 | var url = this.prefix + 'user/info/batchget?access_token=' + accessToken; 82 | var data = {}; 83 | data.user_list = openids.map(function (openid) { 84 | return {openid, lang}; 85 | }); 86 | return this.request(url, postJSON(data)); 87 | }; 88 | 89 | /** 90 | * 获取关注者列表 91 | * 详细细节 http://mp.weixin.qq.com/wiki/index.php?title=获取关注者列表 92 | * Examples: 93 | * ``` 94 | * api.getFollowers(); 95 | * // or 96 | * api.getFollowers(nextOpenid); 97 | * ``` 98 | 99 | * Result: 100 | * ``` 101 | * { 102 | * "total":2, 103 | * "count":2, 104 | * "data":{ 105 | * "openid":["","OPENID1","OPENID2"] 106 | * }, 107 | * "next_openid":"NEXT_OPENID" 108 | * } 109 | * ``` 110 | * @param {String} nextOpenid 调用一次之后,传递回来的nextOpenid。第一次获取时可不填 111 | */ 112 | exports.getFollowers = async function (nextOpenid) { 113 | const { accessToken } = await this.ensureAccessToken(); 114 | // https://api.weixin.qq.com/cgi-bin/user/get?access_token=ACCESS_TOKEN&next_openid=NEXT_OPENID 115 | nextOpenid = nextOpenid || ''; 116 | var url = this.prefix + 'user/get?next_openid=' + nextOpenid + '&access_token=' + accessToken; 117 | return this.request(url, {dataType: 'json'}); 118 | }; 119 | 120 | /** 121 | * 设置用户备注名 122 | * 详细细节 http://mp.weixin.qq.com/wiki/index.php?title=设置用户备注名接口 123 | * Examples: 124 | * ``` 125 | * api.updateRemark(openid, remark); 126 | * ``` 127 | 128 | * Result: 129 | * ``` 130 | * { 131 | * "errcode":0, 132 | * "errmsg":"ok" 133 | * } 134 | * ``` 135 | * @param {String} openid 用户的openid 136 | * @param {String} remark 新的备注名,长度必须小于30字符 137 | */ 138 | exports.updateRemark = async function (openid, remark) { 139 | const { accessToken } = await this.ensureAccessToken(); 140 | // https://api.weixin.qq.com/cgi-bin/user/info/updateremark?access_token=ACCESS_TOKEN 141 | var url = this.prefix + 'user/info/updateremark?access_token=' + accessToken; 142 | var data = { 143 | openid: openid, 144 | remark: remark 145 | }; 146 | return this.request(url, postJSON(data)); 147 | }; 148 | 149 | /** 150 | * 创建标签 151 | * 详细细节 https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140837&token=&lang=zh_CN 152 | * Examples: 153 | * ``` 154 | * api.createTags(name); 155 | * ``` 156 | 157 | * Result: 158 | * ``` 159 | * { 160 | * "id":tagId, 161 | * "name":tagName 162 | * } 163 | * ``` 164 | * @param {String} name 标签名 165 | */ 166 | exports.createTags = async function (name){ 167 | const { accessToken } = await this.ensureAccessToken(); 168 | // https://api.weixin.qq.com/cgi-bin/tags/create?access_token=ACCESS_TOKEN 169 | var url = this.prefix + 'tags/create?access_token=' + accessToken; 170 | var data = { 171 | tag: { 172 | name: name 173 | } 174 | }; 175 | return this.request(url, postJSON(data)); 176 | }; 177 | 178 | /** 179 | * 获取公众号已创建的标签 180 | * 详细细节 https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140837&token=&lang=zh_CN 181 | * Examples: 182 | * ``` 183 | * api.getTags(); 184 | * ``` 185 | 186 | * Result: 187 | * ``` 188 | * { 189 | * "tags":[{ 190 | * "id":1, 191 | * "name":"每天一罐可乐星人", 192 | * "count":0 //此标签下粉丝数 193 | * },{ 194 | * "id":2, 195 | * "name":"星标组", 196 | * "count":0 197 | * },{ 198 | * "id":127, 199 | * "name":"广东", 200 | * "count":5 201 | * } 202 | * ] 203 | * } 204 | * ``` 205 | */ 206 | exports.getTags = async function (){ 207 | const { accessToken } = await this.ensureAccessToken(); 208 | // https://api.weixin.qq.com/cgi-bin/tags/get?access_token=ACCESS_TOKEN 209 | var url = this.prefix + 'tags/get?access_token=' + accessToken; 210 | return this.request(url); 211 | }; 212 | 213 | /** 214 | * 编辑标签 215 | * 详细细节 https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140837&token=&lang=zh_CN 216 | * Examples: 217 | * ``` 218 | * api.updateTag(id,name); 219 | * ``` 220 | 221 | * Result: 222 | * ``` 223 | * { 224 | * "errcode":0, 225 | * "errmsg":"ok" 226 | * } 227 | * ``` 228 | * @param {String} id 标签id 229 | * @param {String} name 标签名 230 | */ 231 | exports.updateTag = async function (id, name) { 232 | const { accessToken } = await this.ensureAccessToken(); 233 | // https://api.weixin.qq.com/cgi-bin/tags/update?access_token=ACCESS_TOKEN 234 | var url = this.prefix + 'tags/update?access_token=' + accessToken; 235 | var data = { 236 | tag: { 237 | id: id, 238 | name: name 239 | } 240 | }; 241 | return this.request(url, postJSON(data)); 242 | }; 243 | 244 | /** 245 | * 删除标签 246 | * 详细细节 https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140837&token=&lang=zh_CN 247 | * Examples: 248 | * ``` 249 | * api.deleteTag(id); 250 | * ``` 251 | 252 | * Result: 253 | * ``` 254 | * { 255 | * "errcode":0, 256 | * "errmsg":"ok" 257 | * } 258 | * ``` 259 | * @param {String} id 标签id 260 | */ 261 | exports.deleteTag = async function (id){ 262 | const { accessToken } = await this.ensureAccessToken(); 263 | // https://api.weixin.qq.com/cgi-bin/tags/delete?access_token=ACCESS_TOKEN 264 | var url = this.prefix + 'tags/delete?access_token=' + accessToken; 265 | var data = { 266 | tag: { 267 | id: id 268 | } 269 | }; 270 | return this.request(url, postJSON(data)); 271 | }; 272 | 273 | /** 274 | * 获取标签下粉丝列表 275 | * 详细细节 https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140837&token=&lang=zh_CN 276 | * Examples: 277 | * ``` 278 | * api.getUsersFromTag(tagId,nextOpenId); 279 | * ``` 280 | 281 | * Result: 282 | * ``` 283 | * { 284 | * "count":2,//这次获取的粉丝数量 285 | * "data":{//粉丝列表 286 | * "openid":[ 287 | * "ocYxcuAEy30bX0NXmGn4ypqx3tI0", 288 | * "ocYxcuBt0mRugKZ7tGAHPnUaOW7Y" 289 | * ] 290 | * }, 291 | * ``` 292 | * @param {String} tagId 标签id 293 | * @param {String} nextOpenId 第一个拉取的OPENID,不填默认从头开始拉取 294 | */ 295 | exports.getUsersFromTag = async function (tagId, nextOpenId){ 296 | const { accessToken } = await this.ensureAccessToken(); 297 | // https://api.weixin.qq.com/cgi-bin/user/tag/get?access_token=ACCESS_TOKEN 298 | var url = this.prefix + 'user/tag/get?access_token=' + accessToken; 299 | var data = { 300 | tagid: tagId, 301 | next_openid: nextOpenId || ''//第一个拉取的OPENID,不填默认从头开始拉取 302 | }; 303 | return this.request(url,postJSON(data)); 304 | }; 305 | 306 | /** 307 | * 批量为用户打标签 308 | * 详细细节 https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140837&token=&lang=zh_CN 309 | * Examples: 310 | * ``` 311 | * api.batchTagging(openIdList,tagId); 312 | * ``` 313 | 314 | * Result: 315 | * ``` 316 | * { 317 | * "errcode":0, 318 | * "errmsg":"ok" 319 | * } 320 | * ``` 321 | * @param {Array} openIdList openId列表 322 | * @param {String} tagId 标签id 323 | */ 324 | exports.batchTagging = async function (openIdList, tagId) { 325 | const { accessToken } = await this.ensureAccessToken(); 326 | // https://api.weixin.qq.com/cgi-bin/tags/members/batchtagging?access_token=ACCESS_TOKEN 327 | var url = this.prefix + 'tags/members/batchtagging?access_token=' + accessToken; 328 | var data = { 329 | openid_list: openIdList || [], 330 | tagid: tagId 331 | }; 332 | return this.request(url,postJSON(data)); 333 | }; 334 | 335 | /** 336 | * 批量为用户取消标签 337 | * 详细细节 https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140837&token=&lang=zh_CN 338 | * Examples: 339 | * ``` 340 | * api.batchUnTagging(openIdList,tagId); 341 | * ``` 342 | 343 | * Result: 344 | * ``` 345 | * { 346 | * "errcode":0, 347 | * "errmsg":"ok" 348 | * } 349 | * ``` 350 | * @param {Array} openIdList openId列表 351 | * @param {String} tagId 标签id 352 | */ 353 | exports.batchUnTagging = async function (openIdList, tagId) { 354 | const { accessToken } = await this.ensureAccessToken(); 355 | // https://api.weixin.qq.com/cgi-bin/tags/members/batchuntagging?access_token=ACCESS_TOKEN 356 | var url = this.prefix + 'tags/members/batchuntagging?access_token=' + accessToken; 357 | var data = { 358 | openid_list: openIdList || [], 359 | tagid: tagId 360 | }; 361 | return this.request(url,postJSON(data)); 362 | }; 363 | 364 | /** 365 | * 获取用户身上的标签列表 366 | * 详细细节 https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140837&token=&lang=zh_CN 367 | * Examples: 368 | * ``` 369 | * api.getIdList(openId); 370 | * ``` 371 | 372 | * Result: 373 | * ``` 374 | * { 375 | * "tagid_list":[//被置上的标签列表 134,2] 376 | * } 377 | * ``` 378 | */ 379 | exports.getIdList = async function (openId) { 380 | const { accessToken } = await this.ensureAccessToken(); 381 | // https://api.weixin.qq.com/cgi-bin/tags/getidlist?access_token=ACCESS_TOKEN 382 | var url = this.prefix + 'tags/getidlist?access_token=' + accessToken; 383 | var data = { 384 | openid: openId 385 | }; 386 | return this.request(url, postJSON(data)); 387 | }; 388 | -------------------------------------------------------------------------------- /lib/api_wxacode.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { postJSON } = require('./util'); 4 | 5 | /** 6 | * 获取小程序二维码,适用于需要的码数量较少的业务场景 7 | * https://developers.weixin.qq.com/miniprogram/dev/api/createWXAQRCode.html 8 | * Examples: 9 | * ``` 10 | * var path = 'index?foo=bar'; // 小程序页面路径 11 | * api.createWXAQRCode(path); 12 | * ``` 13 | * @param {String} path 扫码进入的小程序页面路径,最大长度 128 字节,不能为空 14 | * @param {String} width 二维码的宽度,单位 px。最小 280px,最大 1280px 15 | */ 16 | exports.createWXAQRCode = async function (path, width = 430) { 17 | const { accessToken } = await this.ensureAccessToken(); 18 | var apiUrl = this.prefix + 'wxaapp/createwxaqrcode?access_token=' + accessToken; 19 | var data = { 20 | path, 21 | width 22 | }; 23 | return this.request(apiUrl, postJSON(data)); 24 | }; 25 | 26 | 27 | /** 28 | * 获取小程序码,适用于需要的码数量较少的业务场景 29 | * https://developers.weixin.qq.com/miniprogram/dev/api/getWXACode.html 30 | * Examples: 31 | * ``` 32 | * var path = 'index?foo=bar'; // 小程序页面路径 33 | * api.getWXACode(path); 34 | * ``` 35 | * @param {String} path 扫码进入的小程序页面路径,最大长度 128 字节,不能为空 36 | * @param {String} width 二维码的宽度,单位 px。最小 280px,最大 1280px 37 | * @param {String} auto_color 自动配置线条颜色,如果颜色依然是黑色,则说明不建议配置主色调 38 | * @param {Object} line_color auto_color 为 false 时生效,使用 rgb 设置颜色 例如 {"r":"xxx","g":"xxx","b":"xxx"} 十进制表示 39 | * @param {Bool} is_hyaline 是否需要透明底色,为 true 时,生成透明底色的小程序码 40 | */ 41 | exports.getWXACode = async function (path, width = 430, auto_color = false, line_color = {'r':0,'g':0,'b':0}, is_hyaline = false) { 42 | const { accessToken } = await this.ensureAccessToken(); 43 | var apiUrl = this.wxaPrefix + 'getwxacode?access_token=' + accessToken; 44 | var data = { 45 | path, 46 | width, 47 | auto_color, 48 | line_color, 49 | is_hyaline 50 | }; 51 | return this.request(apiUrl, postJSON(data)); 52 | }; 53 | 54 | 55 | /** 56 | * 获取小程序码,适用于需要的码数量极多的业务场景 57 | * https://developers.weixin.qq.com/miniprogram/dev/api/getWXACodeUnlimit.html 58 | * Examples: 59 | * ``` 60 | * var scene = 'foo=bar'; 61 | * var page = 'pages/index/index'; // 小程序页面路径 62 | * api.getWXACodeUnlimit(scene, page); 63 | * ``` 64 | * @param {String} scene 最大32个可见字符,只支持数字,大小写英文以及部分特殊字符:!#$&'()*+,/:;=?@-._~,其它字符请自行编码为合法字符(因不支持%,中文无法使用 urlencode 处理,请使用其他编码方式) 65 | * @param {String} page 必须是已经发布的小程序存在的页面(否则报错),例如 pages/index/index, 根路径前不要填加 /,不能携带参数(参数请放在scene字段里),如果不填写这个字段,默认跳主页面 66 | * @param {String} width 二维码的宽度,单位 px。最小 280px,最大 1280px 67 | * @param {String} auto_color 自动配置线条颜色,如果颜色依然是黑色,则说明不建议配置主色调 68 | * @param {Object} line_color auto_color 为 false 时生效,使用 rgb 设置颜色 例如 {"r":"xxx","g":"xxx","b":"xxx"} 十进制表示 69 | * @param {Bool} is_hyaline 是否需要透明底色,为 true 时,生成透明底色的小程序码 70 | */ 71 | exports.getWXACodeUnlimit = async function (scene, page, width = 430, auto_color = false, line_color = {'r':0,'g':0,'b':0}, is_hyaline = false) { 72 | const { accessToken } = await this.ensureAccessToken(); 73 | var apiUrl = this.wxaPrefix + 'getwxacodeunlimit?access_token=' + accessToken; 74 | var data = { 75 | scene, 76 | page, 77 | width, 78 | auto_color, 79 | line_color, 80 | is_hyaline 81 | }; 82 | return this.request(apiUrl, postJSON(data)); 83 | }; 84 | -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /*! 4 | * 对提交参数一层封装,当POST JSON,并且结果也为JSON时使用 */ 5 | exports.postJSON = function (data) { 6 | return { 7 | dataType: 'json', 8 | method: 'POST', 9 | data: JSON.stringify(data), 10 | headers: { 11 | 'Content-Type': 'application/json', 12 | 'Accept': 'application/json' 13 | } 14 | }; 15 | }; 16 | 17 | exports.getData = function () { 18 | return { 19 | method: 'GET', 20 | headers: { 21 | 'Accept': 'application/json' 22 | } 23 | }; 24 | }; 25 | 26 | 27 | const JSONCtlCharsMap = { 28 | '"': '\\"', // \u0022 29 | '\\': '\\', // \u005c 30 | '\b': '\\b', // \u0008 31 | '\f': '\\f', // \u000c 32 | '\n': '\\n', // \u000a 33 | '\r': '\\r', // \u000d 34 | '\t': '\\t' // \u0009 35 | }; 36 | const JSONCtlCharsRE = /[\u0000-\u001F\u005C]/g; 37 | 38 | function _replaceOneChar(c) { 39 | return JSONCtlCharsMap[c] || '\\u' + (c.charCodeAt(0) + 0x10000).toString(16).substr(1); 40 | } 41 | 42 | exports.replaceJSONCtlChars = function (str) { 43 | return str.replace(JSONCtlCharsRE, _replaceOneChar); 44 | }; 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "co-wechat-api", 3 | "version": "3.11.0", 4 | "description": "微信公共平台Node库API,ES6版本", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "make test-all", 8 | "test-file": "mocha -R spec" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git://github.com/node-webot/co-wechat-api.git" 13 | }, 14 | "keywords": [ 15 | "weixin", 16 | "wechat" 17 | ], 18 | "dependencies": { 19 | "formstream": ">=0.0.8", 20 | "httpx": "^2.1.1", 21 | "json-bigint": "^0.3.0" 22 | }, 23 | "devDependencies": { 24 | "coveralls": "*", 25 | "eslint": "*", 26 | "expect.js": "*", 27 | "mocha": "^4.0.1", 28 | "muk": "*", 29 | "nyc": "^10.2.0", 30 | "rewire": "*", 31 | "travis-cov": "*" 32 | }, 33 | "author": "Jackson Tian", 34 | "license": "MIT", 35 | "readmeFilename": "README.md", 36 | "directories": { 37 | "test": "test" 38 | }, 39 | "files": [ 40 | "index.js", 41 | "lib" 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /test/api_common.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const API = require('../'); 4 | const expect = require('expect.js'); 5 | const config = require('./config'); 6 | 7 | describe('api_common', function () { 8 | describe('isAccessTokenValid', function () { 9 | it('should invalid', function () { 10 | var token = new API.AccessToken('token', new Date().getTime() - 7200 * 1000); 11 | expect(token.isValid()).not.to.be.ok(); 12 | }); 13 | 14 | it('should valid', function () { 15 | var token = new API.AccessToken('token', new Date().getTime() + 7200 * 1000); 16 | expect(token.isValid()).to.be.ok(); 17 | }); 18 | }); 19 | 20 | describe('mixin', function () { 21 | it('should ok', function () { 22 | API.mixin({sayHi: function () {}}); 23 | expect(API.prototype).to.have.property('sayHi'); 24 | }); 25 | 26 | it('should not ok when override method', function () { 27 | var obj = {sayHi: function () {}}; 28 | expect(API.mixin).withArgs(obj).to.throwException(/Don't allow override existed prototype method\./); 29 | }); 30 | }); 31 | 32 | describe('getAccessToken', function () { 33 | it('should ok', async function () { 34 | var api = new API(config.appid, config.appsecret); 35 | var token = await api.getAccessToken(); 36 | expect(token).to.only.have.keys('accessToken', 'expireTime'); 37 | }); 38 | 39 | it('should not ok with invalid appid', async function () { 40 | var api = new API('appid', 'secret'); 41 | try { 42 | await api.getAccessToken(); 43 | } catch (err) { 44 | expect(err).to.have.property('name', 'WeChatAPIError'); 45 | expect(err).to.have.property('message'); 46 | expect(err.message).to.match(/invalid appid/); 47 | } 48 | }); 49 | 50 | it('should not ok with invalid appsecret', async function () { 51 | var api = new API(config.appid, 'appsecret'); 52 | try { 53 | await api.getAccessToken(); 54 | } catch (err) { 55 | expect(err).to.have.property('name', 'WeChatAPIError'); 56 | expect(err).to.have.property('message'); 57 | expect(err.message).to.match(/invalid appsecret/); 58 | } 59 | }); 60 | }); 61 | 62 | }); 63 | -------------------------------------------------------------------------------- /test/api_user.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const API = require('../'); 4 | const expect = require('expect.js'); 5 | 6 | describe('api_user', function () { 7 | describe('batchGetUsers', function () { 8 | var api = new API('appid', 'appsecret'); 9 | api.ensureAccessToken = async () => ({accessToken: 'accessToken'}); 10 | it('should have default language "zh_CN"', function () { 11 | api.request = (url, req) => { 12 | const data = JSON.parse(req.data); 13 | const isExpectedLang = data.user_list.every((item) => item.lang === 'zh_CN'); 14 | expect(isExpectedLang).to.be.ok(); 15 | }; 16 | api.batchGetUsers(['openId1', 'openId2']); 17 | }); 18 | it('should use the language if set', function () { 19 | api.request = (url, req) => { 20 | const data = JSON.parse(req.data); 21 | const isExpectedLang = data.user_list.every((item) => item.lang === 'en'); 22 | expect(isExpectedLang).to.be.ok(); 23 | }; 24 | api.batchGetUsers(['openId1', 'openId2'], 'en'); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /test/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | appid: 'wxc9135aade4e81d57', 5 | appsecret: '0461795e98b8ffde5a212b5098f1b9b6', 6 | menuid: '208379533', 7 | user_id: 'nick' 8 | }; 9 | -------------------------------------------------------------------------------- /test/fixture/conditional_menu.json: -------------------------------------------------------------------------------- 1 | { 2 | "button": [{ 3 | "type": "click", 4 | "name": "今日歌曲", 5 | "key": "V1001_TODAY_MUSIC" 6 | }, { 7 | "name": "菜单", 8 | "sub_button": [{ 9 | "type": "view", 10 | "name": "搜索", 11 | "url": "http://www.soso.com/" 12 | }, { 13 | "type": "view", 14 | "name": "视频", 15 | "url": "http://v.qq.com/" 16 | }, { 17 | "type": "click", 18 | "name": "赞一下我们", 19 | "key": "V1001_GOOD" 20 | }] 21 | }], 22 | "matchrule": { 23 | "group_id": "2", 24 | "sex": "1", 25 | "country": "中国", 26 | "province": "广东", 27 | "city": "广州", 28 | "client_platform_type": "2" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /test/fixture/image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/node-webot/co-wechat-api/42c6e31b15b0c7d05a0231fe5ad17f139121200e/test/fixture/image.jpg -------------------------------------------------------------------------------- /test/fixture/invalid.json: -------------------------------------------------------------------------------- 1 | {"subscribe":1,"openid":"ogW8rt6i8BmyHaXvleP5SBULysSk","nickname":"Íõ¾ü","sex":1,"language":"zh_CN","city":"y?v\","province":"l?S","country":"","headimgurl":"http://wx.qlogo.cn/mmopen/Q3auHgzwzM4nXzLJEGv1SUt5ibMeibUjmaK45y06UOtaKDc2NgjvjjnsiccgwMPyKOwvstIKA85bM7zziac5m9zmfw/0","subscribe_time":1504246660,"unionid":"oKfK5s5qiqgWMHaeJ6f3sy5TYvx8","remark":"","groupid":0,"tagid_list":[]} 2 | -------------------------------------------------------------------------------- /test/fixture/menu.json: -------------------------------------------------------------------------------- 1 | { 2 | "button":[ 3 | { 4 | "type":"click", 5 | "name":"今日歌曲", 6 | "key":"V1001_TODAY_MUSIC" 7 | }, 8 | { 9 | "type":"click", 10 | "name":"歌手简介", 11 | "key":"V1001_TODAY_SINGER" 12 | }, 13 | { 14 | "name":"菜单", 15 | "sub_button":[ 16 | { 17 | "type":"view", 18 | "name":"搜索", 19 | "url":"http://www.soso.com/" 20 | }, 21 | { 22 | "type":"view", 23 | "name":"视频", 24 | "url":"http://v.qq.com/" 25 | }, 26 | { 27 | "type":"click", 28 | "name":"赞一下我们", 29 | "key":"V1001_GOOD" 30 | } 31 | ] 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /test/fixture/movie.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/node-webot/co-wechat-api/42c6e31b15b0c7d05a0231fe5ad17f139121200e/test/fixture/movie.mp4 -------------------------------------------------------------------------------- /test/fixture/pic.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/node-webot/co-wechat-api/42c6e31b15b0c7d05a0231fe5ad17f139121200e/test/fixture/pic.jpg -------------------------------------------------------------------------------- /test/fixture/test.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/node-webot/co-wechat-api/42c6e31b15b0c7d05a0231fe5ad17f139121200e/test/fixture/test.mp3 -------------------------------------------------------------------------------- /test/util.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | 6 | const expect = require('expect.js'); 7 | 8 | const util = require('../lib/util'); 9 | 10 | const filepath = path.join(__dirname, 'fixture/invalid.json'); 11 | const str = fs.readFileSync(filepath, 'utf8').trim(); 12 | 13 | describe('util', function () { 14 | // FIXME: skipe the test since it will fail 15 | it.skip('json parse with invalid chars', function () { 16 | expect(() => { 17 | JSON.parse(str); 18 | }).to.throwException(/Unexpected token/); 19 | 20 | expect(() => { 21 | JSON.parse(util.replaceJSONCtlChars(str)); 22 | }).not.to.throwException(); 23 | }); 24 | }); 25 | --------------------------------------------------------------------------------