diff --git a/ordinis-cuod-bundle/src/main/java/cloud/nstart/terravault/ordinis/cuod/BundleManifest.java b/ordinis-cuod-bundle/src/main/java/cloud/nstart/terravault/ordinis/cuod/BundleManifest.java index 46af1e7..00edaf1 100644 --- a/ordinis-cuod-bundle/src/main/java/cloud/nstart/terravault/ordinis/cuod/BundleManifest.java +++ b/ordinis-cuod-bundle/src/main/java/cloud/nstart/terravault/ordinis/cuod/BundleManifest.java @@ -24,5 +24,6 @@ public record BundleManifest( String schemaResource, String schemaVersion, List supportedLocales, - String defaultLocale) {} + String defaultLocale, + String recordsResource) {} } diff --git a/ordinis-cuod-bundle/src/main/java/cloud/nstart/terravault/ordinis/cuod/CuodBundleImporter.java b/ordinis-cuod-bundle/src/main/java/cloud/nstart/terravault/ordinis/cuod/CuodBundleImporter.java index 1908421..5bb32d6 100644 --- a/ordinis-cuod-bundle/src/main/java/cloud/nstart/terravault/ordinis/cuod/CuodBundleImporter.java +++ b/ordinis-cuod-bundle/src/main/java/cloud/nstart/terravault/ordinis/cuod/CuodBundleImporter.java @@ -3,9 +3,12 @@ package cloud.nstart.terravault.ordinis.cuod; import cloud.nstart.terravault.ordinis.domain.DataScope; import cloud.nstart.terravault.ordinis.domain.definition.DictionaryDefinition; import cloud.nstart.terravault.ordinis.domain.definition.DictionaryDefinitionRepository; +import cloud.nstart.terravault.ordinis.domain.record.DictionaryRecord; +import cloud.nstart.terravault.ordinis.domain.record.DictionaryRecordRepository; import cloud.nstart.terravault.ordinis.events.EventEnvelope; import cloud.nstart.terravault.ordinis.events.definition.DictionaryDefinitionCreated; import cloud.nstart.terravault.ordinis.events.definition.DictionaryDefinitionUpdated; +import cloud.nstart.terravault.ordinis.events.record.DictionaryRecordCreated; import cloud.nstart.terravault.ordinis.outbox.OutboxRecorder; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; @@ -19,6 +22,7 @@ import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.time.OffsetDateTime; import java.util.List; import java.util.Optional; import java.util.UUID; @@ -40,6 +44,7 @@ public class CuodBundleImporter { private final CuodBundleProperties props; private final DictionaryDefinitionRepository repository; + private final DictionaryRecordRepository recordRepository; private final OutboxRecorder outboxRecorder; private final ObjectMapper objectMapper; private final PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); @@ -47,10 +52,12 @@ public class CuodBundleImporter { public CuodBundleImporter( CuodBundleProperties props, DictionaryDefinitionRepository repository, + DictionaryRecordRepository recordRepository, OutboxRecorder outboxRecorder, ObjectMapper objectMapper) { this.props = props; this.repository = repository; + this.recordRepository = recordRepository; this.outboxRecorder = outboxRecorder; this.objectMapper = objectMapper; } @@ -151,6 +158,72 @@ public class CuodBundleImporter { EventEnvelope.of(saved.getName(), toEventScope(saved.getScope()), manifest.bundle(), event)); log.info("Справочник '{}' создан (scope={}, locales={}, default={})", saved.getName(), saved.getScope(), entryLocales, entryDefault); + + importRecords(manifest, entry, saved); + } + + /** + * Подгружает seed records из {@code records/.records.json} (если файл задан в + * manifest через {@code recordsResource} или существует по конвенции). Идемпотентно: + * при повторном вызове не создаёт дубликатов — пропускает существующие businessKey. + */ + private void importRecords(BundleManifest manifest, BundleManifest.DictionaryEntry entry, + DictionaryDefinition definition) { + String recordsPath = entry.recordsResource() != null + ? entry.recordsResource() + : "records/" + entry.name() + ".records.json"; + Resource recordsResource = resolver.getResource(props.resourceRoot() + recordsPath); + if (!recordsResource.exists()) { + log.debug("Records file для '{}' не найден ({}), пропускаем seed", entry.name(), recordsPath); + return; + } + + JsonNode recordsArray; + try (var in = recordsResource.getInputStream()) { + recordsArray = objectMapper.readTree(in); + } catch (Exception e) { + log.warn("Не удалось прочитать records '{}': {}", recordsPath, e.getMessage()); + return; + } + if (!recordsArray.isArray()) { + log.warn("Records '{}' не массив, пропускаем", recordsPath); + return; + } + + int created = 0; + int skipped = 0; + OffsetDateTime now = OffsetDateTime.now(); + for (JsonNode rec : recordsArray) { + String businessKey = rec.path("businessKey").asText(null); + JsonNode data = rec.path("data"); + if (businessKey == null || businessKey.isBlank() || data.isMissingNode()) { + log.warn("Пропуск некорректной записи в '{}': missing businessKey/data", recordsPath); + continue; + } + List existing = recordRepository + .findByDictionaryIdAndBusinessKeyOrderByValidFromDesc(definition.getId(), businessKey); + if (!existing.isEmpty()) { + skipped++; + continue; + } + DictionaryRecord record = new DictionaryRecord( + UUID.randomUUID(), definition.getId(), businessKey, data, definition.getScope(), now); + DictionaryRecord savedRecord = recordRepository.save(record); + + var recEvent = new DictionaryRecordCreated( + savedRecord.getId(), savedRecord.getBusinessKey(), savedRecord.getData(), null, + savedRecord.getValidFrom(), savedRecord.getValidTo(), + savedRecord.getCreatedAt(), savedRecord.getCreatedBy()); + outboxRecorder.record( + "RecordCreated", "DictionaryRecord", savedRecord.getId().toString(), + definition.getName(), savedRecord.getDataScope(), manifest.bundle(), + "record:" + definition.getName() + ":" + savedRecord.getBusinessKey(), + EventEnvelope.of(definition.getName(), toEventScope(savedRecord.getDataScope()), + manifest.bundle(), recEvent)); + created++; + } + log.info("Records для '{}': создано {}, пропущено (уже есть) {}", + entry.name(), created, skipped); } private static cloud.nstart.terravault.ordinis.events.DataScope toEventScope(DataScope d) { diff --git a/ordinis-cuod-bundle/src/main/resources/bundles/cuod/records/ground_station.records.json b/ordinis-cuod-bundle/src/main/resources/bundles/cuod/records/ground_station.records.json new file mode 100644 index 0000000..6c24b61 --- /dev/null +++ b/ordinis-cuod-bundle/src/main/resources/bundles/cuod/records/ground_station.records.json @@ -0,0 +1,105 @@ +[ + { + "businessKey": "MOSCOW_OTRADNOE", + "data": { + "code": "MOSCOW_OTRADNOE", + "name": { + "ru-RU": "Отрадное", + "en-US": "Otradnoye" + }, + "location": { + "ru-RU": "Московская область, Россия", + "en-US": "Moscow Region, Russia" + }, + "country": "RU", + "lat": 56.099, + "lon": 37.566, + "elevation_m": 200, + "elevation_min_deg": 5, + "antennas": [ + { + "id": "A1", + "diameter_m": 12, + "bands": ["X_BAND", "S_BAND"], + "polarizations": ["LHCP", "RHCP"], + "tx_capable": true, + "rx_capable": true, + "g_t_db_per_k": 32 + } + ], + "operator": { + "ru-RU": "Роскосмос", + "en-US": "Roscosmos" + }, + "status": "OPERATIONAL" + } + }, + { + "businessKey": "SVALBARD_SVALSAT", + "data": { + "code": "SVALBARD_SVALSAT", + "name": { + "ru-RU": "SvalSat (Шпицберген)", + "en-US": "SvalSat (Svalbard)" + }, + "location": { + "ru-RU": "Шпицберген, Норвегия", + "en-US": "Svalbard, Norway" + }, + "country": "NO", + "lat": 78.23, + "lon": 15.39, + "elevation_m": 458, + "elevation_min_deg": 3, + "antennas": [ + { + "id": "S11", + "diameter_m": 13, + "bands": ["X_BAND", "S_BAND"], + "polarizations": ["LHCP", "RHCP"], + "tx_capable": true, + "rx_capable": true, + "g_t_db_per_k": 33 + } + ], + "operator": { + "ru-RU": "KSAT", + "en-US": "KSAT" + }, + "status": "OPERATIONAL" + } + }, + { + "businessKey": "BAIKONUR_TM", + "data": { + "code": "BAIKONUR_TM", + "name": { + "ru-RU": "Байконур (телеметрия)", + "en-US": "Baikonur (telemetry)" + }, + "location": { + "ru-RU": "Байконур, Казахстан", + "en-US": "Baikonur, Kazakhstan" + }, + "country": "KZ", + "lat": 45.965, + "lon": 63.305, + "elevation_m": 90, + "elevation_min_deg": 5, + "antennas": [ + { + "id": "BK-1", + "diameter_m": 9, + "bands": ["S_BAND", "UHF"], + "rx_capable": true, + "tx_capable": true + } + ], + "operator": { + "ru-RU": "Роскосмос", + "en-US": "Roscosmos" + }, + "status": "OPERATIONAL" + } + } +] diff --git a/ordinis-cuod-bundle/src/main/resources/bundles/cuod/records/satellite_type.records.json b/ordinis-cuod-bundle/src/main/resources/bundles/cuod/records/satellite_type.records.json new file mode 100644 index 0000000..d9667b0 --- /dev/null +++ b/ordinis-cuod-bundle/src/main/resources/bundles/cuod/records/satellite_type.records.json @@ -0,0 +1,89 @@ +[ + { + "businessKey": "SAR_X_HIRES", + "data": { + "code": "SAR_X_HIRES", + "name": { + "ru-RU": "Радиолокатор синтезированной апертуры, X-диапазон, высокое разрешение", + "en-US": "Synthetic Aperture Radar, X-band, high resolution" + }, + "description": { + "ru-RU": "SAR X-band КА с разрешением до 1 м", + "en-US": "SAR X-band satellite with up to 1 m resolution" + }, + "mission_category": "RADAR_SAR", + "mass_class": "MEDIUM", + "typical_orbit_classes": ["LEO", "SSO"], + "typical_resolution_m": 1, + "typical_revisit_hours": 24 + } + }, + { + "businessKey": "OPT_HIRES_VIS", + "data": { + "code": "OPT_HIRES_VIS", + "name": { + "ru-RU": "Оптический КА высокого разрешения, видимый диапазон", + "en-US": "Optical high-resolution satellite, visible band" + }, + "description": { + "ru-RU": "Оптика 0.5-1 м VIS, узкая полоса", + "en-US": "Optical 0.5-1 m VIS, narrow swath" + }, + "mission_category": "OPTICAL_HIRES", + "mass_class": "MEDIUM", + "typical_orbit_classes": ["SSO"], + "typical_resolution_m": 0.5, + "typical_revisit_hours": 48 + } + }, + { + "businessKey": "OPT_MIDRES_MS", + "data": { + "code": "OPT_MIDRES_MS", + "name": { + "ru-RU": "Оптический мультиспектральный, среднее разрешение", + "en-US": "Optical multispectral, medium resolution" + }, + "description": { + "ru-RU": "Multispectral 5-30 м, широкая полоса (типа Landsat/Sentinel-2)", + "en-US": "Multispectral 5-30 m, wide swath (Landsat/Sentinel-2 class)" + }, + "mission_category": "OPTICAL_MIDRES", + "mass_class": "LARGE", + "typical_orbit_classes": ["SSO"], + "typical_resolution_m": 10, + "typical_revisit_hours": 120 + } + }, + { + "businessKey": "HYPER", + "data": { + "code": "HYPER", + "name": { + "ru-RU": "Гиперспектральный сканер", + "en-US": "Hyperspectral imager" + }, + "mission_category": "HYPERSPECTRAL", + "mass_class": "MEDIUM", + "typical_orbit_classes": ["SSO"], + "typical_resolution_m": 30, + "typical_revisit_hours": 168 + } + }, + { + "businessKey": "METEO_GEO", + "data": { + "code": "METEO_GEO", + "name": { + "ru-RU": "Метеоспутник на геостационарной орбите", + "en-US": "Geostationary meteorological satellite" + }, + "mission_category": "METEO", + "mass_class": "LARGE", + "typical_orbit_classes": ["GEO"], + "typical_resolution_m": 1000, + "typical_revisit_hours": 0.25 + } + } +] diff --git a/ordinis-cuod-bundle/src/main/resources/bundles/cuod/records/spacecraft.records.json b/ordinis-cuod-bundle/src/main/resources/bundles/cuod/records/spacecraft.records.json new file mode 100644 index 0000000..cbbad24 --- /dev/null +++ b/ordinis-cuod-bundle/src/main/resources/bundles/cuod/records/spacecraft.records.json @@ -0,0 +1,122 @@ +[ + { + "businessKey": "1999-068A", + "data": { + "designator": "1999-068A", + "norad_id": 25994, + "name": { + "ru-RU": "Terra (EOS AM-1)", + "en-US": "Terra (EOS AM-1)" + }, + "operator": { + "ru-RU": "НАСА", + "en-US": "NASA" + }, + "country": "US", + "satellite_type_code": "OPT_MIDRES_MS", + "mass_kg": 4864, + "launch_date": "1999-12-18", + "status": "ACTIVE", + "orbit": { + "type": "SSO", + "semimajor_axis_km": 7083, + "eccentricity": 0.0001, + "inclination_deg": 98.2 + }, + "spectrum_bands": ["VIS", "NIR", "SWIR", "TIR"], + "mission": { + "ru-RU": "Глобальный мониторинг суши и атмосферы", + "en-US": "Global land and atmosphere monitoring" + } + } + }, + { + "businessKey": "2014-016A", + "data": { + "designator": "2014-016A", + "norad_id": 39634, + "name": { + "ru-RU": "Sentinel-1A", + "en-US": "Sentinel-1A" + }, + "operator": { + "ru-RU": "ESA / Copernicus", + "en-US": "ESA / Copernicus" + }, + "country": "FR", + "satellite_type_code": "SAR_X_HIRES", + "mass_kg": 2300, + "launch_date": "2014-04-03", + "status": "ACTIVE", + "orbit": { + "type": "SSO", + "semimajor_axis_km": 7064, + "eccentricity": 0.0001, + "inclination_deg": 98.18 + }, + "spectrum_bands": ["C_BAND"], + "mission": { + "ru-RU": "C-SAR радиолокация для Copernicus", + "en-US": "C-SAR radar for Copernicus" + } + } + }, + { + "businessKey": "2015-028A", + "data": { + "designator": "2015-028A", + "norad_id": 40697, + "name": { + "ru-RU": "Sentinel-2A", + "en-US": "Sentinel-2A" + }, + "operator": { + "ru-RU": "ESA / Copernicus", + "en-US": "ESA / Copernicus" + }, + "country": "FR", + "satellite_type_code": "OPT_MIDRES_MS", + "mass_kg": 1140, + "launch_date": "2015-06-23", + "status": "ACTIVE", + "orbit": { + "type": "SSO", + "semimajor_axis_km": 7167, + "inclination_deg": 98.62 + }, + "spectrum_bands": ["VIS", "NIR", "SWIR"], + "mission": { + "ru-RU": "Multispectral imaging для Copernicus", + "en-US": "Multispectral imaging for Copernicus" + } + } + }, + { + "businessKey": "2024-149E", + "data": { + "designator": "2024-149E", + "name": { + "ru-RU": "Кондор-ФКА №2", + "en-US": "Kondor-FKA No.2" + }, + "operator": { + "ru-RU": "Роскосмос", + "en-US": "Roscosmos" + }, + "country": "RU", + "satellite_type_code": "SAR_X_HIRES", + "mass_kg": 1050, + "launch_date": "2024-08-13", + "status": "ACTIVE", + "orbit": { + "type": "SSO", + "inclination_deg": 97.5 + }, + "spectrum_bands": ["S_BAND"], + "mission": { + "ru-RU": "Радиолокационное наблюдение Земли", + "en-US": "Radar Earth observation" + } + } + } +]