feat: otomatik e-posta bildirimi
Kullanıcı kriter kaydediyor; sunucu periyodik olarak eşleşen YENİ ihaleleri bulup özet e-posta gönderiyor. - server/notify-store.mjs: abonelik deposu (SQLite, kalıcı volume'de) kriterler: il · sektör · alım türü · anahtar kelime · favori kurumlar - server/notifier.mjs: eşleştirme + gönderim motoru · aynı ihale iki kez bildirilmez (sonGorulenler takibi) · ilk abonelikte geçmiş bildirilmez, yalnızca temel alınır (spam önleme) · sonuçlanmış/arşiv kayıtları bildirime girmez - /api/notify/subscriptions (GET/POST/DELETE) + /api/notify/run (admin, anlık test) - scheduler: NOTIFY_INTERVAL_SAAT (varsayılan 6) ile periyodik tur - NotifySubscriptions paneli: kriter ekle/sil, 'ekrandaki filtreleri kullan' - tendersPayload ortak yardımcısına çıkarıldı (API ve bildirim aynı veriyi kullanır)
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* 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
|
||||
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);
|
||||
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, aktif, olusturma)
|
||||
VALUES (?,?,?,?,?,?,?,1,?)
|
||||
ON CONFLICT(email, sehir, sektor, alim_turu, anahtar) DO UPDATE SET
|
||||
aktif=1, kurumlar=excluded.kurumlar, ad=excluded.ad
|
||||
`).run(
|
||||
email,
|
||||
ad,
|
||||
bosaNull(kriter.sehir),
|
||||
bosaNull(kriter.sektor),
|
||||
bosaNull(kriter.alimTuru),
|
||||
bosaNull(kriter.anahtar),
|
||||
JSON.stringify(kriter.kurumlar || []),
|
||||
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 []; } })(),
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user