mod: raw sql --> ORM

This commit is contained in:
2026-05-13 18:33:53 +03:00
parent 81680b75ab
commit 998e6caf8f
2 changed files with 71 additions and 53 deletions

View File

@@ -528,6 +528,7 @@ fSetRating = k1 + k2 + k3 (итого от 0.0 до 5.0 звёзд)
[OK!] ПЕРЕСЧЁТ РЕЙТИНГОВ ЗАВЕРШЁН УСПЕШНО! [OK!] ПЕРЕСЧЁТ РЕЙТИНГОВ ЗАВЕРШЁН УСПЕШНО!
• Обновлено профилей: 94 • Обновлено профилей: 94
• Обновлено стеклопакетов: 97 • Обновлено стеклопакетов: 97
• Обновлено наборов: 27
``` ```
## Оркестрация и reload веб-сервера ## Оркестрация и reload веб-сервера

View File

@@ -6,7 +6,12 @@ Django management command: make_rating
Вычисление базируется на ренкинге методом Манна-Уитни (Mann-Whitney U test шаг). Вычисление базируется на ренкинге методом Манна-Уитни (Mann-Whitney U test шаг).
В базовом ренкинге участвуют только профили и стеклопакеты, присутствующие В базовом ренкинге участвуют только профили и стеклопакеты, присутствующие
в коммерческих предложениях размещённых в ОКНАРДИИ. в коммерческих предложениях, размещённых в ОКНАРДИИ.
ОБНОВЛЕНИЕ: Все raw SQL запросы заменены на Django ORM (v2.0):
- Профили: использует PVCprofiles.objects.annotate(Count(...))
- Стеклопакеты: используется Glazing.objects.annotate(Count(...))
- Наборы: используется SetKit.objects.select_related().annotate(Count/Max/F(...))
АЛГОРИТМ РАНЖИРОВАНИЯ (Mann-Whitney U Step Rank): АЛГОРИТМ РАНЖИРОВАНИЯ (Mann-Whitney U Step Rank):
==================================================== ====================================================
@@ -101,6 +106,7 @@ Django management command: make_rating
""" """
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.db.models import Count, Max, F
import json import json
import time import time
@@ -199,7 +205,7 @@ def get_rank(set_dictionary, key, key_weight: float = 1, rank_name="", revers=Fa
def prepare_pvc_dictionary(profile_use_list): def prepare_pvc_dictionary(profile_use_list):
""" """
Конвертирует RawQuerySet в список словарей и исправляет данные для ранжирования. Конвертирует QuerySet объектов профилей (с аннотациями) в список словарей и исправляет данные для ранжирования.
Коррекции: Коррекции:
1. iProfileCameras: конвертирует строку "3/3" в число суммированием цифр 1. iProfileCameras: конвертирует строку "3/3" в число суммированием цифр
@@ -207,13 +213,19 @@ def prepare_pvc_dictionary(profile_use_list):
2. iProfileHeight: для значений < 10 см → ставим 1000 (невалидные данные) 2. iProfileHeight: для значений < 10 см → ставим 1000 (невалидные данные)
3. NumOffer: добавляем поле если его нет (количество коммерческих предложений) 3. NumOffer: добавляем поле если его нет (количество коммерческих предложений)
:param profile_use_list: RawQuerySet с объектами профилей :param profile_use_list: список объектов профилей (QuerySet или list)
:return: список словарей с исправленными данными :return: список словарей с исправленными данными
""" """
pvc_dictionary = [] pvc_dictionary = []
for profile in profile_use_list: for profile in profile_use_list:
# Конвертируем объект модели в dict, включая аннотированные поля
profile_dict = profile.__dict__.copy() profile_dict = profile.__dict__.copy()
# Добавляем annotate-поля которые не в __dict__
if hasattr(profile, 'NumOffer'):
profile_dict['NumOffer'] = profile.NumOffer
else:
profile_dict['NumOffer'] = 0
# Коррекция: количество камер из строки в число # Коррекция: количество камер из строки в число
if 'iProfileCameras' in profile_dict: if 'iProfileCameras' in profile_dict:
@@ -237,19 +249,24 @@ def prepare_pvc_dictionary(profile_use_list):
def prepare_glaz_dictionary(glazing_use_list): def prepare_glaz_dictionary(glazing_use_list):
""" """
Конвертирует RawQuerySet в список словарей и исправляет данные для ранжирования. Конвертирует QuerySet объектов стеклопакетов (с аннотациями) в список словарей и исправляет данные для ранжирования.
Коррекции: Коррекции:
1. fGlazingLightTransmission: если < 1% → ставим 10000 (нет данных о светопропускании) 1. fGlazingLightTransmission: если < 1% → ставим 10000 (нет данных о светопропускании)
2. fGlazingPassingSun: если < 1% → ставим 10000 (нет данных о солнцепропускании) 2. fGlazingPassingSun: если < 1% → ставим 10000 (нет данных о солнцепропускании)
:param glazing_use_list: RawQuerySet с объектами стеклопакетов :param glazing_use_list: список объектов стеклопакетов (QuerySet или list)
:return: список словарей с исправленными данными :return: список словарей с исправленными данными
""" """
glaz_dictionary = [] glaz_dictionary = []
for glazing in glazing_use_list: for glazing in glazing_use_list:
glazing_dict = glazing.__dict__.copy() glazing_dict = glazing.__dict__.copy()
# Добавляем annotate-поля которые не в __dict__
if hasattr(glazing, 'NumOffer'):
glazing_dict['NumOffer'] = glazing.NumOffer
else:
glazing_dict['NumOffer'] = 0
# Коррекция: светопропускание < 1% означает нет данных # Коррекция: светопропускание < 1% означает нет данных
if glazing_dict.get("fGlazingLightTransmission", 0) < 1: if glazing_dict.get("fGlazingLightTransmission", 0) < 1:
@@ -371,15 +388,16 @@ def ranking_pvc(pvc_dictionary):
def prepare_setkit_dictionary(setkit_use_list): def prepare_setkit_dictionary(setkit_use_list):
""" """
Конвертирует RawQuerySet в список словарей и исправляет данные для ранжирования. Конвертирует QuerySet объектов наборов (с аннотациями) в список словарей и исправляет данные для ранжирования.
Коррекции: Коррекции:
1. Строковые поля (sSetSill, sSetPanes, sSetSlope, sSetClimateControl) преобразуются в 0/1 1. Строковые поля (sSetSill, sSetPanes, sSetSlope, sSetClimateControl) преобразуются в 0/1
2. sOfficeDiscountMetaFormula парсится для максимальной скидки и количества вариантов 2. sOfficeDiscountMetaFormula парсится для максимальной скидки и количества вариантов
3. dModify (datetime) преобразуется в timestamp (число) для ранжирования 3. dModify (datetime/timestamp) преобразуется в timestamp (число) для ранжирования
4. Логические поля должны быть числами для алгоритма ранжирования 4. Логические поля должны быть числами для алгоритма ранжирования
5. Аннотированные поля (NumOffer, fProfileRating, fGlazingRating) извлекаются из атрибутов модели
:param setkit_use_list: RawQuerySet с объектами наборов :param setkit_use_list: список объектов наборов (QuerySet или list)
:return: список словарей с исправленными данными :return: список словарей с исправленными данными
""" """
import datetime import datetime
@@ -389,6 +407,19 @@ def prepare_setkit_dictionary(setkit_use_list):
for setkit in setkit_use_list: for setkit in setkit_use_list:
setkit_dict = setkit.__dict__.copy() setkit_dict = setkit.__dict__.copy()
# Добавляем annotate-поля которые не в __dict__
if hasattr(setkit, 'NumOffer'):
setkit_dict['NumOffer'] = setkit.NumOffer or 0
else:
setkit_dict['NumOffer'] = 0
if hasattr(setkit, 'fProfileRating'):
setkit_dict['fProfileRating'] = setkit.fProfileRating or 0.0
if hasattr(setkit, 'fGlazingRating'):
setkit_dict['fGlazingRating'] = setkit.fGlazingRating or 0.0
if hasattr(setkit, 'sOfficeDiscountMetaFormula'):
setkit_dict['sOfficeDiscountMetaFormula'] = setkit.sOfficeDiscountMetaFormula or ""
# Коррекция: преобразуем дату в timestamp для ранжирования # Коррекция: преобразуем дату в timestamp для ранжирования
# Свежие данные (большие timestamp) должны иметь более высокий ранг # Свежие данные (большие timestamp) должны иметь более высокий ранг
if 'dModify' in setkit_dict and setkit_dict['dModify']: if 'dModify' in setkit_dict and setkit_dict['dModify']:
@@ -811,17 +842,14 @@ class Command(BaseCommand):
profile_all_num = PVCprofiles.objects.all().update(fProfileRating=0.0) profile_all_num = PVCprofiles.objects.all().update(fProfileRating=0.0)
self.stdout.write(f' ✓ Обнулены рейтинги у {profile_all_num} профилей') self.stdout.write(f' ✓ Обнулены рейтинги у {profile_all_num} профилей')
# Извлекаем профили которые используются в коммерческих предложениях # Извлекаем ВСЕ профили с количеством коммерческих предложений
q = PVCprofiles.objects.raw( # (включая те что без предложений - для них NumOffer = 0)
"SELECT oknardia_pvcprofiles.*, COUNT(oknardia_priceoffer.id) AS NumOffer " # ORM эквивалент: RIGHT OUTER JOIN в SQL
"FROM oknardia_setkit " profile_use_list = list(
"INNER JOIN oknardia_priceoffer " PVCprofiles.objects.annotate(
" ON oknardia_setkit.id = oknardia_priceoffer.kOffer2SetKit_id " NumOffer=Count('setkit__priceoffer', distinct=True)
"RIGHT OUTER JOIN oknardia_pvcprofiles " )
" ON oknardia_setkit.kSet2PVCprofiles_id = oknardia_pvcprofiles.id "
"GROUP BY oknardia_pvcprofiles.id"
) )
profile_use_list = list(q)
self.stdout.write(f' ✓ Найдено {len(profile_use_list)} профилей для ранжирования') self.stdout.write(f' ✓ Найдено {len(profile_use_list)} профилей для ранжирования')
# Подготавливаем словарь профилей с коррекциями # Подготавливаем словарь профилей с коррекциями
@@ -900,17 +928,13 @@ class Command(BaseCommand):
glazing_all_num = Glazing.objects.all().update(fGlazingRating=0.0) glazing_all_num = Glazing.objects.all().update(fGlazingRating=0.0)
self.stdout.write(f' ✓ Обнулены рейтинги у {glazing_all_num} стеклопакетов') self.stdout.write(f' ✓ Обнулены рейтинги у {glazing_all_num} стеклопакетов')
# Извлекаем стеклопакеты которые используются в коммерческих предложениях # Извлекаем ВСЕ стеклопакеты с количеством коммерческих предложений
q = Glazing.objects.raw( # (включая те что без предложений - для них NumOffer = 0)
"SELECT oknardia_glazing.*, COUNT(oknardia_priceoffer.id) AS NumOffer " glazing_use_list = list(
"FROM oknardia_setkit " Glazing.objects.annotate(
"INNER JOIN oknardia_priceoffer " NumOffer=Count('setkit__priceoffer', distinct=True)
" ON oknardia_setkit.id = oknardia_priceoffer.kOffer2SetKit_id " )
"RIGHT OUTER JOIN oknardia_glazing "
" ON oknardia_setkit.kSet2Glazing_id = oknardia_glazing.id "
"GROUP BY oknardia_glazing.id"
) )
glazing_use_list = list(q)
self.stdout.write(f' ✓ Найдено {len(glazing_use_list)} стеклопакетов для ранжирования') self.stdout.write(f' ✓ Найдено {len(glazing_use_list)} стеклопакетов для ранжирования')
# Подготавливаем словарь стеклопакетов с коррекциями # Подготавливаем словарь стеклопакетов с коррекциями
@@ -990,32 +1014,25 @@ class Command(BaseCommand):
self.stdout.write(f' ✓ Обнулены рейтинги у {setkit_all_num} наборов') self.stdout.write(f' ✓ Обнулены рейтинги у {setkit_all_num} наборов')
# Извлекаем наборы которые используются в коммерческих предложениях # Извлекаем наборы которые используются в коммерческих предложениях
q = SetKit.objects.raw( # ORM эквивалент: SELECT setkit с JOIN к PriceOffer, Glazing, PVCprofiles, OurUser, MerchantOffice
"SELECT oknardia_setkit.*, COUNT(oknardia_priceoffer.id) AS NumOffer, " # и агрегацией COUNT(PriceOffer), MAX(dOfferModify)
"MAX(oknardia_priceoffer.dOfferModify) AS dModify, " setkit_use_list = list(
"oknardia_pvcprofiles.fProfileRating, " SetKit.objects.filter(
"oknardia_glazing.fGlazingRating, " sSetActive=True
"oknardia_merchantoffice.sOfficeDiscountMetaFormula " ).select_related(
"FROM oknardia_setkit " 'kSet2PVCprofiles', # PVCprofiles для получения fProfileRating
"INNER JOIN oknardia_priceoffer " 'kSet2Glazing', # Glazing для получения fGlazingRating
" ON oknardia_priceoffer.kOffer2SetKit_id = oknardia_setkit.id " 'kSet2User__kMerchantOffice' # OurUser -> MerchantOffice для sOfficeDiscountMetaFormula
"INNER JOIN oknardia_glazing " ).annotate(
" ON oknardia_setkit.kSet2Glazing_id = oknardia_glazing.id " NumOffer=Count('priceoffer', distinct=True), # COUNT(PriceOffer)
"INNER JOIN oknardia_pvcprofiles " dModify=Max('priceoffer__dOfferModify'), # MAX(dOfferModify)
" ON oknardia_pvcprofiles.id = oknardia_setkit.kSet2PVCprofiles_id " fProfileRating=F('kSet2PVCprofiles__fProfileRating'),
"INNER JOIN oknardia_ouruser " fGlazingRating=F('kSet2Glazing__fGlazingRating'),
" ON oknardia_setkit.kSet2User_id = oknardia_ouruser.id " sOfficeDiscountMetaFormula=F('kSet2User__kMerchantOffice__sOfficeDiscountMetaFormula')
"INNER JOIN oknardia_merchantoffice " ).filter(
" ON oknardia_ouruser.kMerchantOffice_id = oknardia_merchantoffice.id " NumOffer__gt=0
"WHERE oknardia_setkit.sSetActive = TRUE " ).order_by('-dModify')
"GROUP BY oknardia_pvcprofiles.fProfileRating, "
" oknardia_glazing.fGlazingRating, "
" oknardia_merchantoffice.sOfficeDiscountMetaFormula, "
" oknardia_setkit.sSetActive, oknardia_setkit.id "
"ORDER BY MAX(oknardia_priceoffer.dOfferModify)"
) )
setkit_use_list = list(q)
self.stdout.write(f' ✓ Найдено {len(setkit_use_list)} наборов для ранжирования') self.stdout.write(f' ✓ Найдено {len(setkit_use_list)} наборов для ранжирования')
# Подготавливаем словарь наборов с коррекциями # Подготавливаем словарь наборов с коррекциями