@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 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 } = app.config.wechat;
14
-
15
- if (!componentAppId || !componentAppSecret) {
16
- throw new Error('未配置微信开放平台 Component AppID 或 Secret');
17
- }
105
+ const { componentAppId, componentAppSecret } = this.wechatConfig;
18
106
 
19
- const cacheKey = `wechat:component_access_token:${componentAppId}`;
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
- // 获取 Component Verify Ticket
27
- const ticketKey = `wechat:component_verify_ticket:${componentAppId}`;
28
- const ticket = await app.redis.get(ticketKey);
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
- if (!ticket) {
31
- throw new Error('未收到微信推送的 Component Verify Ticket,请等待微信服务器推送');
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
- await app.redis.set(cacheKey, token, 'EX', expiresIn - 300);
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 } = app.config.wechat;
67
-
216
+ const { componentAppId } = this.wechatConfig;
217
+
68
218
  if (!componentAppId) return;
69
219
 
70
- const key = `wechat:component_verify_ticket:${componentAppId}`;
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 } = app.config.wechat;
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
- return result.data.pre_auth_code;
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: 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 { app } = this;
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
- return `https://mp.weixin.qq.com/cgi-bin/componentloginpage?component_appid=${componentAppId}&pre_auth_code=${preAuthCode}&redirect_uri=${encodedRedirectUri}&auth_type=${authType}`;
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
 
@@ -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',