From 14f24e00f4b0f5506e7d028aca1c3d40d3566b4e Mon Sep 17 00:00:00 2001 From: ht Date: Sun, 30 Aug 2026 17:45:35 +0300 Subject: [PATCH] =?UTF-8?q?fix:=20CPV=20kapisi=20havuzu=20blokluyordu=20?= =?UTF-8?q?=E2=80=94=203000=20ilan=20icin=20AI=20beklenirken=20kullanici?= =?UTF-8?q?=20120=20ilan=20goruyordu;=20havuz=20aninda=20yayinlaniyor,=20A?= =?UTF-8?q?I=20arka=20planda=20tamamliyor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/index.js | 68 ++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 53 insertions(+), 15 deletions(-) diff --git a/server/index.js b/server/index.js index 5322ba0..fc0e8b4 100644 --- a/server/index.js +++ b/server/index.js @@ -21,7 +21,7 @@ import { SECTOR_RULES, INSTITUTION_RULES, classify, sektorBul, SEKTOR_ETIKETLERI import { loginWithGoogle, userFromAuthHeader } from './auth.mjs'; import { emailConfigured, sendEmail, tenderDigestHtml, emailFrom } from './email.mjs'; import { aiConfigured, availableProviders, analyzeSpec } from './ai.mjs'; -import { classifyTenderCpv } from './cpv-classify.mjs'; +import { classifyTenderCpv, classifyCpvByKeyword } from './cpv-classify.mjs'; import { billingConfigured, webhookConfigured, planCatalog, createCheckoutSession, verifyWebhook, planForPrice, cancelSubscription, IPTAL_NEDENLERI } from './billing.mjs'; import * as store from './user-store.mjs'; import { planOf, haklar, kullanim, kotaKontrol, ozellikVar } from './entitlements.mjs'; @@ -292,16 +292,54 @@ async function cpvUygula(t) { return t; } -/** Bir tender listesini, sınırlı paralellikle, CPV işlenmiş hale getirir. */ -async function cpvUygulaListe(tenders) { - for (let i = 0; i < tenders.length; i += CPV_ESZAMANLI) { - const grup = tenders.slice(i, i + CPV_ESZAMANLI); - await Promise.all(grup.map((t) => cpvUygula(t))); +/** + * HIZLI CPV geçişi — yalnızca önbellek + anahtar kelime, AI YOK. + * + * Eskiden havuz, her ilan için AI çağrısı bitene kadar yayınlanmıyordu: + * 3.000 ilan × AI = saatler, bu sürede kullanıcı yalnızca ilk 120 ilanı + * görüyordu. Artık havuz anında yayınlanır; anahtar kelimenin çözemediği + * ilanlar arka planda AI ile tamamlanır. + */ +function cpvHizli(t) { + const ikn = t.ikn || ''; + if (ikn) { + const onbellek = cpvCacheOku(ikn); + if (onbellek) { Object.assign(t, onbellek); return true; } } - return tenders; + const anahtar = classifyCpvByKeyword(t); + if (anahtar) { + Object.assign(t, anahtar); + if (ikn) cpvCacheYaz(ikn, anahtar); + return true; + } + Object.assign(t, { cpv: [], cpvBolum: [], cpvLabel: 'Genel', cpvSource: 'bekliyor' }); + return false; } -async function ilanlariBirlestir(mevcut, yeniAds) { +// Arka planda AI ile CPV tamamlama (havuzu bloklamaz) +const CPV_TUR_LIMIT = Number(process.env.CPV_TUR_LIMIT || 200); +let cpvCalisiyor = false; +async function cpvZenginlestir() { + if (cpvCalisiyor || !cache.tenders?.length) return; + cpvCalisiyor = true; + try { + const kuyruk = cache.tenders.filter((t) => t.cpvSource === 'bekliyor').slice(0, CPV_TUR_LIMIT); + if (!kuyruk.length) return; + const bekleyenToplam = cache.tenders.filter((t) => t.cpvSource === 'bekliyor').length; + console.log(`[cpv-zengin] ${kuyruk.length} ilan AI ile sınıflandırılacak (bekleyen ${bekleyenToplam})`); + for (let i = 0; i < kuyruk.length; i += CPV_ESZAMANLI) { + const grup = kuyruk.slice(i, i + CPV_ESZAMANLI); + await Promise.all(grup.map((t) => cpvUygula(t))); + } + console.log(`[cpv-zengin] ${kuyruk.length} ilan tamamlandı`); + } catch (e) { + console.warn('[cpv-zengin] hata:', e.message); + } finally { + cpvCalisiyor = false; + } +} + +function ilanlariBirlestir(mevcut, yeniAds) { const seen = new Set(mevcut.map((t) => t.id)); const eklenen = []; for (const ad of yeniAds) { @@ -310,9 +348,9 @@ async function ilanlariBirlestir(mevcut, yeniAds) { seen.add(t.id); eklenen.push(t); } - // GATE: CPV sınıflandırması tamamlanmadan hiçbir ilan çağırana dönmez — - // bu dizi doğrudan arkaPlandaTamamla() içinde cache.tenders'a eklenir. - await cpvUygulaListe(eklenen); + // Hızlı sınıflandırma (önbellek + anahtar kelime). AI'a kalanlar arka planda + // tamamlanır; havuzun yayınlanması AI'ı beklemez. + for (const t of eklenen) cpvHizli(t); return eklenen; } @@ -331,7 +369,7 @@ async function arkaPlandaTamamla(numFound, ilkListe) { const sonuclar = await ilanSayfalari(page, grup); const ads = sonuclar.flatMap((r) => r.ads || []); if (!ads.length) break; - const eklenen = await ilanlariBirlestir(yeni, ads); + const eklenen = ilanlariBirlestir(yeni, ads); if (eklenen.length) yeni.push(...eklenen); page += grup; await new Promise((r) => setTimeout(r, 120)); // kaynağı yormamak için @@ -345,6 +383,7 @@ async function arkaPlandaTamamla(numFound, ilkListe) { havuzKaydet(numFound, cache.tenders.length); zenginUygula(); // önceden çözülmüşleri hemen uygula setTimeout(() => sektorZenginlestir().catch(() => {}), 5000); // kalanları arka planda + setTimeout(() => cpvZenginlestir().catch(() => {}), 15000); // CPV'yi AI ile tamamla } catch (e) { console.warn('[ihaleist-api] arka plan doldurma hatası:', e.message); } finally { @@ -462,9 +501,8 @@ async function getTenders(force = false) { const ilkListe = ads .filter((ad) => (seen.has(ad.id) ? false : seen.add(ad.id))) .map(mapAd); - // GATE: CPV sınıflandırması tamamlanmadan bu liste ne hemen servis edilir - // ne de arkaPlandaTamamla()'nın temeli olur (bkz. cpvUygula yorumu). - await cpvUygulaListe(ilkListe); + // Hızlı sınıflandırma; AI'a kalanlar arka planda tamamlanır. + for (const t of ilkListe) cpvHizli(t); cache.totalCount = numFound || ilkListe.length; cache.fetchedAt = Date.now();