598 lines
No EOL
19 KiB
PHP
598 lines
No EOL
19 KiB
PHP
<?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 ?BusinessUserItem $sponsor = null;
|
|
private array $processedUserIds = [];
|
|
|
|
private BusinessUserRepository $repository;
|
|
private TreeHtmlRenderer $renderer;
|
|
private LoggerInterface $logger;
|
|
|
|
public function __construct(
|
|
int $month,
|
|
int $year,
|
|
string $initFrom = 'member',
|
|
?BusinessUserRepository $repository = null,
|
|
?TreeHtmlRenderer $renderer = null,
|
|
?LoggerInterface $logger = null
|
|
) {
|
|
$this->validateInput($month, $year);
|
|
$this->initializeDate($month, $year);
|
|
$this->initFrom = $initFrom;
|
|
|
|
// Dependency Injection mit Fallback
|
|
$this->repository = $repository ?? new BusinessUserRepository($month, $year);
|
|
$this->renderer = $renderer ?? new TreeHtmlRenderer($initFrom);
|
|
$this->logger = $logger ?? app(LoggerInterface::class);
|
|
}
|
|
|
|
/**
|
|
* Initialisiert die Business-Struktur für Admin-Ansicht
|
|
*/
|
|
public function initStructureAdmin(bool $check = true): void
|
|
{
|
|
try {
|
|
$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
|
|
*/
|
|
public function initStructureUser(int $userId): void
|
|
{
|
|
try {
|
|
$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); // ✅ Nutzt bereits geladene Relations
|
|
$this->addUserIdToProcessed($userId);
|
|
$this->businessUsers[] = $businessUserItem;
|
|
|
|
$storedStructure = $this->repository->getStoredStructure();
|
|
if ($storedStructure) {
|
|
$this->loadStoredParentsUsers($storedStructure);
|
|
if (isset($this->businessUsers[0]) && $this->businessUsers[0]->sponsor) {
|
|
$this->loadStoredSponsorUser($this->businessUsers[0]->sponsor->user_id);
|
|
}
|
|
} else {
|
|
$this->loadParentsUsers();
|
|
$this->loadSponsorUser($userId);
|
|
}
|
|
|
|
} catch (\Exception $e) {
|
|
$this->logger->error("Error initializing user structure for {$userId}: " . $e->getMessage());
|
|
throw $e;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialisiert detaillierte Business-User-Informationen
|
|
*/
|
|
public function initBusinesslUserDetail(User $user): void
|
|
{
|
|
try {
|
|
$this->logger->info("Initializing business user details for: {$user->id}");
|
|
|
|
$this->businessUser = new BusinessUserItemOptimized($this->date);
|
|
$this->businessUser->makeUserFromModel($user); // ✅ Nutzt bereits User-Objekt
|
|
$this->businessUser->checkSponsor($user);
|
|
|
|
if (!$this->businessUser->isSave()) {
|
|
// Aufbau der Struktur für den User in die unendliche Tiefe
|
|
$this->businessUser->readParentsBusinessUsers();
|
|
|
|
// Calculate Points in Lines (optimiert für Memory-Effizienz)
|
|
if (count($this->businessUser->businessUserItems) > 0) {
|
|
$this->calculateUserPointsOptimized($this->businessUser->businessUserItems, 1);
|
|
}
|
|
|
|
// 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)
|
|
*/
|
|
public function getGrowthBonus(): array
|
|
{
|
|
if (!$this->businessUser || !$this->businessUser->business_lines) {
|
|
return [];
|
|
}
|
|
|
|
if (count($this->businessUser->business_lines) > 6) {
|
|
$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;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
/**
|
|
* Baut frische Struktur auf
|
|
*/
|
|
private function buildFreshStructure(): void
|
|
{
|
|
$this->loadRootUsers();
|
|
$this->loadParentsUsers();
|
|
$this->loadParentlessUsers();
|
|
}
|
|
|
|
/**
|
|
* 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); // ✅ Nutzt bereits geladene Relations
|
|
$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
|
|
{
|
|
foreach ($this->businessUsers as $businessUser) {
|
|
$businessUser->readParentsBusinessUsers();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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); // ✅ Nutzt bereits geladene Relations
|
|
$this->parentless[] = $businessUserItem;
|
|
$count++;
|
|
}
|
|
|
|
$this->logger->info("Loaded {$count} parentless users with optimized relations");
|
|
}
|
|
|
|
/**
|
|
* Lädt Sponsor für User
|
|
*/
|
|
private function loadSponsorUser(int $userId): void
|
|
{
|
|
try {
|
|
$sponsorUser = $this->repository->getSponsorForUser($userId);
|
|
|
|
if ($sponsorUser) {
|
|
$this->sponsor = new BusinessUserItem($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 BusinessUserItem($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 BusinessUserItem($this->date);
|
|
$businessUserItem->makeUser($obj->user_id);
|
|
$this->parentless[] = $businessUserItem;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gespeicherten Sponsor laden
|
|
*/
|
|
private function loadStoredSponsorUser(int $userId): void
|
|
{
|
|
$this->sponsor = new BusinessUserItem($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): 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($this->businessUser->business_lines[$line])) {
|
|
$obj = new stdClass();
|
|
$obj->points = 0;
|
|
$this->businessUser->addBusinessLineToUser($line, $obj);
|
|
}
|
|
|
|
// Punkte hinzufügen (mit Validierung)
|
|
$points = (float) ($item->sales_volume_points_TP_sum ?? 0);
|
|
if ($points > 0) {
|
|
$this->businessUser->addBusinessLinePoints($line, $points);
|
|
$this->businessUser->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");
|
|
}
|
|
}
|
|
|
|
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");
|
|
}
|
|
}
|
|
} |