Files
kt-financial-system/apps/backend/utils/sqlite.ts
你的用户名 6108b9c5ed
Some checks failed
Deploy to Production / Build and Test (push) Has been cancelled
Deploy to Production / Deploy to Server (push) Has been cancelled
feat: 添加Telegram通知增强功能
 新增功能:
- 通知频率控制(防止消息轰炸)
- 消息去重机制(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
- 测试消息:  发送成功
2025-11-04 23:22:39 +08:00

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;