facilityPointService.js 4.7 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
  1. /**
  2. * 设施点取数(票 68):设施台账枚举 + 实体详情补坐标。
  3. *
  4. * 为什么是两跳(而不是一条几何端点):
  5. * · cockpit 没有「设施列表」端点,四类设施的实体标识只出现在
  6. * `/infrastructure/operating-information` 的指标行上(`facilityId` + `facilityKind`);
  7. * · 坐标只出现在 `/entities/{id}/detail?category=…` 的 `coordinates` 上。
  8. * 两条都是 `GET /api/cockpit/v1/**` 免鉴权只读端点,不碰业务 GIS 路径(ADR 0005)。
  9. * **契约缺口**(票 68 登记):实体详情不带几何图层那套 `coordinateValidation`,
  10. * 基础设施也还没进 `/infrastructure/geometry` —— 后端补齐后可去掉这层两跳拼装。
  11. *
  12. * 失败纪律沿用全舱:台账取不到 → 设施点图层整体 `failed`(图例如实「未取到」;
  13. * 不是「这里没有设施」);单条详情取不到 → 该条进「无地图位置」(原因是详情未取到),
  14. * 其余照常上图 —— 一条坏数据不掀掉整层。
  15. */
  16. import { requestCockpitJson } from './cockpitApi.js'
  17. import { entityDetailPath } from './entityDetailApi.js'
  18. import { SECTION_STATE, section, sectionStateFromReason } from '../domain/sectionState.js'
  19. import { REQUEST_REASON } from '../domain/requestReason.js'
  20. import {
  21. facilityDetailRequests,
  22. facilityInventory,
  23. facilityPointsWithDetails,
  24. facilityRowsFromOperating,
  25. } from '../domain/facilityPoints.js'
  26. export const FACILITY_OPERATING_PATH = '/infrastructure/operating-information'
  27. export const FACILITY_ARCHIVES_PATH = '/infrastructure/base-archives'
  28. /** 详情并发上限:现网四类设施合计个位数,4 路并发足够,不把一次启动打成请求风暴。 */
  29. const DETAIL_CONCURRENCY = 4
  30. /**
  31. * 逐条拉实体详情(并发有界)。失败只记该条,不抛。
  32. * @param {Array} requests `facilityDetailRequests()` 的输出
  33. * @returns {Promise<object>} `key → { ok, detail }`
  34. */
  35. async function loadFacilityDetails(requests, options) {
  36. const results = {}
  37. let cursor = 0
  38. async function worker() {
  39. while (cursor < requests.length) {
  40. const request = requests[cursor]
  41. cursor += 1
  42. const path = entityDetailPath({ kind: 'facility', id: request.id, category: request.category })
  43. if (!path) {
  44. results[request.key] = { ok: false, reason: 'invalid-identity' }
  45. continue
  46. }
  47. const result = await requestCockpitJson(path, options)
  48. results[request.key] = result.ok && result.data && typeof result.data === 'object'
  49. ? { ok: true, detail: result.data }
  50. : { ok: false, reason: result.reason || REQUEST_REASON.BAD_PAYLOAD }
  51. }
  52. }
  53. await Promise.all(Array.from({ length: Math.min(DETAIL_CONCURRENCY, requests.length) }, worker))
  54. return results
  55. }
  56. /**
  57. * 取设施点图层。
  58. *
  59. * @param {object} [options] `{ config, fetchImpl }`(单测/走查注入)
  60. * @returns {Promise<object>} 分区(`section()`):
  61. * 成功时 `data` 是 `{ points, inventory, archivesAvailable, detailFailures }`;
  62. * 台账失败时 `data` 仍给出 `inventory`(档案若取到则计数可见)与 `points: null`。
  63. */
  64. export async function fetchFacilityPoints(options = {}) {
  65. const requestOptions = { config: options.config, fetchImpl: options.fetchImpl }
  66. const [operating, archives] = await Promise.all([
  67. requestCockpitJson(FACILITY_OPERATING_PATH, requestOptions),
  68. requestCockpitJson(FACILITY_ARCHIVES_PATH, requestOptions),
  69. ])
  70. const archivesData = archives.ok && archives.data && typeof archives.data === 'object' ? archives.data : null
  71. if (!operating.ok) {
  72. return section(sectionStateFromReason(operating.reason), {
  73. reason: operating.reason || '',
  74. mode: operating.mode || '',
  75. // `points: null` 是「图层没取到」的唯一形状(空数组=取到了、确实没有设施)。
  76. data: { points: null, inventory: facilityInventory(archivesData, null), archivesAvailable: archivesData !== null, detailFailures: 0 },
  77. })
  78. }
  79. const rows = facilityRowsFromOperating(operating.data)
  80. const details = await loadFacilityDetails(facilityDetailRequests(rows), requestOptions)
  81. const points = facilityPointsWithDetails(rows, details)
  82. const detailFailures = Object.values(details).filter((result) => !result.ok).length
  83. return section(SECTION_STATE.SUCCESS, {
  84. mode: operating.mode || '',
  85. data: {
  86. points,
  87. inventory: facilityInventory(archivesData, points),
  88. archivesAvailable: archivesData !== null,
  89. detailFailures,
  90. },
  91. })
  92. }