@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 } = app.config.wechat;
14
-
15
- if (!componentAppId || !componentAppSecret) {
16
- throw new Error('未配置微信开放平台 Component AppID 或 Secret');
17
- }
73
+ const { componentAppId, componentAppSecret } = this.wechatConfig;
74
+
75
+ this._assertComponentConfig();
18
76
 
19
- const cacheKey = `wechat:component_access_token:${componentAppId}`;
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
- // 获取 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
- }
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
- await app.redis.set(cacheKey, token, 'EX', expiresIn - 300);
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 } = app.config.wechat;
67
-
134
+ const { componentAppId } = this.wechatConfig;
135
+
68
136
  if (!componentAppId) return;
69
137
 
70
- const key = `wechat:component_verify_ticket:${componentAppId}`;
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 } = app.config.wechat;
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
- return result.data.pre_auth_code;
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: 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 { app } = this;
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
- return `https://mp.weixin.qq.com/cgi-bin/componentloginpage?component_appid=${componentAppId}&pre_auth_code=${preAuthCode}&redirect_uri=${encodedRedirectUri}&auth_type=${authType}`;
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
 
@@ -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.24",
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 && egg-bin pkgfiles && npm run test-local",
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": "egg-bin pkgfiles --check && npm run lint && npm run cov",
44
- "pkgfiles": "egg-bin pkgfiles",
43
+ "ci": "npm run lint && npm run cov",
44
+ "pkgfiles": "echo 'pkgfiles step skipped'",
45
45
  "autod": "autod"
46
46
  },
47
47
  "files": [