feat: konu bazlı bildirim + gerçek site tarama

1) Bildirim artık KONU odaklı (il değil)
   - Kullanıcı hangi konularda ihale alabildiğini seçer; varsayılan tüm Türkiye
   - İl/alım türü/favori kurum 'daha da daralt' altında isteğe bağlı
   - notify-store: konular kolonu + otomatik şema geçişi
   - notifier: konu başlıkları ihale başlığı/sektör/branş/kalemlerde aranır

2) Gerçek firma sitesi tarama (webScraperService mock'unun yerine)
   - server/site-scan.mjs: siteyi GERÇEKTEN çeker, ana sayfa + ürün/hizmet/
     hakkımızda iç sayfalarını tarar, HTML'i metne çevirir, AI ile firmanın
     tedarik edebildiği ihale konularını çıkarır ('uydurma' yasaklı prompt)
   - /api/company/scan (auth'lu)
   - ai.mjs: systemOverride desteği (farklı görevler için ayrı sistem promptu)
   - Arayüz: 'Sitenizi tarayıp konuları otomatik önerelim' → tıklayarak ekleme

Not: eski webScraperService.js sabit anahtar-kelime sözlüğüydü; domain adında
'tanriverdi' geçince tıbbi ürün listesi uyduruyordu.
This commit is contained in:
2026-08-29 10:19:44 +03:00
parent b44b45c67e
commit f73c7bd6cb
6 changed files with 350 additions and 114 deletions
+10 -3
View File
@@ -29,6 +29,7 @@ CREATE TABLE IF NOT EXISTS abonelikler (
alim_turu TEXT,
anahtar TEXT,
kurumlar TEXT, -- JSON: favori kurum listesi
konular TEXT, -- JSON: konu başlıkları (kullanıcı seçer)
aktif INTEGER NOT NULL DEFAULT 1,
sonGonderim TEXT,
sonGorulenler TEXT, -- JSON: son gönderimde bildirilen ihale id'leri
@@ -50,6 +51,10 @@ function open({ create = true } = {}) {
}
const db = new DatabaseSync(DB_PATH);
db.exec(SCHEMA);
try {
const k = db.prepare('PRAGMA table_info(abonelikler)').all().map((c) => c.name);
if (!k.includes('konular')) db.exec('ALTER TABLE abonelikler ADD COLUMN konular TEXT');
} catch (e) { console.warn('[notify-store] şema geçişi:', e.message); }
return db;
}
@@ -60,10 +65,10 @@ export function subscribe(email, kriter = {}, ad = null) {
if (!db) throw new Error('Bildirim deposu kullanılamıyor');
try {
db.prepare(`
INSERT INTO abonelikler (email, ad, sehir, sektor, alim_turu, anahtar, kurumlar, aktif, olusturma)
VALUES (?,?,?,?,?,?,?,1,?)
INSERT INTO abonelikler (email, ad, sehir, sektor, alim_turu, anahtar, kurumlar, konular, aktif, olusturma)
VALUES (?,?,?,?,?,?,?,?,1,?)
ON CONFLICT(email, sehir, sektor, alim_turu, anahtar) DO UPDATE SET
aktif=1, kurumlar=excluded.kurumlar, ad=excluded.ad
aktif=1, kurumlar=excluded.kurumlar, konular=excluded.konular, ad=excluded.ad
`).run(
email,
ad,
@@ -72,6 +77,7 @@ export function subscribe(email, kriter = {}, ad = null) {
bosaNull(kriter.alimTuru),
bosaNull(kriter.anahtar),
JSON.stringify(kriter.kurumlar || []),
JSON.stringify(kriter.konular || []),
new Date().toISOString()
);
return list(email);
@@ -96,6 +102,7 @@ export function list(email) {
alimTuru: r.alim_turu || null,
anahtar: r.anahtar || null,
kurumlar: (() => { try { return JSON.parse(r.kurumlar || '[]'); } catch { return []; } })(),
konular: (() => { try { return JSON.parse(r.konular || '[]'); } catch { return []; } })(),
aktif: Boolean(r.aktif),
sonGonderim: r.sonGonderim,
sonGorulenler: (() => { try { return JSON.parse(r.sonGorulenler || '[]'); } catch { return []; } })(),