Files
ihaleist/server/notify-store.mjs
T
hikmettan f73c7bd6cb 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.
2026-08-29 10:19:44 +03:00

136 lines
4.1 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.
/**
* Otomatik e-posta bildirimi abonelikleri (SQLite).
* Kullanıcı kendi filtre kriterlerini kaydeder; zamanlayıcı yeni eşleşen
* ihaleleri bulup özet e-posta gönderir.
*/
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
let DatabaseSync = null;
try {
({ DatabaseSync } = await import('node:sqlite'));
} catch {
console.warn('[notify-store] node:sqlite yok — otomatik bildirim devre dışı');
}
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const DATA_DIR = path.join(__dirname, 'data');
const DB_PATH = path.join(DATA_DIR, 'notify.sqlite');
const SCHEMA = `
CREATE TABLE IF NOT EXISTS abonelikler (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL,
ad TEXT,
sehir TEXT,
sektor TEXT,
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
olusturma TEXT,
UNIQUE(email, sehir, sektor, alim_turu, anahtar)
);
CREATE INDEX IF NOT EXISTS idx_abone_email ON abonelikler(email);
`;
export function available() {
return Boolean(DatabaseSync);
}
function open({ create = true } = {}) {
if (!DatabaseSync) return null;
if (!fs.existsSync(DB_PATH)) {
if (!create) return null;
fs.mkdirSync(DATA_DIR, { recursive: 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;
}
const bosaNull = (v) => (v == null || v === '' ? '' : String(v).trim());
export function subscribe(email, kriter = {}, ad = null) {
const db = open();
if (!db) throw new Error('Bildirim deposu kullanılamıyor');
try {
db.prepare(`
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, konular=excluded.konular, ad=excluded.ad
`).run(
email,
ad,
bosaNull(kriter.sehir),
bosaNull(kriter.sektor),
bosaNull(kriter.alimTuru),
bosaNull(kriter.anahtar),
JSON.stringify(kriter.kurumlar || []),
JSON.stringify(kriter.konular || []),
new Date().toISOString()
);
return list(email);
} finally {
db.close();
}
}
export function list(email) {
const db = open({ create: false });
if (!db) return [];
try {
const rows = email
? db.prepare('SELECT * FROM abonelikler WHERE email=? ORDER BY id DESC').all(email)
: db.prepare('SELECT * FROM abonelikler WHERE aktif=1 ORDER BY id').all();
return rows.map((r) => ({
id: r.id,
email: r.email,
ad: r.ad,
sehir: r.sehir || null,
sektor: r.sektor || null,
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 []; } })(),
}));
} finally {
db.close();
}
}
export function remove(id, email) {
const db = open({ create: false });
if (!db) return false;
try {
const r = db.prepare('DELETE FROM abonelikler WHERE id=? AND email=?').run(id, email);
return r.changes > 0;
} finally {
db.close();
}
}
export function markSent(id, ihaleIdleri) {
const db = open({ create: false });
if (!db) return;
try {
db.prepare('UPDATE abonelikler SET sonGonderim=?, sonGorulenler=? WHERE id=?')
.run(new Date().toISOString(), JSON.stringify(ihaleIdleri.slice(0, 400)), id);
} finally {
db.close();
}
}