/** * 设施点取数(票 68):设施台账枚举 + 实体详情补坐标。 * * 为什么是两跳(而不是一条几何端点): * · cockpit 没有「设施列表」端点,四类设施的实体标识只出现在 * `/infrastructure/operating-information` 的指标行上(`facilityId` + `facilityKind`); * · 坐标只出现在 `/entities/{id}/detail?category=…` 的 `coordinates` 上。 * 两条都是 `GET /api/cockpit/v1/**` 免鉴权只读端点,不碰业务 GIS 路径(ADR 0005)。 * **契约缺口**(票 68 登记):实体详情不带几何图层那套 `coordinateValidation`, * 基础设施也还没进 `/infrastructure/geometry` —— 后端补齐后可去掉这层两跳拼装。 * * 失败纪律沿用全舱:台账取不到 → 设施点图层整体 `failed`(图例如实「未取到」; * 不是「这里没有设施」);单条详情取不到 → 该条进「无地图位置」(原因是详情未取到), * 其余照常上图 —— 一条坏数据不掀掉整层。 */ import { requestCockpitJson } from './cockpitApi.js' import { entityDetailPath } from './entityDetailApi.js' import { SECTION_STATE, section, sectionStateFromReason } from '../domain/sectionState.js' import { REQUEST_REASON } from '../domain/requestReason.js' import { facilityDetailRequests, facilityInventory, facilityPointsWithDetails, facilityRowsFromOperating, } from '../domain/facilityPoints.js' export const FACILITY_OPERATING_PATH = '/infrastructure/operating-information' export const FACILITY_ARCHIVES_PATH = '/infrastructure/base-archives' /** 详情并发上限:现网四类设施合计个位数,4 路并发足够,不把一次启动打成请求风暴。 */ const DETAIL_CONCURRENCY = 4 /** * 逐条拉实体详情(并发有界)。失败只记该条,不抛。 * @param {Array} requests `facilityDetailRequests()` 的输出 * @returns {Promise} `key → { ok, detail }` */ async function loadFacilityDetails(requests, options) { const results = {} let cursor = 0 async function worker() { while (cursor < requests.length) { const request = requests[cursor] cursor += 1 const path = entityDetailPath({ kind: 'facility', id: request.id, category: request.category }) if (!path) { results[request.key] = { ok: false, reason: 'invalid-identity' } continue } const result = await requestCockpitJson(path, options) results[request.key] = result.ok && result.data && typeof result.data === 'object' ? { ok: true, detail: result.data } : { ok: false, reason: result.reason || REQUEST_REASON.BAD_PAYLOAD } } } await Promise.all(Array.from({ length: Math.min(DETAIL_CONCURRENCY, requests.length) }, worker)) return results } /** * 取设施点图层。 * * @param {object} [options] `{ config, fetchImpl }`(单测/走查注入) * @returns {Promise} 分区(`section()`): * 成功时 `data` 是 `{ points, inventory, archivesAvailable, detailFailures }`; * 台账失败时 `data` 仍给出 `inventory`(档案若取到则计数可见)与 `points: null`。 */ export async function fetchFacilityPoints(options = {}) { const requestOptions = { config: options.config, fetchImpl: options.fetchImpl } const [operating, archives] = await Promise.all([ requestCockpitJson(FACILITY_OPERATING_PATH, requestOptions), requestCockpitJson(FACILITY_ARCHIVES_PATH, requestOptions), ]) const archivesData = archives.ok && archives.data && typeof archives.data === 'object' ? archives.data : null if (!operating.ok) { return section(sectionStateFromReason(operating.reason), { reason: operating.reason || '', mode: operating.mode || '', // `points: null` 是「图层没取到」的唯一形状(空数组=取到了、确实没有设施)。 data: { points: null, inventory: facilityInventory(archivesData, null), archivesAvailable: archivesData !== null, detailFailures: 0 }, }) } const rows = facilityRowsFromOperating(operating.data) const details = await loadFacilityDetails(facilityDetailRequests(rows), requestOptions) const points = facilityPointsWithDetails(rows, details) const detailFailures = Object.values(details).filter((result) => !result.ok).length return section(SECTION_STATE.SUCCESS, { mode: operating.mode || '', data: { points, inventory: facilityInventory(archivesData, points), archivesAvailable: archivesData !== null, detailFailures, }, }) }