134 lines
5.6 KiB
TypeScript
134 lines
5.6 KiB
TypeScript
import type {
|
||
CarrierParamsInput,
|
||
CheckSceneInput,
|
||
PollingFailureQuery
|
||
} from '@/types/api/pollingScheduler'
|
||
import { validateGatewayInteger, compactGatewayQuery, gatewayDisplayValue } from './gateway'
|
||
|
||
const schedulerLabels: Record<string, Record<string, string>> = {
|
||
asset_type: { iot_card: '卡', device: '设备' },
|
||
alert_level: { card: '卡级', carrier: '账户级' },
|
||
scheduler_mode: { legacy: '旧分片调度', carrier: '按运营商账户调度' },
|
||
queue: { priority: '优先队列', queue: '普通队列', none: '不在轮询中' },
|
||
network_status: { 0: '停机', 1: '开机' },
|
||
card_category: { normal: '普通卡', industry: '行业卡' },
|
||
check_types: { card_status: '卡状态', carddata: '流量', realname: '实名' },
|
||
action: { stop: '停机', start: '复机' }
|
||
}
|
||
export function schedulerDisplayValue(value: unknown, key = ''): string {
|
||
if (Array.isArray(value) && schedulerLabels[key])
|
||
return value.map((item) => schedulerLabels[key][String(item)] || String(item)).join('、') || '-'
|
||
return schedulerLabels[key]?.[String(value)] || gatewayDisplayValue(value, key)
|
||
}
|
||
|
||
export const isPollingPlatform = (userType: unknown) => [1, 2].includes(Number(userType))
|
||
export const canPollingBatch = (userType: unknown) => [1, 2, 3].includes(Number(userType))
|
||
export const pollingScope = (user: {
|
||
info: { id?: unknown; user_type?: unknown; shop_id?: unknown; enterprise_id?: unknown }
|
||
isLogin?: boolean
|
||
}) =>
|
||
JSON.stringify([
|
||
user.info.id,
|
||
user.info.user_type,
|
||
user.info.shop_id,
|
||
user.info.enterprise_id,
|
||
user.isLogin
|
||
])
|
||
export function validatePollingIds(ids: number[]) {
|
||
if (!ids.length || ids.length > 500 || new Set(ids).size !== ids.length)
|
||
throw new Error('每次选择 1–500 项且不能重复')
|
||
ids.forEach((id) => validateGatewayInteger(id, 1, Number.MAX_SAFE_INTEGER, '资产/告警 ID'))
|
||
}
|
||
export function pollingReason(value: string) {
|
||
if (!value.trim()) throw new Error('请填写操作原因')
|
||
return value.trim()
|
||
}
|
||
export function schedulerTime(range: unknown) {
|
||
if (!Array.isArray(range) || !range.length) return {}
|
||
if (range.length !== 2 || range.some((value) => !value)) throw new Error('请选择完整时间范围')
|
||
const dates = range.map((value) => new Date(value))
|
||
if (dates.some((value) => !Number.isFinite(value.getTime())) || dates[1] < dates[0])
|
||
throw new Error('时间范围无效')
|
||
return {
|
||
start_time: dates[0].toISOString().replace(/\.\d{3}Z$/, 'Z'),
|
||
end_time: dates[1].toISOString().replace(/\.\d{3}Z$/, 'Z')
|
||
}
|
||
}
|
||
export function failureQuery(query: PollingFailureQuery): PollingFailureQuery {
|
||
if (query.view !== 'stop_refused') return compactGatewayQuery(query)
|
||
const { page, page_size, carrier_id, status, iccid, start_time, end_time } = query
|
||
return compactGatewayQuery({
|
||
page,
|
||
page_size,
|
||
carrier_id,
|
||
status,
|
||
iccid,
|
||
start_time,
|
||
end_time,
|
||
view: 'stop_refused'
|
||
})
|
||
}
|
||
export function validateCarrierParams(data: CarrierParamsInput, capacity: number) {
|
||
for (const field of [
|
||
'card_max_in_flight',
|
||
'inflight_limit',
|
||
'sr_inflight_limit',
|
||
'stop_resume_max_in_flight'
|
||
] as const) {
|
||
if (data[field] != null) validateGatewayInteger(data[field], 1, Number.MAX_SAFE_INTEGER, field)
|
||
}
|
||
for (const field of ['stop_resume_confirm_delay', 'stop_resume_min_interval'] as const) {
|
||
if (data[field] != null) validateGatewayInteger(data[field], 0, Number.MAX_SAFE_INTEGER, field)
|
||
}
|
||
if (
|
||
data.priority_max_ratio != null &&
|
||
(!(data.priority_max_ratio > 0) || data.priority_max_ratio > 1)
|
||
)
|
||
throw new Error('优先队列占比须大于 0 且不超过 1')
|
||
if (data.inflight_limit != null && data.inflight_limit > capacity)
|
||
throw new Error('执行上限不能大于订阅进程容量之和')
|
||
}
|
||
export function validateScene(data: CheckSceneInput) {
|
||
if (
|
||
!data.check_types.length ||
|
||
data.check_types.some((value) => !['card_status', 'carddata', 'realname'].includes(value))
|
||
)
|
||
throw new Error('请选择有效检查类型')
|
||
data.confirm_delays.forEach((value, index) => {
|
||
validateGatewayInteger(value, 1, Number.MAX_SAFE_INTEGER, '确认延迟')
|
||
if (index && value <= data.confirm_delays[index - 1]) throw new Error('确认延迟必须严格递增')
|
||
})
|
||
if (data.cooldown != null)
|
||
validateGatewayInteger(data.cooldown, 0, Number.MAX_SAFE_INTEGER, '冷却时长')
|
||
}
|
||
export function validateDryAssumptions(on: number[], off: number[]) {
|
||
if (on.some((id) => off.includes(id))) throw new Error('同一账户不能同时假设开启和关闭实名管控')
|
||
}
|
||
export function validatePollingCsv(fileName: string, text: string) {
|
||
if (!/\.csv$/i.test(fileName)) throw new Error('只支持 CSV 文件')
|
||
// 识别引号内换行与转义双引号,按 CSV 记录计数;标识符始终为文本。
|
||
const lines: string[] = []
|
||
let quoted = false,
|
||
line = ''
|
||
const content = text.replace(/^\uFEFF/, '')
|
||
for (let i = 0; i < content.length; i++) {
|
||
const char = content[i]
|
||
if (char === '"') {
|
||
if (quoted && content[i + 1] === '"') {
|
||
line += '"'
|
||
i++
|
||
} else quoted = !quoted
|
||
} else if ((char === '\n' || char === '\r') && !quoted) {
|
||
if (line.trim()) lines.push(line)
|
||
line = ''
|
||
if (char === '\r' && content[i + 1] === '\n') i++
|
||
} else line += char
|
||
}
|
||
if (quoted) throw new Error('CSV 引号未闭合')
|
||
if (line.trim()) lines.push(line)
|
||
if (lines.length < 2) throw new Error('CSV 须有表头和数据行')
|
||
if (lines.length - 1 > 50000) throw new Error('CSV 最多 50000 行数据')
|
||
// 这里只计行数,不把 ICCID、IMEI 等标识符转换成数字。
|
||
return lines.length - 1
|
||
}
|