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.
136 lines
4.1 KiB
JavaScript
136 lines
4.1 KiB
JavaScript
/**
|
||
* 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();
|
||
}
|
||
}
|