commit 08-2025
This commit is contained in:
parent
9ae662f63e
commit
480fdc65ed
404 changed files with 65310 additions and 2600431 deletions
910
app/Services/BusinessPlan/TreeCalcBotOptimized.php
Normal file
910
app/Services/BusinessPlan/TreeCalcBotOptimized.php
Normal file
|
|
@ -0,0 +1,910 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\BusinessPlan;
|
||||
|
||||
use App\User;
|
||||
use stdClass;
|
||||
use Carbon\Carbon;
|
||||
use App\Models\UserBusinessStructure;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
/**
|
||||
* Optimierte Version der TreeCalcBot Klasse
|
||||
*
|
||||
* Verbesserungen:
|
||||
* - Trennung von Datenzugriff (Repository Pattern)
|
||||
* - Trennung von HTML-Rendering (Renderer Pattern)
|
||||
* - Optimierte Datenbankabfragen (N+1 Problem gelöst)
|
||||
* - Memory-effiziente Verarbeitung großer Datenmengen
|
||||
* - Robuste Fehlerbehandlung mit Logging
|
||||
* - Dependency Injection für bessere Testbarkeit
|
||||
*/
|
||||
class TreeCalcBotOptimized
|
||||
{
|
||||
private stdClass $date;
|
||||
private string $initFrom;
|
||||
private array $businessUsers = [];
|
||||
private array $parentless = [];
|
||||
private ?BusinessUserItemOptimized $businessUser = null;
|
||||
private ?BusinessUserItemOptimized $sponsor = null;
|
||||
private array $processedUserIds = [];
|
||||
private bool $forceLiveCalculation = false;
|
||||
|
||||
private BusinessUserRepository $repository;
|
||||
private TreeHtmlRenderer $renderer;
|
||||
private LoggerInterface $logger;
|
||||
|
||||
public function __construct(
|
||||
int $month,
|
||||
int $year,
|
||||
string $initFrom = 'member',
|
||||
bool $forceLiveCalculation = false,
|
||||
?BusinessUserRepository $repository = null,
|
||||
?TreeHtmlRenderer $renderer = null,
|
||||
?LoggerInterface $logger = null
|
||||
) {
|
||||
$this->validateInput($month, $year);
|
||||
$this->initializeDate($month, $year);
|
||||
$this->initFrom = $initFrom;
|
||||
$this->forceLiveCalculation = $forceLiveCalculation;
|
||||
|
||||
// Dependency Injection mit Fallback
|
||||
$this->repository = $repository ?? new BusinessUserRepository($month, $year);
|
||||
$this->renderer = $renderer ?? new TreeHtmlRenderer($initFrom, $forceLiveCalculation);
|
||||
$this->logger = $logger ?? app(LoggerInterface::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialisiert die Business-Struktur für Admin-Ansicht
|
||||
*
|
||||
* @param bool $check Prüft auf gespeicherte Struktur
|
||||
* @param bool $forceLiveCalculation Erzwingt Live-Berechnung und überspringt gespeicherte Daten
|
||||
*/
|
||||
public function initStructureAdmin(bool $check = true, bool $forceLiveCalculation = false): void
|
||||
{
|
||||
|
||||
try {
|
||||
$this->forceLiveCalculation = $forceLiveCalculation;
|
||||
|
||||
if ($forceLiveCalculation) {
|
||||
$this->logger->info("Building fresh business structure for {$this->date->month}/{$this->date->year} with forced live calculation");
|
||||
$this->buildFreshStructure();
|
||||
return;
|
||||
}
|
||||
|
||||
$storedStructure = null;
|
||||
if ($check) {
|
||||
$storedStructure = $this->repository->getStoredStructure();
|
||||
}
|
||||
if ($storedStructure) {
|
||||
$this->logger->info("Loading stored business structure for {$this->date->month}/{$this->date->year}");
|
||||
$this->loadStoredStructure($storedStructure);
|
||||
} else {
|
||||
$this->logger->info("Building fresh business structure for {$this->date->month}/{$this->date->year}");
|
||||
$this->buildFreshStructure();
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->error("Error initializing admin structure: " . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialisiert die Struktur für einen spezifischen User
|
||||
*
|
||||
* @param int $userId Die User-ID
|
||||
* @param bool $forceLiveCalculation Erzwingt Live-Berechnung und überspringt gespeicherte Daten
|
||||
*/
|
||||
public function initStructureUser(int $userId, bool $forceLiveCalculation = false): void
|
||||
{
|
||||
try {
|
||||
$this->forceLiveCalculation = $forceLiveCalculation;
|
||||
|
||||
if ($forceLiveCalculation) {
|
||||
$this->logger->info("Initializing structure for user: {$userId} with forced live calculation");
|
||||
} else {
|
||||
$this->logger->info("Initializing structure for user: {$userId}");
|
||||
}
|
||||
|
||||
$user = $this->repository->getUserWithRelations($userId);
|
||||
|
||||
if (!$user) {
|
||||
$this->logger->warning("User not found: {$userId}");
|
||||
return;
|
||||
}
|
||||
|
||||
$businessUserItem = new BusinessUserItemOptimized($this->date);
|
||||
$businessUserItem->makeUserFromModel($user); // Erst User-Model laden, ohne forceLiveCalculation
|
||||
$this->addUserIdToProcessed($userId);
|
||||
$this->businessUsers[] = $businessUserItem;
|
||||
|
||||
$this->logger->info("Created businessUserItem for user {$userId}, total businessUsers: " . count($this->businessUsers));
|
||||
|
||||
// Prüfe gespeicherte Struktur nur, wenn Live-Berechnung nicht erzwungen wird
|
||||
$storedStructure = null;
|
||||
if (!$forceLiveCalculation) {
|
||||
$storedStructure = $this->repository->getStoredStructure();
|
||||
$this->logger->info("Stored structure " . ($storedStructure ? "found" : "not found"));
|
||||
}
|
||||
|
||||
if ($storedStructure && !$forceLiveCalculation) {
|
||||
$this->loadStoredParentsUsers($storedStructure);
|
||||
if (isset($this->businessUsers[0]) && $this->businessUsers[0]->sponsor) {
|
||||
$this->loadStoredSponsorUser($this->businessUsers[0]->sponsor->user_id);
|
||||
}
|
||||
} else {
|
||||
if ($forceLiveCalculation) {
|
||||
$this->logger->info("Forcing live calculation - skipping stored structure for user {$userId}");
|
||||
}
|
||||
$this->loadParentsUsers();
|
||||
$this->loadSponsorUser($userId);
|
||||
|
||||
$totalSubUsers = 0;
|
||||
foreach ($this->businessUsers as $businessUser) {
|
||||
$totalSubUsers += count($businessUser->businessUserItems);
|
||||
}
|
||||
$this->logger->info("After loadParentsUsers: {$totalSubUsers} total sub-users loaded across " . count($this->businessUsers) . " business users");
|
||||
|
||||
// WICHTIG: calcQualPP() erst NACH loadParentsUsers() aufrufen, da Points benötigt werden
|
||||
if ($forceLiveCalculation) {
|
||||
$this->logger->info("Calculating qualification levels for all business users");
|
||||
foreach ($this->businessUsers as $businessUser) {
|
||||
$businessUser->calcQualPP();
|
||||
}
|
||||
//wird nicht benötigt, da hier nur die Points berechnet werden
|
||||
//$this->calculateQualPPForAllUsers(); // Auch für alle Sub-User
|
||||
}
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->error("Error initializing user structure for {$userId}: " . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialisiert detaillierte Business-User-Informationen
|
||||
*
|
||||
* @param User $user Das User-Model
|
||||
* @param bool $forceLiveCalculation Erzwingt Live-Berechnung und überspringt gespeicherte Daten
|
||||
*/
|
||||
public function initBusinesslUserDetail(User $user, bool $forceLiveCalculation = false): void
|
||||
{
|
||||
try {
|
||||
$this->logger->info("Initializing business user details for: {$user->id}");
|
||||
|
||||
$this->businessUser = new BusinessUserItemOptimized($this->date);
|
||||
$this->businessUser->makeUserFromModel($user, $forceLiveCalculation); // ✅ Nutzt bereits User-Objekt
|
||||
$this->businessUser->checkSponsor($user);
|
||||
|
||||
// Führe vollständige Berechnung durch, wenn:
|
||||
// 1. Daten nicht gespeichert sind ODER
|
||||
// 2. Live-Berechnung erzwungen wird
|
||||
if (!$this->businessUser->isSave() || $forceLiveCalculation) {
|
||||
if ($forceLiveCalculation) {
|
||||
$this->logger->info("Forcing live calculation for user {$user->id}");
|
||||
}
|
||||
|
||||
// Aufbau der Struktur für den User in die unendliche Tiefe
|
||||
$this->businessUser->readParentsBusinessUsers($forceLiveCalculation);
|
||||
|
||||
// Calculate Points in Lines (optimiert für Memory-Effizienz)
|
||||
if (count($this->businessUser->businessUserItems) > 0) {
|
||||
$this->calculateUserPointsOptimized($this->businessUser->businessUserItems, 1, $this->businessUser);
|
||||
}
|
||||
// Qualifikation nach qual_kp (KundenPoints) und qual_pp (PaylinePoints)
|
||||
$this->businessUser->calcQualPP();
|
||||
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->error("Error initializing business user details for {$user->id}: " . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt Growth Bonus zurück (ab Linie 6)
|
||||
* Erweitert um Array/Object-Kompatibilität für business_lines
|
||||
*/
|
||||
public function getGrowthBonus(): array
|
||||
{
|
||||
if (!$this->businessUser || !$this->businessUser->business_lines) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (count($this->businessUser->business_lines) > 6) {
|
||||
// Handle both array and object types (JSON deserialization inconsistency)
|
||||
if (is_array($this->businessUser->business_lines)) {
|
||||
$bLines = $this->businessUser->business_lines;
|
||||
} else {
|
||||
$bLines = $this->businessUser->business_lines->toArray();
|
||||
}
|
||||
return array_slice($bLines, 6);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt Wert für spezifische Linie zurück
|
||||
*/
|
||||
public function getKeybyLine(int $line, string $key)
|
||||
{
|
||||
if (!$this->businessUser || !$this->businessUser->business_lines) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$bLines = $this->businessUser->business_lines;
|
||||
if (!isset($bLines[$line])) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$lineData = $bLines[$line];
|
||||
|
||||
if ($lineData instanceof stdClass) {
|
||||
return $lineData->{$key} ?? 0;
|
||||
}
|
||||
|
||||
if (is_array($lineData)) {
|
||||
return $lineData[$key] ?? 0;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML-Rendering Methoden (Delegation an Renderer)
|
||||
*/
|
||||
public function makeHtmlTree(): string
|
||||
{
|
||||
return $this->renderer->renderTree($this->businessUsers);
|
||||
}
|
||||
|
||||
public function makeParentlessHtml(): string
|
||||
{
|
||||
return $this->renderer->renderParentless($this->parentless);
|
||||
}
|
||||
|
||||
public function makeSponsorHtml(): string
|
||||
{
|
||||
return $this->renderer->renderSponsor($this->sponsor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter-Methoden (Rückwärtskompatibilität)
|
||||
*/
|
||||
public function getItems(): array
|
||||
{
|
||||
return $this->businessUsers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Zählt die Gesamtanzahl aller User in der Struktur (rekursiv)
|
||||
*/
|
||||
public function getTotalUserCount(): int
|
||||
{
|
||||
$totalCount = 0;
|
||||
|
||||
// Zähle alle Root-User
|
||||
$totalCount += count($this->businessUsers);
|
||||
|
||||
// Zähle alle Unter-User rekursiv
|
||||
foreach ($this->businessUsers as $businessUser) {
|
||||
$totalCount += $this->countBusinessUserItems($businessUser);
|
||||
}
|
||||
|
||||
// Zähle parentless User
|
||||
$totalCount += count($this->parentless);
|
||||
|
||||
return $totalCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Zählt BusinessUserItems rekursiv
|
||||
*/
|
||||
private function countBusinessUserItems($businessUserItem): int
|
||||
{
|
||||
$count = 0;
|
||||
|
||||
if (isset($businessUserItem->businessUserItems) && is_array($businessUserItem->businessUserItems)) {
|
||||
$count += count($businessUserItem->businessUserItems);
|
||||
|
||||
// Rekursiv durch alle Unter-Items zählen
|
||||
foreach ($businessUserItem->businessUserItems as $subItem) {
|
||||
$count += $this->countBusinessUserItems($subItem);
|
||||
}
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
public function isParentless(): bool
|
||||
{
|
||||
return !empty($this->parentless);
|
||||
}
|
||||
|
||||
/**
|
||||
* Static Methoden (Rückwärtskompatibilität)
|
||||
*/
|
||||
public static function isFromStored(int $month, int $year): ?UserBusinessStructure
|
||||
{
|
||||
$structure = UserBusinessStructure::where('year', $year)
|
||||
->where('month', $month)
|
||||
->first();
|
||||
|
||||
return ($structure && $structure->completed) ? $structure : null;
|
||||
}
|
||||
|
||||
public static function addUserID(int $id): void
|
||||
{
|
||||
// Deprecated: Wird durch Instanz-Methode ersetzt
|
||||
// Bleibt für Rückwärtskompatibilität erhalten
|
||||
}
|
||||
|
||||
// ===== Private Methoden =====
|
||||
|
||||
/**
|
||||
* Validiert Eingabeparameter
|
||||
*/
|
||||
private function validateInput(int $month, int $year): void
|
||||
{
|
||||
if ($month < 1 || $month > 12) {
|
||||
throw new \InvalidArgumentException("Invalid month: {$month}");
|
||||
}
|
||||
|
||||
$currentYear = (int) date('Y');
|
||||
if ($year < 2020 || $year > $currentYear + 1) {
|
||||
throw new \InvalidArgumentException("Invalid year: {$year}");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialisiert Datums-Objekt
|
||||
*/
|
||||
private function initializeDate(int $month, int $year): void
|
||||
{
|
||||
$this->date = new stdClass();
|
||||
$date = Carbon::parse($year . '-' . $month . '-1');
|
||||
$this->date->month = $month;
|
||||
$this->date->year = $year;
|
||||
$this->date->start_date = $date->format('Y-m-d H:i:s');
|
||||
$this->date->end_date = $date->endOfMonth()->format('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt gespeicherte Struktur
|
||||
*/
|
||||
private function loadStoredStructure(UserBusinessStructure $structure): void
|
||||
{
|
||||
$this->loadStoredRootUsers($structure);
|
||||
$this->loadStoredParentsUsers($structure);
|
||||
$this->loadStoredParentlessUsers($structure);
|
||||
|
||||
// Prüfe ob gespeicherte Daten vollständig sind, ansonsten berechne neu
|
||||
$this->validateAndRecalculateIfNeeded();
|
||||
$this->validateAndRecalculateParentlessIfNeeded();
|
||||
}
|
||||
|
||||
/**
|
||||
* Baut frische Struktur auf
|
||||
*/
|
||||
private function buildFreshStructure(): void
|
||||
{
|
||||
$this->loadRootUsers();
|
||||
$this->loadParentsUsers();
|
||||
$this->loadParentlessUsers();
|
||||
|
||||
// WICHTIG: Berechne Punkte und Qualifikationen für alle Business-Users
|
||||
$this->calculateAllBusinessUsers();
|
||||
$this->calculateAllParentlessUsers();
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt Root-Users (optimiert mit Memory-Monitoring)
|
||||
*/
|
||||
private function loadRootUsers(): void
|
||||
{
|
||||
$startMemory = memory_get_usage();
|
||||
$users = $this->repository->getRootUsers();
|
||||
|
||||
foreach ($users as $user) {
|
||||
// Memory-Check vor jeder User-Verarbeitung
|
||||
$this->checkMemoryUsage('loadRootUsers', $user->id);
|
||||
|
||||
$businessUserItem = new BusinessUserItemOptimized($this->date);
|
||||
$businessUserItem->makeUserFromModel($user, $this->forceLiveCalculation); // ✅ Nutzt bereits geladene Relations mit forceLiveCalculation
|
||||
$this->addUserIdToProcessed($user->id);
|
||||
$this->businessUsers[] = $businessUserItem;
|
||||
}
|
||||
|
||||
$endMemory = memory_get_usage();
|
||||
$memoryUsed = $this->formatBytes($endMemory - $startMemory);
|
||||
|
||||
$this->logger->info("Loaded " . count($users) . " root users with optimized relations. Memory used: {$memoryUsed}");
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt Parent-Users für alle Business-Users
|
||||
*/
|
||||
private function loadParentsUsers(): void
|
||||
{
|
||||
$this->logger->info("Loading parent users for " . count($this->businessUsers) . " business users");
|
||||
|
||||
foreach ($this->businessUsers as $businessUser) {
|
||||
$businessUser->readParentsBusinessUsers($this->forceLiveCalculation);
|
||||
|
||||
$this->logger->debug("Loaded " . count($businessUser->businessUserItems) . " parent users for user " . ($businessUser->b_user->user_id ?? 'unknown'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt parentlose Users (Memory-optimiert)
|
||||
*/
|
||||
private function loadParentlessUsers(): void
|
||||
{
|
||||
$count = 0;
|
||||
$excludeIds = array_keys($this->processedUserIds);
|
||||
|
||||
foreach ($this->repository->getParentlessUsers($excludeIds) as $user) {
|
||||
$businessUserItem = new BusinessUserItemOptimized($this->date);
|
||||
$businessUserItem->makeUserFromModel($user, $this->forceLiveCalculation); // ✅ Nutzt bereits geladene Relations mit forceLiveCalculation
|
||||
$this->parentless[] = $businessUserItem;
|
||||
$count++;
|
||||
}
|
||||
|
||||
$this->logger->info("Loaded {$count} parentless users with optimized relations");
|
||||
}
|
||||
|
||||
/**
|
||||
* Berechnet Punkte und Qualifikationen für alle Business-Users
|
||||
*/
|
||||
private function calculateAllBusinessUsers(): void
|
||||
{
|
||||
$startTime = microtime(true);
|
||||
$this->logger->info("Starting calculation for " . count($this->businessUsers) . " business users");
|
||||
|
||||
foreach ($this->businessUsers as $businessUser) {
|
||||
try {
|
||||
// Berechne Punkte in Linien (wie bei initBusinesslUserDetail)
|
||||
if (count($businessUser->businessUserItems) > 0) {
|
||||
$this->calculateUserPointsOptimized($businessUser->businessUserItems, 1, $businessUser);
|
||||
}
|
||||
|
||||
// Qualifikation nach qual_kp und qual_pp berechnen
|
||||
$businessUser->calcQualPP();
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->error("Error calculating business user {$businessUser->__get('user_id')}: " . $e->getMessage());
|
||||
// Weiter mit dem nächsten User, nicht abbrechen
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$endTime = microtime(true);
|
||||
$executionTime = round(($endTime - $startTime) * 1000, 2);
|
||||
$this->logger->info("Completed calculations for all business users in {$executionTime}ms");
|
||||
}
|
||||
|
||||
/**
|
||||
* Berechnet Punkte und Qualifikationen für alle Parentless-Users
|
||||
*/
|
||||
private function calculateAllParentlessUsers(): void
|
||||
{
|
||||
if (empty($this->parentless)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$startTime = microtime(true);
|
||||
$this->logger->info("Starting calculation for " . count($this->parentless) . " parentless users");
|
||||
|
||||
foreach ($this->parentless as $parentlessUser) {
|
||||
try {
|
||||
// Berechne Punkte in Linien
|
||||
if (count($parentlessUser->businessUserItems) > 0) {
|
||||
$this->calculateUserPointsOptimized($parentlessUser->businessUserItems, 1, $parentlessUser);
|
||||
}
|
||||
|
||||
// Qualifikation berechnen
|
||||
$parentlessUser->calcQualPP();
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->error("Error calculating parentless user {$parentlessUser->__get('user_id')}: " . $e->getMessage());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$endTime = microtime(true);
|
||||
$executionTime = round(($endTime - $startTime) * 1000, 2);
|
||||
$this->logger->info("Completed calculations for all parentless users in {$executionTime}ms");
|
||||
}
|
||||
|
||||
/**
|
||||
* Validiert gespeicherte Daten und berechnet bei Bedarf neu
|
||||
*/
|
||||
private function validateAndRecalculateIfNeeded(): void
|
||||
{
|
||||
$incompleteUsers = 0;
|
||||
|
||||
foreach ($this->businessUsers as $businessUser) {
|
||||
// Prüfe ob grundlegende Berechnungen vorhanden sind
|
||||
if ($this->isBusinessUserIncomplete($businessUser)) {
|
||||
$incompleteUsers++;
|
||||
|
||||
try {
|
||||
// Führe fehlende Berechnungen durch
|
||||
if (count($businessUser->businessUserItems) > 0) {
|
||||
$this->calculateUserPointsOptimized($businessUser->businessUserItems, 1, $businessUser);
|
||||
}
|
||||
$businessUser->calcQualPP();
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->error("Error recalculating business user {$businessUser->__get('user_id')}: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($incompleteUsers > 0) {
|
||||
$this->logger->info("Recalculated {$incompleteUsers} incomplete business users from stored data");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob ein BusinessUser unvollständige Daten hat
|
||||
* Erweitert um Level-Qualifikationsdaten für Struktur-Ansicht
|
||||
*/
|
||||
private function isBusinessUserIncomplete($businessUser): bool
|
||||
{
|
||||
// Prüfe grundlegende Felder die nach Berechnungen vorhanden sein sollten
|
||||
$salesVolumeSum = $businessUser->__get('sales_volume_points_sum');
|
||||
$qualKp = $businessUser->__get('qual_kp');
|
||||
|
||||
// Prüfe Level-Qualifikationsdaten für Struktur-Ansicht
|
||||
$nextQualUserLevel = $businessUser->__get('next_qual_user_level');
|
||||
$nextCanUserLevel = $businessUser->__get('next_can_user_level');
|
||||
$hasLevelQualificationData = !empty($nextQualUserLevel) || !empty($nextCanUserLevel);
|
||||
|
||||
// User ist unvollständig wenn:
|
||||
// 1. Grundlegende berechnete Werte fehlen ODER
|
||||
// 2. Level-Qualifikationsdaten fehlen (wichtig für Struktur-Ansicht mit grünen Pfeilen)
|
||||
$missingBasicData = ($salesVolumeSum === null || $salesVolumeSum === 0) &&
|
||||
($qualKp === null || $qualKp === 0);
|
||||
|
||||
$missingLevelData = !$hasLevelQualificationData;
|
||||
|
||||
return $missingBasicData || $missingLevelData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validiert und berechnet parentless Users bei Bedarf neu
|
||||
*/
|
||||
private function validateAndRecalculateParentlessIfNeeded(): void
|
||||
{
|
||||
if (empty($this->parentless)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$incompleteUsers = 0;
|
||||
|
||||
foreach ($this->parentless as $parentlessUser) {
|
||||
if ($this->isBusinessUserIncomplete($parentlessUser)) {
|
||||
$incompleteUsers++;
|
||||
|
||||
try {
|
||||
if (count($parentlessUser->businessUserItems) > 0) {
|
||||
$this->calculateUserPointsOptimized($parentlessUser->businessUserItems, 1, $parentlessUser);
|
||||
}
|
||||
$parentlessUser->calcQualPP();
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->error("Error recalculating parentless user {$parentlessUser->__get('user_id')}: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($incompleteUsers > 0) {
|
||||
$this->logger->info("Recalculated {$incompleteUsers} incomplete parentless users from stored data");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt Sponsor für User
|
||||
*/
|
||||
private function loadSponsorUser(int $userId): void
|
||||
{
|
||||
try {
|
||||
$sponsorUser = $this->repository->getSponsorForUser($userId);
|
||||
|
||||
if ($sponsorUser) {
|
||||
$this->sponsor = new BusinessUserItemOptimized($this->date);
|
||||
$this->sponsor->makeUser($sponsorUser->id);
|
||||
$this->logger->info("Loaded sponsor {$sponsorUser->id} for user {$userId}");
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->warning("Could not load sponsor for user {$userId}: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gespeicherte Root-Users laden
|
||||
*/
|
||||
private function loadStoredRootUsers(UserBusinessStructure $structure): void
|
||||
{
|
||||
if (!$structure->structure) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($structure->structure as $obj) {
|
||||
$businessUserItem = new BusinessUserItemOptimized($this->date);
|
||||
$businessUserItem->makeUser($obj->user_id);
|
||||
$this->addUserIdToProcessed($obj->user_id);
|
||||
$this->businessUsers[] = $businessUserItem;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gespeicherte Parent-Users laden
|
||||
*/
|
||||
private function loadStoredParentsUsers(UserBusinessStructure $structure): void
|
||||
{
|
||||
foreach ($this->businessUsers as $businessUser) {
|
||||
$businessUser->readStoredParentsBusinessUsers($structure->structure);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gespeicherte parentlose Users laden
|
||||
*/
|
||||
private function loadStoredParentlessUsers(UserBusinessStructure $structure): void
|
||||
{
|
||||
if (!$structure->parentless) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($structure->parentless as $obj) {
|
||||
if (!isset($this->processedUserIds[$obj->user_id])) {
|
||||
$businessUserItem = new BusinessUserItemOptimized($this->date);
|
||||
$businessUserItem->makeUser($obj->user_id);
|
||||
$this->parentless[] = $businessUserItem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gespeicherten Sponsor laden
|
||||
*/
|
||||
private function loadStoredSponsorUser(int $userId): void
|
||||
{
|
||||
$this->sponsor = new BusinessUserItemOptimized($this->date);
|
||||
$this->sponsor->makeUser($userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimierte Punkte-Berechnung (Stack-basiert mit korrekter Depth-First Reihenfolge)
|
||||
*
|
||||
* KRITISCH: Stack muss gleiche Reihenfolge wie Original-Rekursion produzieren
|
||||
* Original: Depth-First Traversierung (erst tief, dann Punkte addieren)
|
||||
* Stack: Muss umgekehrt arbeiten - erst alle Kinder sammeln, dann von tief zu flach verarbeiten
|
||||
*/
|
||||
private function calculateUserPointsOptimized(array $businessUserItems, int $startLine, BusinessUserItemOptimized $businessUserToUpdate): void
|
||||
{
|
||||
$processingStack = [];
|
||||
$collectionStack = []; // Sammelt Items in korrekter Reihenfolge
|
||||
|
||||
// Phase 1: Sammle alle Items in Depth-First Reihenfolge
|
||||
foreach ($businessUserItems as $item) {
|
||||
$collectionStack[] = ['item' => $item, 'line' => $startLine, 'depth' => 0];
|
||||
}
|
||||
|
||||
// Expandiere alle Kinder (Depth-First)
|
||||
$processedItems = [];
|
||||
while (!empty($collectionStack)) {
|
||||
$current = array_shift($collectionStack); // FIFO für Breadth-First Sammlung
|
||||
$item = $current['item'];
|
||||
$line = $current['line'];
|
||||
$depth = $current['depth'];
|
||||
|
||||
// Markiere für Verarbeitung (mit Tiefe für spätere Sortierung)
|
||||
$processingStack[] = [
|
||||
'item' => $item,
|
||||
'line' => $line,
|
||||
'depth' => $depth,
|
||||
'id' => $item->user_id ?? uniqid()
|
||||
];
|
||||
|
||||
// Füge Kinder hinzu (werden später verarbeitet = Depth-First)
|
||||
if (isset($item->businessUserItems) && count($item->businessUserItems) > 0) {
|
||||
// Kinder in umgekehrter Reihenfolge hinzufügen für korrekte Stack-Verarbeitung
|
||||
$children = array_reverse($item->businessUserItems);
|
||||
foreach ($children as $childItem) {
|
||||
array_unshift($collectionStack, [
|
||||
'item' => $childItem,
|
||||
'line' => $line + 1,
|
||||
'depth' => $depth + 1
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 2: Sortiere nach Tiefe (tiefste zuerst, wie bei Rekursion)
|
||||
usort($processingStack, function($a, $b) {
|
||||
return $b['depth'] <=> $a['depth']; // Tiefste zuerst
|
||||
});
|
||||
|
||||
// Phase 3: Verarbeite in korrekter Reihenfolge (von tief zu flach)
|
||||
foreach ($processingStack as $current) {
|
||||
$item = $current['item'];
|
||||
$line = $current['line'];
|
||||
|
||||
try {
|
||||
// Business Line initialisieren falls nötig
|
||||
if (!isset($businessUserToUpdate->business_lines[$line])) {
|
||||
$obj = new stdClass();
|
||||
$obj->points = 0;
|
||||
$businessUserToUpdate->addBusinessLineToUser($line, $obj);
|
||||
}
|
||||
|
||||
// Punkte hinzufügen (mit Validierung)
|
||||
$points = (float) ($item->sales_volume_points_TP_sum ?? 0);
|
||||
if ($points > 0) {
|
||||
$businessUserToUpdate->addBusinessLinePoints($line, $points);
|
||||
$businessUserToUpdate->addTotalTP($points);
|
||||
}
|
||||
|
||||
$this->logger->debug("Processed user {$current['id']} at line {$line} with {$points} points");
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->error("Error processing user points for {$current['id']}: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
$this->logger->info("Processed " . count($processingStack) . " business user items in depth-first order");
|
||||
}
|
||||
|
||||
/**
|
||||
* User-ID zu verarbeiteten IDs hinzufügen
|
||||
*/
|
||||
private function addUserIdToProcessed(int $id): void
|
||||
{
|
||||
$this->processedUserIds[$id] = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob User bereits verarbeitet wurde
|
||||
*/
|
||||
private function isUserProcessed(int $id): bool
|
||||
{
|
||||
return isset($this->processedUserIds[$id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Memory-Monitoring Methoden
|
||||
*/
|
||||
private function checkMemoryUsage(string $operation, $identifier = null): void
|
||||
{
|
||||
$currentMemory = memory_get_usage();
|
||||
$memoryLimit = $this->parseMemoryLimit(ini_get('memory_limit'));
|
||||
$memoryPercent = ($currentMemory / $memoryLimit) * 100;
|
||||
|
||||
if ($memoryPercent > 80) {
|
||||
$currentFormatted = $this->formatBytes($currentMemory);
|
||||
$limitFormatted = $this->formatBytes($memoryLimit);
|
||||
|
||||
$this->logger->warning("High memory usage detected in {$operation}", [
|
||||
'identifier' => $identifier,
|
||||
'current_memory' => $currentFormatted,
|
||||
'memory_limit' => $limitFormatted,
|
||||
'usage_percent' => round($memoryPercent, 2)
|
||||
]);
|
||||
|
||||
// Garbage Collection bei hohem Memory-Verbrauch
|
||||
if ($memoryPercent > 90) {
|
||||
$this->logger->warning("Critical memory usage - forcing garbage collection");
|
||||
gc_collect_cycles();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function parseMemoryLimit(string $limit): int
|
||||
{
|
||||
$limit = trim($limit);
|
||||
$last = strtolower($limit[strlen($limit)-1]);
|
||||
$number = (int) $limit;
|
||||
|
||||
switch($last) {
|
||||
case 'g': $number *= 1024;
|
||||
case 'm': $number *= 1024;
|
||||
case 'k': $number *= 1024;
|
||||
}
|
||||
|
||||
return $number;
|
||||
}
|
||||
|
||||
private function formatBytes(int $bytes, int $precision = 2): string
|
||||
{
|
||||
$units = array('B', 'KB', 'MB', 'GB', 'TB');
|
||||
|
||||
for ($i = 0; $bytes > 1024 && $i < count($units) - 1; $i++) {
|
||||
$bytes /= 1024;
|
||||
}
|
||||
|
||||
return round($bytes, $precision) . ' ' . $units[$i];
|
||||
}
|
||||
|
||||
/**
|
||||
* Public Properties für Rückwärtskompatibilität
|
||||
*/
|
||||
public function __get(string $name)
|
||||
{
|
||||
switch ($name) {
|
||||
case 'date':
|
||||
return $this->date;
|
||||
case 'business_user':
|
||||
return $this->businessUser;
|
||||
case 'business_users':
|
||||
return $this->businessUsers;
|
||||
case 'parentless':
|
||||
return $this->parentless;
|
||||
default:
|
||||
throw new \InvalidArgumentException("Property {$name} does not exist");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Berechnet calcQualPP() für alle BusinessUsers rekursiv
|
||||
* Muss NACH loadParentsUsers() aufgerufen werden, da Points benötigt werden
|
||||
*/
|
||||
private function calculateQualPPForAllUsers(): void
|
||||
{
|
||||
$this->logger->info("Starting recursive calcQualPP for all users");
|
||||
$totalCalculated = 0;
|
||||
|
||||
foreach ($this->businessUsers as $businessUser) {
|
||||
$totalCalculated += $this->calculateQualPPRecursive($businessUser);
|
||||
}
|
||||
|
||||
$this->logger->info("Completed calcQualPP for {$totalCalculated} users");
|
||||
}
|
||||
|
||||
/**
|
||||
* Rekursive Hilfsmethode für calcQualPP
|
||||
*/
|
||||
private function calculateQualPPRecursive($businessUser): int
|
||||
{
|
||||
$calculated = 0;
|
||||
|
||||
if (isset($businessUser->businessUserItems) && is_array($businessUser->businessUserItems)) {
|
||||
foreach ($businessUser->businessUserItems as $subBusinessUser) {
|
||||
if ($subBusinessUser->b_user && $subBusinessUser->b_user->user_id) {
|
||||
try {
|
||||
$subBusinessUser->calcQualPP();
|
||||
$calculated++;
|
||||
$this->logger->debug("Calculated calcQualPP for user " . $subBusinessUser->b_user->user_id);
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->warning("Error calculating calcQualPP for user " . $subBusinessUser->b_user->user_id . ": " . $e->getMessage());
|
||||
}
|
||||
|
||||
// Rekursiver Aufruf
|
||||
$calculated += $this->calculateQualPPRecursive($subBusinessUser);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $calculated;
|
||||
}
|
||||
|
||||
public function __set(string $name, $value)
|
||||
{
|
||||
switch ($name) {
|
||||
case 'business_users':
|
||||
$this->businessUsers = $value;
|
||||
break;
|
||||
case 'parentless':
|
||||
$this->parentless = $value;
|
||||
break;
|
||||
default:
|
||||
throw new \InvalidArgumentException("Property {$name} cannot be set");
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue