fix: CPV kapisi havuzu blokluyordu — 3000 ilan icin AI beklenirken kullanici 120 ilan goruyordu; havuz aninda yayinlaniyor, AI arka planda tamamliyor

This commit is contained in:
2026-08-30 17:45:35 +03:00
parent 7701ce43e3
commit 14f24e00f4
+53 -15
View File
@@ -21,7 +21,7 @@ import { SECTOR_RULES, INSTITUTION_RULES, classify, sektorBul, SEKTOR_ETIKETLERI
import { loginWithGoogle, userFromAuthHeader } from './auth.mjs'; import { loginWithGoogle, userFromAuthHeader } from './auth.mjs';
import { emailConfigured, sendEmail, tenderDigestHtml, emailFrom } from './email.mjs'; import { emailConfigured, sendEmail, tenderDigestHtml, emailFrom } from './email.mjs';
import { aiConfigured, availableProviders, analyzeSpec } from './ai.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 { billingConfigured, webhookConfigured, planCatalog, createCheckoutSession, verifyWebhook, planForPrice, cancelSubscription, IPTAL_NEDENLERI } from './billing.mjs';
import * as store from './user-store.mjs'; import * as store from './user-store.mjs';
import { planOf, haklar, kullanim, kotaKontrol, ozellikVar } from './entitlements.mjs'; import { planOf, haklar, kullanim, kotaKontrol, ozellikVar } from './entitlements.mjs';
@@ -292,16 +292,54 @@ async function cpvUygula(t) {
return t; return t;
} }
/** Bir tender listesini, sınırlı paralellikle, CPV işlenmiş hale getirir. */ /**
async function cpvUygulaListe(tenders) { * HIZLI CPV geçişi — yalnızca önbellek + anahtar kelime, AI YOK.
for (let i = 0; i < tenders.length; i += CPV_ESZAMANLI) { *
const grup = tenders.slice(i, i + CPV_ESZAMANLI); * Eskiden havuz, her ilan için AI çağrısı bitene kadar yayınlanmıyordu:
await Promise.all(grup.map((t) => cpvUygula(t))); * 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 seen = new Set(mevcut.map((t) => t.id));
const eklenen = []; const eklenen = [];
for (const ad of yeniAds) { for (const ad of yeniAds) {
@@ -310,9 +348,9 @@ async function ilanlariBirlestir(mevcut, yeniAds) {
seen.add(t.id); seen.add(t.id);
eklenen.push(t); eklenen.push(t);
} }
// GATE: CPV sınıflandırması tamamlanmadan hiçbir ilan çağırana dönmez — // Hızlı sınıflandırma (önbellek + anahtar kelime). AI'a kalanlar arka planda
// bu dizi doğrudan arkaPlandaTamamla() içinde cache.tenders'a eklenir. // tamamlanır; havuzun yayınlanması AI'ı beklemez.
await cpvUygulaListe(eklenen); for (const t of eklenen) cpvHizli(t);
return eklenen; return eklenen;
} }
@@ -331,7 +369,7 @@ async function arkaPlandaTamamla(numFound, ilkListe) {
const sonuclar = await ilanSayfalari(page, grup); const sonuclar = await ilanSayfalari(page, grup);
const ads = sonuclar.flatMap((r) => r.ads || []); const ads = sonuclar.flatMap((r) => r.ads || []);
if (!ads.length) break; if (!ads.length) break;
const eklenen = await ilanlariBirlestir(yeni, ads); const eklenen = ilanlariBirlestir(yeni, ads);
if (eklenen.length) yeni.push(...eklenen); if (eklenen.length) yeni.push(...eklenen);
page += grup; page += grup;
await new Promise((r) => setTimeout(r, 120)); // kaynağı yormamak için 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); havuzKaydet(numFound, cache.tenders.length);
zenginUygula(); // önceden çözülmüşleri hemen uygula zenginUygula(); // önceden çözülmüşleri hemen uygula
setTimeout(() => sektorZenginlestir().catch(() => {}), 5000); // kalanları arka planda setTimeout(() => sektorZenginlestir().catch(() => {}), 5000); // kalanları arka planda
setTimeout(() => cpvZenginlestir().catch(() => {}), 15000); // CPV'yi AI ile tamamla
} catch (e) { } catch (e) {
console.warn('[ihaleist-api] arka plan doldurma hatası:', e.message); console.warn('[ihaleist-api] arka plan doldurma hatası:', e.message);
} finally { } finally {
@@ -462,9 +501,8 @@ async function getTenders(force = false) {
const ilkListe = ads const ilkListe = ads
.filter((ad) => (seen.has(ad.id) ? false : seen.add(ad.id))) .filter((ad) => (seen.has(ad.id) ? false : seen.add(ad.id)))
.map(mapAd); .map(mapAd);
// GATE: CPV sınıflandırması tamamlanmadan bu liste ne hemen servis edilir // Hızlı sınıflandırma; AI'a kalanlar arka planda tamamlanır.
// ne de arkaPlandaTamamla()'nın temeli olur (bkz. cpvUygula yorumu). for (const t of ilkListe) cpvHizli(t);
await cpvUygulaListe(ilkListe);
cache.totalCount = numFound || ilkListe.length; cache.totalCount = numFound || ilkListe.length;
cache.fetchedAt = Date.now(); cache.fetchedAt = Date.now();