#!/usr/bin/env node /** * EKAP geçmiş sonuç tarayıcısı — kazanan firma + sözleşme bedeli + teklif sayısı biriktirir. * * Kullanım: * node server/ekap-scan.mjs --gun 7 # son 7 günün sonuçlananları * node server/ekap-scan.mjs --bas 2026-01-01 --son 2026-06-30 * node server/ekap-scan.mjs --gun 1 --limit 200 # cron için (günlük artımlı) * * Notlar: * - EKAP arama indeksi sonuç ilanlarını kapsamaz; bu yüzden sonuçlanmış ihaleler * sayfalanır ve HER ihalenin ilanları ayrıca çekilir (N+1 kaçınılmaz). * - Eşzamanlılık 6 (repo referansı 8) — EKAP'ı yormamak için. * - Zaten kayıtlı IKN'ler atlanır, böylece tekrar çalıştırmak ucuzdur. */ import { searchTenders, fetchResults, getIhaleDetay, normalizeCompanyName, DURUM_SONUC_ILANI, } from './ekap-v2.mjs'; import { openDb, saveResults, hasIkn, logScan, available } from './history-store.mjs'; const args = process.argv.slice(2); const arg = (n, d = null) => { const i = args.indexOf(`--${n}`); return i >= 0 && args[i + 1] ? args[i + 1] : d; }; const has = (n) => args.includes(`--${n}`); const iso = (d) => d.toISOString().slice(0, 10); const gun = Number(arg('gun', 0)); let bas = arg('bas'); let son = arg('son'); if (!bas || !son) { const bugun = new Date(); const geri = new Date(bugun.getTime() - (gun || 7) * 86400000); bas = bas || iso(geri); son = son || iso(bugun); } const LIMIT = Number(arg('limit', 500)); const SAYFA = 50; const ESZAMANLI = Number(arg('paralel', 6)); const ATLA_KALEM = has('kalemsiz'); // --kalemsiz ile OKAS çekimini kapat (daha hızlı) const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); async function havuz(items, worker, n) { const sonuc = []; let i = 0; const calisanlar = Array.from({ length: Math.min(n, items.length) }, async () => { while (i < items.length) { const idx = i++; try { sonuc.push(await worker(items[idx])); } catch (e) { sonuc.push({ hata: e.message }); } } }); await Promise.all(calisanlar); return sonuc; } // --- Mevcut kayıtlara OKAS kalemi doldur --- // Eski kayıtlarda EKAP'ın anlık `id` token'ı saklanmadığı için IKN ile yeniden // arama yapıp taze bir id alınır, sonra detaydan kalemler çekilir. async function kalemDoldur(db, limit) { const eksik = db.prepare( 'SELECT ikn FROM sonuclar WHERE okas_kalemler IS NULL GROUP BY ikn LIMIT ?' ).all(limit); console.log(`[ekap-scan] OKAS kalemi eksik ${eksik.length} ihale işlenecek`); let ok = 0, bos = 0, hata = 0; const upd = db.prepare('UPDATE sonuclar SET okas_kalemler=? WHERE ikn=?'); await havuz(eksik, async (row) => { const [yil, sayi] = String(row.ikn).split('/'); if (!yil || !sayi) { bos++; return; } const r = await searchTenders({ iknYili: Number(yil), iknSayi: Number(sayi), take: 1 }); const t = r.list[0]; if (!t) { bos++; return; } const d = await getIhaleDetay(t.id); const okas = d?.ihtiyacKalemiOkasList || d?.IhtiyacKalemiOkasList || []; if (!Array.isArray(okas) || !okas.length) { bos++; return; } upd.run(JSON.stringify(okas.map((k) => ({ kodu: k.kodu, adi: k.adi || k.koduAdi }))), row.ikn); ok++; if ((ok + bos) % 25 === 0) console.log(`[ekap-scan] kalem doldurma: ${ok} dolduruldu, ${bos} boş`); }, ESZAMANLI).catch(() => { hata++; }); console.log(`[ekap-scan] KALEM DOLDURMA BİTTİ — ${ok} ihaleye kalem yazıldı, ${bos} kalemsiz, ${hata} hata.`); } async function main() { if (!available()) { console.error('[ekap-scan] node:sqlite yok — bu Node sürümünde tarama yapılamaz.'); process.exit(1); } const db = openDb({ create: true }); const baslangic = new Date().toISOString(); // Kalem doldurma modu: mevcut kayıtlara OKAS ekler, tarama yapmaz const doldur = arg('kalem-doldur'); if (doldur) { await kalemDoldur(db, Number(doldur) || 200); db.close(); return; } console.log(`[ekap-scan] aralık ${bas} → ${son}, limit ${LIMIT}, paralel ${ESZAMANLI}`); let skip = 0; let taranan = 0; let eklenen = 0; let hata = 0; let kalemli = 0; let toplamHavuz = null; while (taranan < LIMIT) { let sayfa; try { sayfa = await searchTenders({ ihaleDurumIdList: [DURUM_SONUC_ILANI], ihaleTarihBaslangic: bas, ihaleTarihBitis: son, orderBy: 'ihaleTarihi', siralamaTipi: 'desc', skip, take: Math.min(SAYFA, LIMIT - taranan), }); } catch (e) { console.error('[ekap-scan] arama hatası:', e.message); hata++; break; } if (toplamHavuz == null) { toplamHavuz = sayfa.totalCount; console.log(`[ekap-scan] aralıkta ${toplamHavuz} sonuçlanmış ihale bulundu`); } if (!sayfa.list.length) break; // Zaten kayıtlı olanları atla (IKN kalıcı anahtar) const yeni = sayfa.list.filter((t) => !hasIkn(db, t.ikn)); const atlanan = sayfa.list.length - yeni.length; const sonuclar = await havuz(yeni, async (t) => { const rs = await fetchResults(t.id); if (!rs.length) return { bos: true }; for (const r of rs) r.kazananNorm = normalizeCompanyName(r.kazananFirma); // OKAS ihtiyaç kalemleri (kategori bazlı fiyat analizi için) if (!ATLA_KALEM) { try { const d = await getIhaleDetay(t.id); const okas = d?.ihtiyacKalemiOkasList || d?.IhtiyacKalemiOkasList || []; if (Array.isArray(okas) && okas.length) { t.okasJson = JSON.stringify(okas.map((k) => ({ kodu: k.kodu, adi: k.adi || k.koduAdi }))); } } catch { /* kalem alınamazsa sonucu yine de kaydet */ } } const n = saveResults(db, t, rs); return { kayit: n, ikn: t.ikn, firma: rs[0].kazananFirma, kalem: t.okasJson ? 1 : 0 }; }, ESZAMANLI); for (const s of sonuclar) { if (s?.hata) hata++; else if (s?.kayit) { eklenen += s.kayit; kalemli += s.kalem || 0; } } taranan += sayfa.list.length; skip += sayfa.list.length; console.log(`[ekap-scan] ${taranan}/${Math.min(LIMIT, toplamHavuz)} tarandı · ${eklenen} kayıt · ${kalemli} kalemli · ${atlanan} atlandı · ${hata} hata`); if (skip >= toplamHavuz) break; await sleep(300); // EKAP'ı yormamak için nazik ara } logScan(db, { baslangic, bitis: new Date().toISOString(), aralikBas: bas, aralikSon: son, taranan, eklenen, hata, not: `havuz=${toplamHavuz}`, }); db.close(); console.log(`[ekap-scan] BİTTİ — ${taranan} ihale tarandı, ${eklenen} sonuç kaydı (${kalemli} OKAS kalemli), ${hata} hata.`); } main().catch((e) => { console.error('[ekap-scan] ölümcül hata:', e); process.exit(1); });