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!] ПЕРЕСЧЁТ РЕЙТИНГОВ ЗАВЕРШЁН УСПЕШНО!
• Обновлено профилей: 94
• Обновлено стеклопакетов: 97
• Обновлено наборов: 27
```
## Оркестрация и reload веб-сервера

View File

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