You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

295 lines
8.0 KiB
JavaScript

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import { ref } from 'vue'
// 公网统一入口POST http://<gateway-host>/public
// 这里留一个可配置的占位符,实际项目中建议从配置文件或环境变量读取
const baseURL = 'http://padapi.queuingsystem.cn/public'
// const baseURL = 'https://api-dsb.dingtax.cn/dsb/api/tax-appoint/dx/third/request/public'
// 签名计算也走统一入口,通过约定的签名 tag/path 转发到内部 /api/auth
const SIGN_TAG = 'pad.auth'
const SIGN_PATH = '/sign'
// 全局 loading 状态,可在页面上直接使用
export const loading = ref(false)
// 生成 traceId时间戳 + 随机数(满足文档“可追踪”的要求即可)
const genTraceId = () => {
const ts = new Date()
.toISOString()
.replace(/[-:TZ.]/g, '')
const rand = Math.floor(Math.random() * 10000)
.toString()
.padStart(4, '0')
return `${ts}${rand}`
}
// 生成随机 nonce用于签名防重放
const genNonce = () => {
return `n-${Math.random().toString(36).slice(2, 10)}`
}
// 组装接口要求的公共请求头
const buildHeaders = (withToken = true) => {
const headers = {
'Content-Type': 'application/json'
}
// 如有额外网关签名、appId 等,统一在这里追加
// headers['X-App-Id'] = 'xxx'
// headers['X-Sign'] = 'xxx'
if (withToken) {
const token = uni.getStorageSync('token')
if (token) {
headers.Authorization = `Bearer ${token}`
}
}
return headers
}
// 统一处理 HTTP 状态码和业务状态码(文档规范)
const handleResponse = (res) => {
const { statusCode, data } = res
// 1. HTTP 层异常
if (statusCode !== 200) {
let msg = '网络请求异常'
if (statusCode === 400) msg = '请求参数错误'
if (statusCode === 401) msg = '未授权访问'
if (statusCode === 403) msg = '权限不足'
if (statusCode === 404) msg = '资源不存在'
if (statusCode >= 500) msg = '服务器内部错误'
uni.showToast({
title: `${msg} (${statusCode})`,
icon: 'none'
})
console.log('[request][http-error]', res)
throw res
}
// 2. 业务层:文档约定 code=200 成功,其它为错误
if (data && data.code === 200) {
// 返回真正的业务数据
return data.data !== undefined ? data.data : data
}
const bizCode = data && data.code
const bizMsg = (data && data.msg) || '请求失败'
uni.showToast({
title: `${bizMsg}${bizCode ? ` (${bizCode})` : ''}`,
icon: 'none'
})
console.log('[request][biz-error]', data)
throw data
}
/**
* 统一 PAD 公网入口请求封装
*
* 签名流程:
* 1. 前端先准备业务参数(不含 signature
* 2. 调用签名服务(同样走 /public传入 { tag, path, query, body, timestamp, nonce }
* 3. 后端用 secret key 计算 HMAC-SHA256返回 signature
* 4. 前端携带 signature/timestamp/nonce 调用真正网关入口 /public
*
* @param {Object} options
* - tag: 路由标签(必填),如 'pad.auth'
* - path: map.path 子路径(必填),如 '/login'
* - method: 业务含义上的 method必填如 'GET' / 'POST',会写入 map.head.method
* - query: 对应 map.query 对象
* - body: 对应 map.body 对象
* - traceId: 可选,自定义链路号;不传则自动生成
* - withToken: 是否携带 token登录等接口传 false
* - skipSign: 是否跳过签名(登录 /login 时传 true不调 /api/sign、不校验签名
* - loading: 是否展示 loading
* - loadingText: loading 文案
*/
const request = (options) => {
const {
tag,
path,
method = 'GET',
query = {},
body = {},
traceId,
withToken = true,
skipSign = false,
loading: showLoading = true,
loadingText = '加载中...'
} = options
if (!tag) {
throw new Error('request 需要传入 tag')
}
if (!path) {
throw new Error('request 需要传入 path')
}
const finalTraceId = traceId || genTraceId()
// 登录等 skipSign 场景:不携带 token
const useToken = skipSign ? false : withToken
const doRequest = (payload) => {
return new Promise((resolve, reject) => {
uni.request({
url: baseURL,
method: 'POST',
data: payload,
header: buildHeaders(useToken),
success: (res) => {
try {
const result = handleResponse(res)
resolve(result)
} catch (e) {
console.log('[request][handleResponse-error]', e)
reject(e)
}
},
fail: (err) => {
uni.showToast({
title: '网络请求失败',
icon: 'none'
})
console.log('[request][network-fail]', err)
reject(err)
},
complete: () => {
if (showLoading) {
loading.value = false
uni.hideLoading()
}
}
})
})
}
if (showLoading) {
loading.value = true
uni.showLoading({
title: loadingText,
mask: true
})
}
// 登录等接口:跳过 /api/sign直接请求业务网关head 中不含 signature/timestamp/nonce
if (skipSign) {
const payload = {
tag,
map: {
traceId: finalTraceId,
head: {
method,
contentType: 'application/json'
},
path,
query,
body
}
}
return doRequest(payload)
}
// 常规流程:先走统一入口调用签名服务,再携带 signature/timestamp/nonce 调业务接口
const timestamp = Date.now()
const nonce = genNonce()
const tokenForSign = uni.getStorageSync('token')
const signPayload = {
tag: SIGN_TAG,
map: {
head: {
method: 'POST',
contentType: 'application/json',
timestamp,
nonce,
// 调用签名服务时在 head 中显式传入 token
...(tokenForSign ? { Authorization: `Bearer ${tokenForSign}` } : {})
},
path: SIGN_PATH,
query: {},
// 把真实业务请求关键信息放到 body 里交给签名服务计算:
// { tag, path, query, body, timestamp, nonce }
body: {
tag,
path,
query,
body,
timestamp,
nonce
}
}
}
return new Promise((resolve, reject) => {
uni.request({
// 签名服务也通过公网统一入口 /public由 SIGN_TAG + SIGN_PATH 路由到内部 /api/sign
url: baseURL,
method: 'POST',
data: signPayload,
header: buildHeaders(useToken),
success: (signRes) => {
try {
if (!(signRes.statusCode === 200 && signRes.data && signRes.data.code === 200)) {
const msg = (signRes.data && signRes.data.msg) || '获取签名失败'
uni.showToast({
title: msg,
icon: 'none'
})
console.log('[request][sign-response-error]', signRes)
throw signRes
}
const signature =
(signRes.data.data && signRes.data.data.signature) || signRes.data.signature
if (!signature) {
console.log('[request][sign-empty]', signRes)
throw new Error('签名结果为空')
}
const payload = {
tag,
map: {
traceId: finalTraceId,
head: {
method,
contentType: 'application/json',
signature,
timestamp,
nonce
},
path,
query,
body
}
}
doRequest(payload).then(resolve).catch(reject)
} catch (err) {
if (showLoading) {
loading.value = false
uni.hideLoading()
}
console.log('[request][sign-process-error]', err)
reject(err)
}
},
fail: (err) => {
if (showLoading) {
loading.value = false
uni.hideLoading()
}
uni.showToast({
title: '获取签名接口失败',
icon: 'none'
})
console.log('[request][sign-network-fail]', err)
reject(err)
}
})
})
}
export default request