q-koa 13.8.61 → 13.8.62

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/core/app.js CHANGED
@@ -21,7 +21,7 @@ const util = require('util')
21
21
  const jwt = require('jsonwebtoken')
22
22
  const verify = util.promisify(jwt.verify)
23
23
  const fsExtra = require('fs-extra')
24
- const { getObject, getAppConfig, formatLiteralObj } = require('./file/utils')
24
+ const { getObjectFn, getAppConfig, formatLiteralObj } = require('./file/utils')
25
25
  const redisVirtual = {
26
26
  hSet: () => {
27
27
  console.log('redisVirtual hSet')
@@ -2427,20 +2427,11 @@ APP.getConfig = (app) => ({
2427
2427
  },
2428
2428
  ],
2429
2429
  })
2430
- await app.cache.set(
2431
- 'configList',
2432
- configList.map((item) => ({
2433
- ...item,
2434
- }))
2435
- )
2436
-
2437
- const result = getObject(configList, app)(type)
2438
- if (!result) throw new Error('没有这个type')
2439
- return result
2430
+ // raw: true 已是纯对象数组,直接缓存;getObjectFn 只读取不修改
2431
+ app.cache.set('configList', configList)
2440
2432
  }
2441
- const result = getObject(configList, app)(type)
2442
- if (!result) throw new Error('没有这个type')
2443
- return result
2433
+ // getObjectFn 始终返回对象(生产环境找不到配置时在其内部 throw),无需再判空
2434
+ return getObjectFn(configList, app)(type)
2444
2435
  },
2445
2436
  update(target = 'configList') {
2446
2437
  if (app.redisClient) {
@@ -5,9 +5,10 @@ const getTicketUrl =
5
5
  'https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=%s&type=jsapi'
6
6
  const sha1 = require('sha1')
7
7
  const { LRUCache } = require('lru-cache')
8
+ // 微信 access_token / jsapi_ticket 有效期均为 2 小时,缓存 110 分钟(留 10 分钟边界余量)
8
9
  const cache = new LRUCache({
9
10
  max: 100,
10
- ttl: 1000 * 60 * 60,
11
+ ttl: 1000 * 60 * 110,
11
12
  })
12
13
 
13
14
  module.exports = class Singleton {
@@ -5,8 +5,8 @@
5
5
  * 1. 删除 Singleton.instance + lodash.isEqual 的假单例
6
6
  * 2. 新增 static _instanceMap = new Map(),key 为 config.appid
7
7
  * 3. 构造函数按 appid 直接命中缓存,不再做深比较
8
- * 4. 新增内部 _withTokenRetry:遇 40001/40014/42001 强刷当前 appid 的
9
- * access_token 并重试一次,公共方法签名不暴露任何重试参数
8
+ * 4. 新增内部 _withTokenRetry:遇 40001/40014/42001 先普通刷新重试,
9
+ * 仍失败才 force_refresh 强刷(并发去重),公共方法签名不暴露任何重试参数
10
10
  */
11
11
 
12
12
  const util = require('util')
@@ -119,10 +119,13 @@ const b2bSendMessageUrl =
119
119
  const fsPromise = require('fs/promises')
120
120
  const { LRUCache } = require('lru-cache')
121
121
  const FormData = require('form-data')
122
+ // 微信 access_token 有效期 2 小时,缓存 110 分钟(留 10 分钟边界余量)
122
123
  const cache = new LRUCache({
123
124
  max: 100,
124
- ttl: 1000 * 60 * 60,
125
+ ttl: 1000 * 60 * 110,
125
126
  })
127
+ // 同一 appid 并发强刷去重(single-flight),避免多请求重复调 stable_token
128
+ const refreshingMap = new Map()
126
129
  const uuid = require('node-uuid')
127
130
  const hmacSha256 = (data, secret) => {
128
131
  return crypto.createHmac('sha256', secret).update(data).digest('hex')
@@ -175,12 +178,14 @@ module.exports = class Singleton {
175
178
  return this
176
179
  }
177
180
 
178
- async getAccessToken(force_refresh = false) {
181
+ // 实际请求 stable_token;同一 appid 同类型(普通/强刷)的并发请求共享 Promise
182
+ _fetchAccessToken(force_refresh) {
179
183
  const { appid, secrect } = this.config
180
- if (!force_refresh && cache.get(appid)) {
181
- return cache.get(appid)
184
+ const flightKey = `${appid}:${force_refresh ? 1 : 0}`
185
+ if (refreshingMap.has(flightKey)) {
186
+ return refreshingMap.get(flightKey)
182
187
  }
183
- const result = await wxAxios
188
+ const promise = wxAxios
184
189
  .post(getAccessTokenUrl, {
185
190
  grant_type: 'client_credential',
186
191
  appid,
@@ -188,24 +193,42 @@ module.exports = class Singleton {
188
193
  force_refresh,
189
194
  })
190
195
  .then((res) => res.data)
196
+ .then((result) => {
197
+ const { errcode, errmsg, access_token } = result
198
+ if (errcode) {
199
+ throw new Error(errmsg)
200
+ }
201
+ cache.set(appid, access_token)
202
+ return access_token
203
+ })
204
+ .finally(() => {
205
+ refreshingMap.delete(flightKey)
206
+ })
207
+ refreshingMap.set(flightKey, promise)
208
+ return promise
209
+ }
191
210
 
192
- const { errcode, errmsg, access_token } = result
193
-
194
- if (errcode) {
195
- throw new Error(errmsg)
211
+ async getAccessToken(force_refresh = false) {
212
+ const { appid } = this.config
213
+ if (!force_refresh && cache.get(appid)) {
214
+ return cache.get(appid)
196
215
  }
197
-
198
- cache.set(appid, access_token)
199
- return access_token
216
+ return this._fetchAccessToken(force_refresh)
200
217
  }
201
218
 
202
- // access_token 失效/过期统一重试:强刷当前 appid 的 token 后重试一次
219
+ // access_token 失效/过期统一重试:
220
+ // 第一次绕过缓存普通刷新(取微信服务端当前有效 token,不作废旧 token);
221
+ // 仍失败才 force_refresh 强刷(并发去重)
203
222
  async _withTokenRetry(request) {
204
223
  let access_token = await this.getAccessToken()
205
224
  let result = await request(access_token)
206
225
  if (result.errcode && [40001, 40014, 42001].includes(result.errcode)) {
207
- access_token = await this.getAccessToken(true)
226
+ access_token = await this._fetchAccessToken(false)
208
227
  result = await request(access_token)
228
+ if (result.errcode && [40001, 40014, 42001].includes(result.errcode)) {
229
+ access_token = await this._fetchAccessToken(true)
230
+ result = await request(access_token)
231
+ }
209
232
  }
210
233
  return result
211
234
  }
@@ -34,7 +34,7 @@ exports.getList = (config) => (type) => {
34
34
  return list
35
35
  }
36
36
 
37
- exports.getObject = (config, app) => async (type) => {
37
+ exports.getObjectFn = (config, app) => async (type) => {
38
38
  const obj = config.find((i) => i.code === type)
39
39
  let flag = false
40
40
  const temObj = {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "q-koa",
3
- "version": "13.8.61",
3
+ "version": "13.8.62",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {