@ddn/egg-wechat 1.0.24 → 1.0.28
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/README.md +21 -0
- package/app/service/wechat/component.js +352 -40
- package/app/service/wechat/mp.js +1 -1
- package/app/service/wechat/platform_http.js +821 -0
- package/app/service/wechat/wcs.js +4 -4
- package/app.js +198 -0
- package/config/config.default.js +8 -0
- package/package.json +6 -4
package/README.md
CHANGED
|
@@ -63,6 +63,27 @@ exports.wechat = {
|
|
|
63
63
|
componentAppSecret: '',
|
|
64
64
|
componentToken: '',
|
|
65
65
|
componentEncodingAESKey: '',
|
|
66
|
+
|
|
67
|
+
// ===== 可选:动态注入/覆盖配置(覆盖所有模块) =====
|
|
68
|
+
// 场景:配置存储在数据库/远程配置中心,不希望只靠环境变量。
|
|
69
|
+
// init 支持同步/异步,返回的对象会合并进 wechat 配置。
|
|
70
|
+
init: async (app) => {
|
|
71
|
+
// 例:从你自己的配置系统拉取(伪代码)
|
|
72
|
+
// const ctx = app.createAnonymousContext();
|
|
73
|
+
// const componentAppId = await ctx.service.config.get('wechat.component.appid');
|
|
74
|
+
// const componentAppSecret = await ctx.service.config.get('wechat.component.secret');
|
|
75
|
+
// return { componentAppId, componentAppSecret };
|
|
76
|
+
return {};
|
|
77
|
+
},
|
|
78
|
+
// 或者用静态 override(无需异步)
|
|
79
|
+
// override: { componentAppId: 'wx...', componentAppSecret: '...' },
|
|
80
|
+
|
|
81
|
+
// ===== 可选:运行时从统一配置读取(无需把密钥写进 config.wechat) =====
|
|
82
|
+
// 若宿主项目存在 ctx.service.config.unifiedConfig.get(key)(例如 ddn-hub),
|
|
83
|
+
// 插件可在运行时读取 wechat_platform.* 并周期刷新到内存。
|
|
84
|
+
// 注意:这是“读取方式”的开关,不要求你在 config.wechat 里填 appid/secret。
|
|
85
|
+
useUnifiedConfig: true,
|
|
86
|
+
unifiedConfigRefreshIntervalMs: 30000,
|
|
66
87
|
};
|
|
67
88
|
```
|
|
68
89
|
|
|
@@ -3,34 +3,171 @@
|
|
|
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
|
+
get mockMode() {
|
|
25
|
+
const v = this.wechatConfig.mockMode;
|
|
26
|
+
if (typeof v === 'string') return v === '1' || v.toLowerCase() === 'true';
|
|
27
|
+
return Boolean(v);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
_assertComponentConfig() {
|
|
31
|
+
const { componentAppId, componentAppSecret } = this.wechatConfig;
|
|
32
|
+
if (!componentAppId || !componentAppSecret) {
|
|
33
|
+
throw new Error('未配置微信开放平台 Component AppID 或 Secret');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
_getKeyComponentAccessToken(componentAppId) {
|
|
38
|
+
if (this.redisKeyStrategy === 'legacy') return 'wechat:component:access_token';
|
|
39
|
+
return `wechat:component_access_token:${componentAppId}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
_getKeyComponentVerifyTicket(componentAppId) {
|
|
43
|
+
if (this.redisKeyStrategy === 'legacy') return 'wechat:component:verify_ticket';
|
|
44
|
+
return `wechat:component_verify_ticket:${componentAppId}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
_getKeyPreAuthCode(componentAppId) {
|
|
48
|
+
if (this.redisKeyStrategy === 'legacy') return 'wechat:component:pre_auth_code';
|
|
49
|
+
return `wechat:component_preauth_code:${componentAppId}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
_getKeyAuthorizerAccessToken(authorizerAppId) {
|
|
53
|
+
if (this.redisKeyStrategy === 'legacy') return `wechat:authorizer:${authorizerAppId}:access_token`;
|
|
54
|
+
return `wechat:authorizer_access_token:${authorizerAppId}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async getComponentVerifyTicket() {
|
|
58
|
+
const { app } = this;
|
|
59
|
+
const { componentAppId } = this.wechatConfig;
|
|
60
|
+
this._assertComponentConfig();
|
|
61
|
+
|
|
62
|
+
const logger = app.logger || app.coreLogger;
|
|
63
|
+
|
|
64
|
+
const primaryKey = this._getKeyComponentVerifyTicket(componentAppId);
|
|
65
|
+
const ticket = await app.redis.get(primaryKey);
|
|
66
|
+
if (ticket) {
|
|
67
|
+
if (logger && logger.info) logger.info('[egg-wechat] component_verify_ticket hit', {
|
|
68
|
+
redisKeyStrategy: this.redisKeyStrategy,
|
|
69
|
+
componentAppId,
|
|
70
|
+
key: primaryKey,
|
|
71
|
+
ticketLength: String(ticket).length,
|
|
72
|
+
ticketPrefix: String(ticket).slice(0, 8) + '***',
|
|
73
|
+
});
|
|
74
|
+
return ticket;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 兼容旧 key(历史项目可能写入该 key)
|
|
78
|
+
const legacyCompat = await app.redis.get('wechat:component:ticket');
|
|
79
|
+
if (legacyCompat) {
|
|
80
|
+
if (logger && logger.info) logger.info('[egg-wechat] component_verify_ticket hit (legacy compat)', {
|
|
81
|
+
redisKeyStrategy: this.redisKeyStrategy,
|
|
82
|
+
componentAppId,
|
|
83
|
+
key: 'wechat:component:ticket',
|
|
84
|
+
ticketLength: String(legacyCompat).length,
|
|
85
|
+
ticketPrefix: String(legacyCompat).slice(0, 8) + '***',
|
|
86
|
+
});
|
|
87
|
+
return legacyCompat;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (logger && logger.info) logger.info('[egg-wechat] component_verify_ticket miss', {
|
|
91
|
+
redisKeyStrategy: this.redisKeyStrategy,
|
|
92
|
+
componentAppId,
|
|
93
|
+
triedKeys: [ primaryKey, 'wechat:component:ticket' ],
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
throw new Error('未收到微信推送的 Component Verify Ticket,请等待微信服务器推送');
|
|
97
|
+
}
|
|
98
|
+
|
|
7
99
|
/**
|
|
8
100
|
* 获取 Component Access Token
|
|
9
101
|
* 优先从 Redis 获取,过期则刷新
|
|
10
102
|
*/
|
|
11
103
|
async getComponentAccessToken() {
|
|
12
104
|
const { ctx, app } = this;
|
|
13
|
-
const { componentAppId, componentAppSecret } =
|
|
14
|
-
|
|
15
|
-
if (!componentAppId || !componentAppSecret) {
|
|
16
|
-
throw new Error('未配置微信开放平台 Component AppID 或 Secret');
|
|
17
|
-
}
|
|
105
|
+
const { componentAppId, componentAppSecret } = this.wechatConfig;
|
|
18
106
|
|
|
19
|
-
|
|
107
|
+
this._assertComponentConfig();
|
|
108
|
+
|
|
109
|
+
const logger = app.logger || app.coreLogger;
|
|
110
|
+
|
|
111
|
+
const cacheKey = this._getKeyComponentAccessToken(componentAppId);
|
|
20
112
|
let token = await app.redis.get(cacheKey);
|
|
21
113
|
|
|
114
|
+
// 兼容策略切换:当从 scoped 切到 legacy(或反向)时,兜底尝试另一个 key
|
|
115
|
+
if (!token) {
|
|
116
|
+
const fallbackKey = this.redisKeyStrategy === 'legacy'
|
|
117
|
+
? `wechat:component_access_token:${componentAppId}`
|
|
118
|
+
: 'wechat:component:access_token';
|
|
119
|
+
token = await app.redis.get(fallbackKey);
|
|
120
|
+
}
|
|
121
|
+
|
|
22
122
|
if (token) {
|
|
23
123
|
return token;
|
|
24
124
|
}
|
|
25
125
|
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
126
|
+
// 纯本地伪联调:不请求真实微信接口,直接返回 mock token 并缓存
|
|
127
|
+
if (this.mockMode) {
|
|
128
|
+
token = `mock_component_access_token_${componentAppId}_${Date.now()}`;
|
|
129
|
+
const ttl = 7200;
|
|
130
|
+
await app.redis.set(cacheKey, token, 'EX', ttl);
|
|
131
|
+
if (this.redisKeyStrategy === 'legacy') {
|
|
132
|
+
await app.redis.set(`wechat:component_access_token:${componentAppId}`, token, 'EX', ttl);
|
|
133
|
+
} else {
|
|
134
|
+
await app.redis.set('wechat:component:access_token', token, 'EX', ttl);
|
|
135
|
+
}
|
|
136
|
+
if (logger && logger.info) {
|
|
137
|
+
logger.info('[egg-wechat] mockMode enabled: return mocked component_access_token', {
|
|
138
|
+
componentAppId,
|
|
139
|
+
redisKeyStrategy: this.redisKeyStrategy,
|
|
140
|
+
ttl,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
return token;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const ticket = await this.getComponentVerifyTicket();
|
|
29
147
|
|
|
30
|
-
|
|
31
|
-
|
|
148
|
+
// 本地伪联调常用 mock ticket,但微信开放平台会直接判无效(errcode=61006)。
|
|
149
|
+
// 非 mockMode 时提前失败,提示如何拿到真实 ticket。
|
|
150
|
+
if (!this.mockMode && typeof ticket === 'string' && ticket.startsWith('mock_')) {
|
|
151
|
+
if (logger && logger.warn) {
|
|
152
|
+
logger.warn('[egg-wechat] component_verify_ticket looks like mock, abort calling wechat api', {
|
|
153
|
+
componentAppId,
|
|
154
|
+
ticketLength: ticket.length,
|
|
155
|
+
ticketPrefix: ticket.slice(0, 8) + '***',
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
throw new Error(
|
|
159
|
+
'当前使用 mock component_verify_ticket,无法向微信开放平台换取 component_access_token。' +
|
|
160
|
+
'请配置开放平台“授权事件接收 URL”可被微信公网访问,并等待微信推送真实 ticket 后再试。'
|
|
161
|
+
);
|
|
32
162
|
}
|
|
33
163
|
|
|
164
|
+
if (logger && logger.info) logger.info('[egg-wechat] component_access_token refreshing', {
|
|
165
|
+
redisKeyStrategy: this.redisKeyStrategy,
|
|
166
|
+
componentAppId,
|
|
167
|
+
ticketLength: String(ticket).length,
|
|
168
|
+
ticketPrefix: String(ticket).slice(0, 8) + '***',
|
|
169
|
+
});
|
|
170
|
+
|
|
34
171
|
// 请求微信接口获取 Token
|
|
35
172
|
const url = 'https://api.weixin.qq.com/cgi-bin/component/api_component_token';
|
|
36
173
|
const result = await ctx.curl(url, {
|
|
@@ -40,11 +177,16 @@ class ComponentService extends Service {
|
|
|
40
177
|
data: {
|
|
41
178
|
component_appid: componentAppId,
|
|
42
179
|
component_appsecret: componentAppSecret,
|
|
43
|
-
component_verify_ticket: ticket
|
|
44
|
-
}
|
|
180
|
+
component_verify_ticket: ticket,
|
|
181
|
+
},
|
|
45
182
|
});
|
|
46
183
|
|
|
47
184
|
if (result.data.errcode) {
|
|
185
|
+
if (logger && logger.warn) logger.warn('[egg-wechat] api_component_token failed', {
|
|
186
|
+
componentAppId,
|
|
187
|
+
errcode: result.data.errcode,
|
|
188
|
+
errmsg: result.data.errmsg,
|
|
189
|
+
});
|
|
48
190
|
throw new Error(`获取 Component Access Token 失败: ${result.data.errmsg}`);
|
|
49
191
|
}
|
|
50
192
|
|
|
@@ -52,7 +194,15 @@ class ComponentService extends Service {
|
|
|
52
194
|
const expiresIn = result.data.expires_in;
|
|
53
195
|
|
|
54
196
|
// 缓存 Token (提前 5 分钟过期)
|
|
55
|
-
|
|
197
|
+
const ttl = Math.max(Number(expiresIn || 7200) - 300, 60);
|
|
198
|
+
await app.redis.set(cacheKey, token, 'EX', ttl);
|
|
199
|
+
|
|
200
|
+
// legacy / scoped 兼容:同时写入兼容 key,避免灰度期间读取不到
|
|
201
|
+
if (this.redisKeyStrategy === 'legacy') {
|
|
202
|
+
await app.redis.set(`wechat:component_access_token:${componentAppId}`, token, 'EX', ttl);
|
|
203
|
+
} else {
|
|
204
|
+
await app.redis.set('wechat:component:access_token', token, 'EX', ttl);
|
|
205
|
+
}
|
|
56
206
|
|
|
57
207
|
return token;
|
|
58
208
|
}
|
|
@@ -63,16 +213,23 @@ class ComponentService extends Service {
|
|
|
63
213
|
*/
|
|
64
214
|
async saveComponentVerifyTicket(ticket) {
|
|
65
215
|
const { app } = this;
|
|
66
|
-
const { componentAppId } =
|
|
67
|
-
|
|
216
|
+
const { componentAppId } = this.wechatConfig;
|
|
217
|
+
|
|
68
218
|
if (!componentAppId) return;
|
|
69
219
|
|
|
70
|
-
const key =
|
|
220
|
+
const key = this._getKeyComponentVerifyTicket(componentAppId);
|
|
71
221
|
// Ticket 有效期通常为 12 小时,这里设置 12 小时过期
|
|
72
222
|
await app.redis.set(key, ticket, 'EX', 12 * 3600);
|
|
73
|
-
|
|
223
|
+
|
|
74
224
|
// 兼容旧的 Redis Key
|
|
75
225
|
await app.redis.set('wechat:component:ticket', ticket, 'EX', 12 * 3600);
|
|
226
|
+
|
|
227
|
+
// legacy / scoped 兼容:同时写入兼容 key,避免灰度期间读取不到
|
|
228
|
+
if (this.redisKeyStrategy === 'legacy') {
|
|
229
|
+
await app.redis.set(`wechat:component_verify_ticket:${componentAppId}`, ticket, 'EX', 12 * 3600);
|
|
230
|
+
} else {
|
|
231
|
+
await app.redis.set('wechat:component:verify_ticket', ticket, 'EX', 12 * 3600);
|
|
232
|
+
}
|
|
76
233
|
}
|
|
77
234
|
|
|
78
235
|
/**
|
|
@@ -80,25 +237,112 @@ class ComponentService extends Service {
|
|
|
80
237
|
*/
|
|
81
238
|
async getPreAuthCode() {
|
|
82
239
|
const { ctx, app } = this;
|
|
83
|
-
const { componentAppId } =
|
|
84
|
-
|
|
240
|
+
const { componentAppId } = this.wechatConfig;
|
|
241
|
+
|
|
242
|
+
const cacheKey = this._getKeyPreAuthCode(componentAppId);
|
|
243
|
+
let cached = await app.redis.get(cacheKey);
|
|
244
|
+
if (!cached) {
|
|
245
|
+
const fallbackKey = this.redisKeyStrategy === 'legacy'
|
|
246
|
+
? `wechat:component_preauth_code:${componentAppId}`
|
|
247
|
+
: 'wechat:component:pre_auth_code';
|
|
248
|
+
cached = await app.redis.get(fallbackKey);
|
|
249
|
+
}
|
|
250
|
+
if (cached) return cached;
|
|
251
|
+
|
|
85
252
|
const token = await this.getComponentAccessToken();
|
|
86
|
-
|
|
253
|
+
|
|
87
254
|
const url = `https://api.weixin.qq.com/cgi-bin/component/api_create_preauthcode?component_access_token=${token}`;
|
|
88
255
|
const result = await ctx.curl(url, {
|
|
89
256
|
method: 'POST',
|
|
90
257
|
contentType: 'json',
|
|
91
258
|
dataType: 'json',
|
|
92
259
|
data: {
|
|
93
|
-
component_appid: componentAppId
|
|
94
|
-
}
|
|
260
|
+
component_appid: componentAppId,
|
|
261
|
+
},
|
|
95
262
|
});
|
|
96
263
|
|
|
97
264
|
if (result.data.errcode) {
|
|
98
265
|
throw new Error(`获取预授权码失败: ${result.data.errmsg}`);
|
|
99
266
|
}
|
|
100
267
|
|
|
101
|
-
|
|
268
|
+
const preAuthCode = result.data.pre_auth_code;
|
|
269
|
+
const expiresIn = Number(result.data.expires_in || 600);
|
|
270
|
+
const ttl = Math.max(expiresIn - 30, 60);
|
|
271
|
+
|
|
272
|
+
await app.redis.set(cacheKey, preAuthCode, 'EX', ttl);
|
|
273
|
+
|
|
274
|
+
// legacy / scoped 兼容:同时写入兼容 key
|
|
275
|
+
if (this.redisKeyStrategy === 'legacy') {
|
|
276
|
+
await app.redis.set(`wechat:component_preauth_code:${componentAppId}`, preAuthCode, 'EX', ttl);
|
|
277
|
+
} else {
|
|
278
|
+
await app.redis.set('wechat:component:pre_auth_code', preAuthCode, 'EX', ttl);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return preAuthCode;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* 获取授权方 authorizer_access_token(缓存)
|
|
286
|
+
* https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/api/api_authorizer_token.html
|
|
287
|
+
*/
|
|
288
|
+
async getAuthorizerAccessToken(authorizerAppId, authorizerRefreshToken) {
|
|
289
|
+
const { ctx, app } = this;
|
|
290
|
+
this._assertComponentConfig();
|
|
291
|
+
|
|
292
|
+
if (!authorizerAppId) {
|
|
293
|
+
throw new Error('authorizerAppId is required');
|
|
294
|
+
}
|
|
295
|
+
if (!authorizerRefreshToken) {
|
|
296
|
+
throw new Error('authorizerRefreshToken is required');
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const cacheKey = this._getKeyAuthorizerAccessToken(authorizerAppId);
|
|
300
|
+
let cached = await app.redis.get(cacheKey);
|
|
301
|
+
if (!cached) {
|
|
302
|
+
const fallbackKey = this.redisKeyStrategy === 'legacy'
|
|
303
|
+
? `wechat:authorizer_access_token:${authorizerAppId}`
|
|
304
|
+
: `wechat:authorizer:${authorizerAppId}:access_token`;
|
|
305
|
+
cached = await app.redis.get(fallbackKey);
|
|
306
|
+
}
|
|
307
|
+
if (cached) {
|
|
308
|
+
return { authorizer_access_token: cached, from_cache: true };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const { componentAppId } = this.wechatConfig;
|
|
312
|
+
const componentAccessToken = await this.getComponentAccessToken();
|
|
313
|
+
const url = `https://api.weixin.qq.com/cgi-bin/component/api_authorizer_token?component_access_token=${encodeURIComponent(componentAccessToken)}`;
|
|
314
|
+
|
|
315
|
+
const result = await ctx.curl(url, {
|
|
316
|
+
method: 'POST',
|
|
317
|
+
contentType: 'json',
|
|
318
|
+
dataType: 'json',
|
|
319
|
+
data: {
|
|
320
|
+
component_appid: componentAppId,
|
|
321
|
+
authorizer_appid: authorizerAppId,
|
|
322
|
+
authorizer_refresh_token: authorizerRefreshToken,
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
const data = result.data || {};
|
|
327
|
+
if (data.errcode) {
|
|
328
|
+
throw new Error(`获取 authorizer_access_token 失败: ${data.errmsg}`);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const token = data.authorizer_access_token;
|
|
332
|
+
const expiresIn = Number(data.expires_in || 7200);
|
|
333
|
+
const ttl = Math.max(expiresIn - 300, 60);
|
|
334
|
+
|
|
335
|
+
if (token) {
|
|
336
|
+
await app.redis.set(cacheKey, token, 'EX', ttl);
|
|
337
|
+
// legacy / scoped 兼容:同时写入兼容 key
|
|
338
|
+
if (this.redisKeyStrategy === 'legacy') {
|
|
339
|
+
await app.redis.set(`wechat:authorizer_access_token:${authorizerAppId}`, token, 'EX', ttl);
|
|
340
|
+
} else {
|
|
341
|
+
await app.redis.set(`wechat:authorizer:${authorizerAppId}:access_token`, token, 'EX', ttl);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return data;
|
|
102
346
|
}
|
|
103
347
|
|
|
104
348
|
/**
|
|
@@ -109,17 +353,36 @@ class ComponentService extends Service {
|
|
|
109
353
|
const { ctx, app } = this;
|
|
110
354
|
const { componentAppId } = app.config.wechat;
|
|
111
355
|
|
|
356
|
+
if (this.mockMode) {
|
|
357
|
+
const logger = app.logger || app.coreLogger;
|
|
358
|
+
const authorizationInfo = {
|
|
359
|
+
authorizer_appid: 'mock_authorizer_appid',
|
|
360
|
+
authorizer_access_token: `mock_authorizer_access_token_${Date.now()}`,
|
|
361
|
+
expires_in: 7200,
|
|
362
|
+
authorizer_refresh_token: `mock_authorizer_refresh_token_${Date.now()}`,
|
|
363
|
+
func_info: [],
|
|
364
|
+
};
|
|
365
|
+
if (logger && logger.info) {
|
|
366
|
+
logger.info('[egg-wechat] mockMode enabled: return mocked authorization_info', {
|
|
367
|
+
componentAppId,
|
|
368
|
+
authCodeMasked: authCode ? String(authCode).slice(0, 8) + '***' : '',
|
|
369
|
+
authorizerAppid: authorizationInfo.authorizer_appid,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
return authorizationInfo;
|
|
373
|
+
}
|
|
374
|
+
|
|
112
375
|
const token = await this.getComponentAccessToken();
|
|
113
376
|
const url = `https://api.weixin.qq.com/cgi-bin/component/api_query_auth?component_access_token=${token}`;
|
|
114
|
-
|
|
377
|
+
|
|
115
378
|
const result = await ctx.curl(url, {
|
|
116
379
|
method: 'POST',
|
|
117
380
|
contentType: 'json',
|
|
118
381
|
dataType: 'json',
|
|
119
382
|
data: {
|
|
120
383
|
component_appid: componentAppId,
|
|
121
|
-
authorization_code: authCode
|
|
122
|
-
}
|
|
384
|
+
authorization_code: authCode,
|
|
385
|
+
},
|
|
123
386
|
});
|
|
124
387
|
|
|
125
388
|
if (result.data.errcode) {
|
|
@@ -137,17 +400,46 @@ class ComponentService extends Service {
|
|
|
137
400
|
const { ctx, app } = this;
|
|
138
401
|
const { componentAppId } = app.config.wechat;
|
|
139
402
|
|
|
403
|
+
if (this.mockMode) {
|
|
404
|
+
const logger = app.logger || app.coreLogger;
|
|
405
|
+
const data = {
|
|
406
|
+
authorizer_info: {
|
|
407
|
+
nick_name: 'Mock公众号',
|
|
408
|
+
head_img: '',
|
|
409
|
+
service_type_info: { id: 2 },
|
|
410
|
+
verify_type_info: { id: 0 },
|
|
411
|
+
user_name: 'gh_mock',
|
|
412
|
+
principal_name: 'Mock Principal',
|
|
413
|
+
alias: 'mock',
|
|
414
|
+
signature: 'mock signature',
|
|
415
|
+
business_info: {},
|
|
416
|
+
},
|
|
417
|
+
qrcode_url: '',
|
|
418
|
+
authorization_info: {
|
|
419
|
+
authorizer_appid: authorizerAppId,
|
|
420
|
+
func_info: [],
|
|
421
|
+
},
|
|
422
|
+
};
|
|
423
|
+
if (logger && logger.info) {
|
|
424
|
+
logger.info('[egg-wechat] mockMode enabled: return mocked authorizer_info', {
|
|
425
|
+
componentAppId,
|
|
426
|
+
authorizerAppId,
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
return data;
|
|
430
|
+
}
|
|
431
|
+
|
|
140
432
|
const token = await this.getComponentAccessToken();
|
|
141
433
|
const url = `https://api.weixin.qq.com/cgi-bin/component/api_get_authorizer_info?component_access_token=${token}`;
|
|
142
|
-
|
|
434
|
+
|
|
143
435
|
const result = await ctx.curl(url, {
|
|
144
436
|
method: 'POST',
|
|
145
437
|
contentType: 'json',
|
|
146
438
|
dataType: 'json',
|
|
147
439
|
data: {
|
|
148
440
|
component_appid: componentAppId,
|
|
149
|
-
authorizer_appid: authorizerAppId
|
|
150
|
-
}
|
|
441
|
+
authorizer_appid: authorizerAppId,
|
|
442
|
+
},
|
|
151
443
|
});
|
|
152
444
|
|
|
153
445
|
if (result.data.errcode) {
|
|
@@ -166,7 +458,7 @@ class ComponentService extends Service {
|
|
|
166
458
|
async sendCustomMessage(accessToken, toUser, content) {
|
|
167
459
|
const { ctx } = this;
|
|
168
460
|
const url = `https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=${accessToken}`;
|
|
169
|
-
|
|
461
|
+
|
|
170
462
|
const result = await ctx.curl(url, {
|
|
171
463
|
method: 'POST',
|
|
172
464
|
contentType: 'json',
|
|
@@ -175,9 +467,9 @@ class ComponentService extends Service {
|
|
|
175
467
|
touser: toUser,
|
|
176
468
|
msgtype: 'text',
|
|
177
469
|
text: {
|
|
178
|
-
content
|
|
179
|
-
}
|
|
180
|
-
}
|
|
470
|
+
content,
|
|
471
|
+
},
|
|
472
|
+
},
|
|
181
473
|
});
|
|
182
474
|
|
|
183
475
|
return result.data;
|
|
@@ -203,12 +495,32 @@ class ComponentService extends Service {
|
|
|
203
495
|
* @param {number} authType 授权类型 (1: 公众号, 2: 小程序, 3: 两者都可)
|
|
204
496
|
*/
|
|
205
497
|
async getAuthUrl(redirectUri, authType = 3) {
|
|
206
|
-
const {
|
|
207
|
-
const { componentAppId } = app.config.wechat;
|
|
208
|
-
const preAuthCode = await this.getPreAuthCode();
|
|
498
|
+
const { componentAppId } = this.wechatConfig;
|
|
209
499
|
const encodedRedirectUri = encodeURIComponent(redirectUri);
|
|
210
|
-
|
|
211
|
-
|
|
500
|
+
|
|
501
|
+
// 纯本地伪联调:返回本地 mock 授权页,打开后会重定向回 redirectUri 并携带 auth_code
|
|
502
|
+
if (this.mockMode) {
|
|
503
|
+
let base = String(this.wechatConfig.publicBaseUrl || '').trim();
|
|
504
|
+
try {
|
|
505
|
+
if (!base) base = new URL(String(redirectUri)).origin;
|
|
506
|
+
} catch (e) {
|
|
507
|
+
// ignore
|
|
508
|
+
}
|
|
509
|
+
if (!base) base = 'http://localhost:7001';
|
|
510
|
+
if (!/^https?:\/\//i.test(base)) base = `https://${base}`;
|
|
511
|
+
base = base.replace(/\/$/, '');
|
|
512
|
+
|
|
513
|
+
const url = `${base}/api/v1/wechat/component/mock_authorize?redirect_uri=${encodedRedirectUri}&auth_type=${encodeURIComponent(String(authType))}`;
|
|
514
|
+
if (this.authUrlReturnObject) return { pc: url, mobile: url };
|
|
515
|
+
return url;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const preAuthCode = await this.getPreAuthCode();
|
|
519
|
+
const url = `https://mp.weixin.qq.com/cgi-bin/componentloginpage?component_appid=${componentAppId}&pre_auth_code=${preAuthCode}&redirect_uri=${encodedRedirectUri}&auth_type=${authType}`;
|
|
520
|
+
if (this.authUrlReturnObject) {
|
|
521
|
+
return { pc: url, mobile: url };
|
|
522
|
+
}
|
|
523
|
+
return url;
|
|
212
524
|
}
|
|
213
525
|
}
|
|
214
526
|
|
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',
|