q-koa 13.8.29 → 13.8.31
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 +35 -16
- package/core/file/plugins/weixin/controller.js +141 -4
- package/package.json +1 -1
package/core/app.js
CHANGED
|
@@ -143,22 +143,36 @@ class APP {
|
|
|
143
143
|
// }));
|
|
144
144
|
const { is_off = false } = this.config.restc
|
|
145
145
|
if (!is_off) {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
146
|
+
/**
|
|
147
|
+
* restc中间件
|
|
148
|
+
* 注意:这里只构建不注册。实例按 appName 惰性创建并缓存(app 数量有限),
|
|
149
|
+
* 避免在中间件内部调用 app.use() 导致 middleware 数组每个请求增长一个实例。
|
|
150
|
+
* 实际注册在 initApp() 中路由注册之后,保持"兜底生效"的原有顺序。
|
|
151
|
+
*/
|
|
152
|
+
const globalExcludes = [...this.config.restc.excludes]
|
|
153
|
+
// 兜底实例:无法识别 appName 时使用原始全局 excludes(不含 appName 前缀)
|
|
154
|
+
const fallbackRestc = restc.koa2({ excludes: globalExcludes })
|
|
155
|
+
const restcCache = new Map()
|
|
156
|
+
this.restcMiddleware = async (ctx, next) => {
|
|
157
|
+
const appName = ctx.request.url.split('/')[1]
|
|
158
|
+
const app = this.app[appName]
|
|
159
|
+
// 未注册应用统一走兜底实例,保证缓存有界(避免随机 appName 撑爆内存)
|
|
160
|
+
if (!app || !app.appConfig) {
|
|
161
|
+
return await fallbackRestc(ctx, next)
|
|
162
|
+
}
|
|
163
|
+
if (!restcCache.has(appName)) {
|
|
164
|
+
const excludes = [
|
|
165
|
+
// 全局 excludes 也必须拼 /${appName}/ 前缀,
|
|
166
|
+
// 否则 Gateway.test 做 indexOf(rule) === 0 前缀匹配时永远不命中
|
|
167
|
+
...globalExcludes.map((item) => `/${appName}/${item}`),
|
|
168
|
+
..._.get(app, 'appConfig.restc.excludes', []).map(
|
|
169
|
+
(item) => `/${appName}/${item}`
|
|
170
|
+
),
|
|
171
|
+
]
|
|
172
|
+
restcCache.set(appName, restc.koa2({ excludes }))
|
|
173
|
+
}
|
|
174
|
+
return await restcCache.get(appName)(ctx, next)
|
|
175
|
+
}
|
|
162
176
|
}
|
|
163
177
|
|
|
164
178
|
/**
|
|
@@ -614,6 +628,11 @@ class APP {
|
|
|
614
628
|
})
|
|
615
629
|
this.app.use(router.routes()).use(router.allowedMethods())
|
|
616
630
|
|
|
631
|
+
// restc 排在路由之后注册:仅当没有其他中间件响应时才兜底生效(保持原有行为)
|
|
632
|
+
if (this.restcMiddleware) {
|
|
633
|
+
this.app.use(this.restcMiddleware)
|
|
634
|
+
}
|
|
635
|
+
|
|
617
636
|
spinner.stop()
|
|
618
637
|
|
|
619
638
|
console.log(chalk.green('==================== end ===================='))
|
|
@@ -24,6 +24,64 @@ const { getCachedLoginData } = require('../../utils')
|
|
|
24
24
|
|
|
25
25
|
const AllinPay = require('../../services/allinpay')
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* 微信支付 V2 回调验签(MD5 / HMAC-SHA256)
|
|
29
|
+
* @param {Object} xmlObj 解析后的 xmlBody.xml 字段
|
|
30
|
+
* @param {string} partner_key 商户 API 密钥(32 位)
|
|
31
|
+
* @returns {boolean}
|
|
32
|
+
*/
|
|
33
|
+
const validateWxPayV2Sign = (xmlObj, partner_key) => {
|
|
34
|
+
if (!xmlObj || !xmlObj.sign) return false
|
|
35
|
+
const signType = (xmlObj.sign_type || 'MD5').toUpperCase()
|
|
36
|
+
const params = { ...xmlObj }
|
|
37
|
+
delete params.sign
|
|
38
|
+
// 按微信规则:过滤空值/undefined/key;ASCII 字典序;k=v&k=v&key=partner_key
|
|
39
|
+
const querystring =
|
|
40
|
+
Object.keys(params)
|
|
41
|
+
.filter(
|
|
42
|
+
(k) =>
|
|
43
|
+
params[k] !== undefined &&
|
|
44
|
+
params[k] !== '' &&
|
|
45
|
+
k !== 'key' &&
|
|
46
|
+
k !== 'pfx' &&
|
|
47
|
+
k !== 'partner_key'
|
|
48
|
+
)
|
|
49
|
+
.sort()
|
|
50
|
+
.map((k) => `${k}=${params[k]}`)
|
|
51
|
+
.join('&') + `&key=${partner_key}`
|
|
52
|
+
let expected
|
|
53
|
+
if (signType === 'HMAC-SHA256') {
|
|
54
|
+
expected = crypto
|
|
55
|
+
.createHmac('sha256', partner_key)
|
|
56
|
+
.update(querystring, 'utf8')
|
|
57
|
+
.digest('hex')
|
|
58
|
+
.toUpperCase()
|
|
59
|
+
} else {
|
|
60
|
+
expected = crypto
|
|
61
|
+
.createHash('md5')
|
|
62
|
+
.update(querystring, 'utf8')
|
|
63
|
+
.digest('hex')
|
|
64
|
+
.toUpperCase()
|
|
65
|
+
}
|
|
66
|
+
return expected === String(xmlObj.sign).toUpperCase()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 发送微信 V2 标准的 FAIL XML 响应(供回调异常/验签失败使用)
|
|
71
|
+
*/
|
|
72
|
+
const sendWxFail = (ctx, msg = 'FAIL') => {
|
|
73
|
+
ctx.status = 200
|
|
74
|
+
ctx.res.setHeader('Content-Type', 'application/xml')
|
|
75
|
+
ctx.res.end(
|
|
76
|
+
jsonToXml({
|
|
77
|
+
xml: {
|
|
78
|
+
return_code: 'FAIL',
|
|
79
|
+
return_msg: msg,
|
|
80
|
+
},
|
|
81
|
+
})
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
27
85
|
const check = ({ timestamp, nonce, signature, token }) => {
|
|
28
86
|
const tmp = [token, timestamp, nonce].sort().join('')
|
|
29
87
|
const currSign = crypto.createHash('sha1').update(tmp).digest('hex')
|
|
@@ -1490,6 +1548,49 @@ exports.union_notify = async (ctx) => {
|
|
|
1490
1548
|
exports.notify = async (ctx) => {
|
|
1491
1549
|
const { app } = getAppByCtx(ctx)
|
|
1492
1550
|
const result = ctx.request.xmlBody
|
|
1551
|
+
if (!result || !result.xml) return sendWxFail(ctx, 'INVALID_BODY')
|
|
1552
|
+
|
|
1553
|
+
// ===== 微信 V2 回调验签 + mch_id 匹配 =====
|
|
1554
|
+
// 未识别到 xml.sign / return_code 时一律拒绝,防止未签名/异常报文进入业务
|
|
1555
|
+
if (result.xml.return_code === 'SUCCESS' && !result.xml.sign) {
|
|
1556
|
+
return sendWxFail(ctx, 'SIGN_MISSING')
|
|
1557
|
+
}
|
|
1558
|
+
const appConfig = getConfig(app)
|
|
1559
|
+
const pay_config = 'weixin_pay'
|
|
1560
|
+
const {
|
|
1561
|
+
key,
|
|
1562
|
+
partner_key,
|
|
1563
|
+
mch_id: cfg_mch_id,
|
|
1564
|
+
mchId,
|
|
1565
|
+
} = await appConfig.getObject(pay_config)
|
|
1566
|
+
const app_key = key && key.length > 10 ? key : partner_key
|
|
1567
|
+
if (!validateWxPayV2Sign(result.xml, app_key)) {
|
|
1568
|
+
app.service.log.push({
|
|
1569
|
+
app,
|
|
1570
|
+
message: `微信支付回调验签失败: ${JSON.stringify(result.xml)}`,
|
|
1571
|
+
})
|
|
1572
|
+
return sendWxFail(ctx, 'SIGN_ERROR')
|
|
1573
|
+
}
|
|
1574
|
+
// 商户号绑定校验:防止 A 商户的回调被伪装成 B 商户
|
|
1575
|
+
const boundMchId = cfg_mch_id || mchId
|
|
1576
|
+
if (boundMchId && result.xml.mch_id && boundMchId !== result.xml.mch_id) {
|
|
1577
|
+
app.service.log.push({
|
|
1578
|
+
app,
|
|
1579
|
+
message: `微信支付回调商户号不匹配: cfg=${boundMchId} got=${result.xml.mch_id}`,
|
|
1580
|
+
})
|
|
1581
|
+
return sendWxFail(ctx, 'MCH_MISMATCH')
|
|
1582
|
+
}
|
|
1583
|
+
// return_code !== SUCCESS 一般是微信端通知失败/上游异常,不处理业务但仍应答 SUCCESS 让微信不再重放
|
|
1584
|
+
if (result.xml.return_code !== 'SUCCESS') {
|
|
1585
|
+
ctx.status = 200
|
|
1586
|
+
ctx.res.setHeader('Content-Type', 'application/xml')
|
|
1587
|
+
ctx.res.end(
|
|
1588
|
+
jsonToXml({
|
|
1589
|
+
xml: { return_code: 'SUCCESS' },
|
|
1590
|
+
})
|
|
1591
|
+
)
|
|
1592
|
+
return
|
|
1593
|
+
}
|
|
1493
1594
|
|
|
1494
1595
|
let prefix = ''
|
|
1495
1596
|
let order_id = result.xml.out_trade_no.split('_')[1]
|
|
@@ -1510,7 +1611,6 @@ exports.notify = async (ctx) => {
|
|
|
1510
1611
|
transactionid = result.xml.transaction_id
|
|
1511
1612
|
} catch (e) {}
|
|
1512
1613
|
|
|
1513
|
-
// console.log(order_id, '微信支付回调-----', model, order_price, transactionid)
|
|
1514
1614
|
if (app.service[model] && app.service[model].notify) {
|
|
1515
1615
|
await app.service[model].notify({
|
|
1516
1616
|
ctx,
|
|
@@ -1722,15 +1822,52 @@ const getRefundResultJson = async ({ req_info, app_key }) => {
|
|
|
1722
1822
|
exports.refund_notify = async (ctx) => {
|
|
1723
1823
|
const { app } = getAppByCtx(ctx)
|
|
1724
1824
|
const result = ctx.request.xmlBody
|
|
1725
|
-
|
|
1825
|
+
if (!result || !result.xml) return sendWxFail(ctx, 'INVALID_BODY')
|
|
1726
1826
|
|
|
1827
|
+
const pay_config = ctx.params.sub || 'weixin_pay'
|
|
1727
1828
|
const appConfig = getConfig(app)
|
|
1728
|
-
const {
|
|
1829
|
+
const {
|
|
1830
|
+
key,
|
|
1831
|
+
partner_key,
|
|
1832
|
+
mch_id: cfg_mch_id,
|
|
1833
|
+
mchId,
|
|
1834
|
+
} = await appConfig.getObject(pay_config)
|
|
1835
|
+
const app_key = key && key.length > 10 ? key : partner_key
|
|
1836
|
+
|
|
1837
|
+
// ===== 退款回调外层 XML 先验签(req_info 是加密块,但外层 XML 必须签名防伪造) =====
|
|
1838
|
+
if (result.xml.return_code === 'SUCCESS' && !result.xml.sign) {
|
|
1839
|
+
return sendWxFail(ctx, 'SIGN_MISSING')
|
|
1840
|
+
}
|
|
1841
|
+
if (!validateWxPayV2Sign(result.xml, app_key)) {
|
|
1842
|
+
app.service.log.push({
|
|
1843
|
+
app,
|
|
1844
|
+
message: `微信退款回调验签失败: ${JSON.stringify(result.xml)}`,
|
|
1845
|
+
})
|
|
1846
|
+
return sendWxFail(ctx, 'SIGN_ERROR')
|
|
1847
|
+
}
|
|
1848
|
+
const boundMchId = cfg_mch_id || mchId
|
|
1849
|
+
if (boundMchId && result.xml.mch_id && boundMchId !== result.xml.mch_id) {
|
|
1850
|
+
app.service.log.push({
|
|
1851
|
+
app,
|
|
1852
|
+
message: `微信退款回调商户号不匹配: cfg=${boundMchId} got=${result.xml.mch_id}`,
|
|
1853
|
+
})
|
|
1854
|
+
return sendWxFail(ctx, 'MCH_MISMATCH')
|
|
1855
|
+
}
|
|
1856
|
+
if (result.xml.return_code !== 'SUCCESS') {
|
|
1857
|
+
ctx.status = 200
|
|
1858
|
+
ctx.res.setHeader('Content-Type', 'application/xml')
|
|
1859
|
+
ctx.res.end(
|
|
1860
|
+
jsonToXml({
|
|
1861
|
+
xml: { return_code: 'SUCCESS' },
|
|
1862
|
+
})
|
|
1863
|
+
)
|
|
1864
|
+
return
|
|
1865
|
+
}
|
|
1729
1866
|
|
|
1730
1867
|
const { req_info } = result.xml
|
|
1731
1868
|
const refundResult = await getRefundResultJson({
|
|
1732
1869
|
req_info,
|
|
1733
|
-
app_key
|
|
1870
|
+
app_key,
|
|
1734
1871
|
})
|
|
1735
1872
|
|
|
1736
1873
|
const {
|