@ddn/egg-wechat 1.0.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +114 -0
- package/app/extend/helper.js +33 -0
- package/app/service/wechat/component.js +215 -0
- package/app/service/wechat/mp.js +201 -0
- package/app/service/wechat/security.js +114 -0
- package/app/service/wechat/sign.js +66 -0
- package/app/service/wechat/wcs.js +219 -0
- package/config/config.default.js +23 -0
- package/index.d.ts +18 -0
- package/package.json +65 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) unclexiao.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# @ddn/egg-wechat
|
|
2
|
+
|
|
3
|
+
[](https://badge.fury.io/js/@ddn/egg-wechat)
|
|
4
|
+
|
|
5
|
+
[微信公众平台](https://mp.weixin.qq.com/)常规的后端服务,献给了不起的[EggJS](https://eggjs.org/zh-cn/)。
|
|
6
|
+
|
|
7
|
+
本项目 fork 自 [egg-mp](https://github.com/unclexiao/egg-mp),感谢原作者 [unclexiao](https://github.com/unclexiao) 的贡献。
|
|
8
|
+
|
|
9
|
+
## 特性
|
|
10
|
+
### 微信小程序
|
|
11
|
+
- [X] 小程序登录
|
|
12
|
+
- [X] 小程序授权
|
|
13
|
+
- [X] 小程序支付
|
|
14
|
+
- [X] 推送模板消息
|
|
15
|
+
- [X] 检测是否含有敏感词
|
|
16
|
+
- [ ] 生成二维码/小程序码
|
|
17
|
+
- [ ] 接入在线客服消息
|
|
18
|
+
|
|
19
|
+
### 微信服务号
|
|
20
|
+
- [X] 网页授权
|
|
21
|
+
- [X] 发送模板消息
|
|
22
|
+
- [X] 获取用户基础信息
|
|
23
|
+
- [X] 获取用户列表
|
|
24
|
+
- [X] 服务号网页支付
|
|
25
|
+
- [X] 前端调用JSSDK
|
|
26
|
+
|
|
27
|
+
### 微信开放平台 (第三方平台)
|
|
28
|
+
- [X] 获取 Component Access Token
|
|
29
|
+
- [X] 获取预授权码 (PreAuthCode)
|
|
30
|
+
- [X] 获取授权方信息 (QueryAuth)
|
|
31
|
+
- [X] 发送客服消息
|
|
32
|
+
- [X] 获取授权页 URL
|
|
33
|
+
|
|
34
|
+
## 安装
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
$ npm i @ddn/egg-wechat --save
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## 启用插件
|
|
41
|
+
|
|
42
|
+
```js
|
|
43
|
+
// {app_root}/config/plugin.js
|
|
44
|
+
exports.wechat = {
|
|
45
|
+
enable: true,
|
|
46
|
+
package: '@ddn/egg-wechat',
|
|
47
|
+
};
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## 应用配置
|
|
51
|
+
|
|
52
|
+
```js
|
|
53
|
+
// {app_root}/config/config.default.js
|
|
54
|
+
exports.wechat = {
|
|
55
|
+
appId: '', // 公众平台应用编号
|
|
56
|
+
appSecret: '', // 公众平台应用密钥
|
|
57
|
+
mchId: '', // 商户平台商家编号
|
|
58
|
+
apiKey: '', // 商户支付密钥
|
|
59
|
+
notifyUrl: '', // 支付结果回调地址
|
|
60
|
+
|
|
61
|
+
// 开放平台第三方平台配置
|
|
62
|
+
componentAppId: '',
|
|
63
|
+
componentAppSecret: '',
|
|
64
|
+
componentToken: '',
|
|
65
|
+
componentEncodingAESKey: '',
|
|
66
|
+
};
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
请查看 [doc/CONFIG.md](doc/CONFIG.md) 获取更详细说明.
|
|
70
|
+
|
|
71
|
+
## 简单实例
|
|
72
|
+
|
|
73
|
+
### 小程序/公众号
|
|
74
|
+
|
|
75
|
+
```javascript
|
|
76
|
+
async login() {
|
|
77
|
+
const { ctx } = this;
|
|
78
|
+
const { code } = ctx.request.query;
|
|
79
|
+
// 注意命名空间变为 ctx.service.wechat.mp
|
|
80
|
+
let res = await ctx.service.wechat.mp.login(code);
|
|
81
|
+
// {
|
|
82
|
+
// session_key: "...",
|
|
83
|
+
// openid: "..."
|
|
84
|
+
// };
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### 开放平台 (Component)
|
|
89
|
+
|
|
90
|
+
```javascript
|
|
91
|
+
async getAuthUrl() {
|
|
92
|
+
const { ctx } = this;
|
|
93
|
+
// 获取授权页 URL
|
|
94
|
+
const url = await ctx.service.wechat.component.getOAuthDomainUrl();
|
|
95
|
+
// ...
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## 基础教程
|
|
100
|
+
- [配置项如何找到?](doc/CONFIG.md)
|
|
101
|
+
- 如何搭建环境?
|
|
102
|
+
- 如何本地调试?
|
|
103
|
+
- 登录与授权(获取用户信息)
|
|
104
|
+
- 微信支付(小程序、服务号)
|
|
105
|
+
- 推送消息(服务通知、模板消息)
|
|
106
|
+
- 生成二维码(或小程序码)
|
|
107
|
+
|
|
108
|
+
## 问题与建议
|
|
109
|
+
|
|
110
|
+
请在[这里](https://github.com/ddnlink/egg-wechat/issues)向我提出问题
|
|
111
|
+
|
|
112
|
+
## 开源协议
|
|
113
|
+
|
|
114
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const xml = require('xml-js');
|
|
3
|
+
|
|
4
|
+
module.exports = {
|
|
5
|
+
xml2json(xmlStr) {
|
|
6
|
+
let result = xml.xml2json(xmlStr, {
|
|
7
|
+
compact: true,
|
|
8
|
+
spaces: 4,
|
|
9
|
+
});
|
|
10
|
+
result = JSON.parse(result);
|
|
11
|
+
return this.deleteCDATA(result.xml);
|
|
12
|
+
},
|
|
13
|
+
|
|
14
|
+
json2xml(json) {
|
|
15
|
+
const result = xml.json2xml(json, {
|
|
16
|
+
compact: true,
|
|
17
|
+
spaces: 4,
|
|
18
|
+
});
|
|
19
|
+
return '<xml>\n' + result + '\n</xml>';
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
deleteCDATA(args) {
|
|
23
|
+
const keys = Object.keys(args);
|
|
24
|
+
const obj = {};
|
|
25
|
+
for (let i = 0; i < keys.length; i++) {
|
|
26
|
+
const k = keys[i];
|
|
27
|
+
if (typeof args[k] === 'object') {
|
|
28
|
+
obj[k] = args[k]._cdata;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return obj;
|
|
32
|
+
},
|
|
33
|
+
};
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const Service = require('egg').Service;
|
|
4
|
+
|
|
5
|
+
class ComponentService extends Service {
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 获取 Component Access Token
|
|
9
|
+
* 优先从 Redis 获取,过期则刷新
|
|
10
|
+
*/
|
|
11
|
+
async getComponentAccessToken() {
|
|
12
|
+
const { ctx, app } = this;
|
|
13
|
+
const { componentAppId, componentAppSecret } = app.config.wechat;
|
|
14
|
+
|
|
15
|
+
if (!componentAppId || !componentAppSecret) {
|
|
16
|
+
throw new Error('未配置微信开放平台 Component AppID 或 Secret');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const cacheKey = `wechat:component_access_token:${componentAppId}`;
|
|
20
|
+
let token = await app.redis.get(cacheKey);
|
|
21
|
+
|
|
22
|
+
if (token) {
|
|
23
|
+
return token;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// 获取 Component Verify Ticket
|
|
27
|
+
const ticketKey = `wechat:component_verify_ticket:${componentAppId}`;
|
|
28
|
+
const ticket = await app.redis.get(ticketKey);
|
|
29
|
+
|
|
30
|
+
if (!ticket) {
|
|
31
|
+
throw new Error('未收到微信推送的 Component Verify Ticket,请等待微信服务器推送');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 请求微信接口获取 Token
|
|
35
|
+
const url = 'https://api.weixin.qq.com/cgi-bin/component/api_component_token';
|
|
36
|
+
const result = await ctx.curl(url, {
|
|
37
|
+
method: 'POST',
|
|
38
|
+
contentType: 'json',
|
|
39
|
+
dataType: 'json',
|
|
40
|
+
data: {
|
|
41
|
+
component_appid: componentAppId,
|
|
42
|
+
component_appsecret: componentAppSecret,
|
|
43
|
+
component_verify_ticket: ticket
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
if (result.data.errcode) {
|
|
48
|
+
throw new Error(`获取 Component Access Token 失败: ${result.data.errmsg}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
token = result.data.component_access_token;
|
|
52
|
+
const expiresIn = result.data.expires_in;
|
|
53
|
+
|
|
54
|
+
// 缓存 Token (提前 5 分钟过期)
|
|
55
|
+
await app.redis.set(cacheKey, token, 'EX', expiresIn - 300);
|
|
56
|
+
|
|
57
|
+
return token;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 保存 Component Verify Ticket
|
|
62
|
+
* @param {string} ticket 微信推送的 Ticket
|
|
63
|
+
*/
|
|
64
|
+
async saveComponentVerifyTicket(ticket) {
|
|
65
|
+
const { app } = this;
|
|
66
|
+
const { componentAppId } = app.config.wechat;
|
|
67
|
+
|
|
68
|
+
if (!componentAppId) return;
|
|
69
|
+
|
|
70
|
+
const key = `wechat:component_verify_ticket:${componentAppId}`;
|
|
71
|
+
// Ticket 有效期通常为 12 小时,这里设置 12 小时过期
|
|
72
|
+
await app.redis.set(key, ticket, 'EX', 12 * 3600);
|
|
73
|
+
|
|
74
|
+
// 兼容旧的 Redis Key
|
|
75
|
+
await app.redis.set('wechat:component:ticket', ticket, 'EX', 12 * 3600);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 获取预授权码 (Pre-Auth Code)
|
|
80
|
+
*/
|
|
81
|
+
async getPreAuthCode() {
|
|
82
|
+
const { ctx, app } = this;
|
|
83
|
+
const { componentAppId } = app.config.wechat;
|
|
84
|
+
|
|
85
|
+
const token = await this.getComponentAccessToken();
|
|
86
|
+
|
|
87
|
+
const url = `https://api.weixin.qq.com/cgi-bin/component/api_create_preauthcode?component_access_token=${token}`;
|
|
88
|
+
const result = await ctx.curl(url, {
|
|
89
|
+
method: 'POST',
|
|
90
|
+
contentType: 'json',
|
|
91
|
+
dataType: 'json',
|
|
92
|
+
data: {
|
|
93
|
+
component_appid: componentAppId
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
if (result.data.errcode) {
|
|
98
|
+
throw new Error(`获取预授权码失败: ${result.data.errmsg}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return result.data.pre_auth_code;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* 使用授权码换取公众号/小程序的接口调用凭据和授权信息
|
|
106
|
+
* @param {string} authCode 授权码
|
|
107
|
+
*/
|
|
108
|
+
async queryAuth(authCode) {
|
|
109
|
+
const { ctx, app } = this;
|
|
110
|
+
const { componentAppId } = app.config.wechat;
|
|
111
|
+
|
|
112
|
+
const token = await this.getComponentAccessToken();
|
|
113
|
+
const url = `https://api.weixin.qq.com/cgi-bin/component/api_query_auth?component_access_token=${token}`;
|
|
114
|
+
|
|
115
|
+
const result = await ctx.curl(url, {
|
|
116
|
+
method: 'POST',
|
|
117
|
+
contentType: 'json',
|
|
118
|
+
dataType: 'json',
|
|
119
|
+
data: {
|
|
120
|
+
component_appid: componentAppId,
|
|
121
|
+
authorization_code: authCode
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
if (result.data.errcode) {
|
|
126
|
+
throw new Error(`换取授权信息失败: ${result.data.errmsg}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return result.data.authorization_info;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* 获取授权方的帐号基本信息
|
|
134
|
+
* @param {string} authorizerAppId 授权方AppID
|
|
135
|
+
*/
|
|
136
|
+
async getAuthorizerInfo(authorizerAppId) {
|
|
137
|
+
const { ctx, app } = this;
|
|
138
|
+
const { componentAppId } = app.config.wechat;
|
|
139
|
+
|
|
140
|
+
const token = await this.getComponentAccessToken();
|
|
141
|
+
const url = `https://api.weixin.qq.com/cgi-bin/component/api_get_authorizer_info?component_access_token=${token}`;
|
|
142
|
+
|
|
143
|
+
const result = await ctx.curl(url, {
|
|
144
|
+
method: 'POST',
|
|
145
|
+
contentType: 'json',
|
|
146
|
+
dataType: 'json',
|
|
147
|
+
data: {
|
|
148
|
+
component_appid: componentAppId,
|
|
149
|
+
authorizer_appid: authorizerAppId
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
if (result.data.errcode) {
|
|
154
|
+
throw new Error(`获取授权方基本信息失败: ${result.data.errmsg}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return result.data;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* 发送客服消息 (文本)
|
|
162
|
+
* @param {string} accessToken 授权方接口调用凭据
|
|
163
|
+
* @param {string} toUser 接收者OpenID
|
|
164
|
+
* @param {string} content 文本内容
|
|
165
|
+
*/
|
|
166
|
+
async sendCustomMessage(accessToken, toUser, content) {
|
|
167
|
+
const { ctx } = this;
|
|
168
|
+
const url = `https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=${accessToken}`;
|
|
169
|
+
|
|
170
|
+
const result = await ctx.curl(url, {
|
|
171
|
+
method: 'POST',
|
|
172
|
+
contentType: 'json',
|
|
173
|
+
dataType: 'json',
|
|
174
|
+
data: {
|
|
175
|
+
touser: toUser,
|
|
176
|
+
msgtype: 'text',
|
|
177
|
+
text: {
|
|
178
|
+
content: content
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
return result.data;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* 生成代公众号发起网页授权的链接
|
|
188
|
+
* @param {string} appid 公众号AppID
|
|
189
|
+
* @param {string} redirectUri 回调地址
|
|
190
|
+
* @param {string} scope 作用域
|
|
191
|
+
* @param {string} state 状态参数
|
|
192
|
+
*/
|
|
193
|
+
getOAuthDomainUrl(appid, redirectUri, scope = 'snsapi_userinfo', state = 'STATE') {
|
|
194
|
+
const { app } = this;
|
|
195
|
+
const { componentAppId } = app.config.wechat;
|
|
196
|
+
const encodedRedirectUri = encodeURIComponent(redirectUri);
|
|
197
|
+
return `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appid}&redirect_uri=${encodedRedirectUri}&response_type=code&scope=${scope}&state=${state}&component_appid=${componentAppId}#wechat_redirect`;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* 获取授权页 URL (用于公众号/小程序管理员授权给第三方平台)
|
|
202
|
+
* @param {string} redirectUri 回调地址
|
|
203
|
+
* @param {number} authType 授权类型 (1: 公众号, 2: 小程序, 3: 两者都可)
|
|
204
|
+
*/
|
|
205
|
+
async getAuthUrl(redirectUri, authType = 3) {
|
|
206
|
+
const { app } = this;
|
|
207
|
+
const { componentAppId } = app.config.wechat;
|
|
208
|
+
const preAuthCode = await this.getPreAuthCode();
|
|
209
|
+
const encodedRedirectUri = encodeURIComponent(redirectUri);
|
|
210
|
+
|
|
211
|
+
return `https://mp.weixin.qq.com/cgi-bin/componentloginpage?component_appid=${componentAppId}&pre_auth_code=${preAuthCode}&redirect_uri=${encodedRedirectUri}&auth_type=${authType}`;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
module.exports = ComponentService;
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const Service = require('egg').Service;
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
|
|
5
|
+
// 微信相关接口常量
|
|
6
|
+
const jscode2sessionUri = 'https://api.weixin.qq.com/sns/jscode2session'; // 微信临时授权码
|
|
7
|
+
const tokenUri = 'https://api.weixin.qq.com/cgi-bin/token'; // 微信凭据
|
|
8
|
+
const msgSecCheck = 'https://api.weixin.qq.com/wxa/msg_sec_check'; // 微信敏感词
|
|
9
|
+
const sendMsgUri =
|
|
10
|
+
'https://api.weixin.qq.com/cgi-bin/message/wxopen/template/send'; // 微信服务通知
|
|
11
|
+
const payUri = 'https://api.mch.weixin.qq.com/pay/unifiedorder'; // 微信统一下单
|
|
12
|
+
|
|
13
|
+
class MPService extends Service {
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 登录凭证校验
|
|
17
|
+
* @param {String} code 临时授权码
|
|
18
|
+
* @return {Object} 微信返回的数据
|
|
19
|
+
* @see https://developers.weixin.qq.com/miniprogram/dev/api/code2Session.html?search-key=jscode2session
|
|
20
|
+
*/
|
|
21
|
+
async login(code) {
|
|
22
|
+
const {
|
|
23
|
+
appId,
|
|
24
|
+
appSecret,
|
|
25
|
+
} = this.app.config.wechat;
|
|
26
|
+
const url = `${jscode2sessionUri}?appid=${appId}&secret=${appSecret}&js_code=${code}&grant_type=authorization_code`;
|
|
27
|
+
const res = await this.ctx.curl(url, {
|
|
28
|
+
dataType: 'json',
|
|
29
|
+
});
|
|
30
|
+
return res.data;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 获取Token
|
|
35
|
+
* @return {Object} 微信返回的数据
|
|
36
|
+
* @see https://developers.weixin.qq.com/miniprogram/dev/api/getAccessToken.html
|
|
37
|
+
*/
|
|
38
|
+
async getToken() {
|
|
39
|
+
const {
|
|
40
|
+
appId,
|
|
41
|
+
appSecret,
|
|
42
|
+
} = this.app.config.wechat;
|
|
43
|
+
const url = `${tokenUri}?grant_type=client_credential&appid=${appId}&secret=${appSecret}`;
|
|
44
|
+
const res = await this.ctx.curl(url, {
|
|
45
|
+
dataType: 'json',
|
|
46
|
+
});
|
|
47
|
+
return res.data;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 加密数据解密算法
|
|
52
|
+
* @param {String} sessionKey sessionKey
|
|
53
|
+
* @param {String} encryptedData encryptedData
|
|
54
|
+
* @param {String} iv iv
|
|
55
|
+
* @return {Object} 微信返回的数据
|
|
56
|
+
* @see https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/signature.html
|
|
57
|
+
*/
|
|
58
|
+
async decryptData(sessionKey, encryptedData, iv) {
|
|
59
|
+
const {
|
|
60
|
+
appId,
|
|
61
|
+
} = this.app.config.wechat;
|
|
62
|
+
const sessionKeyBuffer = new Buffer.from(sessionKey, 'base64');
|
|
63
|
+
const encryptedDataBuffer = new Buffer.from(encryptedData, 'base64');
|
|
64
|
+
const ivStr = new Buffer.from(iv, 'base64');
|
|
65
|
+
const decipher = crypto.createDecipheriv('aes-128-cbc', sessionKeyBuffer, ivStr);
|
|
66
|
+
let decoded = '';
|
|
67
|
+
try {
|
|
68
|
+
decipher.setAutoPadding(true);
|
|
69
|
+
decoded = decipher.update(encryptedDataBuffer, 'binary', 'utf8');
|
|
70
|
+
decoded += decipher.final('utf8');
|
|
71
|
+
decoded = JSON.parse(decoded);
|
|
72
|
+
} catch (err) {
|
|
73
|
+
throw new Error('Illegal Buffer');
|
|
74
|
+
}
|
|
75
|
+
if (decoded.watermark.appid !== appId) {
|
|
76
|
+
throw new Error('Illegal Appid');
|
|
77
|
+
}
|
|
78
|
+
return decoded;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 是否含有敏感内容
|
|
83
|
+
* @param {String} content 文本内容
|
|
84
|
+
* @return {Boolean} 是否敏感内容
|
|
85
|
+
* @see https://developers.weixin.qq.com/miniprogram/dev/api/msgSecCheck.html?search-key=msg_sec_check
|
|
86
|
+
*/
|
|
87
|
+
async checkIsSensitive(content) {
|
|
88
|
+
const token = await this.getToken();
|
|
89
|
+
const access_token = token.access_token;
|
|
90
|
+
const res = await this.ctx.curl(`${msgSecCheck}?access_token=${access_token}`, {
|
|
91
|
+
method: 'POST',
|
|
92
|
+
contentType: 'json',
|
|
93
|
+
dataType: 'json',
|
|
94
|
+
data: {
|
|
95
|
+
content,
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
return res.data.errcode === 87014;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 推送模板消息
|
|
103
|
+
* @param {Object} params 推送消息
|
|
104
|
+
* @return {Boolean} 微信返回的数据
|
|
105
|
+
* @see https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/template-message.html
|
|
106
|
+
*/
|
|
107
|
+
async pushMessage(params) {
|
|
108
|
+
const body = {
|
|
109
|
+
touser: params.openid,
|
|
110
|
+
template_id: params.templateid,
|
|
111
|
+
page: params.page,
|
|
112
|
+
form_id: params.formid,
|
|
113
|
+
data: params.data,
|
|
114
|
+
emphasis_keyword: params.emphasis_keyword,
|
|
115
|
+
};
|
|
116
|
+
const token = await this.getToken();
|
|
117
|
+
const access_token = token.access_token;
|
|
118
|
+
const res = await this.ctx.curl(`${sendMsgUri}?access_token=${access_token}`, {
|
|
119
|
+
method: 'POST',
|
|
120
|
+
contentType: 'json',
|
|
121
|
+
dataType: 'json',
|
|
122
|
+
data: body,
|
|
123
|
+
});
|
|
124
|
+
return res.data;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* 统一下单
|
|
129
|
+
* @param {String} openid 目标用户
|
|
130
|
+
* @param {Object} data 推送消息
|
|
131
|
+
* @return {Object} 用于小程序发起支付
|
|
132
|
+
* @see https://api.mch.weixin.qq.com/pay/unifiedorder
|
|
133
|
+
*/
|
|
134
|
+
async createOrder(openid, data) {
|
|
135
|
+
const {
|
|
136
|
+
ctx,
|
|
137
|
+
} = this;
|
|
138
|
+
const signedParams = this._firstSignOrder(openid, data);
|
|
139
|
+
const successXml = await ctx.curl(payUri, {
|
|
140
|
+
method: 'POST',
|
|
141
|
+
data: ctx.helper.json2xml(signedParams),
|
|
142
|
+
});
|
|
143
|
+
const json = ctx.helper.xml2json(successXml.data);
|
|
144
|
+
if (json.return_code === 'FAIL') {
|
|
145
|
+
return {
|
|
146
|
+
code: -1,
|
|
147
|
+
msg: json.return_msg,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
return this._secondSignOrder(json);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 第一次签名
|
|
154
|
+
_firstSignOrder(openid, data) {
|
|
155
|
+
const {
|
|
156
|
+
app,
|
|
157
|
+
ctx,
|
|
158
|
+
service,
|
|
159
|
+
} = this;
|
|
160
|
+
const {
|
|
161
|
+
appId,
|
|
162
|
+
mchId,
|
|
163
|
+
} = app.config.wechat;
|
|
164
|
+
const params = {
|
|
165
|
+
openid,
|
|
166
|
+
appid: appId,
|
|
167
|
+
mch_id: mchId,
|
|
168
|
+
nonce_str: service.wechat.sign.createNonceStr(),
|
|
169
|
+
out_trade_no: data.tradeNo || new Date().getTime(), // 内部订单号
|
|
170
|
+
total_fee: data.totalFee || 1, // 单位为分的标价金额
|
|
171
|
+
body: data.body || '未知产品-测试商品', // 应用市场上的APP名字-商品概述
|
|
172
|
+
spbill_create_ip: ctx.ip, // 支付提交用户端ip
|
|
173
|
+
notify_url: data.notifyUrl || '', // 异步接收微信支付结果通知
|
|
174
|
+
trade_type: 'JSAPI',
|
|
175
|
+
};
|
|
176
|
+
params.sign = service.wechat.sign.getPaySign(params); // 首次签名,用于验证支付通知
|
|
177
|
+
return params;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// 第二次签名
|
|
181
|
+
_secondSignOrder(json) {
|
|
182
|
+
const {
|
|
183
|
+
app,
|
|
184
|
+
service,
|
|
185
|
+
} = this;
|
|
186
|
+
const {
|
|
187
|
+
appId,
|
|
188
|
+
} = app.config.wechat;
|
|
189
|
+
const res = {
|
|
190
|
+
appId,
|
|
191
|
+
timeStamp: service.wechat.sign.createTimestamp(),
|
|
192
|
+
nonceStr: json.nonce_str,
|
|
193
|
+
package: `prepay_id=${json.prepay_id}`,
|
|
194
|
+
signType: 'MD5',
|
|
195
|
+
}; // 不能随意增减,必须是这些字段
|
|
196
|
+
res.paySign = service.wechat.sign.getPaySign(res); // 第二次签名,用于提交到微信
|
|
197
|
+
return res;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
module.exports = MPService;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const Service = require('egg').Service;
|
|
4
|
+
|
|
5
|
+
const MSG_SEC_CHECK_URL = 'https://api.weixin.qq.com/wxa/msg_sec_check';
|
|
6
|
+
const MEDIA_CHECK_ASYNC_URL = 'https://api.weixin.qq.com/wxa/media_check_async';
|
|
7
|
+
|
|
8
|
+
class SecurityService extends Service {
|
|
9
|
+
|
|
10
|
+
async _getAccessToken(providedAccessToken) {
|
|
11
|
+
if (providedAccessToken) return providedAccessToken;
|
|
12
|
+
|
|
13
|
+
const tokenRes = await this.ctx.service.wechat.mp.getToken();
|
|
14
|
+
const accessToken = tokenRes && tokenRes.access_token;
|
|
15
|
+
if (!accessToken) {
|
|
16
|
+
const errMsg = tokenRes && tokenRes.errmsg ? tokenRes.errmsg : 'missing access_token';
|
|
17
|
+
throw new Error(`wechat getToken failed: ${errMsg}`);
|
|
18
|
+
}
|
|
19
|
+
return accessToken;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 文本内容安全识别(2.0)
|
|
24
|
+
* @param {Object} params
|
|
25
|
+
* @param {string} params.content 需检测的文本内容(<=2500字)
|
|
26
|
+
* @param {number} params.version 固定为 2
|
|
27
|
+
* @param {number} params.scene 场景(1资料;2评论;3论坛;4社交日志)
|
|
28
|
+
* @param {string} params.openid 用户 openid(近两小时访问过小程序)
|
|
29
|
+
* @param {string} [params.title] 文本标题
|
|
30
|
+
* @param {string} [params.nickname] 用户昵称
|
|
31
|
+
* @param {string} [params.signature] 个性签名(仅 scene=1)
|
|
32
|
+
* @param {Object} [options]
|
|
33
|
+
* @param {string} [options.accessToken] access_token 或 authorizer_access_token
|
|
34
|
+
*/
|
|
35
|
+
async msgSecCheck(params, options = {}) {
|
|
36
|
+
const {
|
|
37
|
+
content,
|
|
38
|
+
openid,
|
|
39
|
+
version = 2,
|
|
40
|
+
scene = 2,
|
|
41
|
+
title,
|
|
42
|
+
nickname,
|
|
43
|
+
signature,
|
|
44
|
+
} = params || {};
|
|
45
|
+
|
|
46
|
+
if (!content || typeof content !== 'string') throw new Error('content is required');
|
|
47
|
+
if (!openid || typeof openid !== 'string') throw new Error('openid is required');
|
|
48
|
+
|
|
49
|
+
const accessToken = await this._getAccessToken(options.accessToken);
|
|
50
|
+
|
|
51
|
+
const data = {
|
|
52
|
+
content,
|
|
53
|
+
version,
|
|
54
|
+
scene,
|
|
55
|
+
openid,
|
|
56
|
+
};
|
|
57
|
+
if (title) data.title = title;
|
|
58
|
+
if (nickname) data.nickname = nickname;
|
|
59
|
+
if (signature) data.signature = signature;
|
|
60
|
+
|
|
61
|
+
const res = await this.ctx.curl(`${MSG_SEC_CHECK_URL}?access_token=${accessToken}`, {
|
|
62
|
+
method: 'POST',
|
|
63
|
+
contentType: 'json',
|
|
64
|
+
dataType: 'json',
|
|
65
|
+
data,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return res.data;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 多媒体内容安全识别(异步,2.0)
|
|
73
|
+
* @param {Object} params
|
|
74
|
+
* @param {string} params.mediaUrl 要检测的媒体 URL
|
|
75
|
+
* @param {number} params.mediaType 1:音频 2:图片
|
|
76
|
+
* @param {number} params.version 固定为 2
|
|
77
|
+
* @param {number} params.scene 场景(1资料;2评论;3论坛;4社交日志)
|
|
78
|
+
* @param {string} params.openid 用户 openid(近两小时访问过小程序)
|
|
79
|
+
* @param {Object} [options]
|
|
80
|
+
* @param {string} [options.accessToken] access_token 或 authorizer_access_token
|
|
81
|
+
*/
|
|
82
|
+
async mediaCheckAsync(params, options = {}) {
|
|
83
|
+
const {
|
|
84
|
+
mediaUrl,
|
|
85
|
+
mediaType,
|
|
86
|
+
openid,
|
|
87
|
+
version = 2,
|
|
88
|
+
scene = 2,
|
|
89
|
+
} = params || {};
|
|
90
|
+
|
|
91
|
+
if (!mediaUrl || typeof mediaUrl !== 'string') throw new Error('mediaUrl is required');
|
|
92
|
+
if (mediaType !== 1 && mediaType !== 2) throw new Error('mediaType must be 1 or 2');
|
|
93
|
+
if (!openid || typeof openid !== 'string') throw new Error('openid is required');
|
|
94
|
+
|
|
95
|
+
const accessToken = await this._getAccessToken(options.accessToken);
|
|
96
|
+
|
|
97
|
+
const res = await this.ctx.curl(`${MEDIA_CHECK_ASYNC_URL}?access_token=${accessToken}`, {
|
|
98
|
+
method: 'POST',
|
|
99
|
+
contentType: 'json',
|
|
100
|
+
dataType: 'json',
|
|
101
|
+
data: {
|
|
102
|
+
media_url: mediaUrl,
|
|
103
|
+
media_type: mediaType,
|
|
104
|
+
version,
|
|
105
|
+
scene,
|
|
106
|
+
openid,
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
return res.data;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
module.exports = SecurityService;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const crypto = require('crypto');
|
|
3
|
+
|
|
4
|
+
const Service = require('egg').Service;
|
|
5
|
+
|
|
6
|
+
class SignService extends Service {
|
|
7
|
+
// 生成随机字符串,用于微信支付
|
|
8
|
+
createNonceStr() {
|
|
9
|
+
return Math.random()
|
|
10
|
+
.toString(36)
|
|
11
|
+
.substr(2, 15);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// 生成时间戳,单位为秒,用于微信支付
|
|
15
|
+
createTimestamp() {
|
|
16
|
+
return parseInt(new Date().getTime() / 1000) + '';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// 序列化字符串
|
|
20
|
+
raw(args) {
|
|
21
|
+
const keys = Object.keys(args).sort(); // 参数名ASCII码从小到大排序(字典序);
|
|
22
|
+
let string = '';
|
|
23
|
+
for (let i = 0; i < keys.length; i++) {
|
|
24
|
+
const k = keys[i];
|
|
25
|
+
if (k === 'sign' || !args[k]) {
|
|
26
|
+
continue; // 如果参数的值为空不参与签名
|
|
27
|
+
}
|
|
28
|
+
if (typeof args[k] === 'array') {
|
|
29
|
+
// 兼容xml场景,值为数组
|
|
30
|
+
args[k] = args[k][0];
|
|
31
|
+
}
|
|
32
|
+
string += '&' + k + '=' + args[k];
|
|
33
|
+
}
|
|
34
|
+
string = string.substr(1);
|
|
35
|
+
return string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 生成加密SHA1字符串
|
|
39
|
+
sha1(str) {
|
|
40
|
+
return crypto.createHash('sha1').update(str).digest('hex');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 生成配置签名
|
|
44
|
+
getConfigSign(args) {
|
|
45
|
+
const rawStr = this.raw(args);
|
|
46
|
+
const shaStr = this.sha1(rawStr);
|
|
47
|
+
return shaStr.toLocaleLowerCase();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 生成加密MD5字符串
|
|
51
|
+
md5(str) {
|
|
52
|
+
return crypto.createHash('md5').update(str).digest('hex');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 生成支付签名
|
|
56
|
+
getPaySign(args) {
|
|
57
|
+
const {
|
|
58
|
+
apiKey,
|
|
59
|
+
} = this.app.config.wechat;
|
|
60
|
+
const rawStr = this.raw(args);
|
|
61
|
+
const md5Str = this.md5(rawStr + '&key=' + apiKey);
|
|
62
|
+
return md5Str.toUpperCase();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = SignService;
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const Service = require('egg').Service;
|
|
4
|
+
|
|
5
|
+
const tokenUri = 'https://api.weixin.qq.com/cgi-bin/token'; // 换取统一令牌
|
|
6
|
+
const ticketUri = 'https://api.weixin.qq.com/cgi-bin/ticket/getticket'; // Web端临时票据
|
|
7
|
+
const templateUri = 'https://api.weixin.qq.com/cgi-bin/message/template/send'; // 推送模板消息
|
|
8
|
+
const usersUri = 'https://api.weixin.qq.com/cgi-bin/user/get'; // 获取用户信息
|
|
9
|
+
const userInfoBatch = 'https://api.weixin.qq.com/cgi-bin/user/info/batchget'; // 批量获取用户信息
|
|
10
|
+
const authUri = 'https://api.weixin.qq.com/sns/oauth2/access_token'; // 微信网页授权
|
|
11
|
+
const payUri = 'https://api.mch.weixin.qq.com/pay/unifiedorder'; // 微信统一下单
|
|
12
|
+
|
|
13
|
+
const jsonType = {
|
|
14
|
+
dataType: 'json',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
class WCSService extends Service {
|
|
18
|
+
/**
|
|
19
|
+
* 获取Token
|
|
20
|
+
* @return {String} 令牌
|
|
21
|
+
* @see https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140183
|
|
22
|
+
*/
|
|
23
|
+
async getToken() {
|
|
24
|
+
const {
|
|
25
|
+
appId,
|
|
26
|
+
appSecret,
|
|
27
|
+
} = this.app.config.wechat;
|
|
28
|
+
const url = `${tokenUri}?grant_type=client_credential&appid=${appId}&secret=${appSecret}`;
|
|
29
|
+
const res = await this.ctx.curl(url, jsonType);
|
|
30
|
+
if (res.data.errcode){
|
|
31
|
+
throw new Error(res.data.errmsg)
|
|
32
|
+
}
|
|
33
|
+
return res.data;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 获取Ticket
|
|
38
|
+
* @param {String} token 令牌
|
|
39
|
+
* @return {Object} 票据信息
|
|
40
|
+
* @see https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421141115
|
|
41
|
+
*/
|
|
42
|
+
async getTicket(token) {
|
|
43
|
+
const url = `${ticketUri}?access_token=${token}&type=jsapi`;
|
|
44
|
+
const res = await this.ctx.curl(url, jsonType);
|
|
45
|
+
if (res.data.errcode){
|
|
46
|
+
throw new Error(res.data.errmsg)
|
|
47
|
+
}
|
|
48
|
+
return res.data;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 获取权限验证配置
|
|
53
|
+
* @param {String} url 调用JSAPI的网址
|
|
54
|
+
* @return {Object} JSSDK初始化配置
|
|
55
|
+
* @see https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421141115
|
|
56
|
+
*/
|
|
57
|
+
async getConfig(url) {
|
|
58
|
+
const tokenRes = await this.getToken();
|
|
59
|
+
const ticketRes = await this.getTicket(tokenRes.access_token);
|
|
60
|
+
const params = this._createConfigSign(ticketRes.ticket, url);
|
|
61
|
+
params.appId = this.app.config.wechat.appId;
|
|
62
|
+
return params;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 微信网页授权
|
|
67
|
+
* @param {String} code 临时授权码
|
|
68
|
+
* @return {Object} 微信返回的授权信息
|
|
69
|
+
* @see https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140842
|
|
70
|
+
*/
|
|
71
|
+
async auth(code) {
|
|
72
|
+
const {
|
|
73
|
+
appId,
|
|
74
|
+
appSecret,
|
|
75
|
+
} = this.app.config.wechat;
|
|
76
|
+
const url = `${authUri}?grant_type=authorization_code&appid=${appId}&secret=${appSecret}&code=${code}`;
|
|
77
|
+
const res = await this.ctx.curl(url, jsonType);
|
|
78
|
+
return res.data;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 发送模板消息
|
|
83
|
+
* @param {String} accessToken accessToken
|
|
84
|
+
* @param {Object} data 模板消息数据
|
|
85
|
+
* @return {Object} 微信返回的推送结果
|
|
86
|
+
* @see https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1433751277
|
|
87
|
+
*/
|
|
88
|
+
async sendTemplateMsg(accessToken, data) {
|
|
89
|
+
const url = `${templateUri}?access_token=${accessToken}`;
|
|
90
|
+
const res = await this.ctx.curl(url, {
|
|
91
|
+
method: 'POST',
|
|
92
|
+
dataType: 'json',
|
|
93
|
+
data: JSON.stringify(data),
|
|
94
|
+
});
|
|
95
|
+
return res.data;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 获取用户列表
|
|
100
|
+
* @param {String} accessToken accessToken
|
|
101
|
+
* @return {Object} 用户列表
|
|
102
|
+
* @see https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140840
|
|
103
|
+
*/
|
|
104
|
+
async getUserList(accessToken) {
|
|
105
|
+
const url = `${usersUri}?access_token=${accessToken}`;
|
|
106
|
+
const res = await this.ctx.curl(url, jsonType);
|
|
107
|
+
const openids = res.data.data.openid;
|
|
108
|
+
return openids;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 批量获取用户信息
|
|
113
|
+
* @param {String} accessToken accessToken
|
|
114
|
+
* @param {Array} openids 用户数据
|
|
115
|
+
* @return {Object} 批量用户信息
|
|
116
|
+
* @see https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140839
|
|
117
|
+
*/
|
|
118
|
+
async getBatchUserInfo(accessToken, openids) {
|
|
119
|
+
const url = `${userInfoBatch}?access_token=${accessToken}`;
|
|
120
|
+
const res = await this.ctx.curl(url, {
|
|
121
|
+
method: 'POST',
|
|
122
|
+
dataType: 'json',
|
|
123
|
+
data: JSON.stringify(openids),
|
|
124
|
+
});
|
|
125
|
+
return res.data;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 统一下单
|
|
130
|
+
* @param {String} openid 开放平台编号
|
|
131
|
+
* @param {Object} order 订单数据
|
|
132
|
+
* @return {Object} 用于JSSDK调用支付接口
|
|
133
|
+
* @see https://api.mch.weixin.qq.com/pay/unifiedorder
|
|
134
|
+
*/
|
|
135
|
+
async createOrder(openid, order) {
|
|
136
|
+
const {
|
|
137
|
+
ctx,
|
|
138
|
+
} = this;
|
|
139
|
+
const signedParams = this._firstSignOrder(openid, order);
|
|
140
|
+
const successXml = await ctx.curl(payUri, {
|
|
141
|
+
method: 'POST',
|
|
142
|
+
data: ctx.helper.json2xml(signedParams),
|
|
143
|
+
});
|
|
144
|
+
const json = ctx.helper.xml2json(successXml.data);
|
|
145
|
+
if (json.return_code === 'FAIL') {
|
|
146
|
+
return {
|
|
147
|
+
code: -1,
|
|
148
|
+
msg: json.return_msg,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
return this._secondSignOrder(json);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 生成配置签名
|
|
155
|
+
_createConfigSign(ticket, url) {
|
|
156
|
+
const {
|
|
157
|
+
service,
|
|
158
|
+
} = this;
|
|
159
|
+
const timestamp = parseInt(new Date().getTime() / 1000);
|
|
160
|
+
const params = {
|
|
161
|
+
jsapi_ticket: ticket,
|
|
162
|
+
url,
|
|
163
|
+
timestamp,
|
|
164
|
+
noncestr: service.wechat.sign.createNonceStr(),
|
|
165
|
+
};
|
|
166
|
+
params.signature = service.wechat.sign.getConfigSign(params); // 配置签名,用于Web端调用接口
|
|
167
|
+
return params;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// 生成支付签名
|
|
171
|
+
_firstSignOrder(openid, order) {
|
|
172
|
+
const {
|
|
173
|
+
app,
|
|
174
|
+
ctx,
|
|
175
|
+
service,
|
|
176
|
+
} = this;
|
|
177
|
+
const {
|
|
178
|
+
appId,
|
|
179
|
+
mchId,
|
|
180
|
+
notifyUrl,
|
|
181
|
+
} = app.config.wechat;
|
|
182
|
+
const params = {
|
|
183
|
+
openid: openid || '',
|
|
184
|
+
appid: appId,
|
|
185
|
+
mch_id: mchId,
|
|
186
|
+
nonce_str: service.wechat.sign.createNonceStr(),
|
|
187
|
+
body: order.body || '我是测试商品',
|
|
188
|
+
out_trade_no: order.tradeNo || new Date().getTime(), // 内部订单号
|
|
189
|
+
total_fee: order.totalFee || 1, // 单位为分的标价金额
|
|
190
|
+
spbill_create_ip: ctx.ip || '127.0.0.1', // 支付提交用户端ip
|
|
191
|
+
notify_url: notifyUrl, // 异步接收微信支付结果通知
|
|
192
|
+
trade_type: 'JSAPI',
|
|
193
|
+
};
|
|
194
|
+
params.sign = service.wechat.sign.getPaySign(params); // 订单签名,用于验证支付通知
|
|
195
|
+
return params;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// 第二次签名
|
|
199
|
+
_secondSignOrder(json) {
|
|
200
|
+
const {
|
|
201
|
+
app,
|
|
202
|
+
service,
|
|
203
|
+
} = this;
|
|
204
|
+
const {
|
|
205
|
+
appId,
|
|
206
|
+
} = app.config.wechat;
|
|
207
|
+
const res = {
|
|
208
|
+
appId,
|
|
209
|
+
timeStamp: service.wechat.sign.createTimestamp(),
|
|
210
|
+
nonceStr: json.nonce_str,
|
|
211
|
+
package: `prepay_id=${json.prepay_id}`,
|
|
212
|
+
signType: 'MD5',
|
|
213
|
+
}; // 不能随意增减,必须是这些字段
|
|
214
|
+
res.paySign = service.wechat.sign.getPaySign(res); // 第二次签名,用于提交到微信
|
|
215
|
+
return res;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
module.exports = WCSService;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 微信公众平台的相关配置
|
|
5
|
+
* @member Config Egg配置
|
|
6
|
+
* @property {String} appId - 应用号
|
|
7
|
+
* @property {number} appSecret - 应用密钥
|
|
8
|
+
* @property {number} mchId - 商户平台商家编号
|
|
9
|
+
* @property {number} apiKey - 商户支付接口密钥
|
|
10
|
+
* @property {number} notifyUrl - 支付结果回调地址
|
|
11
|
+
*/
|
|
12
|
+
exports.wechat = {
|
|
13
|
+
appId: '',
|
|
14
|
+
appSecret: '',
|
|
15
|
+
mchId: '',
|
|
16
|
+
apiKey: '',
|
|
17
|
+
notifyUrl: '',
|
|
18
|
+
// 开放平台第三方平台配置
|
|
19
|
+
componentAppId: '',
|
|
20
|
+
componentAppSecret: '',
|
|
21
|
+
componentToken: '',
|
|
22
|
+
componentEncodingAESKey: '',
|
|
23
|
+
};
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import MPService = require('./app/service/wechat/mp');
|
|
2
|
+
import WCSService = require('./app/service/wechat/wcs');
|
|
3
|
+
import SignService = require('./app/service/wechat/sign');
|
|
4
|
+
import ComponentService = require('./app/service/wechat/component');
|
|
5
|
+
import SecurityService = require('./app/service/wechat/security');
|
|
6
|
+
|
|
7
|
+
declare module 'egg' {
|
|
8
|
+
// extend service
|
|
9
|
+
interface IService {
|
|
10
|
+
wechat: {
|
|
11
|
+
mp: MPService;
|
|
12
|
+
wcs: WCSService;
|
|
13
|
+
sign: SignService;
|
|
14
|
+
component: ComponentService;
|
|
15
|
+
security: SecurityService;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ddn/egg-wechat",
|
|
3
|
+
"version": "1.0.24",
|
|
4
|
+
"description": "Eggjs plugin for wechat",
|
|
5
|
+
"types": "./index.d.ts",
|
|
6
|
+
"eggPlugin": {
|
|
7
|
+
"name": "wechat"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"egg",
|
|
11
|
+
"egg-plugin",
|
|
12
|
+
"egg-wx",
|
|
13
|
+
"egg-weixin",
|
|
14
|
+
"egg-wechat",
|
|
15
|
+
"wechat",
|
|
16
|
+
"wxapp"
|
|
17
|
+
],
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"xml-js": "^1.6.11"
|
|
20
|
+
},
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"egg": ">=3.0.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"autod": "^3.1.0",
|
|
26
|
+
"autod-egg": "^1.1.0",
|
|
27
|
+
"egg": "^3.0.0",
|
|
28
|
+
"egg-bin": "^6.13.0",
|
|
29
|
+
"egg-ci": "^1.13.0",
|
|
30
|
+
"egg-mock": "^5.15.2",
|
|
31
|
+
"eslint": "^6.2.1",
|
|
32
|
+
"eslint-config-egg": "^7.4.1",
|
|
33
|
+
"lodash": "^4.17.14"
|
|
34
|
+
},
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=8.0.0"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"test": "npm run lint -- --fix && egg-bin pkgfiles && npm run test-local",
|
|
40
|
+
"test-local": "egg-bin test",
|
|
41
|
+
"cov": "egg-bin cov",
|
|
42
|
+
"lint": "eslint .",
|
|
43
|
+
"ci": "egg-bin pkgfiles --check && npm run lint && npm run cov",
|
|
44
|
+
"pkgfiles": "egg-bin pkgfiles",
|
|
45
|
+
"autod": "autod"
|
|
46
|
+
},
|
|
47
|
+
"files": [
|
|
48
|
+
"app",
|
|
49
|
+
"config",
|
|
50
|
+
"index.d.ts"
|
|
51
|
+
],
|
|
52
|
+
"ci": {
|
|
53
|
+
"version": "8, 10"
|
|
54
|
+
},
|
|
55
|
+
"repository": {
|
|
56
|
+
"type": "git",
|
|
57
|
+
"url": "git+https://github.com/ddnlink/egg-wechat.git"
|
|
58
|
+
},
|
|
59
|
+
"bugs": {
|
|
60
|
+
"url": "https://github.com/ddnlink/egg-wechat/issues"
|
|
61
|
+
},
|
|
62
|
+
"homepage": "https://github.com/ddnlink/egg-wechat#readme",
|
|
63
|
+
"author": "ddnlink <kubying@qq.com>",
|
|
64
|
+
"license": "MIT"
|
|
65
|
+
}
|