|
|
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 |