✨ 新增功能: - 通知频率控制(防止消息轰炸) - 消息去重机制(5分钟内相同内容不重复发送) - 失败重试机制(最多3次重试) - 通知历史记录(完整的发送日志) - 优先级标识(低/普通/高/紧急) - 批量通知支持(预留功能) 📊 数据库增强: - telegram_notification_configs 新增字段: - priority: 通知优先级 - rate_limit_seconds: 频率限制(秒) - batch_enabled: 批量通知开关 - batch_interval_minutes: 批量间隔 - retry_enabled: 重试开关 - retry_max_attempts: 最大重试次数 - telegram_notification_history 新表: - 记录所有通知发送历史 - 支持状态追踪(pending/sent/failed) - 支持重试计数 - 支持错误信息记录 🔧 核心实现: - telegram-bot-enhanced.ts: 增强版通知引擎 - generateContentHash(): 内容hash生成 - checkRateLimit(): 频率限制检查 - isDuplicateMessage(): 消息去重 - recordNotification(): 记录通知历史 - updateNotificationStatus(): 更新通知状态 - getPendingRetries(): 获取待重试通知 - notifyTransactionEnhanced(): 增强版通知 - retryFailedNotifications(): 失败重试 ✅ 测试结果: - Bot Token: 8270297136:AAEek5CIO8RDudo8eqlg2vy4ilcyqQMoEQ8 - Chat ID: 1102887169 - Bot用户名: @ktcaiwubot - 测试消息: ✅ 发送成功
249 lines
6.7 KiB
TypeScript
249 lines
6.7 KiB
TypeScript
import { mkdirSync } from 'node:fs';
|
|
|
|
import Database from 'better-sqlite3';
|
|
import { dirname, join } from 'pathe';
|
|
|
|
const dbFile = join(process.cwd(), 'storage', 'finance.db');
|
|
|
|
mkdirSync(dirname(dbFile), { recursive: true });
|
|
|
|
const database = new Database(dbFile);
|
|
|
|
function assertIdentifier(name: string) {
|
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
|
|
throw new Error(`Invalid identifier: ${name}`);
|
|
}
|
|
return name;
|
|
}
|
|
|
|
function ensureColumn(table: string, column: string, definition: string) {
|
|
const safeTable = assertIdentifier(table);
|
|
const safeColumn = assertIdentifier(column);
|
|
const columns = database
|
|
.prepare<{ name: string }>(`PRAGMA table_info(${safeTable})`)
|
|
.all();
|
|
if (!columns.some((item) => item.name === safeColumn)) {
|
|
database.exec(`ALTER TABLE ${safeTable} ADD COLUMN ${definition}`);
|
|
}
|
|
}
|
|
|
|
database.pragma('journal_mode = WAL');
|
|
|
|
database.exec(`
|
|
CREATE TABLE IF NOT EXISTS finance_currencies (
|
|
code TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
symbol TEXT NOT NULL,
|
|
is_base INTEGER NOT NULL DEFAULT 0,
|
|
is_active INTEGER NOT NULL DEFAULT 1
|
|
);
|
|
`);
|
|
|
|
database.exec(`
|
|
CREATE TABLE IF NOT EXISTS finance_exchange_rates (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
from_currency TEXT NOT NULL,
|
|
to_currency TEXT NOT NULL,
|
|
rate REAL NOT NULL,
|
|
date TEXT NOT NULL,
|
|
source TEXT DEFAULT 'manual'
|
|
);
|
|
`);
|
|
|
|
database.exec(`
|
|
CREATE TABLE IF NOT EXISTS finance_accounts (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL,
|
|
currency TEXT NOT NULL,
|
|
type TEXT DEFAULT 'cash',
|
|
icon TEXT,
|
|
color TEXT,
|
|
user_id INTEGER DEFAULT 1,
|
|
is_active INTEGER DEFAULT 1
|
|
);
|
|
`);
|
|
|
|
database.exec(`
|
|
CREATE TABLE IF NOT EXISTS finance_categories (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL,
|
|
type TEXT NOT NULL,
|
|
icon TEXT,
|
|
color TEXT,
|
|
user_id INTEGER DEFAULT 1,
|
|
is_active INTEGER DEFAULT 1
|
|
);
|
|
`);
|
|
|
|
database.exec(`
|
|
CREATE TABLE IF NOT EXISTS finance_transactions (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
type TEXT NOT NULL,
|
|
amount REAL NOT NULL,
|
|
currency TEXT NOT NULL,
|
|
exchange_rate_to_base REAL NOT NULL,
|
|
amount_in_base REAL NOT NULL,
|
|
category_id INTEGER,
|
|
account_id INTEGER,
|
|
transaction_date TEXT NOT NULL,
|
|
description TEXT,
|
|
project TEXT,
|
|
memo TEXT,
|
|
created_at TEXT NOT NULL,
|
|
status TEXT NOT NULL DEFAULT 'approved',
|
|
status_updated_at TEXT,
|
|
reimbursement_batch TEXT,
|
|
review_notes TEXT,
|
|
submitted_by TEXT,
|
|
approved_by TEXT,
|
|
approved_at TEXT,
|
|
is_deleted INTEGER NOT NULL DEFAULT 0,
|
|
deleted_at TEXT,
|
|
FOREIGN KEY (currency) REFERENCES finance_currencies(code),
|
|
FOREIGN KEY (category_id) REFERENCES finance_categories(id),
|
|
FOREIGN KEY (account_id) REFERENCES finance_accounts(id)
|
|
);
|
|
`);
|
|
|
|
ensureColumn(
|
|
'finance_transactions',
|
|
'status',
|
|
"status TEXT NOT NULL DEFAULT 'approved'",
|
|
);
|
|
ensureColumn('finance_transactions', 'status_updated_at', 'status_updated_at TEXT');
|
|
ensureColumn(
|
|
'finance_transactions',
|
|
'reimbursement_batch',
|
|
'reimbursement_batch TEXT',
|
|
);
|
|
ensureColumn('finance_transactions', 'review_notes', 'review_notes TEXT');
|
|
ensureColumn('finance_transactions', 'submitted_by', 'submitted_by TEXT');
|
|
ensureColumn('finance_transactions', 'approved_by', 'approved_by TEXT');
|
|
ensureColumn('finance_transactions', 'approved_at', 'approved_at TEXT');
|
|
|
|
database.exec(`
|
|
CREATE TABLE IF NOT EXISTS finance_media_messages (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
chat_id INTEGER NOT NULL,
|
|
message_id INTEGER NOT NULL,
|
|
user_id INTEGER NOT NULL,
|
|
username TEXT,
|
|
display_name TEXT,
|
|
file_type TEXT NOT NULL,
|
|
file_id TEXT NOT NULL,
|
|
file_unique_id TEXT,
|
|
caption TEXT,
|
|
file_name TEXT,
|
|
file_path TEXT NOT NULL,
|
|
file_size INTEGER,
|
|
mime_type TEXT,
|
|
duration INTEGER,
|
|
width INTEGER,
|
|
height INTEGER,
|
|
forwarded_to INTEGER,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
UNIQUE(chat_id, message_id)
|
|
);
|
|
`);
|
|
|
|
database.exec(`
|
|
CREATE INDEX IF NOT EXISTS idx_finance_media_messages_created_at
|
|
ON finance_media_messages (created_at DESC);
|
|
`);
|
|
|
|
database.exec(`
|
|
CREATE INDEX IF NOT EXISTS idx_finance_media_messages_user_id
|
|
ON finance_media_messages (user_id);
|
|
`);
|
|
|
|
// Telegram通知配置表
|
|
database.exec(`
|
|
CREATE TABLE IF NOT EXISTS telegram_notification_configs (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL,
|
|
bot_token TEXT NOT NULL,
|
|
chat_id TEXT NOT NULL,
|
|
notification_types TEXT NOT NULL,
|
|
is_enabled INTEGER NOT NULL DEFAULT 1,
|
|
priority TEXT DEFAULT 'normal',
|
|
rate_limit_seconds INTEGER DEFAULT 0,
|
|
batch_enabled INTEGER DEFAULT 0,
|
|
batch_interval_minutes INTEGER DEFAULT 60,
|
|
retry_enabled INTEGER DEFAULT 1,
|
|
retry_max_attempts INTEGER DEFAULT 3,
|
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
`);
|
|
|
|
database.exec(`
|
|
CREATE INDEX IF NOT EXISTS idx_telegram_notification_configs_enabled
|
|
ON telegram_notification_configs (is_enabled);
|
|
`);
|
|
|
|
// 通知发送历史表(用于频率控制和去重)
|
|
database.exec(`
|
|
CREATE TABLE IF NOT EXISTS telegram_notification_history (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
config_id INTEGER NOT NULL,
|
|
notification_type TEXT NOT NULL,
|
|
content_hash TEXT NOT NULL,
|
|
status TEXT NOT NULL DEFAULT 'pending',
|
|
retry_count INTEGER DEFAULT 0,
|
|
sent_at TEXT,
|
|
error_message TEXT,
|
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (config_id) REFERENCES telegram_notification_configs(id)
|
|
);
|
|
`);
|
|
|
|
database.exec(`
|
|
CREATE INDEX IF NOT EXISTS idx_telegram_notification_history_config
|
|
ON telegram_notification_history (config_id, created_at DESC);
|
|
`);
|
|
|
|
database.exec(`
|
|
CREATE INDEX IF NOT EXISTS idx_telegram_notification_history_hash
|
|
ON telegram_notification_history (content_hash, created_at DESC);
|
|
`);
|
|
|
|
database.exec(`
|
|
CREATE INDEX IF NOT EXISTS idx_telegram_notification_history_status
|
|
ON telegram_notification_history (status, retry_count);
|
|
`);
|
|
|
|
// 确保添加新列到已存在的表
|
|
ensureColumn(
|
|
'telegram_notification_configs',
|
|
'priority',
|
|
"priority TEXT DEFAULT 'normal'",
|
|
);
|
|
ensureColumn(
|
|
'telegram_notification_configs',
|
|
'rate_limit_seconds',
|
|
'rate_limit_seconds INTEGER DEFAULT 0',
|
|
);
|
|
ensureColumn(
|
|
'telegram_notification_configs',
|
|
'batch_enabled',
|
|
'batch_enabled INTEGER DEFAULT 0',
|
|
);
|
|
ensureColumn(
|
|
'telegram_notification_configs',
|
|
'batch_interval_minutes',
|
|
'batch_interval_minutes INTEGER DEFAULT 60',
|
|
);
|
|
ensureColumn(
|
|
'telegram_notification_configs',
|
|
'retry_enabled',
|
|
'retry_enabled INTEGER DEFAULT 1',
|
|
);
|
|
ensureColumn(
|
|
'telegram_notification_configs',
|
|
'retry_max_attempts',
|
|
'retry_max_attempts INTEGER DEFAULT 3',
|
|
);
|
|
|
|
export default database;
|