local QBCore = exports['qb-core']:GetCoreObject() -- Lokale Variablen local licenseCache = {} local cacheTimeout = 300000 -- 5 Minuten -- Hilfsfunktionen local function debugPrint(message) if Config.Debug then print("^3[License-System Server] " .. message .. "^7") end end -- Spieler-Name abrufen local function getPlayerName(src) local Player = QBCore.Functions.GetPlayer(src) if not Player then return "Unbekannt" end local charinfo = Player.PlayerData.charinfo if charinfo and charinfo.firstname and charinfo.lastname then return charinfo.firstname .. " " .. charinfo.lastname end return "Unbekannt" end -- Cache-Funktionen local function getCachedLicense(citizenid, licenseType) local cacheKey = citizenid .. "_" .. licenseType local cached = licenseCache[cacheKey] if cached and (os.time() * 1000 - cached.timestamp) < cacheTimeout then debugPrint("Cache-Hit für: " .. cacheKey) return cached.data end return nil end local function setCachedLicense(citizenid, licenseType, data) local cacheKey = citizenid .. "_" .. licenseType licenseCache[cacheKey] = { data = data, timestamp = os.time() * 1000 } debugPrint("Lizenz gecacht: " .. cacheKey) end -- Berechtigung prüfen local function hasPermission(src) local Player = QBCore.Functions.GetPlayer(src) if not Player then return false end local job = Player.PlayerData.job if not job then return false end local hasAuth = Config.AuthorizedJobs[job.name] or false debugPrint("Berechtigung für " .. job.name .. ": " .. tostring(hasAuth)) return hasAuth end -- Spieler-Name aus JSON extrahieren (Collation-sicher) local function extractPlayerName(charinfo_json) if not charinfo_json then return "Unbekannt" end local success, charinfo = pcall(json.decode, charinfo_json) if success and charinfo and charinfo.firstname and charinfo.lastname then return charinfo.firstname .. " " .. charinfo.lastname end return "Unbekannt" end -- Sichere Datenbankoperationen mit Retry-Mechanismus local function safeDBOperation(operation, errorMessage, maxRetries) maxRetries = maxRetries or 3 local retries = 0 while retries < maxRetries do retries = retries + 1 local success, result = pcall(operation) if success then return result else debugPrint("^3DB-Retry " .. retries .. "/" .. maxRetries .. ": " .. tostring(result) .. "^7") if retries >= maxRetries then debugPrint("^1DB-Fehler: " .. (errorMessage or "Unbekannt") .. "^7") debugPrint("^1Details: " .. tostring(result) .. "^7") return nil end Wait(1000) -- 1 Sekunde warten vor Retry end end return nil end -- Lizenz aus Datenbank abrufen (KORRIGIERT - Collation-sicher) local function getLicenseFromDB(citizenid, licenseType) debugPrint("=== getLicenseFromDB START ===") debugPrint("CitizenID: " .. tostring(citizenid)) debugPrint("LicenseType: " .. tostring(licenseType)) -- Cache prüfen local cached = getCachedLicense(citizenid, licenseType) if cached then return cached end -- Einfache Query ohne JOINs um Collation-Probleme zu vermeiden local query = [[ SELECT * FROM player_licenses WHERE citizenid = ? AND license_type = ? AND is_active = 1 ORDER BY created_at DESC LIMIT 1 ]] local result = safeDBOperation(function() return MySQL.query.await(query, {citizenid, licenseType}) end, "Fehler beim Abrufen der Lizenz") if result and #result > 0 then local license = result[1] -- Spieler-Namen separat abrufen local holderQuery = "SELECT charinfo FROM players WHERE citizenid = ?" local holderResult = safeDBOperation(function() return MySQL.query.await(holderQuery, {citizenid}) end, "Fehler beim Abrufen des Spieler-Namens") if holderResult and #holderResult > 0 then license.holder_name = extractPlayerName(holderResult[1].charinfo) else license.holder_name = "Unbekannt" end -- Aussteller-Namen separat abrufen if license.issued_by then local issuerQuery = "SELECT charinfo FROM players WHERE citizenid = ?" local issuerResult = safeDBOperation(function() return MySQL.query.await(issuerQuery, {license.issued_by}) end, "Fehler beim Abrufen des Aussteller-Namens") if issuerResult and #issuerResult > 0 then license.issued_by_name = extractPlayerName(issuerResult[1].charinfo) else license.issued_by_name = "System" end else license.issued_by_name = "System" end -- Classes parsen if license.classes then local success, classes = pcall(json.decode, license.classes) if success and type(classes) == "table" then license.classes = classes else license.classes = {} end else license.classes = {} end debugPrint("Lizenz aus DB geladen: " .. license.license_type) -- In Cache speichern setCachedLicense(citizenid, licenseType, license) return license end debugPrint("Keine Lizenz in DB gefunden") return nil end -- Alle Lizenzen eines Spielers abrufen (KORRIGIERT) local function getAllPlayerLicenses(citizenid) debugPrint("=== getAllPlayerLicenses START ===") debugPrint("CitizenID: " .. tostring(citizenid)) -- Einfache Query ohne JOINs local query = [[ SELECT * FROM player_licenses WHERE citizenid = ? AND is_active = 1 ORDER BY license_type, created_at DESC ]] local result = safeDBOperation(function() return MySQL.query.await(query, {citizenid}) end, "Fehler beim Abrufen aller Lizenzen") if result and #result > 0 then local licenses = {} local seenTypes = {} -- Spieler-Namen einmal abrufen local holderQuery = "SELECT charinfo FROM players WHERE citizenid = ?" local holderResult = safeDBOperation(function() return MySQL.query.await(holderQuery, {citizenid}) end, "Fehler beim Abrufen des Spieler-Namens") local holderName = "Unbekannt" if holderResult and #holderResult > 0 then holderName = extractPlayerName(holderResult[1].charinfo) end for _, license in ipairs(result) do -- Nur die neueste Lizenz pro Typ nehmen if not seenTypes[license.license_type] then seenTypes[license.license_type] = true license.holder_name = holderName -- Aussteller-Namen abrufen if license.issued_by then local issuerQuery = "SELECT charinfo FROM players WHERE citizenid = ?" local issuerResult = safeDBOperation(function() return MySQL.query.await(issuerQuery, {license.issued_by}) end, "Fehler beim Abrufen des Aussteller-Namens") if issuerResult and #issuerResult > 0 then license.issued_by_name = extractPlayerName(issuerResult[1].charinfo) else license.issued_by_name = "System" end else license.issued_by_name = "System" end -- Classes parsen if license.classes then local success, classes = pcall(json.decode, license.classes) if success and type(classes) == "table" then license.classes = classes else license.classes = {} end else license.classes = {} end table.insert(licenses, license) end end debugPrint("Gefundene Lizenzen: " .. #licenses) return licenses end debugPrint("Keine Lizenzen gefunden") return {} end -- Lizenz in Datenbank speichern (KORRIGIERT - DateTime-Fix) local function saveLicenseToDB(citizenid, licenseType, issuedBy, classes) debugPrint("=== saveLicenseToDB START ===") debugPrint("CitizenID: " .. tostring(citizenid)) debugPrint("LicenseType: " .. tostring(licenseType)) debugPrint("IssuedBy: " .. tostring(issuedBy)) local config = Config.LicenseTypes[licenseType] if not config then debugPrint("^1Fehler: Unbekannter Lizenztyp: " .. licenseType .. "^7") return false end -- Spieler-Name für das name-Feld abrufen local holderQuery = "SELECT charinfo FROM players WHERE citizenid = ?" local holderResult = safeDBOperation(function() return MySQL.query.await(holderQuery, {citizenid}) end, "Fehler beim Abrufen des Spieler-Namens") local holderName = "Unbekannt" if holderResult and #holderResult > 0 and holderResult[1].charinfo then holderName = extractPlayerName(holderResult[1].charinfo) end -- Datum berechnen local issueDate = os.date("%d.%m.%Y") local expireDate = nil if config.validity_days then local expireTimestamp = os.time() + (config.validity_days * 24 * 60 * 60) expireDate = os.date("%d.%m.%Y", expireTimestamp) end -- Classes zu JSON konvertieren local classesJson = json.encode(classes or {}) -- Alte Lizenz deaktivieren local deactivateQuery = "UPDATE player_licenses SET is_active = 0 WHERE citizenid = ? AND license_type = ?" safeDBOperation(function() return MySQL.query.await(deactivateQuery, {citizenid, licenseType}) end, "Fehler beim Deaktivieren alter Lizenz") -- Neue Lizenz einfügen (KORRIGIERT - DateTime als BIGINT oder korrektes Format) local insertQuery = [[ INSERT INTO player_licenses (citizenid, license_type, name, issue_date, expire_date, issued_by, is_active, classes, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ]] -- created_at als BIGINT (Unix Timestamp) oder als DATETIME local createdAt = os.time() -- Unix Timestamp für BIGINT -- Alternativ für DATETIME: local createdAt = os.date("%Y-%m-%d %H:%M:%S") local insertData = { citizenid, licenseType, holderName, issueDate, expireDate, issuedBy, 1, classesJson, createdAt } debugPrint("Führe INSERT-Query aus...") debugPrint("Daten: " .. json.encode(insertData)) local result = safeDBOperation(function() return MySQL.insert.await(insertQuery, insertData) end, "Fehler beim Speichern der Lizenz") if result then debugPrint("Lizenz erfolgreich gespeichert. ID: " .. result) -- Cache invalidieren local cacheKey = citizenid .. "_" .. licenseType licenseCache[cacheKey] = nil return true else debugPrint("^1Fehler beim Speichern der Lizenz^7") return false end end -- Lizenz entziehen local function revokeLicenseInDB(citizenid, licenseType) debugPrint("=== revokeLicenseInDB START ===") debugPrint("CitizenID: " .. tostring(citizenid)) debugPrint("LicenseType: " .. tostring(licenseType)) local query = "UPDATE player_licenses SET is_active = 0 WHERE citizenid = ? AND license_type = ? AND is_active = 1" local result = safeDBOperation(function() return MySQL.query.await(query, {citizenid, licenseType}) end, "Fehler beim Entziehen der Lizenz") if result then debugPrint("Lizenz erfolgreich entzogen") -- Cache invalidieren local cacheKey = citizenid .. "_" .. licenseType licenseCache[cacheKey] = nil return true else debugPrint("^1Fehler beim Entziehen der Lizenz^7") return false end end -- Cache bereinigen local function cleanupCache() local now = os.time() * 1000 local cleaned = 0 for key, cached in pairs(licenseCache) do if (now - cached.timestamp) > cacheTimeout then licenseCache[key] = nil cleaned = cleaned + 1 end end if cleaned > 0 then debugPrint("Cache bereinigt: " .. cleaned .. " Einträge") end end -- Cache-Cleanup Thread CreateThread(function() while true do Wait(300000) -- 5 Minuten cleanupCache() end end) -- EVENT HANDLER: Lizenz anfordern RegisterNetEvent('license-system:server:requestLicense', function(targetId) local src = source debugPrint("=== Event: requestLicense ===") debugPrint("Source: " .. src .. ", Target: " .. targetId) if not hasPermission(src) then debugPrint("Keine Berechtigung für Spieler: " .. src) TriggerClientEvent('license-system:client:receiveLicense', src, nil) return end local targetPlayer = QBCore.Functions.GetPlayer(targetId) if not targetPlayer then debugPrint("Ziel-Spieler nicht gefunden: " .. targetId) TriggerClientEvent('license-system:client:receiveLicense', src, nil) return end local citizenid = targetPlayer.PlayerData.citizenid -- Erste verfügbare Lizenz finden local foundLicense = nil for licenseType, _ in pairs(Config.LicenseTypes) do local license = getLicenseFromDB(citizenid, licenseType) if license and license.is_active == 1 then foundLicense = { license = license, config = Config.LicenseTypes[licenseType] } break end end if foundLicense then debugPrint("Sende Lizenz an Client: " .. foundLicense.license.license_type) TriggerClientEvent('license-system:client:receiveLicense', src, foundLicense) else debugPrint("Keine aktive Lizenz gefunden") TriggerClientEvent('license-system:client:receiveLicense', src, nil) end end) -- EVENT HANDLER: Eigene Lizenz anfordern RegisterNetEvent('license-system:server:requestMyLicense', function(licenseType) local src = source debugPrint("=== Event: requestMyLicense ===") debugPrint("Source: " .. src .. ", LicenseType: " .. licenseType) local Player = QBCore.Functions.GetPlayer(src) if not Player then debugPrint("Spieler nicht gefunden: " .. src) TriggerClientEvent('license-system:client:receiveMyLicense', src, nil, licenseType) return end local citizenid = Player.PlayerData.citizenid local license = getLicenseFromDB(citizenid, licenseType) if license and license.is_active == 1 then local licenseData = { license = license, config = Config.LicenseTypes[licenseType] } debugPrint("Sende eigene Lizenz an Client: " .. licenseType) TriggerClientEvent('license-system:client:receiveMyLicense', src, licenseData, licenseType) else debugPrint("Keine aktive eigene Lizenz gefunden: " .. licenseType) TriggerClientEvent('license-system:client:receiveMyLicense', src, nil, licenseType) end end) -- EVENT HANDLER: Alle Spieler-Lizenzen anfordern RegisterNetEvent('license-system:server:requestPlayerLicenses', function(targetId) local src = source debugPrint("=== Event: requestPlayerLicenses ===") debugPrint("Source: " .. src .. ", Target: " .. targetId) if not hasPermission(src) then debugPrint("Keine Berechtigung für Spieler: " .. src) TriggerClientEvent('license-system:client:receivePlayerLicenses', src, {}, targetId, "Unbekannt") return end local targetPlayer = QBCore.Functions.GetPlayer(targetId) if not targetPlayer then debugPrint("Ziel-Spieler nicht gefunden: " .. targetId) TriggerClientEvent('license-system:client:receivePlayerLicenses', src, {}, targetId, "Unbekannt") return end local citizenid = targetPlayer.PlayerData.citizenid local targetName = getPlayerName(targetId) local licenses = getAllPlayerLicenses(citizenid) debugPrint("Sende " .. #licenses .. " Lizenzen für " .. targetName) TriggerClientEvent('license-system:client:receivePlayerLicenses', src, licenses, targetId, targetName) end) -- EVENT HANDLER: Lizenz ausstellen RegisterNetEvent('license-system:server:issueLicense', function(targetId, licenseType, classes) local src = source debugPrint("=== Event: issueLicense ===") debugPrint("Source: " .. src .. ", Target: " .. targetId .. ", Type: " .. licenseType) if not hasPermission(src) then debugPrint("Keine Berechtigung für Spieler: " .. src) TriggerClientEvent('QBCore:Notify', src, Config.Notifications.no_permission.message, Config.Notifications.no_permission.type) return end local targetPlayer = QBCore.Functions.GetPlayer(targetId) if not targetPlayer then debugPrint("Ziel-Spieler nicht gefunden: " .. targetId) TriggerClientEvent('QBCore:Notify', src, 'Spieler nicht gefunden!', 'error') return end local issuerPlayer = QBCore.Functions.GetPlayer(src) if not issuerPlayer then debugPrint("Aussteller nicht gefunden: " .. src) return end local targetCitizenId = targetPlayer.PlayerData.citizenid local issuerCitizenId = issuerPlayer.PlayerData.citizenid -- Prüfen ob Lizenz bereits existiert local existingLicense = getLicenseFromDB(targetCitizenId, licenseType) if existingLicense and existingLicense.is_active == 1 then debugPrint("Lizenz bereits vorhanden und aktiv") TriggerClientEvent('QBCore:Notify', src, 'Spieler hat bereits eine aktive ' .. (Config.LicenseTypes[licenseType].label or licenseType) .. '!', 'error') return end -- Kosten prüfen local config = Config.LicenseTypes[licenseType] if config.price and config.price > 0 then if issuerPlayer.PlayerData.money.cash < config.price then debugPrint("Nicht genug Geld für Lizenz-Ausstellung") TriggerClientEvent('QBCore:Notify', src, 'Nicht genug Bargeld! Benötigt: $' .. config.price, 'error') return end -- Geld abziehen issuerPlayer.Functions.RemoveMoney('cash', config.price, 'license-issued') TriggerClientEvent('QBCore:Notify', src, 'Lizenz-Gebühr bezahlt: $' .. config.price, 'success') end -- Lizenz in Datenbank speichern local success = saveLicenseToDB(targetCitizenId, licenseType, issuerCitizenId, classes) if success then local targetName = getPlayerName(targetId) local issuerName = getPlayerName(src) debugPrint("Lizenz erfolgreich ausgestellt") -- Benachrichtigungen TriggerClientEvent('QBCore:Notify', src, 'Lizenz erfolgreich ausgestellt für ' .. targetName, 'success') TriggerClientEvent('QBCore:Notify', targetId, 'Du hast eine neue Lizenz erhalten: ' .. config.label, 'success') -- Events senden TriggerClientEvent('license-system:client:licenseIssued', src, targetId, licenseType) TriggerClientEvent('license-system:client:refreshMenu', src) -- Log debugPrint("Lizenz " .. licenseType .. " ausgestellt von " .. issuerName .. " für " .. targetName) else debugPrint("^1Fehler beim Ausstellen der Lizenz^7") TriggerClientEvent('QBCore:Notify', src, 'Fehler beim Ausstellen der Lizenz!', 'error') end end) -- EVENT HANDLER: Lizenz entziehen RegisterNetEvent('license-system:server:revokeLicense', function(targetId, licenseType) local src = source debugPrint("=== Event: revokeLicense ===") debugPrint("Source: " .. src .. ", Target: " .. targetId .. ", Type: " .. licenseType) if not hasPermission(src) then debugPrint("Keine Berechtigung für Spieler: " .. src) TriggerClientEvent('QBCore:Notify', src, Config.Notifications.no_permission.message, Config.Notifications.no_permission.type) return end local targetPlayer = QBCore.Functions.GetPlayer(targetId) if not targetPlayer then debugPrint("Ziel-Spieler nicht gefunden: " .. targetId) TriggerClientEvent('QBCore:Notify', src, 'Spieler nicht gefunden!', 'error') return end local targetCitizenId = targetPlayer.PlayerData.citizenid -- Lizenz entziehen local success = revokeLicenseInDB(targetCitizenId, licenseType) if success then local targetName = getPlayerName(targetId) local issuerName = getPlayerName(src) local config = Config.LicenseTypes[licenseType] debugPrint("Lizenz erfolgreich entzogen") -- Benachrichtigungen TriggerClientEvent('QBCore:Notify', src, 'Lizenz erfolgreich entzogen von ' .. targetName, 'success') TriggerClientEvent('QBCore:Notify', targetId, 'Deine Lizenz wurde entzogen: ' .. (config.label or licenseType), 'error') -- Events senden TriggerClientEvent('license-system:client:licenseRevoked', src, targetId, licenseType) TriggerClientEvent('license-system:client:refreshMenu', src) -- Log debugPrint("Lizenz " .. licenseType .. " entzogen von " .. issuerName .. " für " .. targetName) else debugPrint("^1Fehler beim Entziehen der Lizenz^7") TriggerClientEvent('QBCore:Notify', src, 'Fehler beim Entziehen der Lizenz!', 'error') end end) -- EXPORT FUNKTIONEN exports('hasLicense', function(citizenid, licenseType) if not citizenid or not licenseType then return false end local license = getLicenseFromDB(citizenid, licenseType) return license and license.is_active == 1 end) exports('issueLicense', function(citizenid, licenseType, issuedBy, classes) if not citizenid or not licenseType then return false end issuedBy = issuedBy or 'system' return saveLicenseToDB(citizenid, licenseType, issuedBy, classes) end) exports('revokeLicense', function(citizenid, licenseType) if not citizenid or not licenseType then return false end return revokeLicenseInDB(citizenid, licenseType) end) exports('getPlayerLicenses', function(citizenid) if not citizenid then return {} end return getAllPlayerLicenses(citizenid) end) exports('getPlayerLicense', function(citizenid, licenseType) if not citizenid or not licenseType then return nil end return getLicenseFromDB(citizenid, licenseType) end) -- INITIALISIERUNG CreateThread(function() debugPrint("License-System Server gestartet (Collation & DateTime Fix)") -- Warten bis QBCore geladen ist while not QBCore do Wait(100) end debugPrint("QBCore erfolgreich geladen") -- Datenbank-Verbindung testen local testResult = safeDBOperation(function() return MySQL.query.await("SELECT 1 as test") end, "Datenbank-Verbindungstest") if testResult then debugPrint("Datenbank-Verbindung erfolgreich") else debugPrint("^1Datenbank-Verbindung fehlgeschlagen^7") end debugPrint("License-System Server vollständig initialisiert") end) -- CLEANUP AddEventHandler('onResourceStop', function(resourceName) if GetCurrentResourceName() == resourceName then debugPrint("License-System Server gestoppt") licenseCache = {} end end) -- DEBUG COMMANDS RegisterCommand('licensestats', function(source, args, rawCommand) if source == 0 then -- Console only local cacheCount = 0 for _ in pairs(licenseCache) do cacheCount = cacheCount + 1 end print("=== LICENSE SYSTEM STATS ===") print("Cache Entries: " .. cacheCount) print("Config License Types: " .. (Config.LicenseTypes and #Config.LicenseTypes or 0)) print("============================") end end, true) RegisterCommand('licenseclearcache', function(source, args, rawCommand) if source == 0 then -- Console only local oldCount = 0 for _ in pairs(licenseCache) do oldCount = oldCount + 1 end licenseCache = {} print("License-Cache geleert. Entfernte Einträge: " .. oldCount) end end, true) debugPrint("License-System Server vollständig geladen (Collation & DateTime Fix)")