96 lines
3.0 KiB
TypeScript
96 lines
3.0 KiB
TypeScript
import type { QAOrganizationVersion, QAQuery } from './qaOps'
|
|
import { getPayload, parseQAOrganizationVersion } from './qaOps'
|
|
export interface QAUsageRow {
|
|
key: string
|
|
questions: number
|
|
succeeded: number
|
|
failed: number
|
|
other: number
|
|
users: number | null
|
|
roster_people: number | null
|
|
}
|
|
export interface QAUsage {
|
|
organization: QAOrganizationVersion
|
|
as_of: string
|
|
start: string
|
|
end: string
|
|
keyword: string
|
|
questions: number
|
|
identified_users: number
|
|
rows: QAUsageRow[]
|
|
}
|
|
const bad = (): never => {
|
|
throw new Error('使用统计响应无效,请检查完整性及接口契约')
|
|
}
|
|
const obj = (v: unknown): Record<string, unknown> =>
|
|
v && typeof v === 'object' && !Array.isArray(v) ? (v as Record<string, unknown>) : bad()
|
|
const text = (v: unknown) => (typeof v === 'string' ? v : bad())
|
|
const count = (v: unknown) => (typeof v === 'number' && Number.isSafeInteger(v) && v >= 0 ? v : bad())
|
|
const stamp = (v: unknown) => {
|
|
const s = text(v)
|
|
return !Number.isNaN(Date.parse(s)) ? s : bad()
|
|
}
|
|
export const usageLabels: Record<string, string> = Object.assign(Object.create(null), {
|
|
国内工业: '国内工业',
|
|
APAC: 'APAC',
|
|
EMEA: 'EMEA',
|
|
America: 'America',
|
|
unassigned: '未配置区域',
|
|
cross_region: '跨区域待核对',
|
|
unmatched: '名册未匹配',
|
|
identity_conflict: '身份冲突',
|
|
no_identity: '身份信息缺失',
|
|
directory_missing: '尚无完整通讯录',
|
|
})
|
|
export function parseQAUsage(value: unknown): QAUsage {
|
|
const v = obj(value),
|
|
organization = parseQAOrganizationVersion(v.organization)
|
|
const allowed = new Set([
|
|
...(organization.region_names ?? []),
|
|
'unassigned',
|
|
'cross_region',
|
|
'unmatched',
|
|
'identity_conflict',
|
|
'no_identity',
|
|
'directory_missing',
|
|
])
|
|
if (!Array.isArray(v.rows)) return bad()
|
|
const rows = v.rows.map((raw) => {
|
|
const r = obj(raw)
|
|
const key = text(r.key)
|
|
if (!allowed.has(key)) return bad()
|
|
const row = {
|
|
key,
|
|
questions: count(r.questions),
|
|
succeeded: count(r.succeeded),
|
|
failed: count(r.failed),
|
|
other: count(r.other),
|
|
users: r.users === null ? null : count(r.users),
|
|
roster_people: r.roster_people === null ? null : count(r.roster_people),
|
|
}
|
|
if (row.questions !== row.succeeded + row.failed + row.other) return bad()
|
|
return row
|
|
})
|
|
const result = {
|
|
organization,
|
|
as_of: stamp(v.as_of),
|
|
start: stamp(v.start),
|
|
end: stamp(v.end),
|
|
keyword: text(v.keyword),
|
|
questions: count(v.questions),
|
|
identified_users: count(v.identified_users),
|
|
rows,
|
|
}
|
|
if (
|
|
result.questions !== rows.reduce((n, r) => n + r.questions, 0) ||
|
|
new Set(rows.map((r) => r.key)).size !== rows.length ||
|
|
!(new Date(result.start) < new Date(result.end))
|
|
)
|
|
return bad()
|
|
return result
|
|
}
|
|
export async function fetchQAUsage(query: QAQuery, signal?: AbortSignal): Promise<QAUsage> {
|
|
const params = new URLSearchParams({ start: query.start, end: query.end, keyword: query.keyword })
|
|
return parseQAUsage(await getPayload(`/api/v1/qa-ops/usage?${params}`, signal))
|
|
}
|