This commit is contained in:
2026-05-13 22:28:56 +03:30
parent 46fe62fa04
commit 45fee1dfd3
26 changed files with 2329 additions and 878 deletions
+101 -2
View File
@@ -2,6 +2,8 @@ from __future__ import annotations
from copy import deepcopy
from dataclasses import dataclass
from datetime import date, datetime
from decimal import Decimal
from typing import Any
from django.db.models import Avg
@@ -9,7 +11,7 @@ from django.db.models import Avg
from crop_simulation.growth_simulation import GrowthSimulationContext, _run_projection_engine
from crop_simulation.services import PcseSimulationManager, build_simulation_payload_from_farm
from farm_data.services import get_canonical_farm_record, get_farm_plant_assignments
from .models import AnalysisGridObservation, RemoteSensingClusterBlock
from .models import AnalysisGridObservation, RemoteSensingClusterBlock, RemoteSensingSubdivisionResult
from .satellite_snapshot import build_location_block_satellite_snapshots
@@ -70,6 +72,23 @@ def _clamp(value: float, minimum: float, maximum: float) -> float:
return max(minimum, min(value, maximum))
def _json_safe(value: Any) -> Any:
if isinstance(value, Decimal):
return float(value)
if isinstance(value, datetime):
formatted = value.isoformat()
if formatted.endswith("+00:00"):
return formatted[:-6] + "Z"
return formatted
if isinstance(value, date):
return value.isoformat()
if isinstance(value, dict):
return {str(key): _json_safe(item) for key, item in value.items()}
if isinstance(value, (list, tuple)):
return [_json_safe(item) for item in value]
return value
def _build_cluster_entries(
snapshots: list[dict[str, Any]],
*,
@@ -353,6 +372,21 @@ def build_cluster_crop_recommendations(farm_uuid: str) -> dict[str, Any]:
if not cluster_entries:
raise ClusterRecommendationNotFound("برای این مزرعه هنوز کلاستر قابل استفاده پیدا نشد.")
recommendation_result_ids = sorted(
{
int(cluster_block.result_id)
for cluster_block in cluster_blocks_by_uuid.values()
if cluster_block.result_id
}
)
cached_payload = _load_cached_cluster_recommendations(
farm_uuid=str(farm.farm_uuid),
result_ids=recommendation_result_ids,
plant_assignments=plant_assignments,
)
if cached_payload is not None:
return cached_payload
base_payloads: dict[str, dict[str, Any]] = {}
for assignment in plant_assignments:
plant_name = str(getattr(assignment.plant, "name", "") or "").strip()
@@ -392,7 +426,7 @@ def build_cluster_crop_recommendations(farm_uuid: str) -> dict[str, Any]:
}
)
return {
payload = {
"farm_uuid": str(farm.farm_uuid),
"location_id": location.id,
"evaluated_plant_count": len(base_payloads),
@@ -413,3 +447,68 @@ def build_cluster_crop_recommendations(farm_uuid: str) -> dict[str, Any]:
"snapshot_block_count": len(snapshots),
},
}
_store_cached_cluster_recommendations(
farm_uuid=str(farm.farm_uuid),
result_ids=recommendation_result_ids,
plant_assignments=plant_assignments,
payload=payload,
)
return payload
def _build_assignment_cache_signature(plant_assignments: list[Any]) -> list[dict[str, Any]]:
return [
{
"plant_id": getattr(assignment.plant, "backend_plant_id", None),
"position": int(assignment.position or 0),
"stage": str(assignment.stage or ""),
}
for assignment in plant_assignments
]
def _load_cached_cluster_recommendations(
*,
farm_uuid: str,
result_ids: list[int],
plant_assignments: list[Any],
) -> dict[str, Any] | None:
if not result_ids:
return None
cache_key = f"farm::{farm_uuid}"
assignment_signature = _build_assignment_cache_signature(plant_assignments)
for result in RemoteSensingSubdivisionResult.objects.filter(id__in=result_ids):
metadata = dict(result.metadata or {})
recommendation_cache = dict(metadata.get("cluster_recommendations") or {})
cached_entry = recommendation_cache.get(cache_key)
if not isinstance(cached_entry, dict):
continue
if cached_entry.get("assignment_signature") != assignment_signature:
continue
payload = cached_entry.get("payload")
if isinstance(payload, dict):
return payload
return None
def _store_cached_cluster_recommendations(
*,
farm_uuid: str,
result_ids: list[int],
plant_assignments: list[Any],
payload: dict[str, Any],
) -> None:
if not result_ids:
return
cache_key = f"farm::{farm_uuid}"
assignment_signature = _build_assignment_cache_signature(plant_assignments)
for result in RemoteSensingSubdivisionResult.objects.filter(id__in=result_ids):
metadata = dict(result.metadata or {})
recommendation_cache = dict(metadata.get("cluster_recommendations") or {})
recommendation_cache[cache_key] = {
"assignment_signature": assignment_signature,
"payload": _json_safe(payload),
}
metadata["cluster_recommendations"] = recommendation_cache
result.metadata = metadata
result.save(update_fields=["metadata", "updated_at"])