import { ref } from 'vue' // 公网统一入口:POST http:///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