Files
xk-doctor-wx/common/js/index.js
2026-08-19 16:48:43 +08:00

351 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)