q-koa 13.8.61 → 13.8.63
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 +5 -14
- package/core/file/services/weixin.js +2 -1
- package/core/file/services/weixinMP.js +69 -16
- package/core/file/utils/index.js +1 -1
- package/package.json +1 -1
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 {
|
|
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
|
-
|
|
2431
|
-
|
|
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
|
-
|
|
2442
|
-
|
|
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 *
|
|
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
|
|
9
|
-
*
|
|
8
|
+
* 4. 新增内部 _withTokenRetry:遇 40001/40014/42001 先普通刷新重试,
|
|
9
|
+
* 仍失败才 force_refresh 强刷(并发去重),公共方法签名不暴露任何重试参数
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
const util = require('util')
|
|
@@ -116,13 +116,22 @@ const b2bFinishUrl =
|
|
|
116
116
|
|
|
117
117
|
const b2bSendMessageUrl =
|
|
118
118
|
'https://api.weixin.qq.com/wxa/business/retailnotifybusiness?access_token=%s'
|
|
119
|
+
|
|
120
|
+
// 查询/清零接口调用次数配额(cgi_path 形如 /cgi-bin/stable_token)
|
|
121
|
+
const getApiQuotaUrl =
|
|
122
|
+
'https://api.weixin.qq.com/cgi-bin/openapi/quota/get?access_token=%s'
|
|
123
|
+
const clearApiQuotaUrl =
|
|
124
|
+
'https://api.weixin.qq.com/cgi-bin/openapi/quota/clear?access_token=%s'
|
|
119
125
|
const fsPromise = require('fs/promises')
|
|
120
126
|
const { LRUCache } = require('lru-cache')
|
|
121
127
|
const FormData = require('form-data')
|
|
128
|
+
// 微信 access_token 有效期 2 小时,缓存 110 分钟(留 10 分钟边界余量)
|
|
122
129
|
const cache = new LRUCache({
|
|
123
130
|
max: 100,
|
|
124
|
-
ttl: 1000 * 60 *
|
|
131
|
+
ttl: 1000 * 60 * 110,
|
|
125
132
|
})
|
|
133
|
+
// 同一 appid 并发强刷去重(single-flight),避免多请求重复调 stable_token
|
|
134
|
+
const refreshingMap = new Map()
|
|
126
135
|
const uuid = require('node-uuid')
|
|
127
136
|
const hmacSha256 = (data, secret) => {
|
|
128
137
|
return crypto.createHmac('sha256', secret).update(data).digest('hex')
|
|
@@ -175,12 +184,14 @@ module.exports = class Singleton {
|
|
|
175
184
|
return this
|
|
176
185
|
}
|
|
177
186
|
|
|
178
|
-
|
|
187
|
+
// 实际请求 stable_token;同一 appid 同类型(普通/强刷)的并发请求共享 Promise
|
|
188
|
+
_fetchAccessToken(force_refresh) {
|
|
179
189
|
const { appid, secrect } = this.config
|
|
180
|
-
|
|
181
|
-
|
|
190
|
+
const flightKey = `${appid}:${force_refresh ? 1 : 0}`
|
|
191
|
+
if (refreshingMap.has(flightKey)) {
|
|
192
|
+
return refreshingMap.get(flightKey)
|
|
182
193
|
}
|
|
183
|
-
const
|
|
194
|
+
const promise = wxAxios
|
|
184
195
|
.post(getAccessTokenUrl, {
|
|
185
196
|
grant_type: 'client_credential',
|
|
186
197
|
appid,
|
|
@@ -188,24 +199,42 @@ module.exports = class Singleton {
|
|
|
188
199
|
force_refresh,
|
|
189
200
|
})
|
|
190
201
|
.then((res) => res.data)
|
|
202
|
+
.then((result) => {
|
|
203
|
+
const { errcode, errmsg, access_token } = result
|
|
204
|
+
if (errcode) {
|
|
205
|
+
throw new Error(errmsg)
|
|
206
|
+
}
|
|
207
|
+
cache.set(appid, access_token)
|
|
208
|
+
return access_token
|
|
209
|
+
})
|
|
210
|
+
.finally(() => {
|
|
211
|
+
refreshingMap.delete(flightKey)
|
|
212
|
+
})
|
|
213
|
+
refreshingMap.set(flightKey, promise)
|
|
214
|
+
return promise
|
|
215
|
+
}
|
|
191
216
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
if (
|
|
195
|
-
|
|
217
|
+
async getAccessToken(force_refresh = false) {
|
|
218
|
+
const { appid } = this.config
|
|
219
|
+
if (!force_refresh && cache.get(appid)) {
|
|
220
|
+
return cache.get(appid)
|
|
196
221
|
}
|
|
197
|
-
|
|
198
|
-
cache.set(appid, access_token)
|
|
199
|
-
return access_token
|
|
222
|
+
return this._fetchAccessToken(force_refresh)
|
|
200
223
|
}
|
|
201
224
|
|
|
202
|
-
// access_token
|
|
225
|
+
// access_token 失效/过期统一重试:
|
|
226
|
+
// 第一次绕过缓存普通刷新(取微信服务端当前有效 token,不作废旧 token);
|
|
227
|
+
// 仍失败才 force_refresh 强刷(并发去重)
|
|
203
228
|
async _withTokenRetry(request) {
|
|
204
229
|
let access_token = await this.getAccessToken()
|
|
205
230
|
let result = await request(access_token)
|
|
206
231
|
if (result.errcode && [40001, 40014, 42001].includes(result.errcode)) {
|
|
207
|
-
access_token = await this.
|
|
232
|
+
access_token = await this._fetchAccessToken(false)
|
|
208
233
|
result = await request(access_token)
|
|
234
|
+
if (result.errcode && [40001, 40014, 42001].includes(result.errcode)) {
|
|
235
|
+
access_token = await this._fetchAccessToken(true)
|
|
236
|
+
result = await request(access_token)
|
|
237
|
+
}
|
|
209
238
|
}
|
|
210
239
|
return result
|
|
211
240
|
}
|
|
@@ -1088,6 +1117,30 @@ module.exports = class Singleton {
|
|
|
1088
1117
|
}
|
|
1089
1118
|
return result
|
|
1090
1119
|
}
|
|
1120
|
+
|
|
1121
|
+
// 查询接口调用次数配额,cgi_path 形如 '/cgi-bin/stable_token'
|
|
1122
|
+
async getApiQuota(cgi_path) {
|
|
1123
|
+
const result = await this._withTokenRetry(async (access_token) => {
|
|
1124
|
+
const url = util.format(getApiQuotaUrl, access_token)
|
|
1125
|
+
return await wxAxios.post(url, { cgi_path }).then((res) => res.data)
|
|
1126
|
+
})
|
|
1127
|
+
if (result.errcode) {
|
|
1128
|
+
throw new Error(`${result.errcode};${result.errmsg}`)
|
|
1129
|
+
}
|
|
1130
|
+
return result
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
// 清零接口调用次数配额(每月最多清零 50 次),返回 { errcode, errmsg }
|
|
1134
|
+
async clearApiQuota(cgi_path) {
|
|
1135
|
+
const result = await this._withTokenRetry(async (access_token) => {
|
|
1136
|
+
const url = util.format(clearApiQuotaUrl, access_token)
|
|
1137
|
+
return await wxAxios.post(url, { cgi_path }).then((res) => res.data)
|
|
1138
|
+
})
|
|
1139
|
+
if (result.errcode) {
|
|
1140
|
+
throw new Error(`${result.errcode};${result.errmsg}`)
|
|
1141
|
+
}
|
|
1142
|
+
return result
|
|
1143
|
+
}
|
|
1091
1144
|
getConfig() {
|
|
1092
1145
|
return this.config
|
|
1093
1146
|
}
|
package/core/file/utils/index.js
CHANGED
|
@@ -34,7 +34,7 @@ exports.getList = (config) => (type) => {
|
|
|
34
34
|
return list
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
exports.
|
|
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 = {}
|