n20-common-lib 3.3.21 → 3.3.24
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/package.json +1 -1
- package/src/utils/axios.js +32 -3
- package/src/utils/risk.js +91 -0
package/package.json
CHANGED
package/src/utils/axios.js
CHANGED
|
@@ -2,6 +2,7 @@ import _axios from 'axios'
|
|
|
2
2
|
import ElementUI from 'element-ui'
|
|
3
3
|
import auth from './auth'
|
|
4
4
|
import { $lc } from './i18n/index'
|
|
5
|
+
import { isRiskPause, processRiskPause, buildRiskHeaders } from './risk'
|
|
5
6
|
const _Loading = ElementUI.Loading
|
|
6
7
|
const _Message = ElementUI.Message
|
|
7
8
|
|
|
@@ -124,7 +125,8 @@ function errorFn(status, msg, noMsg, res) {
|
|
|
124
125
|
axios.interceptors.response.use(
|
|
125
126
|
(res) => {
|
|
126
127
|
let status = (res.data && res.data.code) || res.status
|
|
127
|
-
|
|
128
|
+
// 风控暂停包(902/903)放行到 request(),由其弹框确认后重发;其余非200走错误提示
|
|
129
|
+
if (Number(status) === 200 || isRiskPause(res.data)) {
|
|
128
130
|
return res
|
|
129
131
|
} else {
|
|
130
132
|
let msg = (res.data && (res.data.msg || res.data.message)) || ''
|
|
@@ -197,6 +199,9 @@ function request(opt) {
|
|
|
197
199
|
return debounceObj[opt.method + opt.url]
|
|
198
200
|
}
|
|
199
201
|
|
|
202
|
+
// 风控确认流程接管 promise:暂停包到达时赋值;resPro 本身照常落定以释放 loading/防抖
|
|
203
|
+
let riskChain = null
|
|
204
|
+
|
|
200
205
|
let resPro = new Promise((resolve, reject) => {
|
|
201
206
|
axios
|
|
202
207
|
.request(opt)
|
|
@@ -223,6 +228,25 @@ function request(opt) {
|
|
|
223
228
|
resolve(res.data)
|
|
224
229
|
}
|
|
225
230
|
} else {
|
|
231
|
+
if (isRiskPause(res.data) && !opt.__riskRetry) {
|
|
232
|
+
// 风控暂停包:弹框→确认后原参数重发(risk 拼 body 主通道 + 请求头兜底)→返回最终业务包;取消/超限 reject
|
|
233
|
+
riskChain = processRiskPause(res.data, (risk) =>
|
|
234
|
+
request(
|
|
235
|
+
Object.assign({}, opt, {
|
|
236
|
+
// v3.4:risk 对象原样拼进 body(仅普通对象拼;数组/FormData/GET 不拼,走头兜底)
|
|
237
|
+
data:
|
|
238
|
+
opt.data && typeof opt.data === 'object' && !Array.isArray(opt.data) && typeof opt.data.append !== 'function'
|
|
239
|
+
? Object.assign({}, opt.data, { risk })
|
|
240
|
+
: opt.data,
|
|
241
|
+
headers: Object.assign({}, opt.headers, buildRiskHeaders(risk)),
|
|
242
|
+
__riskRetry: true
|
|
243
|
+
})
|
|
244
|
+
)
|
|
245
|
+
)
|
|
246
|
+
// resPro 先落定:finally 释放 loading 与防抖;页面拿到的最终结果由 riskChain 决定
|
|
247
|
+
resolve(res.data)
|
|
248
|
+
return
|
|
249
|
+
}
|
|
226
250
|
if (typeof res.data === 'object' && res.data.data === undefined) {
|
|
227
251
|
res.data.data = {}
|
|
228
252
|
}
|
|
@@ -239,8 +263,13 @@ function request(opt) {
|
|
|
239
263
|
|
|
240
264
|
closeLoading({ loading })
|
|
241
265
|
}).catch(() => {}) // 防止 finally 返回的丢弃 Promise 产生未处理的 rejection
|
|
242
|
-
|
|
243
|
-
|
|
266
|
+
|
|
267
|
+
// 对外结果:resPro 落定时若风险流程已接管(riskChain 已赋值),转发其终态
|
|
268
|
+
let outPro = resPro.then(function (v) {
|
|
269
|
+
return riskChain ? riskChain : v
|
|
270
|
+
})
|
|
271
|
+
debounceObj[opt.method + opt.url] = outPro
|
|
272
|
+
return outPro
|
|
244
273
|
}
|
|
245
274
|
|
|
246
275
|
function getFilename(cd = '') {
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 事中风控暂停包处理(协议2.0,前端公共支持)
|
|
3
|
+
*
|
|
4
|
+
* 约定:后端原业务接口命中风险或风控技术异常时,业务不执行,统一返回 code=902 暂停包:
|
|
5
|
+
* { code: 902, msg, data: null, risk: {...} }
|
|
6
|
+
* 其中 risk.action 区分两类:
|
|
7
|
+
* RISK_CONFIRM 风险确认框,确认后按 RISK_CONFIRM 重发(回传风控事件编号)
|
|
8
|
+
* 其他(DEGRADED_*)异常知悉框,确认后按 DEGRADED_CONFIRM 重发(不带事件编号,后端不调风控直接执行)
|
|
9
|
+
* 重发 = 原 URL/方法/参数 + 风控阶段请求头,由 axios.js 的 request() 内部完成,业务页面零改造。
|
|
10
|
+
*/
|
|
11
|
+
import Vue from 'vue'
|
|
12
|
+
import ElementUI from 'element-ui'
|
|
13
|
+
|
|
14
|
+
/** 风控暂停包统一返回码 */
|
|
15
|
+
const RISK_PAUSE_CODE = 902
|
|
16
|
+
/** 单次提交任务原接口最大请求次数:首次 + 正常确认 + 确认又异常的降级确认 */
|
|
17
|
+
const MAX_REQUESTS = 3
|
|
18
|
+
|
|
19
|
+
const MessageBox = ElementUI.MessageBox
|
|
20
|
+
|
|
21
|
+
/** 是否风控暂停包:码值 + 顶层 risk 元数据双条件,防止误伤普通响应 */
|
|
22
|
+
export function isRiskPause(data) {
|
|
23
|
+
return !!(data && data.code === RISK_PAUSE_CODE && data.risk)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 暂停包主流程:弹框 → 确认则原参数重发(risk 拼进 body,头兜底) → 直到业务包或终态。
|
|
28
|
+
* @param {Object} firstData 首次请求返回的暂停包(res.data)
|
|
29
|
+
* @param {Function} send (risk) => Promise<data> 用原参数重发,risk 为后端下发的确认上下文
|
|
30
|
+
* @returns {Promise<Object>} 最终业务包(code=200);取消/超限 reject
|
|
31
|
+
*/
|
|
32
|
+
export async function processRiskPause(firstData, send) {
|
|
33
|
+
let data = firstData
|
|
34
|
+
let count = 1
|
|
35
|
+
while (isRiskPause(data) && count < MAX_REQUESTS) {
|
|
36
|
+
const confirmed = await openRiskDialog(data)
|
|
37
|
+
if (!confirmed) {
|
|
38
|
+
throw { code: data.code, msg: data.msg || '已取消提交', risk: data.risk, riskCancelled: true }
|
|
39
|
+
}
|
|
40
|
+
count++
|
|
41
|
+
data = await send((data.risk || {}))
|
|
42
|
+
}
|
|
43
|
+
if (isRiskPause(data)) {
|
|
44
|
+
throw { code: data.code, msg: '风险确认次数超限,请检查数据后重新提交', risk: data.risk, riskExhausted: true }
|
|
45
|
+
}
|
|
46
|
+
return data
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 确认重发的请求头兜底(GET 等无 body 场景)。
|
|
51
|
+
* POST 主通道是把 risk 对象原样拼进 body(axios.js 内处理),此处仅保留头兜底。
|
|
52
|
+
*/
|
|
53
|
+
export function buildRiskHeaders(risk) {
|
|
54
|
+
const action = risk.action === 'RISK_CONFIRM' ? 'RISK_CONFIRM' : 'DEGRADED_CONFIRM'
|
|
55
|
+
const headers = {
|
|
56
|
+
'X-Biz-Request-Id': risk.requestId,
|
|
57
|
+
'X-Risk-Action': action
|
|
58
|
+
}
|
|
59
|
+
if (action === 'RISK_CONFIRM' && risk.confirmEventId) {
|
|
60
|
+
headers['X-Risk-Event-Id'] = risk.confirmEventId
|
|
61
|
+
}
|
|
62
|
+
return headers
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 标准风控弹窗:展示后端 msg(含真实原因)。resolve(true)=确认继续,resolve(false)=取消。
|
|
67
|
+
* 文本转义渲染,不注入 HTML。
|
|
68
|
+
*/
|
|
69
|
+
function openRiskDialog(data) {
|
|
70
|
+
const degraded = !data.risk || data.risk.action !== 'RISK_CONFIRM'
|
|
71
|
+
const h = new Vue().$createElement
|
|
72
|
+
const fallback = degraded ? '风控校验未完成,是否知悉并继续提交?' : '请确认风险提示后继续提交'
|
|
73
|
+
const message = h(
|
|
74
|
+
'div',
|
|
75
|
+
{ style: { fontSize: '14px', color: '#303133', lineHeight: '22px', maxWidth: '440px' } },
|
|
76
|
+
String(data.msg || fallback)
|
|
77
|
+
)
|
|
78
|
+
return MessageBox({
|
|
79
|
+
title: degraded ? '风控服务异常' : '风险提示',
|
|
80
|
+
type: degraded ? 'info' : 'warning',
|
|
81
|
+
message,
|
|
82
|
+
customClass: 'n20-risk-msgbox',
|
|
83
|
+
showCancelButton: true,
|
|
84
|
+
confirmButtonText: degraded ? '知悉并继续' : '确认继续',
|
|
85
|
+
cancelButtonText: '取消',
|
|
86
|
+
closeOnClickModal: false,
|
|
87
|
+
closeOnPressEscape: false
|
|
88
|
+
})
|
|
89
|
+
.then(() => true)
|
|
90
|
+
.catch(() => false)
|
|
91
|
+
}
|