Commit 1a610735 authored by 郑磊's avatar 郑磊

v2分支

parent 2b1808c2
root = true
[*]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
spaces_around_operators = true
[*.{yaml,yml}]
indent_size = 2
node_modules/
/coverage
dist/
......@@ -2,12 +2,12 @@
"arrowParens": "always",
"bracketSameLine": false,
"bracketSpacing": true,
"endOfLine": "auto",
"endOfLine": "lf",
"printWidth": 100,
"proseWrap": "preserve",
"quoteProps": "as-needed",
"semi": false,
"singleQuote": true,
"tabWidth": 4,
"trailingComma": "all",
"useTabs": false
}
import axios from 'axios'
import { merge, sample } from 'lodash-es'
import { lang } from './environment'
import { aesDecrypt, aesEncrypt, base64Decode, base64Encode, md5 } from './helpers'
import { app_token } from './token'
/**
* 调用接口的基础配置项(无关具体的接口)
*/
export interface ApiBaseOptions {
/**
* 基地址
*/
baseURL?: string
/**
* 调用接口的token
*/
token?: string
/**
* 设备类型,默认为`wap`
*/
device?: string
/**
* 包名,默认为`xinxiuweb`
*/
source?: string
/**
* 语言标识
*/
lang?: string
/**
* 如果接口响应为失败是否抛出异常
*/
throwOnFail?: boolean
/**
* 版本号,默认为`1.0.0`
*/
appVersion?: string
/**
* SDK容器标识
*/
partnerKey?: string
/**
* SDK容器版本
*/
sdkVersion?: string
}
/**
* 调用api接口的配置项
*/
export interface ApiOptions extends ApiBaseOptions {
/**
* 调用接口的方法,默认为`POST`
*/
method?: string
/**
* 请求头
*/
headers?: Record<string, string>
/**
* 接口地址
*/
url: string
/**
* 请求体
*/
data?: Record<string, any>
}
/**
* 接口响应基础数据结构
*/
export interface ApiResp<TData = any> {
/**
* 其他的响应字段
*/
[name: string]: any
/**
* 接口响应码,`200`表示调用成功
*/
code: number
/**
* 错误消息
*/
msg: string
/**
* 接口返回的数据
*/
data: TData
}
/**
* 默认的配置项
*/
const defaultOptions: ApiBaseOptions = {
device: 'wap',
lang,
source: 'xinxiuweb',
appVersion: '1.0.0',
}
/**
* 调用API接口
* @param options 接口调用配置项
*/
export async function api<TData = void>(options: ApiOptions): Promise<ApiResp<TData>> {
const {
headers = {},
data = {},
token,
device,
lang,
throwOnFail = false,
source,
partnerKey,
appVersion,
sdkVersion,
...rest
} = merge({}, defaultOptions, options)
//向请求头中注入公共公参
injectHeader(headers, 'device', device)
injectHeader(headers, 'lang', lang)
injectHeader(headers, 'source', source)
injectHeader(headers, 'timestamp', () => Math.floor(Date.now() / 1000).toString())
injectHeader(headers, 'request_number', () => md5((Date.now() + Math.random()).toString()))
injectHeader(headers, 'appversion', appVersion)
injectHeader(headers, 'partner_key', partnerKey)
injectHeader(headers, 'sdk_version', sdkVersion)
//向请求头中注入token
let tokenValue = ''
if (typeof tokenValue !== 'string') {
tokenValue = app_token
}
if (tokenValue) {
injectHeader(headers, 'token', tokenValue)
}
//计算并注入签名
injectHeader(headers, 'signature', getSignature(headers, data))
//请求体加密
//先选定一个密钥
const key = sample(ENCRYPTION_KEYS)!
//加密请求体
const sign_data = encryptRequestData(data, key.key, key.iv)
//向请求头中写入加密标识
headers.sign_name = key.name
//执行请求
const resp = await axios.request<ApiResp<TData>>({
...rest,
headers,
data: {
sign_data,
},
})
const result = resp.data
//错误检查和解密
if (typeof result === 'object' && result) {
if (result.code !== 200 && throwOnFail) {
throw new ApiError(result)
}
//响应数据解密
decryptResponseData(result, key.key, key.iv)
}
return result
}
function injectHeader(
headers: Record<string, string>,
name: string,
value: string | null | undefined | (() => string | null | undefined),
) {
if (typeof headers[name] === 'string') return
const val = typeof value === 'function' ? value() : value
if (typeof val === 'string') {
headers[name] = val
}
}
/**
* 请求头中参与签名的字段
*/
const HEADER_SIGN_KEYS = [
'source',
'device',
'appversion',
'token',
'partner_key',
'sdk_version',
'timestamp',
'request_number',
]
/**
* 计算签名
* @param headers
* @param data
*/
function getSignature(headers: Record<string, string>, data: Record<string, any>): string {
//构建待签名的数据
const rawData: Record<string, any> = {}
HEADER_SIGN_KEYS.forEach((key) => {
if (typeof headers[key] === 'string') {
rawData[key] = headers[key]
}
})
merge(rawData, data)
//确定签名用的加密串
const secret = headers.device === 'wap' ? 'asdasgfdwqew' : 'WQdflHhKjdafsj21321jkl531l45'
//先对参数名按字典序排个序
const entries = Object.entries(rawData).sort((t1, t2) => t1[0].localeCompare(t2[0]))
//再根据参数值,生成对应的字符串值
const values = entries.map(([_, value]) => {
if (
typeof value === 'undefined' ||
typeof value === 'function' ||
typeof value === 'symbol' ||
value === null
) {
return ''
} else if (typeof value === 'object') {
return JSON.stringify(value) ?? ''
} else {
return String(value)
}
})
//拼接原始字符串值,并在后面追加签名密钥,得到基础签名串
const baseStr = values.join('') + secret
//进行2次md5,得到最终签名串
return md5(md5(baseStr))
}
/**
* 加解密使用的密钥
*/
interface EncryptionKey {
name: string
key: string
iv: string
}
/**
* 加解密使用的密钥
*/
const ENCRYPTION_KEYS: EncryptionKey[] = [
{
key: 'k0f3JfxEWd3HC7pXQU8tmSkDheUXibmz',
iv: 'Fu2wU73MBcsEZWJk',
name: 'secret_key_1',
},
{
key: 'NdEHDuAZGCQV6C0oaURvBJJWA4z2QHyG',
iv: 'w0j9K4bswcGJKtj5',
name: 'secret_key_2',
},
{
key: 'lBsn3FrYN9hdoFqMdqqNV1dlpGutkcXk',
iv: 'NRHqJYTNKxH8Z9fU',
name: 'secret_key_3',
},
{
key: 'eVdEc44rke6P6rfn9SgEWGPNqkcipWN7',
iv: 'l5HNU6Q0bdc2piB3',
name: 'secret_key_4',
},
{
key: '53BSHwE29I1u4e5cJDMZGHUdi0A7L4E5',
iv: 'zUsjrk58p2RahP08',
name: 'secret_key_5',
},
{ key: 'vgizOWReMzVJA6LEsb9N36LEzPqcFdeO', iv: 'e52YLbnvVv4HpGu7', name: 'secret_key_6' },
]
/**
* 加密请求体数据
*/
function encryptRequestData(data: Record<string, any>, key: string, iv: string) {
//先得到原始请求体的JSON字符串
const json = JSON.stringify(data ?? {})
//先进行aes加密
const encrypted = aesEncrypt(json, key, iv)
//接下来是base64转码
const base64 = base64Encode(encrypted)
//最后做一次urlencode
return encodeURIComponent(base64)
}
/**
* 解密响应体数据
* @param encrypted
* @param key
* @param iv
*/
function decryptResponseData(result: ApiResp, key: string, iv: string) {
//响应体不是对象
if (typeof result !== 'object' || !result) return
//响应体上没有encrypted_data属性
if (typeof result.encrypted_data !== 'string' || !result.data.encrypted_data) return
const decrypted = aesDecrypt(result.encrypted_data, key, iv)
//然后做一次base64解码
const base64_decoded = base64Decode(decrypted)
//最后做一次url解码
const url_decoded = decodeURIComponent(base64_decoded)
//JSON解析并覆盖原data属性
result.data = JSON.parse(url_decoded)
}
/**
* 设置默认配置项
*/
api.setDefaultOptions = function (options: ApiBaseOptions) {
merge(defaultOptions, options)
}
/**
* 由于接口错误响应码引发的异常
*/
export class ApiError extends Error {
readonly response: ApiResp
constructor(response: ApiResp) {
super(response.msg)
this.response = response
}
}
/**
* 读取页面cookie
*/
const cookies = (() => {
const data = new Map<string, string>()
const parts = document.cookie.split('; ')
for (let i = 0; i < parts.length; i++) {
const part = parts[i]
let [name] = part.split('=', 1)
if (name === '') continue
let value = part.substring(name.length + 1)
if (name === '' || typeof value !== 'string') continue
name = decodeURIComponent(name)
value = decodeURIComponent(value)
data.set(name, value)
}
return data
})()
/**
* 获取页面的cookie值
* @param name cookie键名
* @param defaultValue 获取失败时返回的默认值
*/
export function getCookie(name: string, defaultValue: string): string
/**
* 获取页面的cookie值
* @param name cookie键名
*/
export function getCookie(name: string): string | undefined
export function getCookie(name: string, defaultValue?: string) {
return cookies.has(name) ? cookies.get(name)! : defaultValue
}
import { getCookie } from './cookie'
/**
* 设备类型
*/
export type DeviceType = 'iOS' | 'android' | 'wap'
/**
* 包名
*/
export const source = getCookie('source') || 'xinxiuweb'
/**
* 是否在app内运行
*/
export const in_app = getCookie('platform') === 'app' && !!source
/**
* 设备类型
*/
export const device: DeviceType = in_app
? !!navigator.userAgent.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/)
? 'iOS'
: 'android'
: 'wap'
/**
* 是否允许使用内购
*/
export const can_use_iap = in_app ? getCookie('canUseIAP') === 'true' : false
/**
* 当前所在直播间的主播号
*/
export const room_number = (() => {
if (!in_app) return 0
if (getCookie('isOnLiveRoom') !== '1') return 0
const identifier = parseInt(getCookie('roomIdentifier', '0'))
return !isNaN(identifier) && Number.isSafeInteger(identifier) && identifier > 0 ? identifier : 0
})()
/**
* 标准化语言标识
* @param lang
*/
function normalizeLang(lang: string) {
return lang.toLowerCase().split(/-_/)[0]
}
/**
* 语言标识
*/
export const lang = normalizeLang(getCookie('lang') || navigator.language)
import { device } from './environment'
interface IosWebkitInterface {
messageHandlers: {
[name: string]: {
......@@ -16,10 +18,9 @@ declare global {
}
}
function callIosApi<T>(name: string, data: any, ack?: (data: T) => void): void
function callIosApi<T>(name: string, ack?: (data: T) => void): void
function callIosApi<T>(name: string, ...args: any[]): void {
if (typeof window.webkit?.messageHandlers[name]?.postMessage === 'function') {
function callIosApi(name: string, ...args: any[]): void {
if (typeof window.webkit?.messageHandlers[name]?.postMessage !== 'function') {
console.warn('No Bmiss environment detected')
return
}
......@@ -34,16 +35,18 @@ function callIosApi<T>(name: string, ...args: any[]): void {
if (typeof ack === 'function') {
// @ts-ignore
window[name] = ack
window[name] = (...args: any[]) => {
console.log('callback', name, ...args)
ack(...args)
}
}
window.webkit.messageHandlers[name].postMessage(params ?? true)
}
function callAndroidApi<T>(name: string, data: any, ack?: (data: T) => void): void
function callAndroidApi<T>(name: string, ack?: (data: T) => void): void
function callAndroidApi(name: string, ...args: any[]): void {
if (typeof window.liveapp[name] !== 'function') {
console.warn('No Bmiss environment detected')
return
}
......@@ -68,123 +71,54 @@ function callAndroidApi(name: string, ...args: any[]): void {
}
}
const isiOS = !!navigator.userAgent.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/) //ios终端
/**
* 调用原生提供的api
* @param name
* @param params
*/
export function callNativeApi<T>(name: string, data: any, ack?: (data: T) => void): void
export function callNativeApi<T>(name: string, ack?: (data: T) => void): void
export function callNativeApi(name: string, ...args: any[]): void {
isiOS ? callIosApi(name, ...args) : callAndroidApi(name, ...args)
}
export namespace NativeApi {
interface ActionAsPhpData {
app_open: string
url: string
ios_url: string
android_url: string
}
/**
* 跳转到
* @param data 跳转参数
*/
export function actionAsPhp(data: ActionAsPhpData): void {
callNativeApi('actionAsPhp', data)
}
/**
* 关闭当前页面
*/
export function closeWeb() {
callNativeApi('closeWeb')
}
/**
* 获取状态栏高度
* @returns
*/
export function getStatusBarHeight(): Promise<number> {
return new Promise<number>((resolve) => callNativeApi('getStatusBarHeight', resolve))
function callAndroidAsyncApi(name: string, ...args: any[]): void {
if (typeof window.liveapp[name] !== 'function') {
console.warn('No Bmiss environment detected')
return
}
/**
* 获取底部安全区高度
* @returns
*/
export function getBottomSafeHeight(): Promise<number> {
return new Promise<number>((resolve) => callNativeApi('getBottomSafeHeight', resolve))
}
const nativeMethod = window.liveapp[name]
/**
* 绑定支付宝
*/
export function openBindZfb() {
callNativeApi('openBindZfb')
}
const ack = args.pop() as Function
const params = args[0]
/**
* 打开银行列表
*/
export function openBankList() {
callNativeApi('openBankList')
}
interface ShareTipData {
url: string
img: string
title: string
desc: string
}
const invokeArgs: any[] = []
/**
* 分享
* @param data
*/
export function openShareTip(data: ShareTipData) {
callNativeApi('openShareTip', data)
if (typeof params !== 'undefined' && params !== null) {
invokeArgs.push(JSON.stringify(params))
}
/**
* 打开个人中心
* @param userId
*/
export function toPerson(userId: number | string) {
callNativeApi('toPerson', { userid: String(userId) })
}
const callbackName = `${name}_${Date.now()}`
invokeArgs.push(callbackName)
/**
* 验证⽤户身份
* @param action 1-进入手机认证 2-进入实名认证
*/
export function verify(action: '1' | '2') {
callNativeApi('verify', { action })
// @ts-ignore
window[callbackName] = (...args: any[]) => {
// @ts-ignore
delete window[callbackName]
ack(...args)
}
/**
* 进入直播间
* @param userId
* @param avatar
*/
export function toLiveroom(userId: number | string, avatar?: string) {
callNativeApi('toLiveroom', { userid: String(userId), avatar })
}
nativeMethod.call(window.liveapp, ...invokeArgs)
}
/**
* 清除浏览器缓存
/**
* 调用原生提供的api
* @param name
* @param params
*/
export function removeHistory() {
callNativeApi('removeHistory')
}
export function callNativeApi<T>(name: string, data: any, ack?: (data: T) => void): void
export function callNativeApi<T>(name: string, ack?: (data: T) => void): void
export function callNativeApi(name: string, ...args: any[]): void {
device === 'iOS' ? callIosApi(name, ...args) : callAndroidApi(name, ...args)
}
/**
* 去开播
/**
* 调用原生提供的异步api
* @param name
* @param params
*/
export function openLive() {
callNativeApi('openLive')
}
export function callNativeAsyncApi<T>(name: string, data: any, ack: (data: T) => void): void
export function callNativeAsyncApi<T>(name: string, ack: (data: T) => void): void
export function callNativeAsyncApi(name: string, ...args: any[]): void {
device === 'iOS' ? callIosApi(name, ...args) : callAndroidAsyncApi(name, ...args)
}
import { callNativeApi } from './native-api-base'
/**
* 关闭当前页面
*/
export function close() {
console.log('调用框架方法', 'closeWeb')
callNativeApi('closeWeb')
}
/**
* 获取状态栏高度
* @returns
*/
export function getStatusBarHeight(): Promise<number> {
console.log('调用框架方法', 'getStatusBarHeight')
return new Promise<number>((resolve) =>
callNativeApi('getStatusBarHeight', (value: any) => resolve(parseFloat(value))),
)
}
/**
* 获取底部安全区高度
* @returns
*/
export function getBottomSafeHeight(): Promise<number> {
console.log('调用框架方法', 'getBottomSafeHeight')
return new Promise<number>((resolve) =>
callNativeApi('getBottomSafeHeight', (value: any) => resolve(parseFloat(value))),
)
}
import CryptoJS from 'crypto-js'
import Cookies from 'js-cookie'
import { getAppStatus } from './app'
import { getCookie } from './cookie'
import { in_app } from './environment'
/**
* token数据
......@@ -38,7 +38,7 @@ function decodeToken(token: string) {
* 3.cookie里的app_token
* 4.cookie里的token
*/
export function parseToken(): TokenData | undefined {
function parseToken(in_app: boolean): TokenData | undefined {
const url = new URL(location.href)
if (url.hash && url.hash.includes('?')) {
const hashParams = new URLSearchParams(url.hash.substring(url.hash.indexOf('?') + 1))
......@@ -90,16 +90,18 @@ export function parseToken(): TokenData | undefined {
}
//接着尝试从cookie中获取app_token
const appStatus = getAppStatus()
if (appStatus && appStatus.app_token) {
if (in_app) {
const token = getCookie('app_token')
if (token) {
return {
token: appStatus.app_token,
token,
source: 'cookie_app',
}
}
}
//再尝试从cookie中获取token
const token = Cookies.get('token')
const token = getCookie('token')
if (token) {
return {
token,
......@@ -107,3 +109,10 @@ export function parseToken(): TokenData | undefined {
}
}
}
const token = parseToken(in_app)
if (token) {
console.log('解析到token', token)
}
export const app_token = token ? token.token : ''
export default {
preset: 'ts-jest',
testEnvironment: 'node',
collectCoverage: true,
collectCoverageFrom: ['src/**/*.ts'],
}
export * from '../base/api'
export { can_use_iap, room_number, source } from '../base/environment'
export { app_token } from '../base/token'
export * from '../miniapp'
import { getCookie } from '../base/cookie'
export { type DeviceType, device, in_app, lang } from '../base/environment'
/**
* 当前轻应用的appid
*/
export const appid = getCookie('appid', '')
/**
* 当前轻应用登录用户的openid
*/
export const openid = getCookie('openid', '')
/**
* 当前轻应用所在直播间主播的openid
*/
export const room_openid = getCookie('room_openid', '')
export * from './status'
export * from './environment'
export * from './native-api'
import { callNativeAsyncApi } from '../base/native-api-base'
import { appid, openid, room_openid } from './environment'
export * from '../base/native-api'
/**
* 发起支付的请求数据
*/
export interface RequestPaymentData {
/**
* 第三方生成的订单号
*/
out_order_no: string
/**
* 待支付的钻石数
*/
amount: number
}
export interface RequestPaymentResult {
/**
* 是否支付成功
*/
success: boolean
}
/**
* 发起轻应用支付
* @param data
* @returns
*/
export function requestPayment(data: RequestPaymentData): Promise<RequestPaymentResult> {
console.log('调用框架方法', 'requestPayment', data)
return new Promise<RequestPaymentResult>((resolve) =>
callNativeAsyncApi(
'requestPayment',
{
...data,
appid,
openid,
room_openid,
},
resolve,
),
)
}
{
"name": "@3ya/web-lib",
"name": "@bmiss/web-lib",
"version": "1.0.0",
"type": "module",
"main": "src/index.ts",
"private": true,
"scripts": {
"test": "jest ."
"build": "rolldown -c"
},
"devDependencies": {
"@types/crypto-js": "^4.2.2",
"@types/jest": "^29.5.12",
"@types/js-cookie": "^3.0.6",
"@types/lodash-es": "^4.17.12",
"axios": "^1.6.5",
"jest": "^29.7.0",
"ts-jest": "^29.1.2",
"rolldown": "^1.0.0-rc.8",
"typescript": "^5.3.3"
},
"peerDependencies": {
"axios": "^1.0.0"
"axios": "*"
},
"dependencies": {
"crypto-js": "^4.2.0",
"js-cookie": "^3.0.5",
"lodash-es": "^4.17.21"
}
}
import { defineConfig } from 'rolldown'
export default defineConfig([
{
input: './miniapp/index.ts',
output: [
{
file: 'dist/bmiss.js',
format: 'esm',
minify: true,
},
{
file: 'dist/bmiss-mini-lib.js',
format: 'iife',
name: 'bmiss',
minify: true,
generatedCode: {
preset: 'es5',
},
},
],
},
])
import axios, {
AxiosInstance,
AxiosInterceptorManager,
AxiosPromise,
CreateAxiosDefaults,
} from 'axios'
import decryptResponse from './decrypt-response'
import encryptRequest from './encrypt-request'
import injectCommonParams from './inject-common-params'
import injectSignature from './inject-signature'
import {
ApiAxiosRequestConfig,
ApiAxiosResponse,
ApiRequestData,
ApiRequestDefaultOptions,
GetterOrValue,
InternalApiAxiosRequestConfig,
} from './types'
import { sample } from 'lodash-es'
/**
* 用于执行接口请求的,拥有特殊选项的Axios请求实例
*/
export interface ApiAxiosInstance {
<TData = any, TExtend extends object = {}>(config: ApiAxiosRequestConfig): Promise<
ApiAxiosResponse<TData, TExtend>
>
request<TData = any, TExtend extends object = {}>(
config: ApiAxiosRequestConfig,
): Promise<ApiAxiosResponse<TData, TExtend>>
/**
* 设置在实例上的默认配置项
*/
defaults: AxiosInstance['defaults'] & ApiRequestDefaultOptions
/**
* 拦截器
*/
interceptors: {
/**
* 请求拦截器
*/
request: AxiosInterceptorManager<InternalApiAxiosRequestConfig>
/**
* 响应拦截器
*/
response: AxiosInterceptorManager<ApiAxiosResponse>
}
}
export interface CreateInstanceDefaults
extends CreateAxiosDefaults<ApiRequestData>,
ApiRequestDefaultOptions {}
/**
* 可以被简单处理的内置请求配置项
*/
const INTERNAL_OPTION_KEYS: (keyof ApiRequestDefaultOptions)[] = [
'source',
'device',
'app_version',
'token',
'partner_key',
'sdk_version',
'signature_secret',
]
/**
* 默认的API请求配置
*/
export const DEFAULT_API_OPTIONS = {
source: 'xinxiuweb',
app_version: '1.0.0',
device: 'wap',
signature_secret: 'asdasgfdwqew',
method: 'POST',
}
/**
* 创建用于请求接口的Axios实例
*/
export function createAxiosInstance(options: CreateInstanceDefaults = {}): ApiAxiosInstance {
options = {
...DEFAULT_API_OPTIONS,
...options,
}
const instance = axios.create(options)
//按倒序注入请求拦截器,最后处理的最先注入
//加密请求体
instance.interceptors.request.use(encryptRequest)
//计算签名并注入
instance.interceptors.request.use(injectSignature)
//自动填充公参
instance.interceptors.request.use(injectCommonParams)
//响应拦截器,解密响应体
instance.interceptors.response.use(decryptResponse)
/**
* 实际的接口调用方法
* @param config
*/
const request = async (config: ApiAxiosRequestConfig): AxiosPromise => {
const new_config = { ...config }
//先对配置项进行初始化,把默认配置项里的函数型配置项,转换为具体的值
//可以被自动转换的配置
for (const key of INTERNAL_OPTION_KEYS) {
new_config[key] = await getValues<any>(
config[key],
(instance.defaults as ApiRequestDefaultOptions)[key],
)
}
//特殊的加解密密钥配置
const encryption_key = await getValues(
config.encryption_key,
(instance.defaults as ApiRequestDefaultOptions).encryption_key,
)
if (Array.isArray(encryption_key)) {
new_config.encryption_key = sample(encryption_key)
} else {
new_config.encryption_key = encryption_key
}
//最后调用原始axios实例上的方法
return instance.request(new_config)
}
Object.defineProperties(request, {
defaults: {
value: instance.defaults,
writable: false,
},
interceptors: {
value: instance.interceptors,
writable: false,
},
request: {
value: request,
writable: false,
},
})
return request as unknown as ApiAxiosInstance
}
/**
* 读取一个配置项的值,传入的可以是具体的值,也可以是获取的方法
* @param getterOrValue
*/
async function getValue<T>(getterOrValue: GetterOrValue<T> | undefined): Promise<T | undefined> {
return typeof getterOrValue === 'function' ? (getterOrValue as () => T)() : getterOrValue
}
/**
* 读取多个配置项的值,返回找到的第一个有效的配置项,传入的可以是具体的值,也可以是获取的方法
* @param values
* @returns
*/
async function getValues<T>(...values: (GetterOrValue<T> | undefined)[]): Promise<T | undefined> {
for (const getterOrValue of values) {
const value = await getValue(getterOrValue)
if (typeof value !== 'undefined') return value
}
}
import { aesDecrypt, base64Decode } from '../utils/security'
import { ApiAxiosResponse } from './types'
/**
* 解密响应体数据
* @param response
*/
export default function decryptResponse(
response: ApiAxiosResponse<any, { encrypted_data?: string | null }>,
): ApiAxiosResponse {
//响应体不是对象
if (typeof response.data !== 'object' || !response.data) return response
//响应体上没有encrypted_data属性
if (typeof response.data.encrypted_data !== 'string' || !response.data.encrypted_data)
return response
//请求配置上没有加解密参数
if (!response.config.encryption_key) return response
//进行解密,先做一次aes解密
const { key, iv } = response.config.encryption_key
const decrypted = aesDecrypt(response.data.encrypted_data, key, iv)
//然后做一次base64解码
const base64_decoded = base64Decode(decrypted)
//最后做一次url解码
const url_decoded = decodeURIComponent(base64_decoded)
//JSON解析并覆盖原data属性
response.data.data = JSON.parse(url_decoded)
return response
}
import { aesEncrypt, base64Encode } from '../utils/security'
import { InternalApiAxiosRequestConfig } from './types'
/**
* 加密请求的数据
*/
export default function encryptRequest(
config: InternalApiAxiosRequestConfig,
): InternalApiAxiosRequestConfig {
const { encryption_key, data } = config
//如果没有设置密钥就不加密了
if (!encryption_key) return config
const { name, key, iv } = encryption_key
//先得到原始请求体的JSON字符串
const json = JSON.stringify(data ?? {})
//先进行aes加密
const encrypted = aesEncrypt(json, key, iv)
//接下来是base64转码
const base64 = base64Encode(encrypted)
//最后做一次urlencode
const urlEncoded = encodeURIComponent(base64)
//写入请求体
config.data = {
sign_data: urlEncoded,
}
//在请求头中写入密钥名
config.headers.set('sign_name', name)
return config
}
export * from './create'
export * from './types'
import { AxiosRequestHeaders } from 'axios'
import { unixTimestamp } from '../utils/datetime'
import { md5 } from '../utils/security'
import { InternalApiAxiosRequestConfig } from './types'
/**
* 自动向请求中注入公参
* @param config
*/
export default function injectCommonParams(
config: InternalApiAxiosRequestConfig,
): InternalApiAxiosRequestConfig {
fillHeader(config.headers, 'source', () => config.source)
fillHeader(config.headers, 'device', () => config.device)
fillHeader(config.headers, 'appversion', () => config.app_version)
fillHeader(config.headers, 'token', () => config.token)
fillHeader(config.headers, 'partner_key', () => config.partner_key)
fillHeader(config.headers, 'sdk_version', () => config.sdk_version)
fillHeader(config.headers, 'timestamp', () => unixTimestamp().toString())
fillHeader(config.headers, 'request_number', () => md5((Date.now() + Math.random()).toString()))
return config
}
/**
* 向请求头中注入公参,如果已经设置了对应的请求头则不再覆盖
*/
export function fillHeader(
headers: AxiosRequestHeaders,
name: string,
creator: () => string | undefined,
): void {
if (headers.has(name)) return
const value = creator()
if (value) {
headers.set(name, value)
}
}
import { md5 } from '../utils/security'
import { InternalApiAxiosRequestConfig } from './types'
/**
* 请求头中参与签名的字段
*/
const HEADER_KEYS = [
'source',
'device',
'appversion',
'token',
'partner_key',
'sdk_version',
'timestamp',
'request_number',
]
/**
* 注入请求签名
* @param config
*/
export default function injectSignature(
config: InternalApiAxiosRequestConfig,
): InternalApiAxiosRequestConfig {
//先获取签名密钥,如果没有密钥则不签名
const { signature_secret } = config
if (typeof signature_secret !== 'string' || signature_secret === '') return config
/**
* 待签名的数据
*/
const data = {
...config.data,
}
//取出请求头中参与签名的字段
HEADER_KEYS.forEach((key) => {
if (config.headers.has(key)) {
data[key] = config.headers.get(key)
}
})
config.headers.set('signature', createSignature(data, signature_secret))
return config
}
/**
* 计算签名
* @param data
* @param secret
*/
export function createSignature(data: Record<string, any>, secret: string): string {
//先对参数名按字典序排个序
const entries = Object.entries(data).sort((t1, t2) => t1[0].localeCompare(t2[0]))
//再根据参数值,生成对应的字符串值
const values = entries.map(([_, value]) => {
if (
typeof value === 'undefined' ||
typeof value === 'function' ||
typeof value === 'symbol' ||
value === null
) {
return ''
} else if (typeof value === 'object') {
return JSON.stringify(value) ?? ''
} else {
return String(value)
}
})
//拼接原始字符串值,并在后面追加签名密钥,得到基础签名串
const baseStr = values.join('') + secret
//进行2次md5,得到最终签名串
return md5(md5(baseStr))
}
import {
AxiosInstance,
AxiosInterceptorManager,
AxiosRequestConfig,
AxiosResponse,
InternalAxiosRequestConfig,
} from 'axios'
/**
* 接口传递给服务端的数据类型
*/
export type ApiRequestData = Record<string, any>
/**
* 接口响应基础数据结构
*/
export interface ApiRespBase<TData = any> {
/**
* 接口响应码,200表示请求成功,非200表示请求失败
*/
code: number
/**
* 错误消息
*/
msg: string
/**
* 响应数据
*/
data: TData
}
/**
* 接口响应完整数据结构
*/
export type ApiResp<TData = any, TExtend extends object = {}> = TExtend & ApiRespBase<TData>
/**
* 接口请求数据加解密密钥
*/
export interface ApiEncryptionKey {
/**
* 密钥名
*/
name: string
/**
* 密钥值
*/
key: string
/**
* 向量
*/
iv: string
}
/**
* 接口请求的配置项
*/
export interface ApiRequestOptions {
/**
* 请求的包名,如果不设置,默认为`xinxiuweb`
*/
source?: string
/**
* 请求的设备类型,如果不设置,默认为`wap`
*/
device?: string
/**
* 当前客户端的版本号,如果不设置,默认为`1.0.0`
*/
app_version?: string
/**
* 用户的token
*/
token?: string
/**
* 渠道标识
*/
partner_key?: string
/**
* 渠道SDK版本号
*/
sdk_version?: string
/**
* 用于计算签名的密钥,如果不设置,默认为`asdasgfdwqew`
*/
signature_secret?: string
/**
* 接口数据加解密密钥
*/
encryption_key?: ApiEncryptionKey
}
export type GetterOrValue<T> = T | (() => T | Promise<T>)
/**
* 接口请求的默认配置项
*/
export type ApiRequestDefaultOptions = {
[K in keyof ApiRequestOptions]?: GetterOrValue<ApiRequestOptions[K]>
} & {
/**
* 接口数据加解密密钥
*/
encryption_key?: GetterOrValue<ApiEncryptionKey | ApiEncryptionKey[] | undefined>
}
export type InternalApiAxiosRequestConfig = InternalAxiosRequestConfig<ApiRequestData> &
ApiRequestOptions
export interface ApiAxiosResponse<TData = any, TExtend extends object = {}>
extends AxiosResponse<ApiResp<TData, TExtend>, ApiRequestData> {
config: InternalApiAxiosRequestConfig
}
export interface ApiAxiosRequestConfig
extends AxiosRequestConfig<ApiRequestData>,
ApiRequestOptions {
url: string
}
interface IosWebkitInterface {}
declare global {
interface Window {
webkit: IosWebkitInterface
}
}
import Cookies from 'js-cookie'
/**
* 通过app获取到的状态
*/
export interface AppStatus {
/**
* 包名
*/
source: string
/**
* 是否在直播间中
*/
isOnLiveRoom: number
/**
* 当前直播间的房间号
*/
roomIdentifier: number
/**
* 当前用户的token
*/
app_token: string
/**
* 加密后的用户id
*/
userid: string
/**
* 是否能使用内购
*/
canUseIAP: boolean
}
/**
* 读取当前APP的状态
*/
export function getAppStatus(): AppStatus | undefined {
//先判断是否在app内
const platform = Cookies.get('platform')
const source = Cookies.get('source')
if (platform !== 'app' || !source) return
return {
source,
app_token: Cookies.get('app_token') ?? '',
isOnLiveRoom: parseInt(Cookies.get('isOnLiveRoom') ?? '0'),
roomIdentifier: parseInt(Cookies.get('roomIdentifier') || '0'),
userid: Cookies.get('userid') ?? '',
canUseIAP: Cookies.get('canUseIAP') === 'true',
}
}
export * from './api'
export * from './utils'
export * from './app'
export * from './token'
/**
* 获取unix时间戳
* @param time
* @returns
*/
export function unixTimestamp(time: number | Date = Date.now()) {
if (time instanceof Date) {
time = time.valueOf()
}
return Math.floor(time / 1000)
}
export * from './datetime'
export * from './security'
......@@ -11,5 +11,5 @@
"skipLibCheck": true,
"useUnknownInCatchVariables": true
},
"include": ["src/**/*.ts"]
"include": ["base", "miniapp", "miniapp-internal", "web"]
}
export * from '../base/api'
export * from '../base/environment'
export * from '../base/native-api'
export * from '../base/token'
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment