Files
ihaleist/server/ekap-scan.mjs
T
hikmettan 168052c267 feat: geçmiş veri motoru + gerçek AI + KİK kararları + TED + kırmızı bayrak + planlar
Roadmap'in 6 maddesi de gerçek çalışır halde:

1) Geçmiş veri motoru (EKAP v2)
   - server/ekap-v2.mjs: AES-256-CBC imzalı EKAP v2 istemcisi (zero-dep),
     ihale arama + sonuç ilanı parse (kazanan firma, sözleşme bedeli,
     yaklaşık maliyet, teklif sayısı), TR firma adı normalize
   - server/history-store.mjs: SQLite deposu (IKN+kısım anahtarlı, upsert)
   - server/ekap-scan.mjs: tarih aralıklı artımlı tarayıcı (6 paralel)
   - server/scheduler.mjs: sunucu içi periyodik tarama (SCAN_* env ile)
   - /api/tenders artık geçmiş sonuçları da birleştiriyor → kazanan firma,
     rakip ve kırmızı bayrak analizleri gerçek veriyle doluyor

2) Gerçek AI şartname analizi
   - server/ai.mjs: 6 sağlayıcılı fallback zinciri (GLM/DeepSeek/Gemini/
     Grok/GPT/Claude), JSON şemalı çıktı, uydurma yasak promptu
   - pdfjs-dist ile tarayıcıda PDF metin çıkarma (dinamik import)
   - AiSpecAnalyzerModal: sahte sonuç kaldırıldı, gerçek dosya + dürüst hata

3) KİK kurul kararları
   - server/kik.mjs: EKAP v2 JSON API (Sec-1..6 imzalı), uyuşmazlık/
     düzenleyici/mahkeme; karar tam metni (ayrı AES anahtarıyla URL)
   - KikDecisions sekmesi: arama + takipteki idarelerden hızlı öneri

4) Fiyatlandırma + ödeme
   - server/billing.mjs: Stripe Checkout (zero-dep, form-encoded)
   - Pricing sekmesi: 3 plan, aylık/yıllık, yapılandırılmadıysa dürüst uyarı

5) TED (AB ihaleleri)
   - server/ted.mjs: anonim TED v3 API, 3 mod (açık/tüm TR/AB'de kazanan TR)
   - rate-limit korumalı (429 HTML gövdesi ele alınır), çok dilli alan çözümü

6) Kırmızı bayrak & şeffaflık
   - analytics.js: redFlags (tek teklif, düşük kırım, tahmin üstü bedel,
     aşırı düşük teklif, tedarikçi yoğunlaşması) + marketConcentration (HHI)
   - RedFlags sekmesi: seviyeli uyarılar + yöntem şeffaflığı notu
2026-08-28 10:37:50 +03:00

131 lines
4.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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, 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 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;
}
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();
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 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);
const n = saveResults(db, t, rs);
return { kayit: n, ikn: t.ikn, firma: rs[0].kazananFirma };
}, ESZAMANLI);
for (const s of sonuclar) {
if (s?.hata) hata++;
else if (s?.kayit) eklenen += s.kayit;
}
taranan += sayfa.list.length;
skip += sayfa.list.length;
console.log(`[ekap-scan] ${taranan}/${Math.min(LIMIT, toplamHavuz)} tarandı · ${eklenen} kayıt · ${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ı yazıldı, ${hata} hata.`);
}
main().catch((e) => { console.error('[ekap-scan] ölümcül hata:', e); process.exit(1); });