Commit fa018114 authored by 郑磊's avatar 郑磊

init

parents
import { env, http } from '@ball/shared'
import { API_URL } from '../constants'
import { waitInited } from '../init'
import { getToken } from '../token'
import { ApiError } from './error'
/**
* 接口调用基础配置项
*/
export interface ApiBaseOptions {
/**
* 接口基地址
*/
baseURL?: string
/**
* 调用接口时附带的token
*/
token?: string
/**
* 是否等待应用初始化完成再调用接口
*/
waitInit?: boolean
/**
* 如果接口调用失败是否抛出异常
*/
throwOnFail?: boolean
}
/**
* 接口调用配置项
*/
export interface ApiOptions extends ApiBaseOptions {
/**
* 接口地址
*/
url: string
/**
* URL查询参数
*/
params?: Record<string, any>
/**
* 调用方法
*/
method?: string
/**
* 请求头
*/
headers?: Record<string, string>
/**
* 请求体
*/
data?: any
}
/**
* 需要从环境变量上注入到请求头的参数
*/
const ENV_HEADERS = ['platform', 'arch', 'version', 'build_number', 'package']
/**
* 执行接口请求
* @param options
*/
export async function api<T = void>(options: ApiOptions): Promise<ApiResp<T>> {
const {
headers = {},
waitInit = true,
throwOnFail = false,
token,
baseURL = API_URL,
method = 'POST',
...rest
} = options
if (waitInit !== false) {
//等待应用初始化
await waitInited()
}
//在请求头中添加token
let tokenValue = ''
if (typeof token === 'string') {
tokenValue = token
} else {
tokenValue = getToken()
}
if (tokenValue) {
headers.Authorization = `Bearer ${tokenValue}`
}
//向请求头中注入公共参数
ENV_HEADERS.forEach((key) => {
if (key in headers) return
let value = env(key)
if (typeof value !== 'undefined' && value !== null) {
value = String(value)
if (value !== '') {
headers[key] = value
}
}
})
//执行请求
const resp = await http.request<ApiResp<T>>({
...rest,
baseURL,
method,
headers,
})
const result = resp.data
if (typeof result === 'object' && result) {
result.ok = result.code === 0
if (!result.ok && throwOnFail) {
throw new ApiError(result)
}
}
return result
}
/**
* 由接口调用失败引发的异常
*/
export class ApiError extends Error {
/**
* 接口响应码
*/
public readonly code: number
constructor(resp: ApiResp<any>) {
super(resp.msg)
this.code = resp.code
}
}
export * from './api'
export * from './error'
export const API_URL = 'https://v3.188zq.vip'
export const WS_URL = 'wss://ws.188zq.vip'
/**
* 界面显示时使用的时区
*/
export const TIMEZONE_UI = 'Asia/Shanghai'
/**
* 皇冠使用的时区
*/
export const TIMEZONE_CROWN = 'America/Martinique'
/**
* VIP信息
*/
export const VIP_INFO: Record<VipType, VipInfo> = {
day: {
name: '日卡',
days: 1,
},
week: {
name: '周卡',
days: 7,
},
month: {
name: '月卡',
days: 30,
},
}
/**
* 用于支付的链信息
*/
export const CHAIN_INFO: Record<ChainType, ChainInfo> = {
tron: {
name: 'Tron',
token: 'TRC-20',
},
ethereum: {
name: 'Ethereum',
token: 'ERC-20',
},
bsc: {
name: 'BNB Smart Chain',
token: 'BEP-20',
},
}
import { computed, reactive, ref } from '@vue/reactivity'
import Decimal from 'decimal.js'
import { api } from './api'
import { getCrownDayStart } from './helpers/datetime'
import { user } from './user'
import { get } from 'lodash-es'
export const channels = reactive<Record<string, ChannelData>>({})
export const channelList = ref<Channel[]>([])
/**
* 当前展示的频道id
*/
export const currentChannelId = ref<string>('')
/**
* 当前展示的频道数据
*/
export const currentChannel = computed<ChannelData>(() => channels[currentChannelId.value])
/**
* 获取频道的数据
*/
export function getChannelData(channel: string) {
return api<RawChannelData & { is_expired: number }>({
url: `/api/channel/data/${channel}`,
})
}
/**
* 获取所有可用频道
*/
export async function getChannelList() {
const ret = await api<Channel[]>({
url: '/api/channel/list',
waitInit: false,
})
console.log(ret)
if (ret.ok) {
channelList.value = ret.data
ret.data.forEach((channel) => {
const filter = channel.filter
channels[channel.key] = setupChannel(
filter ? () => get(user.value, ['client_config', filter]) : undefined,
)
})
if (ret.data.length > 0) {
currentChannelId.value = ret.data[0].key
}
return ret.data
}
return []
}
/**
* 重置所有频道的数据
*/
export function resetChannels() {
channelList.value.forEach((channel) => {
if (channels[channel.key]) {
channels[channel.key].dateData = undefined
}
})
}
/**
* 初始化频道数据
* @param filterGetter 获取频道筛选器的方法
*/
export function setupChannel(filterGetter?: () => UserFilter[] | undefined | null): ChannelData {
/**
* 过滤器
*/
const filter =
typeof filterGetter === 'function'
? computed(filterGetter)
: ref<UserFilter[] | null | undefined>()
/**
* 按日聚合的数据
*/
const dateData = ref<Record<number, DatePromotedData>>()
/**
* 最大数据日期
*/
const maxDate = ref(getCrownDayStart())
/**
* 过滤后的数据
*/
const filteredData = computed(() => {
if (!dateData.value) {
return {}
}
if (!Array.isArray(filter.value) || filter.value.length === 0) {
return dateData.value
}
const filters = filter.value
return Object.fromEntries(
Object.entries(dateData.value).map(([date, data]) => [
date,
{
hasNewPromoted: data.hasNewPromoted,
list: data.list.filter((item) => {
for (const filter of filters) {
if (filter.period !== item.period) continue
if (filter.variety !== item.variety) continue
if (filter.type !== item.type) continue
if (!Decimal(item.condition).eq(filter.condition)) continue
return false
}
return true
}),
},
]),
)
})
const hasNewPromoted = computed(() => {
if (!dateData.value) {
return false
}
return Object.values(dateData.value).some((t) => t.hasNewPromoted)
})
const preparing = ref<PreparingData[]>([])
const summary = ref<SummaryData>({
total: 0,
win: 0,
loss: 0,
draw: 0,
win_rate: 0,
profit: 0,
})
/**
* 设置通过API接口获得的数据
*/
const setData = (raw: RawChannelData): PromotedData[] => {
preparing.value = raw.preparing
summary.value = raw.summary
if (!dateData.value) {
//还没有初始数据就不需要初始化了,直接赋值
const dateGrouped: Record<number, DatePromotedData> = {}
raw.list.forEach((promoted) => {
const date = getCrownDayStart(promoted.match_time)
if (!dateGrouped[date]) {
dateGrouped[date] = {
hasNewPromoted: false,
list: [promoted],
}
} else {
dateGrouped[date].list.push(promoted)
}
})
dateData.value = dateGrouped
return []
}
const reversed = raw.list.toReversed()
const result: PromotedData[] = []
for (const promoted of reversed) {
const date = getCrownDayStart(promoted.match_time)
if (!dateData.value[date]) {
dateData.value[date] = {
hasNewPromoted: true,
list: [promoted],
}
result.push(promoted)
continue
}
const thisDate = dateData.value[date]
const index = thisDate.list.findIndex((t) => t.id === promoted.id)
if (index === -1) {
thisDate.list.unshift(promoted)
thisDate.hasNewPromoted = true
result.push(promoted)
} else {
thisDate.list.splice(index, 1, promoted)
}
}
return result
}
/**
* 新增通过WebSocket推送而来的数据
* @param promoted
*/
const addData = (promoted: PromotedData): boolean => {
//如果没有日期数据表示数据尚未初始化,那么不算新数据
if (!dateData.value) return false
const date = getCrownDayStart(promoted.match_time)
maxDate.value = Math.max(maxDate.value, date, getCrownDayStart())
if (!dateData.value[date]) {
dateData.value[date] = {
hasNewPromoted: true,
list: [promoted],
}
return true
}
const list = dateData.value[date].list
const index = list.findIndex((t) => t.id === promoted.id)
if (index !== -1) return false
list.unshift(promoted)
dateData.value[date].hasNewPromoted = true
return true
}
return reactive({
preparing,
summary,
maxDate,
hasNewPromoted,
dateData,
filteredData,
setData,
addData,
})
}
import { DateTime } from 'luxon'
import { TIMEZONE_CROWN, TIMEZONE_UI } from '../constants'
/**
* 创建日期时间对象
* @param input
* @returns
*/
export function createDateTime(input?: number | Date | string) {
if (typeof input === 'number') {
return DateTime.fromMillis(input).setZone(TIMEZONE_UI)
} else if (typeof input === 'string') {
return DateTime.fromISO(input).setZone(TIMEZONE_UI)
} else if (input instanceof Date) {
return DateTime.fromJSDate(input).setZone(TIMEZONE_UI)
} else {
return DateTime.now().setZone(TIMEZONE_UI)
}
}
/**
* 获取指定时间对应的皇冠时区日期开始时间
* @param input
*/
export function getCrownDayStart(input?: number | Date | string | DateTime): number {
const dateTime = DateTime.isDateTime(input) ? input : createDateTime(input)
return dateTime.setZone(TIMEZONE_CROWN).startOf('day').toMillis()
}
import Decimal from 'decimal.js'
/**
* 返回时段文本
* @param period
*/
export function periodText(period: Period) {
switch (period) {
case 'period1':
return '上半场'
case 'regularTime':
return '全场'
}
}
export function oddTypeText(type: OddType) {
switch (type) {
case 'ah1':
return '主'
case 'ah2':
return '客'
case 'draw':
return '平'
case 'over':
return '大'
case 'under':
return '小'
}
}
export function varietyText(variety: Variety) {
switch (variety) {
case 'corner':
return '角球'
case 'goal':
return '进球'
}
}
export function conditionText(condition: string | number, type: OddType) {
if (type === 'over' || type === 'under') {
return Number(condition).toString()
} else {
const symbol = Decimal(condition).gt(0) ? '+' : ''
return `${symbol}${Number(condition)}`
}
}
export * from './datetime'
export * from './format'
export * from './singleton'
export * from './validator'
export * from './odd'
import Decimal from 'decimal.js'
/**
* 计算收益用到的参数
*/
interface GetProfitParams {
type: OddType
condition: string | number
amount: number | string
value: string | number
score1: number
score2: number
}
/**
* 计算收益
*/
export function getProfit({
type,
condition,
amount,
value,
score1,
score2,
}: GetProfitParams): string {
let result_value: ReturnType<typeof compareScore>
//确认投注类型
if (type === 'ah1') {
//让球,买主队
result_value = compareScore(Decimal(score1).add(condition), score2)
} else if (type === 'ah2') {
//让球,买客队
result_value = compareScore(Decimal(score2).add(condition), score1)
} else if (type === 'over') {
//大球
result_value = compareScore(score1 + score2, condition)
} else if (type === 'under') {
//小球
result_value = compareScore(condition, score1 + score2)
} else if (type === 'draw') {
result_value = score1 === score2 ? '1' : '-1'
} else {
return '0'
}
//收益计算
let result_profit: string
switch (result_value) {
case '0.5':
case '1':
result_profit = Decimal(value).sub(1).mul(result_value).toString()
break
default:
result_profit = result_value
break
}
return Decimal(result_profit).mul(amount).toString()
}
/**
* 进行比分对比
* @param score1
* @param score2
*/
export function compareScore(
score1: Decimal.Value,
score2: Decimal.Value,
): '-0.5' | '-1' | '0' | '0.5' | '1' {
//给作为比对的结果加上盘口
const delta = Decimal(score1).sub(score2)
if (delta.eq('0')) return '0'
if (delta.gte('0.5')) {
return '1'
}
if (delta.gte('0.25')) {
return '0.5'
}
if (delta.lte('-0.5')) {
return '-1'
}
if (delta.lte('-0.25')) {
return '-0.5'
}
return '0'
}
const _singletons: Record<string, () => any> = {}
/**
* 执行一个单例任务
* @param identity 任务标识
* @param task 任务处理方法
*/
export function singleton<T>(identity: string, task: () => T): T {
const cache = _singletons[identity]
if (typeof cache === 'function') {
return cache()
}
const result = task()
if (result instanceof Promise) {
_singletons[identity] = () => result
result.finally(() => {
delete _singletons[identity]
})
}
return result
}
import Decimal from 'decimal.js'
export function isEmpty(value: any) {
if (typeof value === 'undefined' || value === null) return true
if (value === '') return true
return false
}
/**
* 判断输入值是否为有效的十进制数值
* @param input
* @returns
*/
export function isDecimal(input: any): input is Decimal.Value {
try {
Decimal(input)
return true
} catch {
return false
}
}
/**
* 判断输入值是否为有效的邮件地址
* @param input
*/
export function isEmail(input: any): input is string {
return /^[a-zA-Z0-9_.-]+@[a-zA-Z0-9-_]+(\.[a-zA-Z0-9-]+)*\.[a-zA-Z0-9]+$/.test(input)
}
let onInited = undefined as unknown as () => void
const waiter = new Promise<void>((resolve) => {
onInited = resolve
})
/**
* 标记应用已经初始化完毕
*/
export function setInited() {
onInited()
}
/**
* 等待应用初始化完毕
* @returns
*/
export function waitInited() {
return waiter
}
{
"name": "@ball/core",
"description": "BallPredictAI核心业务库",
"private": true,
"version": "1.0.0",
"dependencies": {
"@ball/shared": "git+http://gitlab.3yakj.com/ball/shared.git",
"@vue/reactivity": "^3.5.26",
"decimal.js": "^10.6.0",
"isomorphic-ws": "^5.0.0",
"lodash-es": "^4.17.22",
"luxon": "^3.7.2",
"ws": "^8.18.3"
},
"devDependencies": {
"@types/ball-predict-ai": "git+http://gitlab.3yakj.com/ball/types.git",
"@types/lodash-es": "^4.17.12",
"@types/luxon": "^3.7.1",
"@types/ws": "^8.18.1"
}
}
import { env, WebSocket } from '@ball/shared'
import { WS_URL } from './constants'
console.log('WebSocket', WebSocket)
let socket = null as unknown as WebSocket
let userToken = ''
/**
* 开启socket连接
*/
export function start(token: string) {
if (userToken === token && socket && socket.readyState !== WebSocket.CLOSED) return
if (socket && socket.readyState !== WebSocket.CLOSED) {
socket.close()
socket = null as any
}
userToken = token
connect()
}
/**
* 主动关闭连接
*/
export function close() {
if (!socket || socket.readyState === WebSocket.CLOSED) return
socket.close()
socket = null as any
}
/**
* 需要从环境变量上注入到连接地址的参数
*/
const ENV_QUERY = ['platform', 'arch', 'version', 'build_number', 'package']
/**
* 启动ws连接
*/
function connect() {
//构建连接地址
const url = new URL(WS_URL)
url.searchParams.append('type', 'user')
url.searchParams.append('token', userToken)
//注入环境变量参数
ENV_QUERY.forEach((key) => {
if (url.searchParams.has(key)) return
let value = env(key)
if (typeof value === 'undefined' || value === null) return
value = String(value)
if (value === '') return
url.searchParams.append(key, value)
})
socket = new WebSocket(url.href)
socket.addEventListener('open', () => {
console.log('[SOCKET]', `connect ${url.href}`)
})
socket.addEventListener('error', (err) => {
console.log('[SOCKET]', 'error', err)
//3秒后自动连接
setTimeout(connect, 3000)
})
socket.addEventListener('close', (...args) => {
console.log('[SOCKET]', `closed`, ...args)
socket = null as any
})
socket.addEventListener('message', (event) => {
const str = event.data as string
let msg: any
try {
msg = JSON.parse(str)
} catch {
return
}
if (msg.type === 'pong') return
if (msg.type === 'ping') {
socket.send(JSON.stringify({ type: 'pong' }))
return
}
console.log('[SOCKET]', 'msg', str)
messageListeners.forEach((listener) => listener(msg))
})
}
const messageListeners: Function[] = []
/**
* 注册WS消息监听器
* @param callback
* @returns
*/
export function addSocketMessageListener(callback: (message: Socket.IncomingMessage) => void) {
messageListeners.push(callback)
return () => {
const index = messageListeners.indexOf(callback)
if (index !== -1) {
messageListeners.splice(index, 1)
}
}
}
let token = ''
/**
* 设置token
* @param value
*/
export function setToken(value: string) {
token = value
}
/**
* 读取token
*/
export function getToken() {
return token
}
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"lib": ["esnext", "dom"],
"types": ["ball-predict-ai"],
"moduleResolution": "bundler",
"noEmit": true,
"isolatedModules": true,
"strict": true,
"allowImportingTsExtensions": true,
"allowArbitraryExtensions": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}
import { ref } from '@vue/reactivity'
import { api, ApiBaseOptions } from './api'
export const user = ref<User>()
export interface RefreshUserInfoOptions extends ApiBaseOptions {
setUser?: boolean
}
/**
* 刷新用户信息
* @param options
*/
export async function refreshUserInfo(options: RefreshUserInfoOptions = {}) {
const { setUser = true, ...rest } = options
const ret = await api<User>({
...rest,
url: '/api/user/info',
})
if (ret.ok && setUser) {
user.value = ret.data
}
return ret
}
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