| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798 |
- /**
- * 设施点取数(票 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<object>} `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<object>} 分区(`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,
- },
- })
- }
|