diff --git a/dbapp/mainapp/utils.py b/dbapp/mainapp/utils.py index fc0fd47..9ddb634 100644 --- a/dbapp/mainapp/utils.py +++ b/dbapp/mainapp/utils.py @@ -765,31 +765,37 @@ def get_points_from_csv(file_content, current_user=None, is_automatic=False): 'errors': список ошибок } """ + # Читаем CSV без предопределенных имен колонок df = pd.read_csv( io.StringIO(file_content), sep=";", - names=[ - "id", - "obj", - "lat", - "lon", - "h", - "time", - "sat", - "norad_id", - "freq", - "f_range", - "et", - "qaul", - "mir_1", - "mir_2", - "mir_3", - "mir_4", - "mir_5", - "mir_6", - "mir_7", - ], + header=None ) + + # Присваиваем имена первым 12 колонкам + base_columns = [ + "id", + "obj", + "lat", + "lon", + "h", + "time", + "sat", + "norad_id", + "freq", + "f_range", + "et", + "qual", + ] + + # Все колонки после "qual" (индекс 11) - это зеркала + num_columns = len(df.columns) + mirror_columns = [f"mir_{i+1}" for i in range(num_columns - len(base_columns))] + + # Объединяем имена колонок + df.columns = base_columns + mirror_columns + + # Преобразуем типы данных df[["lat", "lon", "freq", "f_range"]] = ( df[["lat", "lon", "freq", "f_range"]] .replace(",", ".", regex=True) @@ -873,7 +879,7 @@ def get_points_from_csv(file_content, current_user=None, is_automatic=False): sources_cache[(source_name, sat_name, source.id)] = source # Создаем ObjItem (с Source или без, в зависимости от is_automatic) - _create_objitem_from_csv_row(row, source, user_to_use, is_automatic) + _create_objitem_from_csv_row(row, source, user_to_use, is_automatic, mirror_columns) added_count += 1 except Exception as e: @@ -996,7 +1002,7 @@ def _find_tech_analyze_data(name: str, satellite: Satellite): return None -def _create_objitem_from_csv_row(row, source, user_to_use, is_automatic=False): +def _create_objitem_from_csv_row(row, source, user_to_use, is_automatic=False, mirror_columns=None): """ Вспомогательная функция для создания ObjItem из строки CSV DataFrame. @@ -1008,6 +1014,7 @@ def _create_objitem_from_csv_row(row, source, user_to_use, is_automatic=False): source: объект Source для связи (может быть None если is_automatic=True) user_to_use: пользователь для created_by is_automatic: если True, точка не связывается с Source + mirror_columns: список имен колонок с зеркалами (optional) """ # Определяем поляризацию match row["obj"].split(" ")[-1]: @@ -1035,12 +1042,24 @@ def _create_objitem_from_csv_row(row, source, user_to_use, is_automatic=False): # Обработка зеркал - теперь это спутники mirror_names = [] - if not pd.isna(row["mir_1"]) and row["mir_1"].strip() != "-": - mirror_names.append(row["mir_1"]) - if not pd.isna(row["mir_2"]) and row["mir_2"].strip() != "-": - mirror_names.append(row["mir_2"]) - if not pd.isna(row["mir_3"]) and row["mir_3"].strip() != "-": - mirror_names.append(row["mir_3"]) + + # Если переданы имена колонок зеркал, используем их + if mirror_columns: + for mir_col in mirror_columns: + if mir_col in row.index: + mir_value = row[mir_col] + if not pd.isna(mir_value) and str(mir_value).strip() != "-" and str(mir_value).strip() != "": + mirror_names.append(str(mir_value).strip()) + else: + # Fallback на старый способ (для обратной совместимости) + for i in range(1, 100): # Проверяем до 100 колонок зеркал + mir_col = f"mir_{i}" + if mir_col in row.index: + mir_value = row[mir_col] + if not pd.isna(mir_value) and str(mir_value).strip() != "-" and str(mir_value).strip() != "": + mirror_names.append(str(mir_value).strip()) + else: + break # Находим спутники-зеркала mirror_satellites = find_mirror_satellites(mirror_names) diff --git a/dbapp/mapsapp/utils.py b/dbapp/mapsapp/utils.py index 880db4c..adb404b 100644 --- a/dbapp/mapsapp/utils.py +++ b/dbapp/mapsapp/utils.py @@ -223,8 +223,11 @@ def parse_transponders_from_json(filepath: str, user=None): # Third-party imports (additional) +import logging from lxml import etree +logger = logging.getLogger(__name__) + def parse_transponders_from_xml(data_in: BytesIO, user=None): """ Парсит транспондеры из XML файла. @@ -232,9 +235,23 @@ def parse_transponders_from_xml(data_in: BytesIO, user=None): Если имя спутника содержит альтернативное имя в скобках, оно извлекается и сохраняется в поле alternative_name. + Процесс импорта: + 1. Сначала создаются/обновляются все спутники + 2. Затем для каждого спутника добавляются его транспондеры + Args: data_in: BytesIO объект с XML данными user: пользователь для установки created_by и updated_by (optional) + + Returns: + dict: Статистика импорта с ключами: + - satellites_created: количество созданных спутников + - satellites_updated: количество обновлённых спутников + - satellites_skipped: количество пропущенных спутников (дубликаты) + - satellites_ignored: количество игнорированных спутников (X, DONT USE) + - transponders_created: количество созданных транспондеров + - transponders_existing: количество существующих транспондеров + - errors: список ошибок с деталями """ tree = etree.parse(data_in) ns = { @@ -243,48 +260,38 @@ def parse_transponders_from_xml(data_in: BytesIO, user=None): 'tr': 'http://schemas.datacontract.org/2004/07/Geolocation.Common.Extensions' } satellites = tree.xpath('//ns:SatelliteMemo', namespaces=ns) - for sat in satellites[:]: + + # Статистика импорта + stats = { + 'satellites_created': 0, + 'satellites_updated': 0, + 'satellites_skipped': 0, + 'satellites_ignored': 0, + 'transponders_created': 0, + 'transponders_existing': 0, + 'errors': [] + } + + # Этап 1: Создание/обновление спутников + satellite_map = {} # Словарь для связи XML элементов со спутниками в БД + + for sat in satellites: name_full = sat.xpath('./ns:name/text()', namespaces=ns)[0] + + # Игнорируем служебные записи if name_full == 'X' or 'DONT USE' in name_full: + stats['satellites_ignored'] += 1 + logger.info(f"Игнорирован спутник: {name_full}") continue # Парсим имя спутника и альтернативное имя main_name, alt_name = parse_satellite_name(name_full) norad = sat.xpath('./ns:norad/text()', namespaces=ns) - beams = sat.xpath('.//ns:BeamMemo', namespaces=ns) intl_code = sat.xpath('.//ns:internationalCode/text()', namespaces=ns) sub_sat_point = sat.xpath('.//ns:subSatellitePoint/text()', namespaces=ns) - zones = {} - for zone in beams: - zone_name = zone.xpath('./ns:name/text()', namespaces=ns)[0] if zone.xpath('./ns:name/text()', namespaces=ns) else '-' - zones[zone.xpath('./ns:id/text()', namespaces=ns)[0]] = { - "name": zone_name, - "pol": zone.xpath('./ns:polarization/text()', namespaces=ns)[0], - } - transponders = sat.xpath('.//ns:TransponderMemo', namespaces=ns) - for transponder in transponders: - tr_id = transponder.xpath('./ns:downlinkBeamId/text()', namespaces=ns)[0] - downlink_start = float(transponder.xpath('./ns:downlinkFrequency/tr:start/text()', namespaces=ns)[0]) - downlink_end = float(transponder.xpath('./ns:downlinkFrequency/tr:end/text()', namespaces=ns)[0]) - uplink_start = float(transponder.xpath('./ns:uplinkFrequency/tr:start/text()', namespaces=ns)[0]) - uplink_end = float(transponder.xpath('./ns:uplinkFrequency/tr:end/text()', namespaces=ns)[0]) - tr_data = zones[tr_id] - match tr_data['pol']: - case 'Horizontal': - pol = 'Горизонтальная' - case 'Vertical': - pol = 'Вертикальная' - case 'CircularRight': - pol = 'Правая' - case 'CircularLeft': - pol = 'Левая' - case _: - pol = '-' - tr_name = transponder.xpath('./ns:name/text()', namespaces=ns)[0] - - pol_obj, _ = Polarization.objects.get_or_create(name=pol) - + + try: # Ищем спутник по имени или альтернативному имени sat_obj = find_satellite_by_name(main_name) if not sat_obj: @@ -296,8 +303,10 @@ def parse_transponders_from_xml(data_in: BytesIO, user=None): international_code=intl_code[0] if intl_code else "", undersat_point=float(sub_sat_point[0]) if sub_sat_point else None ) + stats['satellites_created'] += 1 + logger.info(f"Создан спутник: {main_name} (альт. имя: {alt_name})") else: - # Если найден, обновляем альтернативное имя если не установлено + # Если найден, обновляем поля если они не установлены updated = False if alt_name and not sat_obj.alternative_name: sat_obj.alternative_name = alt_name @@ -313,20 +322,130 @@ def parse_transponders_from_xml(data_in: BytesIO, user=None): updated = True if updated: sat_obj.save() + stats['satellites_updated'] += 1 + logger.info(f"Обновлён спутник: {main_name}") - trans_obj, created = Transponders.objects.get_or_create( - polarization=pol_obj, - downlink=(downlink_start+downlink_end)/2/1000000, - uplink=(uplink_start+uplink_end)/2/1000000, - frequency_range=abs(downlink_end-downlink_start)/1000000, - name=tr_name, - defaults={ - "zone_name": tr_data['name'], - "sat_id": sat_obj, - } + # Сохраняем связь XML элемента со спутником в БД + satellite_map[sat] = sat_obj + + except Satellite.MultipleObjectsReturned: + # Найдено несколько спутников - пропускаем + stats['satellites_skipped'] += 1 + duplicates = Satellite.objects.filter( + Q(name__icontains=main_name.lower()) | + Q(alternative_name__icontains=main_name.lower()) ) - if user: - if created: - trans_obj.created_by = user - trans_obj.updated_by = user - trans_obj.save() \ No newline at end of file + duplicate_names = [f"{s.name} (ID: {s.id})" for s in duplicates] + error_msg = f"Найдено несколько спутников для '{name_full}': {', '.join(duplicate_names)}" + stats['errors'].append({ + 'type': 'duplicate_satellite', + 'satellite': name_full, + 'details': duplicate_names + }) + logger.warning(error_msg) + continue + except Exception as e: + # Другие ошибки при обработке спутника + stats['satellites_skipped'] += 1 + error_msg = f"Ошибка при обработке спутника '{name_full}': {str(e)}" + stats['errors'].append({ + 'type': 'satellite_error', + 'satellite': name_full, + 'error': str(e) + }) + logger.error(error_msg, exc_info=True) + continue + + # Этап 2: Добавление транспондеров для каждого спутника + for sat, sat_obj in satellite_map.items(): + sat_name = sat.xpath('./ns:name/text()', namespaces=ns)[0] + + try: + beams = sat.xpath('.//ns:BeamMemo', namespaces=ns) + zones = {} + for zone in beams: + zone_name = zone.xpath('./ns:name/text()', namespaces=ns)[0] if zone.xpath('./ns:name/text()', namespaces=ns) else '-' + zones[zone.xpath('./ns:id/text()', namespaces=ns)[0]] = { + "name": zone_name, + "pol": zone.xpath('./ns:polarization/text()', namespaces=ns)[0], + } + + transponders = sat.xpath('.//ns:TransponderMemo', namespaces=ns) + for transponder in transponders: + try: + tr_id = transponder.xpath('./ns:downlinkBeamId/text()', namespaces=ns)[0] + downlink_start = float(transponder.xpath('./ns:downlinkFrequency/tr:start/text()', namespaces=ns)[0]) + downlink_end = float(transponder.xpath('./ns:downlinkFrequency/tr:end/text()', namespaces=ns)[0]) + uplink_start = float(transponder.xpath('./ns:uplinkFrequency/tr:start/text()', namespaces=ns)[0]) + uplink_end = float(transponder.xpath('./ns:uplinkFrequency/tr:end/text()', namespaces=ns)[0]) + tr_data = zones[tr_id] + + match tr_data['pol']: + case 'Horizontal': + pol = 'Горизонтальная' + case 'Vertical': + pol = 'Вертикальная' + case 'CircularRight': + pol = 'Правая' + case 'CircularLeft': + pol = 'Левая' + case _: + pol = '-' + + tr_name = transponder.xpath('./ns:name/text()', namespaces=ns)[0] + pol_obj, _ = Polarization.objects.get_or_create(name=pol) + + trans_obj, created = Transponders.objects.get_or_create( + polarization=pol_obj, + downlink=(downlink_start+downlink_end)/2/1000000, + uplink=(uplink_start+uplink_end)/2/1000000, + frequency_range=abs(downlink_end-downlink_start)/1000000, + name=tr_name, + defaults={ + "zone_name": tr_data['name'], + "sat_id": sat_obj, + } + ) + + if created: + stats['transponders_created'] += 1 + else: + stats['transponders_existing'] += 1 + + if user: + if created: + trans_obj.created_by = user + trans_obj.updated_by = user + trans_obj.save() + + except Exception as e: + error_msg = f"Ошибка при обработке транспондера спутника '{sat_name}': {str(e)}" + stats['errors'].append({ + 'type': 'transponder_error', + 'satellite': sat_name, + 'error': str(e) + }) + logger.error(error_msg, exc_info=True) + continue + + except Exception as e: + error_msg = f"Ошибка при обработке транспондеров спутника '{sat_name}': {str(e)}" + stats['errors'].append({ + 'type': 'transponders_processing_error', + 'satellite': sat_name, + 'error': str(e) + }) + logger.error(error_msg, exc_info=True) + continue + + # Итоговая статистика в лог + logger.info( + f"Импорт завершён. Спутники: создано {stats['satellites_created']}, " + f"обновлено {stats['satellites_updated']}, пропущено {stats['satellites_skipped']}, " + f"игнорировано {stats['satellites_ignored']}. " + f"Транспондеры: создано {stats['transponders_created']}, " + f"существующих {stats['transponders_existing']}. " + f"Ошибок: {len(stats['errors'])}" + ) + + return stats \ No newline at end of file