From 81cadc8cf90a57fb43a59088c4b2a9a8b4c2ede2 Mon Sep 17 00:00:00 2001 From: Andrey Smakotin Date: Tue, 11 Nov 2025 01:00:21 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D1=83=D0=BC=D0=BD=D0=B0=D1=8F=20=D1=84=D0=B8?= =?UTF-8?q?=D0=BB=D1=8C=D1=82=D1=80=D0=B0=D1=86=D0=B8=D1=8F=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20=D0=BF=D0=BE=D0=B8=D1=81=D0=BA=D0=B0=20=D0=BF=D0=BE=20?= =?UTF-8?q?=D0=BD=D0=BE=D0=BC=D0=B5=D1=80=D0=B0=D0=BC=20=D1=82=D0=B5=D0=BB?= =?UTF-8?q?=D0=B5=D1=84=D0=BE=D0=BD=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Проблема: Поиск "x3m" неправильно находит клиента Наталью потому что её номер содержит цифру "3". Это происходило, потому что система искала по любым цифрам в query, даже если это явно не номер телефона. Решение: Добавлена функция is_query_phone_only() которая проверяет, содержит ли query ТОЛЬКО телефонные символы (цифры, +, -, (), пробелы, точка). Поиск по номеру телефона происходит ТОЛЬКО если: 1. Query состоит ТОЛЬКО из телефонных символов (никаких букв) 2. И количество цифр >= 3 Примеры: - "x3m" → НЕ ищет по цифре "3" (содержит букву) - "29" → НЕ ищет по цифрам (только 2 цифры, нужно минимум 3) - "295" → ИЩЕТ по цифрам "295" (только цифры, 3+ символов) - "+375291234567" → ИЩЕТ по номеру (только телефонные символы) - "team_x3m" → НЕ ищет по цифрам (содержит буквы и _) Изменения: 1. Добавлена функция is_query_phone_only() в views.py 2. Обновлена api_search_customers() для использования новой функции 3. Обновлена customer_list() для использования новой функции 4. Добавлены 19 unit-тестов для is_query_phone_only() Результаты тестирования: ✓ 42 теста всего (23 для determine_search_strategy + 19 для is_query_phone_only) ✓ Все тесты проходят успешно Критические тест-кейсы: ✓ is_query_phone_only('x3m') == False (решает исходную проблему) ✓ is_query_phone_only('295') == True ✓ is_query_phone_only('+375291234567') == True 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- myproject/customers/tests.py | 94 +++++++++++++++++++++++++++++++++++- myproject/customers/views.py | 54 +++++++++++++++++++-- 2 files changed, 144 insertions(+), 4 deletions(-) diff --git a/myproject/customers/tests.py b/myproject/customers/tests.py index 96530c7..e1856da 100644 --- a/myproject/customers/tests.py +++ b/myproject/customers/tests.py @@ -1,5 +1,5 @@ from django.test import TestCase -from .views import determine_search_strategy +from .views import determine_search_strategy, is_query_phone_only class DetermineSearchStrategyTestCase(TestCase): @@ -158,3 +158,95 @@ class DetermineSearchStrategyTestCase(TestCase): strategy, search_value = determine_search_strategy('natul') self.assertEqual(strategy, 'universal') self.assertEqual(search_value, 'natul') + + +class IsQueryPhoneOnlyTestCase(TestCase): + """ + Тесты для функции is_query_phone_only(). + + Проверяют, что функция правильно определяет, содержит ли query + только символы номера телефона (цифры, +, -, (), пробелы). + """ + + # ===== Должны вернуть True (только телефонные символы) ===== + def test_phone_only_digits(self): + """Query '295' должен вернуть True (только цифры)""" + self.assertTrue(is_query_phone_only('295')) + + def test_phone_only_single_digit(self): + """Query '5' должен вернуть True (одна цифра)""" + self.assertTrue(is_query_phone_only('5')) + + def test_phone_with_plus(self): + """Query '+375291234567' должен вернуть True""" + self.assertTrue(is_query_phone_only('+375291234567')) + + def test_phone_with_dashes(self): + """Query '029-123-45' должен вернуть True""" + self.assertTrue(is_query_phone_only('029-123-45')) + + def test_phone_with_parentheses(self): + """Query '(029) 123-45' должен вернуть True""" + self.assertTrue(is_query_phone_only('(029) 123-45')) + + def test_phone_with_spaces(self): + """Query '029 123 45' должен вернуть True""" + self.assertTrue(is_query_phone_only('029 123 45')) + + def test_phone_complex_format(self): + """Query '+375 (29) 123-45-67' должен вернуть True""" + self.assertTrue(is_query_phone_only('+375 (29) 123-45-67')) + + def test_phone_with_dot(self): + """Query '029.123.45' должен вернуть True""" + self.assertTrue(is_query_phone_only('029.123.45')) + + # ===== Должны вернуть False (содержат буквы или другие символы) ===== + def test_query_with_letters_only(self): + """Query 'abc' должен вернуть False (содержит буквы)""" + self.assertFalse(is_query_phone_only('abc')) + + def test_query_with_mixed_letters_digits(self): + """Query 'x3m' должен вернуть False (содержит буквы)""" + self.assertFalse(is_query_phone_only('x3m')) + + def test_query_name_with_digits(self): + """Query 'team_x3m' должен вернуть False (содержит буквы и _)""" + self.assertFalse(is_query_phone_only('team_x3m')) + + def test_query_name_cyrillic(self): + """Query 'Наталья' должен вернуть False (содержит кириллицу)""" + self.assertFalse(is_query_phone_only('Наталья')) + + def test_query_with_underscore(self): + """Query '123_456' должен вернуть False (содержит _)""" + self.assertFalse(is_query_phone_only('123_456')) + + def test_query_with_hash(self): + """Query '123#456' должен вернуть False (содержит #)""" + self.assertFalse(is_query_phone_only('123#456')) + + def test_empty_string(self): + """Query '' должен вернуть False (пустая строка)""" + self.assertFalse(is_query_phone_only('')) + + def test_only_spaces(self): + """Query ' ' должен вернуть True (только пробелы разрешены)""" + self.assertTrue(is_query_phone_only(' ')) + + # ===== Real-world cases ===== + def test_real_world_case_x3m_should_not_be_phone(self): + """ + Real-world case: "x3m" содержит букву, поэтому НЕ похож на телефон. + Это критично для решения проблемы с поиском Натальи. + """ + self.assertFalse(is_query_phone_only('x3m')) + # Значит, при поиске "x3m" НЕ будет поиска по цифре "3" в телефонах + + def test_real_world_case_295_should_be_phone(self): + """Real-world case: '295' только цифры, похож на телефон""" + self.assertTrue(is_query_phone_only('295')) + + def test_real_world_full_phone_number(self): + """Real-world case: полный номер в стандартном формате""" + self.assertTrue(is_query_phone_only('+375 (29) 598-62-62')) diff --git a/myproject/customers/views.py b/myproject/customers/views.py index 26c15e6..baa331f 100644 --- a/myproject/customers/views.py +++ b/myproject/customers/views.py @@ -41,10 +41,26 @@ def customer_list(request): # Строим Q-объект для поиска (единая функция) q_objects = build_customer_search_query(query, strategy, search_value) - # Добавляем поиск по телефону + # Добавляем поиск по телефону (умная логика) if phone_normalized: q_objects |= Q(phone__icontains=phone_normalized) + # Проверяем, похож ли query на номер телефона (только цифры и минимум 3 цифры) + query_digits = ''.join(c for c in query if c.isdigit()) + should_search_by_phone_digits = is_query_phone_only(query) and len(query_digits) >= 3 + + if should_search_by_phone_digits: + # Ищем клиентов, чьи телефоны содержат введенные цифры + customers_by_phone = Customer.objects.filter(phone__isnull=False) + matching_by_digits = [] + for customer in customers_by_phone: + customer_digits = ''.join(c for c in str(customer.phone) if c.isdigit()) + if query_digits in customer_digits: + matching_by_digits.append(customer.pk) + + if matching_by_digits: + q_objects |= Q(pk__in=matching_by_digits) + customers = customers.filter(q_objects) customers = customers.order_by('-created_at') @@ -167,6 +183,32 @@ def determine_search_strategy(query): return ('name_only', query) +def is_query_phone_only(query): + """ + Проверяет, содержит ли query только символы телефонного номера. + + Возвращает True, если query состоит ТОЛЬКО из: + - цифр: 0-9 + - телефонных символов: +, -, (, ), пробелов + + Возвращает False, если есть буквы или другие символы (означает, что это поиск по имени/email). + + Примеры: + - '295' → True (только цифры) + - '+375291234567' → True (цифры и телефонные символы) + - '(029) 123-45' → True (цифры и телефонные символы) + - 'x3m' → False (содержит буквы) + - 'team_x3m' → False (содержит буквы) + - 'Иван' → False (содержит буквы) + """ + if not query: + return False + + # Проверяем, что query содержит ТОЛЬКО цифры и телефонные символы + phone_chars = set('0123456789+- ().') + return all(c in phone_chars for c in query) + + def build_customer_search_query(query, strategy, search_value): """ Строит Q-объект для поиска клиентов на основе стратегии. @@ -254,11 +296,17 @@ def api_search_customers(request): # Строим Q-объект для поиска (единая функция, используется везде) q_objects = build_customer_search_query(query, strategy, search_value) - # Для телефона ищем по нормализованному номеру и по цифрам + # Для поиска по номерам телефонов: применяем умную логику + # Ищем по телефону ТОЛЬКО если query состоит из ТОЛЬКО цифр и телефонных символов + # (никаких букв — потому что если есть буквы, это явно поиск по имени/email, не по телефону) + if phone_normalized: q_objects |= Q(phone__icontains=phone_normalized) - if query_digits: + # Проверяем, похож ли query на номер телефона (только цифры/+/-/() и минимум 3 цифры) + should_search_by_phone_digits = is_query_phone_only(query) and len(query_digits) >= 3 + + if should_search_by_phone_digits: # Ищем клиентов, чьи телефоны содержат введенные цифры customers_by_phone = Customer.objects.filter(phone__isnull=False) matching_by_digits = []