Files
one-pipe-system/src/utils/business/pollingScheduler.ts
luo 06684e1fc0
All checks were successful
构建并部署前端到测试环境 / deploy (push) Successful in 55s
fix: 轮询按运营商账户限流
2026-10-26 14:31:15 +08:00

134 lines
5.6 KiB
TypeScript
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 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
}