351 lines
11 KiB
JavaScript
351 lines
11 KiB
JavaScript
import Request from './request.js';
|
||
import errorCode from './errorCode.js';
|
||
import {Base64} from "js-base64";
|
||
import { getDoctorWxEnv } from '@/config/app-env.js';
|
||
import { resolveDoctorWxAuthToken } from '@/utils/doctorWxAuthToken.js';
|
||
import { setPublicAccountLock } from '@/utils/publicAccountLock.js'
|
||
|
||
const { legacyApiBase } = getDoctorWxEnv();
|
||
|
||
export const config = {
|
||
baseUrl: legacyApiBase
|
||
}
|
||
|
||
const arr = [
|
||
'mobile',
|
||
'call_mobile',
|
||
'express_mobile',
|
||
'doctor',
|
||
'accept_tel',
|
||
'patient_mobile',
|
||
'id_card',
|
||
'idcard',
|
||
'password',
|
||
'phone'
|
||
]
|
||
|
||
// 敏感数据(解密后做脱敏;需与下方 switch 分支一致)
|
||
// 注意:call_mobile 只解密不脱敏,供回访拨号使用
|
||
const sensitiveData = [
|
||
'id_card',
|
||
'idcard',
|
||
'mobile',
|
||
'express_mobile',
|
||
'patient_mobile',
|
||
'phone'
|
||
]
|
||
/**
|
||
* 请求拦截器
|
||
* @param {*} options
|
||
*/
|
||
const reqInterceptor = async (options) => {
|
||
// 加密
|
||
if (options.data != null && options.type !== 'upload') {
|
||
options.data = getRes(options.data, false)
|
||
}
|
||
options.header = {
|
||
...options.header
|
||
}
|
||
const skipToken = options.header.isToken === false
|
||
const isClinicAdminReq = options.header._clinicAdmin === '1'
|
||
|| (options.url && String(options.url).indexOf('clinic-admin') !== -1)
|
||
const isPlatformAdminReq = options.header._platformAdmin === '1'
|
||
|| (options.url && String(options.url).indexOf('platform-admin') !== -1)
|
||
const isSalespersonReq = options.header._salesperson === '1'
|
||
|| (options.url && String(options.url).indexOf('salesperson-') !== -1)
|
||
const isClinicSalespersonReq = options.header._clinicSalesperson === '1'
|
||
|| (options.url && String(options.url).indexOf('clinic-salesperson-') !== -1)
|
||
const isDoctorWxAuthReq = options.url && String(options.url).indexOf('doctor-wx-auth') !== -1
|
||
// 内部控制位,不得作为 HTTP 头发出
|
||
delete options.header.isToken
|
||
delete options.header._clinicAdmin
|
||
delete options.header._platformAdmin
|
||
delete options.header._salesperson
|
||
delete options.header._clinicSalesperson
|
||
const clinicToken = uni.getStorageSync('clinic_admin_token')
|
||
const platformToken = uni.getStorageSync('platform_admin_token')
|
||
const salespersonToken = uni.getStorageSync('salesperson_token')
|
||
const clinicSalespersonToken = uni.getStorageSync('clinic_salesperson_token')
|
||
const doctorToken = uni.getStorageSync('token')
|
||
const loginMode = uni.getStorageSync('loginMode')
|
||
if (!skipToken) {
|
||
if (isDoctorWxAuthReq) {
|
||
const authToken = resolveDoctorWxAuthToken()
|
||
if (authToken) {
|
||
options.header['authorization'] = `Bearer ${authToken}`
|
||
}
|
||
} else if (isPlatformAdminReq && platformToken) {
|
||
options.header['authorization'] = `Bearer ${platformToken}`
|
||
} else if (isSalespersonReq && salespersonToken) {
|
||
options.header['authorization'] = `Bearer ${salespersonToken}`
|
||
} else if (isClinicSalespersonReq && clinicSalespersonToken) {
|
||
options.header['authorization'] = `Bearer ${clinicSalespersonToken}`
|
||
} else if (isClinicAdminReq && clinicToken) {
|
||
options.header['authorization'] = `Bearer ${clinicToken}`
|
||
} else if (doctorToken) {
|
||
options.header['authorization'] = `Bearer ${doctorToken}`
|
||
}
|
||
}
|
||
return options;
|
||
}
|
||
|
||
/**
|
||
* 登录失效统一处理:弹窗提示并按 loginMode 清理 token 后跳转登录页
|
||
* @param {string} tipMsg 提示文案
|
||
* @param {*} response 原始响应
|
||
* @param {object} conf 请求配置(供 _responseLog 使用)
|
||
*/
|
||
const handleAuthExpired = (tipMsg, response, conf) => {
|
||
// 防重复弹窗:并发 401 只提示一次
|
||
if (!uni.__xkAuthModalShowing) {
|
||
uni.__xkAuthModalShowing = true
|
||
uni.showModal({
|
||
title: '登录失效',
|
||
content: tipMsg,
|
||
showCancel: false,
|
||
confirmText: '重新登录',
|
||
success() {
|
||
const loginMode = uni.getStorageSync('loginMode')
|
||
if (loginMode === 'platform_admin') {
|
||
uni.removeStorageSync('platform_admin_token')
|
||
uni.removeStorageSync('platform_admin_user')
|
||
uni.removeStorageSync('loginMode')
|
||
} else if (loginMode === 'salesperson') {
|
||
uni.removeStorageSync('salesperson_token')
|
||
uni.removeStorageSync('salesperson_user')
|
||
uni.removeStorageSync('loginMode')
|
||
} else if (loginMode === 'clinic_salesperson') {
|
||
uni.removeStorageSync('clinic_salesperson_token')
|
||
uni.removeStorageSync('clinic_salesperson_user')
|
||
uni.removeStorageSync('loginMode')
|
||
} else if (loginMode === 'clinic_admin') {
|
||
uni.removeStorageSync('clinic_admin_token')
|
||
uni.removeStorageSync('clinic_admin_user')
|
||
uni.removeStorageSync('loginMode')
|
||
} else {
|
||
uni.removeStorageSync('token')
|
||
uni.removeStorageSync('loginInfo')
|
||
uni.removeStorageSync('identity')
|
||
}
|
||
uni.__xkAuthModalShowing = false
|
||
uni.reLaunch({ url: '/pages/login/index' })
|
||
},
|
||
fail() {
|
||
uni.__xkAuthModalShowing = false
|
||
}
|
||
})
|
||
}
|
||
_responseLog(response, conf, "response 401")
|
||
return {
|
||
wakaryReqToReject: true,
|
||
msg: tipMsg,
|
||
res: response
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 响应拦截器
|
||
*/
|
||
const resInterceptor = (response, conf = {}) => {
|
||
// 网络状态码
|
||
const statusCode = response.statusCode|| response.errcode;
|
||
// 响应拦截器
|
||
if (statusCode >= 200 && statusCode < 300) {
|
||
_responseLog(response, conf, "response 200-299")
|
||
// 解密(Yii:业务在 data;Laravel jok:业务在 result)
|
||
let res = response.data
|
||
if (res == null || typeof res !== 'object') {
|
||
return res
|
||
}
|
||
// 4101 锁店:先解密联系人(会写出 phone_txt 再脱敏 phone),再落库给锁店页拨号
|
||
const bizCode = Number(res.code != null ? res.code : res.errcode)
|
||
if (bizCode === 4101) {
|
||
const payload = res.result || {}
|
||
if (payload.contact_phones) {
|
||
payload.contact_phones = getRes(payload.contact_phones, true)
|
||
}
|
||
setPublicAccountLock(payload)
|
||
return {
|
||
wakaryReqToReject: true,
|
||
msg: res.message || '公账日账单已逾期',
|
||
res: response
|
||
}
|
||
}
|
||
if (res.data != null) {
|
||
res.data = getRes(res.data)
|
||
}
|
||
if (res.result != null) {
|
||
res.result = getRes(res.result)
|
||
}
|
||
// Laravel notAuth 返回 HTTP 200 + 业务码 401,需与 HTTP 401 走同一套登录失效处理
|
||
if (bizCode === 401) {
|
||
return handleAuthExpired(res.msg || res.message || '账号需重新登录', response, conf)
|
||
}
|
||
return res
|
||
} else if (statusCode === 500) {
|
||
uni.showToast({
|
||
icon: 'error',
|
||
title: response['msg'] || errorCode[statusCode]
|
||
})
|
||
_responseLog(response, conf, "response 500")
|
||
// 增加一个控制字段wakaryReqToReject 使 reject的内容更加可控
|
||
return {
|
||
// 根据当前字段来判断是否reject
|
||
wakaryReqToReject: true,
|
||
// 下面可以配置其他返回信息,方便统一处理reject
|
||
// 以下内容作为reject的返回,根据需求处理,返回具体错误信息
|
||
msg: "服务器错误",
|
||
res: response
|
||
}
|
||
} else if (statusCode === 401) {
|
||
return handleAuthExpired(response['msg'] || response['message'] || errorCode[statusCode] || '账号需重新登录', response, conf)
|
||
} else {
|
||
uni.showToast({
|
||
icon: 'error',
|
||
title: response['msg'] || errorCode[statusCode]
|
||
})
|
||
_responseLog(response, conf, "response 300-499");
|
||
// 增加一个控制字段wakaryReqToReject 使 reject的内容更加可控
|
||
return {
|
||
// 根据当前字段来判断是否reject
|
||
wakaryReqToReject: true,
|
||
// 下面可以配置其他返回信息,方便统一处理reject
|
||
// 以下内容作为reject的返回,根据需求处理,返回具体错误信息
|
||
msg: "response 300-499",
|
||
res: response
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* request log
|
||
*/
|
||
function _requestLog(req, describe = null) {
|
||
if (process.env.NODE_ENV === 'development') {}
|
||
}
|
||
|
||
/**
|
||
* response log
|
||
*/
|
||
function _responseLog(res, conf = {}, describe = null) {
|
||
let _statusCode = res.statusCode;
|
||
if (process.env.NODE_ENV === 'development') {}
|
||
|
||
if (_statusCode === 500) {
|
||
// save log to server
|
||
}
|
||
}
|
||
|
||
/** 解密后写出 {字段}_txt 明文,给锁店拨号等场景用;原字段仍脱敏 */
|
||
const PHONE_TXT_KEYS = ['phone', 'mobile', 'call_mobile', 'express_mobile', 'patient_mobile', 'accept_tel']
|
||
|
||
function getRes(obj, isDecode = true) {
|
||
if (obj == null || typeof obj !== 'object') {
|
||
return obj
|
||
}
|
||
if (Array.isArray(obj)) {
|
||
for (let i = 0; i < obj.length; i++) {
|
||
if (obj[i] != null && typeof obj[i] === 'object') {
|
||
obj[i] = getRes(obj[i], isDecode)
|
||
}
|
||
}
|
||
return obj
|
||
}
|
||
try {
|
||
for (const key in obj) {
|
||
if (obj[key] != null && typeof obj[key] === 'object') {
|
||
obj[key] = getRes(obj[key], isDecode)
|
||
} else if (arr.indexOf(key) !== -1) {
|
||
if (obj[key] == null || obj[key] === '') continue
|
||
let aseFile = ''
|
||
if (isDecode === true) {
|
||
aseFile = customBase64Decode(obj[key])
|
||
} else {
|
||
aseFile = customBase64Encode(obj[key])
|
||
}
|
||
const isGarbled = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\xFF]|[\u{E000}-\u{F8FF}]/u.test(
|
||
typeof aseFile === 'string' ? aseFile : ''
|
||
)
|
||
if (isGarbled) {
|
||
aseFile = obj[key]
|
||
}
|
||
// 请求必须加密,不能跳过;解密后先留明文 _txt 再脱敏
|
||
if (isDecode === true && PHONE_TXT_KEYS.indexOf(key) !== -1) {
|
||
const plain = String(aseFile || '')
|
||
if (/^1\d{10}$/.test(plain)) {
|
||
obj[key + '_txt'] = plain
|
||
}
|
||
}
|
||
if (isDecode === true && sensitiveData.indexOf(key) !== -1) {
|
||
switch (key) {
|
||
case 'express_name':
|
||
case 'express_region':
|
||
case 'accept_name':
|
||
case 'patient':
|
||
aseFile = aseFile.slice(0, 1).padEnd(aseFile.length, '*');
|
||
break;
|
||
case 'mobile':
|
||
case 'express_mobile':
|
||
case 'phone':
|
||
aseFile = aseFile.slice(0, 3).padEnd(aseFile.length - 4, '*') + aseFile.slice(-4);
|
||
break;
|
||
case 'id_card':
|
||
case 'idcard':
|
||
aseFile = aseFile.slice(0, 6).padEnd(aseFile.length - 4, '*') + aseFile.slice(-4);
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
}
|
||
obj[key] = aseFile
|
||
}
|
||
}
|
||
return obj
|
||
} catch (error) {
|
||
console.error('错误', error)
|
||
return obj
|
||
}
|
||
}
|
||
|
||
function customBase64Decode(data) {
|
||
if (data == null || data === '') return data
|
||
// return data
|
||
try {
|
||
// 第一次Base64解码
|
||
const decodedFirst = Base64.decode(data)
|
||
// 截取从第30个字符开始往后的内容
|
||
const subStr = decodedFirst.slice(30)
|
||
// 第二次Base64解码
|
||
return Base64.decode(subStr) != ''? Base64.decode(subStr): data
|
||
} catch (error) {
|
||
console.error(`解码Base64字符串【${data}】时发生错误`, error)
|
||
return data
|
||
}
|
||
}
|
||
|
||
// 加密
|
||
function customBase64Encode(data) {
|
||
if (data == null || data === '') return data
|
||
// return data
|
||
try {
|
||
// 和加密方法差不多但是是要生成30个随机字符
|
||
let randomString = ''
|
||
for (let i = 0; i < 30; i++) {
|
||
const randomChar = String.fromCharCode(Math.floor(Math.random() * (126 - 32 + 1)) + 32)
|
||
randomString += randomChar
|
||
}
|
||
const encoded = Base64.encode(data)
|
||
return Base64.encode(randomString + encoded)
|
||
} catch (error) {
|
||
console.error(`加密字符串【${data}】时发生错误`, error)
|
||
return data
|
||
}
|
||
}
|
||
|
||
|
||
/**
|
||
* 请求方法封装
|
||
*/
|
||
|
||
export const req = new Request(config, reqInterceptor, resInterceptor)
|