q-koa 13.8.60 → 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 +14 -56
- package/core/file/plugins/weixin/controller.js +0 -1
- package/core/file/services/weixin.js +2 -1
- package/core/file/services/weixinMP.js +39 -16
- package/core/file/utils/index.js +1 -1
- package/package.json +1 -1
- package/core/file/plugins/user/test.js +0 -86
- package/core/file/services/config.js +0 -38
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')
|
|
@@ -465,11 +465,7 @@ class APP {
|
|
|
465
465
|
let sharedRedisClient = null
|
|
466
466
|
let sharedSubscriber = null
|
|
467
467
|
let redisError = null
|
|
468
|
-
if (
|
|
469
|
-
this.config.redis &&
|
|
470
|
-
this.config.redis.host &&
|
|
471
|
-
this.config.redis.port
|
|
472
|
-
) {
|
|
468
|
+
if (this.config.redis && this.config.redis.host && this.config.redis.port) {
|
|
473
469
|
try {
|
|
474
470
|
sharedRedisClient = redis.createClient({
|
|
475
471
|
socket: {
|
|
@@ -662,14 +658,6 @@ class APP {
|
|
|
662
658
|
}
|
|
663
659
|
}
|
|
664
660
|
|
|
665
|
-
const initFileConfig = _.get(
|
|
666
|
-
this.app[appName],
|
|
667
|
-
'appConfig.initFile',
|
|
668
|
-
false
|
|
669
|
-
)
|
|
670
|
-
if (initFileConfig) {
|
|
671
|
-
await this.initFile(appName)
|
|
672
|
-
}
|
|
673
661
|
await this.initPlugin(appName)
|
|
674
662
|
|
|
675
663
|
const initEvent = _.get(this.app[appName], 'appConfig.initEvent', false)
|
|
@@ -700,19 +688,6 @@ class APP {
|
|
|
700
688
|
}
|
|
701
689
|
}
|
|
702
690
|
|
|
703
|
-
async initFile(appName) {
|
|
704
|
-
const dirExist = fsExtra.existsSync(path.resolve(__dirname, './file'))
|
|
705
|
-
if (dirExist) {
|
|
706
|
-
await fsExtra.copy(
|
|
707
|
-
path.resolve(__dirname, './file'),
|
|
708
|
-
path.resolve(__dirname, `${rootDirectory}/${APP_DIR}/${appName}`),
|
|
709
|
-
{
|
|
710
|
-
clobber: false,
|
|
711
|
-
}
|
|
712
|
-
)
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
|
|
716
691
|
async initPlugin(appName) {
|
|
717
692
|
const pluginDir = path.resolve(
|
|
718
693
|
__dirname,
|
|
@@ -779,8 +754,7 @@ class APP {
|
|
|
779
754
|
filename
|
|
780
755
|
))
|
|
781
756
|
const _attributes =
|
|
782
|
-
Object.keys(currentAttributes).length === 0 &&
|
|
783
|
-
defaultModelExist
|
|
757
|
+
Object.keys(currentAttributes).length === 0 && defaultModelExist
|
|
784
758
|
? require(defaultModelPath)
|
|
785
759
|
: currentAttributes
|
|
786
760
|
|
|
@@ -932,16 +906,13 @@ class APP {
|
|
|
932
906
|
_attributes,
|
|
933
907
|
(item) => item.mock && typeof item.mock === 'function'
|
|
934
908
|
)
|
|
935
|
-
mount(
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
Object.keys(mockObj),
|
|
941
|
-
Object.keys(mockObj).map((key) =>
|
|
942
|
-
_.result(mockObj, `${key}.mock`)
|
|
943
|
-
)
|
|
909
|
+
mount('mock', folder, () =>
|
|
910
|
+
_.zipObject(
|
|
911
|
+
Object.keys(mockObj),
|
|
912
|
+
Object.keys(mockObj).map((key) =>
|
|
913
|
+
_.result(mockObj, `${key}.mock`)
|
|
944
914
|
)
|
|
915
|
+
)
|
|
945
916
|
)
|
|
946
917
|
}
|
|
947
918
|
} else if (
|
|
@@ -964,10 +935,7 @@ class APP {
|
|
|
964
935
|
`${filename}`
|
|
965
936
|
))
|
|
966
937
|
// 用户模块缺失的方法/配置由框架内置同名模块补齐(仅合并当前模块,不波及整个 app)
|
|
967
|
-
if (
|
|
968
|
-
fs.existsSync(defaultPath) &&
|
|
969
|
-
_.isPlainObject(userModule)
|
|
970
|
-
) {
|
|
938
|
+
if (fs.existsSync(defaultPath) && _.isPlainObject(userModule)) {
|
|
971
939
|
_.defaultsDeep(userModule, require(defaultPath))
|
|
972
940
|
}
|
|
973
941
|
mount(n, folder, userModule)
|
|
@@ -1013,7 +981,6 @@ class APP {
|
|
|
1013
981
|
// 直接赋值,避免对整个 app 对象做 defaultsDeep 深合并(且保持对象引用不变)
|
|
1014
982
|
this.app[appName].include = include
|
|
1015
983
|
for (const model of modelList) {
|
|
1016
|
-
|
|
1017
984
|
// if (!this.app[appName].include || !this.app[appName].include[model]) {
|
|
1018
985
|
// this.app[appName] = _.defaultsDeep(
|
|
1019
986
|
// {
|
|
@@ -2460,20 +2427,11 @@ APP.getConfig = (app) => ({
|
|
|
2460
2427
|
},
|
|
2461
2428
|
],
|
|
2462
2429
|
})
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
configList.map((item) => ({
|
|
2466
|
-
...item,
|
|
2467
|
-
}))
|
|
2468
|
-
)
|
|
2469
|
-
|
|
2470
|
-
const result = getObject(configList, app)(type)
|
|
2471
|
-
if (!result) throw new Error('没有这个type')
|
|
2472
|
-
return result
|
|
2430
|
+
// raw: true 已是纯对象数组,直接缓存;getObjectFn 只读取不修改
|
|
2431
|
+
app.cache.set('configList', configList)
|
|
2473
2432
|
}
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
return result
|
|
2433
|
+
// getObjectFn 始终返回对象(生产环境找不到配置时在其内部 throw),无需再判空
|
|
2434
|
+
return getObjectFn(configList, app)(type)
|
|
2477
2435
|
},
|
|
2478
2436
|
update(target = 'configList') {
|
|
2479
2437
|
if (app.redisClient) {
|
|
@@ -2439,7 +2439,6 @@ exports.union_checkPay = async (ctx) => {
|
|
|
2439
2439
|
const payResult = await allinpay.checkOrder(out_trade_no)
|
|
2440
2440
|
|
|
2441
2441
|
if (payResult) {
|
|
2442
|
-
console.log(payResult)
|
|
2443
2442
|
if (payResult.trxstatus === '0000' && payResult.trxcode === 'VSP501') {
|
|
2444
2443
|
const order_price = Number(payResult.trxamt) / 100
|
|
2445
2444
|
|
|
@@ -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')
|
|
@@ -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 *
|
|
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
|
-
|
|
181
|
+
// 实际请求 stable_token;同一 appid 同类型(普通/强刷)的并发请求共享 Promise
|
|
182
|
+
_fetchAccessToken(force_refresh) {
|
|
179
183
|
const { appid, secrect } = this.config
|
|
180
|
-
|
|
181
|
-
|
|
184
|
+
const flightKey = `${appid}:${force_refresh ? 1 : 0}`
|
|
185
|
+
if (refreshingMap.has(flightKey)) {
|
|
186
|
+
return refreshingMap.get(flightKey)
|
|
182
187
|
}
|
|
183
|
-
const
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
if (
|
|
195
|
-
|
|
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
|
|
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.
|
|
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
|
}
|
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 = {}
|
package/package.json
CHANGED
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
const axios = require('axios')
|
|
2
|
-
const APP = require('q-koa')
|
|
3
|
-
const APP_CONFIG = require('../../config')
|
|
4
|
-
const config = require('./config')
|
|
5
|
-
|
|
6
|
-
const modelName = __dirname.split('/').reverse()[0]
|
|
7
|
-
const service = axios.create({
|
|
8
|
-
baseURL: `http://localhost:${APP_CONFIG.port}/${modelName}`,
|
|
9
|
-
})
|
|
10
|
-
service.interceptors.request.use((config) => {
|
|
11
|
-
config.headers['Client-Type'] = 2
|
|
12
|
-
return config
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
service.interceptors.response.use(
|
|
16
|
-
(response) => response.data,
|
|
17
|
-
(error) => Promise.reject(error)
|
|
18
|
-
)
|
|
19
|
-
|
|
20
|
-
const sleep = (time) =>
|
|
21
|
-
new Promise((resolve) => setTimeout(resolve, time * 1000))
|
|
22
|
-
|
|
23
|
-
describe(`${config.name}/${modelName}`, () => {
|
|
24
|
-
beforeAll(async () => {
|
|
25
|
-
try {
|
|
26
|
-
await axios.post('/')
|
|
27
|
-
} catch (e) {
|
|
28
|
-
console.log('正在启动服务器')
|
|
29
|
-
const app = new APP(APP_CONFIG)
|
|
30
|
-
await app.start()
|
|
31
|
-
}
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
test('错误登录', async () => {
|
|
35
|
-
const res = await service.post('login', {
|
|
36
|
-
name: 'chenlin1',
|
|
37
|
-
password: 'sugar23',
|
|
38
|
-
})
|
|
39
|
-
expect(res.code).toBe(500)
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
test('正确登录', async () => {
|
|
43
|
-
const res = await service.post('login', {
|
|
44
|
-
name: 'chenlin',
|
|
45
|
-
password: 'sugar23',
|
|
46
|
-
})
|
|
47
|
-
expect(res.code).toBe(200)
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
test('获取token', async () => {
|
|
51
|
-
const {
|
|
52
|
-
data: { token },
|
|
53
|
-
} = await service.post('login', {
|
|
54
|
-
name: 'chenlin',
|
|
55
|
-
password: 'sugar23',
|
|
56
|
-
})
|
|
57
|
-
expect(Boolean(token)).toBe(true)
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
test('没有token信息', async () => {
|
|
61
|
-
const res = await service.post('getInfo', {})
|
|
62
|
-
expect(res.code).toBe(401)
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
test('获取用户信息', async () => {
|
|
66
|
-
const {
|
|
67
|
-
data: { token },
|
|
68
|
-
} = await service.post('login', {
|
|
69
|
-
name: 'chenlin',
|
|
70
|
-
password: 'sugar23',
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
const res = await service.post(
|
|
74
|
-
'getInfo',
|
|
75
|
-
{},
|
|
76
|
-
{
|
|
77
|
-
headers: {
|
|
78
|
-
'Client-Type': 2,
|
|
79
|
-
'X-Token': token,
|
|
80
|
-
},
|
|
81
|
-
}
|
|
82
|
-
)
|
|
83
|
-
|
|
84
|
-
expect(res.code).toBe(200)
|
|
85
|
-
})
|
|
86
|
-
})
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
const { getObject } = require('../utils')
|
|
2
|
-
|
|
3
|
-
module.exports = class Config {
|
|
4
|
-
constructor(app) {
|
|
5
|
-
this.app = app
|
|
6
|
-
this.configList = []
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
async init() {
|
|
10
|
-
if (this.app.cache && this.app.cache.get('configList')) {
|
|
11
|
-
this.configList = this.app.cache.get('configList')
|
|
12
|
-
return
|
|
13
|
-
}
|
|
14
|
-
const configList = await this.app.model.setting.findAll({
|
|
15
|
-
where: {
|
|
16
|
-
type: {
|
|
17
|
-
excludes: ['textarea', 'rich-text'],
|
|
18
|
-
},
|
|
19
|
-
},
|
|
20
|
-
})
|
|
21
|
-
this.app.cache && this.app.cache.set('configList', configList)
|
|
22
|
-
this.configList = configList
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
async getObject(type) {
|
|
26
|
-
if (this.app.cache && this.app.cache.get(type))
|
|
27
|
-
return this.app.cache.get(type)
|
|
28
|
-
await this.init()
|
|
29
|
-
const result = getObject(this.configList)(type)
|
|
30
|
-
if (!result) throw new Error('没有这个type')
|
|
31
|
-
this.app.cache && this.app.cache.set(type, result)
|
|
32
|
-
return result
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
update() {
|
|
36
|
-
this.app.cache && this.app.cache.set('configList', null)
|
|
37
|
-
}
|
|
38
|
-
}
|