@ddn/egg-wechat 1.0.24 → 1.0.25
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.
|
@@ -3,33 +3,93 @@
|
|
|
3
3
|
const Service = require('egg').Service;
|
|
4
4
|
|
|
5
5
|
class ComponentService extends Service {
|
|
6
|
-
|
|
6
|
+
|
|
7
|
+
get wechatConfig() {
|
|
8
|
+
return this.app.config.wechat || {};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Redis Key 策略:
|
|
13
|
+
* - scoped(默认):key 中包含 componentAppId,适合多应用隔离
|
|
14
|
+
* - legacy:兼容历史项目(如 ddn-hub)使用的固定 key
|
|
15
|
+
*/
|
|
16
|
+
get redisKeyStrategy() {
|
|
17
|
+
return this.wechatConfig.redisKeyStrategy || 'scoped';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
get authUrlReturnObject() {
|
|
21
|
+
return Boolean(this.wechatConfig.authUrlReturnObject);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
_assertComponentConfig() {
|
|
25
|
+
const { componentAppId, componentAppSecret } = this.wechatConfig;
|
|
26
|
+
if (!componentAppId || !componentAppSecret) {
|
|
27
|
+
throw new Error('未配置微信开放平台 Component AppID 或 Secret');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
_getKeyComponentAccessToken(componentAppId) {
|
|
32
|
+
if (this.redisKeyStrategy === 'legacy') return 'wechat:component:access_token';
|
|
33
|
+
return `wechat:component_access_token:${componentAppId}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
_getKeyComponentVerifyTicket(componentAppId) {
|
|
37
|
+
if (this.redisKeyStrategy === 'legacy') return 'wechat:component:verify_ticket';
|
|
38
|
+
return `wechat:component_verify_ticket:${componentAppId}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
_getKeyPreAuthCode(componentAppId) {
|
|
42
|
+
if (this.redisKeyStrategy === 'legacy') return 'wechat:component:pre_auth_code';
|
|
43
|
+
return `wechat:component_preauth_code:${componentAppId}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
_getKeyAuthorizerAccessToken(authorizerAppId) {
|
|
47
|
+
if (this.redisKeyStrategy === 'legacy') return `wechat:authorizer:${authorizerAppId}:access_token`;
|
|
48
|
+
return `wechat:authorizer_access_token:${authorizerAppId}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async getComponentVerifyTicket() {
|
|
52
|
+
const { app } = this;
|
|
53
|
+
const { componentAppId } = this.wechatConfig;
|
|
54
|
+
this._assertComponentConfig();
|
|
55
|
+
|
|
56
|
+
const primaryKey = this._getKeyComponentVerifyTicket(componentAppId);
|
|
57
|
+
const ticket = await app.redis.get(primaryKey);
|
|
58
|
+
if (ticket) return ticket;
|
|
59
|
+
|
|
60
|
+
// 兼容旧 key(历史项目可能写入该 key)
|
|
61
|
+
const legacyCompat = await app.redis.get('wechat:component:ticket');
|
|
62
|
+
if (legacyCompat) return legacyCompat;
|
|
63
|
+
|
|
64
|
+
throw new Error('未收到微信推送的 Component Verify Ticket,请等待微信服务器推送');
|
|
65
|
+
}
|
|
66
|
+
|
|
7
67
|
/**
|
|
8
68
|
* 获取 Component Access Token
|
|
9
69
|
* 优先从 Redis 获取,过期则刷新
|
|
10
70
|
*/
|
|
11
71
|
async getComponentAccessToken() {
|
|
12
72
|
const { ctx, app } = this;
|
|
13
|
-
const { componentAppId, componentAppSecret } =
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
throw new Error('未配置微信开放平台 Component AppID 或 Secret');
|
|
17
|
-
}
|
|
73
|
+
const { componentAppId, componentAppSecret } = this.wechatConfig;
|
|
74
|
+
|
|
75
|
+
this._assertComponentConfig();
|
|
18
76
|
|
|
19
|
-
const cacheKey =
|
|
77
|
+
const cacheKey = this._getKeyComponentAccessToken(componentAppId);
|
|
20
78
|
let token = await app.redis.get(cacheKey);
|
|
21
79
|
|
|
80
|
+
// 兼容策略切换:当从 scoped 切到 legacy(或反向)时,兜底尝试另一个 key
|
|
81
|
+
if (!token) {
|
|
82
|
+
const fallbackKey = this.redisKeyStrategy === 'legacy'
|
|
83
|
+
? `wechat:component_access_token:${componentAppId}`
|
|
84
|
+
: 'wechat:component:access_token';
|
|
85
|
+
token = await app.redis.get(fallbackKey);
|
|
86
|
+
}
|
|
87
|
+
|
|
22
88
|
if (token) {
|
|
23
89
|
return token;
|
|
24
90
|
}
|
|
25
91
|
|
|
26
|
-
|
|
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
|
-
}
|
|
92
|
+
const ticket = await this.getComponentVerifyTicket();
|
|
33
93
|
|
|
34
94
|
// 请求微信接口获取 Token
|
|
35
95
|
const url = 'https://api.weixin.qq.com/cgi-bin/component/api_component_token';
|
|
@@ -40,8 +100,8 @@ class ComponentService extends Service {
|
|
|
40
100
|
data: {
|
|
41
101
|
component_appid: componentAppId,
|
|
42
102
|
component_appsecret: componentAppSecret,
|
|
43
|
-
component_verify_ticket: ticket
|
|
44
|
-
}
|
|
103
|
+
component_verify_ticket: ticket,
|
|
104
|
+
},
|
|
45
105
|
});
|
|
46
106
|
|
|
47
107
|
if (result.data.errcode) {
|
|
@@ -52,7 +112,15 @@ class ComponentService extends Service {
|
|
|
52
112
|
const expiresIn = result.data.expires_in;
|
|
53
113
|
|
|
54
114
|
// 缓存 Token (提前 5 分钟过期)
|
|
55
|
-
|
|
115
|
+
const ttl = Math.max(Number(expiresIn || 7200) - 300, 60);
|
|
116
|
+
await app.redis.set(cacheKey, token, 'EX', ttl);
|
|
117
|
+
|
|
118
|
+
// legacy / scoped 兼容:同时写入兼容 key,避免灰度期间读取不到
|
|
119
|
+
if (this.redisKeyStrategy === 'legacy') {
|
|
120
|
+
await app.redis.set(`wechat:component_access_token:${componentAppId}`, token, 'EX', ttl);
|
|
121
|
+
} else {
|
|
122
|
+
await app.redis.set('wechat:component:access_token', token, 'EX', ttl);
|
|
123
|
+
}
|
|
56
124
|
|
|
57
125
|
return token;
|
|
58
126
|
}
|
|
@@ -63,16 +131,23 @@ class ComponentService extends Service {
|
|
|
63
131
|
*/
|
|
64
132
|
async saveComponentVerifyTicket(ticket) {
|
|
65
133
|
const { app } = this;
|
|
66
|
-
const { componentAppId } =
|
|
67
|
-
|
|
134
|
+
const { componentAppId } = this.wechatConfig;
|
|
135
|
+
|
|
68
136
|
if (!componentAppId) return;
|
|
69
137
|
|
|
70
|
-
const key =
|
|
138
|
+
const key = this._getKeyComponentVerifyTicket(componentAppId);
|
|
71
139
|
// Ticket 有效期通常为 12 小时,这里设置 12 小时过期
|
|
72
140
|
await app.redis.set(key, ticket, 'EX', 12 * 3600);
|
|
73
|
-
|
|
141
|
+
|
|
74
142
|
// 兼容旧的 Redis Key
|
|
75
143
|
await app.redis.set('wechat:component:ticket', ticket, 'EX', 12 * 3600);
|
|
144
|
+
|
|
145
|
+
// legacy / scoped 兼容:同时写入兼容 key,避免灰度期间读取不到
|
|
146
|
+
if (this.redisKeyStrategy === 'legacy') {
|
|
147
|
+
await app.redis.set(`wechat:component_verify_ticket:${componentAppId}`, ticket, 'EX', 12 * 3600);
|
|
148
|
+
} else {
|
|
149
|
+
await app.redis.set('wechat:component:verify_ticket', ticket, 'EX', 12 * 3600);
|
|
150
|
+
}
|
|
76
151
|
}
|
|
77
152
|
|
|
78
153
|
/**
|
|
@@ -80,25 +155,112 @@ class ComponentService extends Service {
|
|
|
80
155
|
*/
|
|
81
156
|
async getPreAuthCode() {
|
|
82
157
|
const { ctx, app } = this;
|
|
83
|
-
const { componentAppId } =
|
|
84
|
-
|
|
158
|
+
const { componentAppId } = this.wechatConfig;
|
|
159
|
+
|
|
160
|
+
const cacheKey = this._getKeyPreAuthCode(componentAppId);
|
|
161
|
+
let cached = await app.redis.get(cacheKey);
|
|
162
|
+
if (!cached) {
|
|
163
|
+
const fallbackKey = this.redisKeyStrategy === 'legacy'
|
|
164
|
+
? `wechat:component_preauth_code:${componentAppId}`
|
|
165
|
+
: 'wechat:component:pre_auth_code';
|
|
166
|
+
cached = await app.redis.get(fallbackKey);
|
|
167
|
+
}
|
|
168
|
+
if (cached) return cached;
|
|
169
|
+
|
|
85
170
|
const token = await this.getComponentAccessToken();
|
|
86
|
-
|
|
171
|
+
|
|
87
172
|
const url = `https://api.weixin.qq.com/cgi-bin/component/api_create_preauthcode?component_access_token=${token}`;
|
|
88
173
|
const result = await ctx.curl(url, {
|
|
89
174
|
method: 'POST',
|
|
90
175
|
contentType: 'json',
|
|
91
176
|
dataType: 'json',
|
|
92
177
|
data: {
|
|
93
|
-
component_appid: componentAppId
|
|
94
|
-
}
|
|
178
|
+
component_appid: componentAppId,
|
|
179
|
+
},
|
|
95
180
|
});
|
|
96
181
|
|
|
97
182
|
if (result.data.errcode) {
|
|
98
183
|
throw new Error(`获取预授权码失败: ${result.data.errmsg}`);
|
|
99
184
|
}
|
|
100
185
|
|
|
101
|
-
|
|
186
|
+
const preAuthCode = result.data.pre_auth_code;
|
|
187
|
+
const expiresIn = Number(result.data.expires_in || 600);
|
|
188
|
+
const ttl = Math.max(expiresIn - 30, 60);
|
|
189
|
+
|
|
190
|
+
await app.redis.set(cacheKey, preAuthCode, 'EX', ttl);
|
|
191
|
+
|
|
192
|
+
// legacy / scoped 兼容:同时写入兼容 key
|
|
193
|
+
if (this.redisKeyStrategy === 'legacy') {
|
|
194
|
+
await app.redis.set(`wechat:component_preauth_code:${componentAppId}`, preAuthCode, 'EX', ttl);
|
|
195
|
+
} else {
|
|
196
|
+
await app.redis.set('wechat:component:pre_auth_code', preAuthCode, 'EX', ttl);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return preAuthCode;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* 获取授权方 authorizer_access_token(缓存)
|
|
204
|
+
* https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/api/api_authorizer_token.html
|
|
205
|
+
*/
|
|
206
|
+
async getAuthorizerAccessToken(authorizerAppId, authorizerRefreshToken) {
|
|
207
|
+
const { ctx, app } = this;
|
|
208
|
+
this._assertComponentConfig();
|
|
209
|
+
|
|
210
|
+
if (!authorizerAppId) {
|
|
211
|
+
throw new Error('authorizerAppId is required');
|
|
212
|
+
}
|
|
213
|
+
if (!authorizerRefreshToken) {
|
|
214
|
+
throw new Error('authorizerRefreshToken is required');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const cacheKey = this._getKeyAuthorizerAccessToken(authorizerAppId);
|
|
218
|
+
let cached = await app.redis.get(cacheKey);
|
|
219
|
+
if (!cached) {
|
|
220
|
+
const fallbackKey = this.redisKeyStrategy === 'legacy'
|
|
221
|
+
? `wechat:authorizer_access_token:${authorizerAppId}`
|
|
222
|
+
: `wechat:authorizer:${authorizerAppId}:access_token`;
|
|
223
|
+
cached = await app.redis.get(fallbackKey);
|
|
224
|
+
}
|
|
225
|
+
if (cached) {
|
|
226
|
+
return { authorizer_access_token: cached, from_cache: true };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const { componentAppId } = this.wechatConfig;
|
|
230
|
+
const componentAccessToken = await this.getComponentAccessToken();
|
|
231
|
+
const url = `https://api.weixin.qq.com/cgi-bin/component/api_authorizer_token?component_access_token=${encodeURIComponent(componentAccessToken)}`;
|
|
232
|
+
|
|
233
|
+
const result = await ctx.curl(url, {
|
|
234
|
+
method: 'POST',
|
|
235
|
+
contentType: 'json',
|
|
236
|
+
dataType: 'json',
|
|
237
|
+
data: {
|
|
238
|
+
component_appid: componentAppId,
|
|
239
|
+
authorizer_appid: authorizerAppId,
|
|
240
|
+
authorizer_refresh_token: authorizerRefreshToken,
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
const data = result.data || {};
|
|
245
|
+
if (data.errcode) {
|
|
246
|
+
throw new Error(`获取 authorizer_access_token 失败: ${data.errmsg}`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const token = data.authorizer_access_token;
|
|
250
|
+
const expiresIn = Number(data.expires_in || 7200);
|
|
251
|
+
const ttl = Math.max(expiresIn - 300, 60);
|
|
252
|
+
|
|
253
|
+
if (token) {
|
|
254
|
+
await app.redis.set(cacheKey, token, 'EX', ttl);
|
|
255
|
+
// legacy / scoped 兼容:同时写入兼容 key
|
|
256
|
+
if (this.redisKeyStrategy === 'legacy') {
|
|
257
|
+
await app.redis.set(`wechat:authorizer_access_token:${authorizerAppId}`, token, 'EX', ttl);
|
|
258
|
+
} else {
|
|
259
|
+
await app.redis.set(`wechat:authorizer:${authorizerAppId}:access_token`, token, 'EX', ttl);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return data;
|
|
102
264
|
}
|
|
103
265
|
|
|
104
266
|
/**
|
|
@@ -111,15 +273,15 @@ class ComponentService extends Service {
|
|
|
111
273
|
|
|
112
274
|
const token = await this.getComponentAccessToken();
|
|
113
275
|
const url = `https://api.weixin.qq.com/cgi-bin/component/api_query_auth?component_access_token=${token}`;
|
|
114
|
-
|
|
276
|
+
|
|
115
277
|
const result = await ctx.curl(url, {
|
|
116
278
|
method: 'POST',
|
|
117
279
|
contentType: 'json',
|
|
118
280
|
dataType: 'json',
|
|
119
281
|
data: {
|
|
120
282
|
component_appid: componentAppId,
|
|
121
|
-
authorization_code: authCode
|
|
122
|
-
}
|
|
283
|
+
authorization_code: authCode,
|
|
284
|
+
},
|
|
123
285
|
});
|
|
124
286
|
|
|
125
287
|
if (result.data.errcode) {
|
|
@@ -139,15 +301,15 @@ class ComponentService extends Service {
|
|
|
139
301
|
|
|
140
302
|
const token = await this.getComponentAccessToken();
|
|
141
303
|
const url = `https://api.weixin.qq.com/cgi-bin/component/api_get_authorizer_info?component_access_token=${token}`;
|
|
142
|
-
|
|
304
|
+
|
|
143
305
|
const result = await ctx.curl(url, {
|
|
144
306
|
method: 'POST',
|
|
145
307
|
contentType: 'json',
|
|
146
308
|
dataType: 'json',
|
|
147
309
|
data: {
|
|
148
310
|
component_appid: componentAppId,
|
|
149
|
-
authorizer_appid: authorizerAppId
|
|
150
|
-
}
|
|
311
|
+
authorizer_appid: authorizerAppId,
|
|
312
|
+
},
|
|
151
313
|
});
|
|
152
314
|
|
|
153
315
|
if (result.data.errcode) {
|
|
@@ -166,7 +328,7 @@ class ComponentService extends Service {
|
|
|
166
328
|
async sendCustomMessage(accessToken, toUser, content) {
|
|
167
329
|
const { ctx } = this;
|
|
168
330
|
const url = `https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=${accessToken}`;
|
|
169
|
-
|
|
331
|
+
|
|
170
332
|
const result = await ctx.curl(url, {
|
|
171
333
|
method: 'POST',
|
|
172
334
|
contentType: 'json',
|
|
@@ -175,9 +337,9 @@ class ComponentService extends Service {
|
|
|
175
337
|
touser: toUser,
|
|
176
338
|
msgtype: 'text',
|
|
177
339
|
text: {
|
|
178
|
-
content
|
|
179
|
-
}
|
|
180
|
-
}
|
|
340
|
+
content,
|
|
341
|
+
},
|
|
342
|
+
},
|
|
181
343
|
});
|
|
182
344
|
|
|
183
345
|
return result.data;
|
|
@@ -203,12 +365,15 @@ class ComponentService extends Service {
|
|
|
203
365
|
* @param {number} authType 授权类型 (1: 公众号, 2: 小程序, 3: 两者都可)
|
|
204
366
|
*/
|
|
205
367
|
async getAuthUrl(redirectUri, authType = 3) {
|
|
206
|
-
const {
|
|
207
|
-
const { componentAppId } = app.config.wechat;
|
|
368
|
+
const { componentAppId } = this.wechatConfig;
|
|
208
369
|
const preAuthCode = await this.getPreAuthCode();
|
|
209
370
|
const encodedRedirectUri = encodeURIComponent(redirectUri);
|
|
210
|
-
|
|
211
|
-
|
|
371
|
+
|
|
372
|
+
const url = `https://mp.weixin.qq.com/cgi-bin/componentloginpage?component_appid=${componentAppId}&pre_auth_code=${preAuthCode}&redirect_uri=${encodedRedirectUri}&auth_type=${authType}`;
|
|
373
|
+
if (this.authUrlReturnObject) {
|
|
374
|
+
return { pc: url, mobile: url };
|
|
375
|
+
}
|
|
376
|
+
return url;
|
|
212
377
|
}
|
|
213
378
|
}
|
|
214
379
|
|
package/app/service/wechat/mp.js
CHANGED
|
@@ -168,7 +168,7 @@ class MPService extends Service {
|
|
|
168
168
|
nonce_str: service.wechat.sign.createNonceStr(),
|
|
169
169
|
out_trade_no: data.tradeNo || new Date().getTime(), // 内部订单号
|
|
170
170
|
total_fee: data.totalFee || 1, // 单位为分的标价金额
|
|
171
|
-
body: data.body || '未知产品-测试商品', // 应用市场上的APP名字-商品概述
|
|
171
|
+
body: data.body || '未知产品-测试商品', // 应用市场上的APP名字-商品概述
|
|
172
172
|
spbill_create_ip: ctx.ip, // 支付提交用户端ip
|
|
173
173
|
notify_url: data.notifyUrl || '', // 异步接收微信支付结果通知
|
|
174
174
|
trade_type: 'JSAPI',
|
|
@@ -27,8 +27,8 @@ class WCSService extends Service {
|
|
|
27
27
|
} = this.app.config.wechat;
|
|
28
28
|
const url = `${tokenUri}?grant_type=client_credential&appid=${appId}&secret=${appSecret}`;
|
|
29
29
|
const res = await this.ctx.curl(url, jsonType);
|
|
30
|
-
if (res.data.errcode){
|
|
31
|
-
throw new Error(res.data.errmsg)
|
|
30
|
+
if (res.data.errcode) {
|
|
31
|
+
throw new Error(res.data.errmsg);
|
|
32
32
|
}
|
|
33
33
|
return res.data;
|
|
34
34
|
}
|
|
@@ -42,8 +42,8 @@ class WCSService extends Service {
|
|
|
42
42
|
async getTicket(token) {
|
|
43
43
|
const url = `${ticketUri}?access_token=${token}&type=jsapi`;
|
|
44
44
|
const res = await this.ctx.curl(url, jsonType);
|
|
45
|
-
if (res.data.errcode){
|
|
46
|
-
throw new Error(res.data.errmsg)
|
|
45
|
+
if (res.data.errcode) {
|
|
46
|
+
throw new Error(res.data.errmsg);
|
|
47
47
|
}
|
|
48
48
|
return res.data;
|
|
49
49
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ddn/egg-wechat",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.25",
|
|
4
4
|
"description": "Eggjs plugin for wechat",
|
|
5
5
|
"types": "./index.d.ts",
|
|
6
6
|
"eggPlugin": {
|
|
@@ -36,12 +36,12 @@
|
|
|
36
36
|
"node": ">=8.0.0"
|
|
37
37
|
},
|
|
38
38
|
"scripts": {
|
|
39
|
-
"test": "npm run lint -- --fix &&
|
|
39
|
+
"test": "npm run lint -- --fix && npm run test-local",
|
|
40
40
|
"test-local": "egg-bin test",
|
|
41
41
|
"cov": "egg-bin cov",
|
|
42
42
|
"lint": "eslint .",
|
|
43
|
-
"ci": "
|
|
44
|
-
"pkgfiles": "
|
|
43
|
+
"ci": "npm run lint && npm run cov",
|
|
44
|
+
"pkgfiles": "echo 'pkgfiles step skipped'",
|
|
45
45
|
"autod": "autod"
|
|
46
46
|
},
|
|
47
47
|
"files": [
|