diff --git a/.codex/auth.json b/.codex/auth.json new file mode 100644 index 00000000..ed4eca1d --- /dev/null +++ b/.codex/auth.json @@ -0,0 +1,3 @@ +{ + "OPENAI_API_KEY": "cr_c9719a63cd3fbcf2a7043da03ccdef29e1e48ab4632e57db68ef1c73b2f6c9ec" +} diff --git a/.codex/config.toml b/.codex/config.toml new file mode 100644 index 00000000..7b54b470 --- /dev/null +++ b/.codex/config.toml @@ -0,0 +1,35 @@ +# Codex 临时配置(基于全局配置) +# SessionID: 4795f195-3362-4043-80c3-10b1f9ce9dec, ChatID: -4846353145 + +# 沙盒和权限配置(强制覆盖) +sandbox_mode = "danger-full-access" +approval_policy = "never" + +model_provider = "crs" +model = "gpt-5-codex" +model_reasoning_effort = "high" +disable_response_storage = true +preferred_auth_method = "apikey" + +[model_providers.crs] +name = "crs" +base_url = "https://ktapi.cc/openai" +wire_api = "responses" + +ask_for_approval = "never" +sandbox = "danger-full-access" + +[projects."/Users/hahaha"] +trust_level = "trusted" +ask_for_approval = "never" +sandbox = "danger-full-access" + +[mcp_servers.funstat-mcp] +url = "http://172.16.74.159:8091/sse" + + +# 会话特定的 MCP 服务器配置 +[mcp_servers.agentapi] +command = "/Users/hahaha/agentapi/agentapi" +args = ["proxy", "http://localhost:8089/mcp/sse?sessionID=4795f195-3362-4043-80c3-10b1f9ce9dec&chatID=-4846353145"] + diff --git a/.codex/sessions/2025/11/04/rollout-2025-11-04T20-59-55-019a4ef3-dade-79b3-a9d9-84a2cb7f22a0.jsonl b/.codex/sessions/2025/11/04/rollout-2025-11-04T20-59-55-019a4ef3-dade-79b3-a9d9-84a2cb7f22a0.jsonl new file mode 100644 index 00000000..46eb842e --- /dev/null +++ b/.codex/sessions/2025/11/04/rollout-2025-11-04T20-59-55-019a4ef3-dade-79b3-a9d9-84a2cb7f22a0.jsonl @@ -0,0 +1,3 @@ +{"timestamp":"2025-11-04T12:59:55.520Z","type":"session_meta","payload":{"id":"019a4ef3-dade-79b3-a9d9-84a2cb7f22a0","timestamp":"2025-11-04T12:59:55.486Z","cwd":"/Users/hahaha/projects/kt-financial-system","originator":"codex_cli_rs","cli_version":"0.53.0","instructions":"\n- Search documentation for knowledge first: Use mrdoc_search when you need project background knowledge, internal knowledge, or cross-project documentation to get relevant technical solutions and historical experience. After modifying modules, you must create or update MrDoc documentation to accumulate and transfer knowledge.\n- Create/update documentation must use mrdoc related MCP tools\n\n- Think deeply and search online when solving problems (high-IQ genius hacker mindset)\n- Memory is only saved in the project folder\n- Use the same language as the user for thinking and answering\n- Code style: Comments are documentation\n","source":"vscode","model_provider":"crs","git":{"commit_hash":"6a11d8a70e918635afad990c108d836207947bc2","branch":"main","repository_url":"https://doudou:4003483870c48a22d637678b35b157d19d57c2ee@gitea.ktyun.cc/chenjiangjiang/kt-financial-system.git"}}} +{"timestamp":"2025-11-04T12:59:55.538Z","type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"\n\n\n- Search documentation for knowledge first: Use mrdoc_search when you need project background knowledge, internal knowledge, or cross-project documentation to get relevant technical solutions and historical experience. After modifying modules, you must create or update MrDoc documentation to accumulate and transfer knowledge.\n- Create/update documentation must use mrdoc related MCP tools\n\n- Think deeply and search online when solving problems (high-IQ genius hacker mindset)\n- Memory is only saved in the project folder\n- Use the same language as the user for thinking and answering\n- Code style: Comments are documentation\n\n\n"}]}} +{"timestamp":"2025-11-04T12:59:55.538Z","type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"\n /Users/hahaha/projects/kt-financial-system\n never\n danger-full-access\n enabled\n zsh\n"}]}} diff --git a/agents.md b/agents.md new file mode 100644 index 00000000..e7cf516e --- /dev/null +++ b/agents.md @@ -0,0 +1,8 @@ + +- Search documentation for knowledge first: Use mrdoc_search when you need project background knowledge, internal knowledge, or cross-project documentation to get relevant technical solutions and historical experience. After modifying modules, you must create or update MrDoc documentation to accumulate and transfer knowledge. +- Create/update documentation must use mrdoc related MCP tools + +- Think deeply and search online when solving problems (high-IQ genius hacker mindset) +- Memory is only saved in the project folder +- Use the same language as the user for thinking and answering +- Code style: Comments are documentation diff --git a/apps/backend/api/finance/transactions.post.ts b/apps/backend/api/finance/transactions.post.ts index e6243f0f..3d7fd0a1 100644 --- a/apps/backend/api/finance/transactions.post.ts +++ b/apps/backend/api/finance/transactions.post.ts @@ -5,6 +5,8 @@ import { } from '~/utils/finance-repository'; import { useResponseError, useResponseSuccess } from '~/utils/response'; import { notifyTransactionWebhook } from '~/utils/telegram-webhook'; +import { notifyTransaction } from '~/utils/telegram-bot'; +import db from '~/utils/sqlite'; const DEFAULT_CURRENCY = 'CNY'; const ALLOWED_STATUSES: TransactionStatus[] = [ @@ -52,9 +54,48 @@ export default defineEventHandler(async (event) => { approvedAt: body.approvedAt ?? undefined, }); + // 发送Webhook通知(保留原有功能) notifyTransactionWebhook(transaction, { action: 'created' }).catch((error) => console.error('[finance][transactions.post] webhook notify failed', error), ); + // 发送Telegram通知(新功能) + try { + // 获取分类和账户名称 + let categoryName: string | undefined; + let accountName: string | undefined; + + if (transaction.categoryId) { + const category = db + .prepare<{ name: string }>('SELECT name FROM finance_categories WHERE id = ?') + .get(transaction.categoryId); + categoryName = category?.name; + } + + if (transaction.accountId) { + const account = db + .prepare<{ name: string }>('SELECT name FROM finance_accounts WHERE id = ?') + .get(transaction.accountId); + accountName = account?.name; + } + + await notifyTransaction( + { + id: transaction.id, + type: transaction.type, + amount: transaction.amount, + currency: transaction.currency, + categoryName, + accountName, + transactionDate: transaction.transactionDate, + description: transaction.description || undefined, + status: transaction.status, + }, + 'created', + ); + } catch (error) { + console.error('[finance][transactions.post] telegram notify failed', error); + } + return useResponseSuccess(transaction); }); diff --git a/apps/backend/api/telegram/notifications.get.ts b/apps/backend/api/telegram/notifications.get.ts new file mode 100644 index 00000000..01183426 --- /dev/null +++ b/apps/backend/api/telegram/notifications.get.ts @@ -0,0 +1,27 @@ +import db from '~/utils/sqlite'; +import { useResponseSuccess } from '~/utils/response'; + +export default defineEventHandler(() => { + const configs = db + .prepare<{ id: number; name: string; bot_token: string; chat_id: string; notification_types: string; is_enabled: number; created_at: string; updated_at: string }>( + ` + SELECT id, name, bot_token, chat_id, notification_types, is_enabled, created_at, updated_at + FROM telegram_notification_configs + ORDER BY created_at DESC + `, + ) + .all(); + + const result = configs.map((row) => ({ + id: row.id, + name: row.name, + botToken: row.bot_token, + chatId: row.chat_id, + notificationTypes: JSON.parse(row.notification_types) as string[], + isEnabled: row.is_enabled === 1, + createdAt: row.created_at, + updatedAt: row.updated_at, + })); + + return useResponseSuccess(result); +}); diff --git a/apps/backend/api/telegram/notifications.post.ts b/apps/backend/api/telegram/notifications.post.ts new file mode 100644 index 00000000..3eb4a873 --- /dev/null +++ b/apps/backend/api/telegram/notifications.post.ts @@ -0,0 +1,55 @@ +import { readBody } from 'h3'; +import db from '~/utils/sqlite'; +import { useResponseError, useResponseSuccess } from '~/utils/response'; +import { testTelegramConfig } from '~/utils/telegram-bot'; + +export default defineEventHandler(async (event) => { + const body = await readBody(event); + + if (!body?.name || !body?.botToken || !body?.chatId) { + return useResponseError('缺少必填字段', -1); + } + + const notificationTypes = Array.isArray(body.notificationTypes) + ? body.notificationTypes + : ['transaction']; + + // 测试配置是否有效 + const testResult = await testTelegramConfig(body.botToken, body.chatId); + if (!testResult.success) { + return useResponseError( + `Telegram配置测试失败: ${testResult.error}`, + -1, + ); + } + + const now = new Date().toISOString(); + + const result = db + .prepare( + ` + INSERT INTO telegram_notification_configs (name, bot_token, chat_id, notification_types, is_enabled, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?) + `, + ) + .run( + body.name, + body.botToken, + body.chatId, + JSON.stringify(notificationTypes), + body.isEnabled !== false ? 1 : 0, + now, + now, + ); + + return useResponseSuccess({ + id: result.lastInsertRowid, + name: body.name, + botToken: body.botToken, + chatId: body.chatId, + notificationTypes, + isEnabled: body.isEnabled !== false, + createdAt: now, + updatedAt: now, + }); +}); diff --git a/apps/backend/api/telegram/notifications/[id].delete.ts b/apps/backend/api/telegram/notifications/[id].delete.ts new file mode 100644 index 00000000..934328de --- /dev/null +++ b/apps/backend/api/telegram/notifications/[id].delete.ts @@ -0,0 +1,19 @@ +import db from '~/utils/sqlite'; +import { useResponseError, useResponseSuccess } from '~/utils/response'; + +export default defineEventHandler((event) => { + const id = event.context.params?.id; + if (!id) { + return useResponseError('缺少ID参数', -1); + } + + const result = db + .prepare('DELETE FROM telegram_notification_configs WHERE id = ?') + .run(id); + + if (result.changes === 0) { + return useResponseError('配置不存在或删除失败', -1); + } + + return useResponseSuccess({ id }); +}); diff --git a/apps/backend/api/telegram/notifications/[id].put.ts b/apps/backend/api/telegram/notifications/[id].put.ts new file mode 100644 index 00000000..bca6870d --- /dev/null +++ b/apps/backend/api/telegram/notifications/[id].put.ts @@ -0,0 +1,96 @@ +import { readBody } from 'h3'; +import db from '~/utils/sqlite'; +import { useResponseError, useResponseSuccess } from '~/utils/response'; +import { testTelegramConfig } from '~/utils/telegram-bot'; + +export default defineEventHandler(async (event) => { + const id = event.context.params?.id; + if (!id) { + return useResponseError('缺少ID参数', -1); + } + + const body = await readBody(event); + + // 如果更新了botToken或chatId,需要测试配置 + if (body.botToken || body.chatId) { + const existing = db + .prepare<{ bot_token: string; chat_id: string }>('SELECT bot_token, chat_id FROM telegram_notification_configs WHERE id = ?') + .get(id); + + if (!existing) { + return useResponseError('配置不存在', -1); + } + + const tokenToTest = body.botToken || existing.bot_token; + const chatIdToTest = body.chatId || existing.chat_id; + + const testResult = await testTelegramConfig(tokenToTest, chatIdToTest); + if (!testResult.success) { + return useResponseError( + `Telegram配置测试失败: ${testResult.error}`, + -1, + ); + } + } + + const updates: string[] = []; + const values: (string | number)[] = []; + + if (body.name !== undefined) { + updates.push('name = ?'); + values.push(body.name); + } + + if (body.botToken !== undefined) { + updates.push('bot_token = ?'); + values.push(body.botToken); + } + + if (body.chatId !== undefined) { + updates.push('chat_id = ?'); + values.push(body.chatId); + } + + if (body.notificationTypes !== undefined) { + updates.push('notification_types = ?'); + values.push(JSON.stringify(body.notificationTypes)); + } + + if (body.isEnabled !== undefined) { + updates.push('is_enabled = ?'); + values.push(body.isEnabled ? 1 : 0); + } + + if (updates.length === 0) { + return useResponseError('没有可更新的字段', -1); + } + + updates.push('updated_at = ?'); + values.push(new Date().toISOString()); + values.push(id); + + db.prepare(`UPDATE telegram_notification_configs SET ${updates.join(', ')} WHERE id = ?`).run( + ...values, + ); + + const updated = db + .prepare<{ id: number; name: string; bot_token: string; chat_id: string; notification_types: string; is_enabled: number; created_at: string; updated_at: string }>( + 'SELECT * FROM telegram_notification_configs WHERE id = ?', + ) + .get(id); + + if (!updated) { + return useResponseError('更新失败', -1); + } + + return useResponseSuccess({ + id: updated.id, + name: updated.name, + botToken: updated.bot_token, + chatId: updated.chat_id, + notificationTypes: JSON.parse(updated.notification_types) as string[], + isEnabled: updated.is_enabled === 1, + createdAt: updated.created_at, + updatedAt: updated.updated_at, + }); +}); diff --git a/apps/backend/api/telegram/test.post.ts b/apps/backend/api/telegram/test.post.ts new file mode 100644 index 00000000..91622ecc --- /dev/null +++ b/apps/backend/api/telegram/test.post.ts @@ -0,0 +1,19 @@ +import { readBody } from 'h3'; +import { useResponseError, useResponseSuccess } from '~/utils/response'; +import { testTelegramConfig } from '~/utils/telegram-bot'; + +export default defineEventHandler(async (event) => { + const body = await readBody(event); + + if (!body?.botToken || !body?.chatId) { + return useResponseError('缺少Bot Token或Chat ID', -1); + } + + const result = await testTelegramConfig(body.botToken, body.chatId); + + if (result.success) { + return useResponseSuccess({ message: '测试消息发送成功' }); + } else { + return useResponseError(result.error || '测试失败', -1); + } +}); diff --git a/apps/backend/utils/sqlite.ts b/apps/backend/utils/sqlite.ts index 1cb61af6..525c0578 100644 --- a/apps/backend/utils/sqlite.ts +++ b/apps/backend/utils/sqlite.ts @@ -157,4 +157,23 @@ database.exec(` 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, + 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); +`); + export default database; diff --git a/apps/backend/utils/telegram-bot.ts b/apps/backend/utils/telegram-bot.ts new file mode 100644 index 00000000..ea988ba7 --- /dev/null +++ b/apps/backend/utils/telegram-bot.ts @@ -0,0 +1,229 @@ +import db from './sqlite'; + +interface TelegramNotificationConfig { + id: number; + name: string; + botToken: string; + chatId: string; + notificationTypes: string[]; + isEnabled: boolean; +} + +interface TransactionNotificationData { + id: number; + type: string; + amount: number; + currency: string; + categoryName?: string; + accountName?: string; + transactionDate: string; + description?: string; + status: string; +} + +/** + * 获取所有启用的Telegram通知配置 + */ +export function getEnabledNotificationConfigs( + notificationType: string = 'transaction', +): TelegramNotificationConfig[] { + const rows = db + .prepare<{ id: number; name: string; bot_token: string; chat_id: string; notification_types: string; is_enabled: number }>( + ` + SELECT id, name, bot_token, chat_id, notification_types, is_enabled + FROM telegram_notification_configs + WHERE is_enabled = 1 + `, + ) + .all(); + + return rows + .map((row) => ({ + id: row.id, + name: row.name, + botToken: row.bot_token, + chatId: row.chat_id, + notificationTypes: JSON.parse(row.notification_types) as string[], + isEnabled: row.is_enabled === 1, + })) + .filter((config) => config.notificationTypes.includes(notificationType)); +} + +/** + * 格式化交易金额 + */ +function formatAmount(amount: number, currency: string): string { + const formatted = amount.toLocaleString('zh-CN', { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }); + return `${currency} ${formatted}`; +} + +/** + * 格式化交易类型 + */ +function formatTransactionType(type: string): string { + const typeMap: Record = { + income: '💰 收入', + expense: '💸 支出', + transfer: '🔄 转账', + }; + return typeMap[type] || type; +} + +/** + * 格式化交易状态 + */ +function formatTransactionStatus(status: string): string { + const statusMap: Record = { + draft: '📝 草稿', + pending: '⏳ 待审核', + approved: '✅ 已批准', + rejected: '❌ 已拒绝', + paid: '💵 已支付', + }; + return statusMap[status] || status; +} + +/** + * 构建交易通知消息 + */ +function buildTransactionMessage( + transaction: TransactionNotificationData, + action: string = 'created', +): string { + const actionMap: Record = { + created: '📋 新增账目记录', + updated: '✏️ 更新账目记录', + deleted: '🗑️ 删除账目记录', + }; + + const lines: string[] = [ + `${actionMap[action] || '📋 账目记录'}`, + '', + `类型:${formatTransactionType(transaction.type)}`, + `金额:${formatAmount(transaction.amount, transaction.currency)}`, + `日期:${transaction.transactionDate}`, + ]; + + if (transaction.categoryName) { + lines.push(`分类:${transaction.categoryName}`); + } + + if (transaction.accountName) { + lines.push(`账户:${transaction.accountName}`); + } + + lines.push(`状态:${formatTransactionStatus(transaction.status)}`); + + if (transaction.description) { + lines.push(``, `备注:${transaction.description}`); + } + + lines.push( + ``, + `🕐 记录时间:${new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' })}`, + ); + + return lines.join('\n'); +} + +/** + * 发送Telegram消息 + */ +async function sendTelegramMessage( + botToken: string, + chatId: string, + message: string, +): Promise { + try { + const url = `https://api.telegram.org/bot${botToken}/sendMessage`; + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + chat_id: chatId, + text: message, + parse_mode: 'HTML', + }), + }); + + if (!response.ok) { + const error = await response.json().catch(() => ({})); + console.error( + '[telegram-bot] Failed to send message:', + response.status, + error, + ); + return false; + } + + return true; + } catch (error) { + console.error('[telegram-bot] Error sending message:', error); + return false; + } +} + +/** + * 通知交易记录 + */ +export async function notifyTransaction( + transaction: TransactionNotificationData, + action: string = 'created', +): Promise { + const configs = getEnabledNotificationConfigs('transaction'); + + if (configs.length === 0) { + console.log('[telegram-bot] No enabled notification configs found'); + return; + } + + const message = buildTransactionMessage(transaction, action); + + const results = await Promise.allSettled( + configs.map((config) => + sendTelegramMessage(config.botToken, config.chatId, message), + ), + ); + + results.forEach((result, index) => { + if (result.status === 'fulfilled' && result.value) { + console.log( + `[telegram-bot] Sent notification via config: ${configs[index].name}`, + ); + } else { + console.error( + `[telegram-bot] Failed to send notification via config: ${configs[index].name}`, + ); + } + }); +} + +/** + * 测试Telegram Bot配置 + */ +export async function testTelegramConfig( + botToken: string, + chatId: string, +): Promise<{ success: boolean; error?: string }> { + try { + const testMessage = `🤖 KT财务系统\n\n✅ Telegram通知配置测试成功!\n\n🕐 ${new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' })}`; + + const success = await sendTelegramMessage(botToken, chatId, testMessage); + + if (success) { + return { success: true }; + } else { + return { success: false, error: '发送消息失败,请检查Bot Token和Chat ID' }; + } + } catch (error: unknown) { + return { + success: false, + error: error instanceof Error ? error.message : '未知错误', + }; + } +} diff --git a/docs/TELEGRAM_NOTIFICATION.md b/docs/TELEGRAM_NOTIFICATION.md new file mode 100644 index 00000000..1a274095 --- /dev/null +++ b/docs/TELEGRAM_NOTIFICATION.md @@ -0,0 +1,247 @@ +# Telegram 通知功能使用说明 + +## 功能概述 + +KT财务系统支持通过Telegram Bot向群组或个人发送账目记录通知。每当添加、更新或删除账目时,系统会自动推送消息到配置的Telegram聊天。 + +## 功能特性 + +- ✅ 支持多个Telegram Bot配置 +- ✅ 支持发送到群组或个人 +- ✅ 自动推送新增账目记录 +- ✅ 包含完整的交易信息(类型、金额、分类、账户等) +- ✅ 支持启用/禁用通知 +- ✅ 提供测试功能验证配置 + +## 准备工作 + +### 1. 创建Telegram Bot + +1. 在Telegram中搜索 `@BotFather` +2. 发送 `/newbot` 命令 +3. 按照提示设置Bot名称和用户名 +4. 获取Bot Token(格式:`1234567890:ABCdefGHIjklMNOpqrsTUVwxyz`) + +### 2. 获取Chat ID + +#### 获取个人Chat ID: +1. 在Telegram中搜索 `@userinfobot` +2. 发送任意消息 +3. Bot会返回你的Chat ID + +#### 获取群组Chat ID: +1. 将你的Bot添加到群组 +2. 在群组中发送任意消息 +3. 访问:`https://api.telegram.org/bot/getUpdates` +4. 在返回的JSON中找到 `chat.id` 字段(群组ID通常是负数,如:`-1001234567890`) + +## API接口 + +### 1. 获取所有通知配置 + +```http +GET /api/telegram/notifications +``` + +**响应示例**: +```json +{ + "code": 0, + "data": [ + { + "id": 1, + "name": "财务通知群", + "botToken": "1234567890:ABCdefGHI...", + "chatId": "-1001234567890", + "notificationTypes": ["transaction"], + "isEnabled": true, + "createdAt": "2025-01-01T00:00:00.000Z", + "updatedAt": "2025-01-01T00:00:00.000Z" + } + ] +} +``` + +### 2. 创建通知配置 + +```http +POST /api/telegram/notifications +Content-Type: application/json + +{ + "name": "财务通知群", + "botToken": "1234567890:ABCdefGHI...", + "chatId": "-1001234567890", + "notificationTypes": ["transaction"], + "isEnabled": true +} +``` + +**说明**: +- `name`: 配置名称(必填) +- `botToken`: Telegram Bot Token(必填) +- `chatId`: 目标聊天ID(必填) +- `notificationTypes`: 通知类型数组,目前支持 `["transaction"]`(可选,默认:`["transaction"]`) +- `isEnabled`: 是否启用(可选,默认:`true`) + +**响应示例**: +```json +{ + "code": 0, + "data": { + "id": 1, + "name": "财务通知群", + "botToken": "1234567890:ABCdefGHI...", + "chatId": "-1001234567890", + "notificationTypes": ["transaction"], + "isEnabled": true, + "createdAt": "2025-01-01T00:00:00.000Z", + "updatedAt": "2025-01-01T00:00:00.000Z" + } +} +``` + +**注意**:创建时会自动测试配置,如果Bot Token或Chat ID无效,会返回错误。 + +### 3. 更新通知配置 + +```http +PUT /api/telegram/notifications/:id +Content-Type: application/json + +{ + "name": "更新后的名称", + "isEnabled": false +} +``` + +**说明**:所有字段都是可选的,只更新提供的字段。 + +### 4. 删除通知配置 + +```http +DELETE /api/telegram/notifications/:id +``` + +### 5. 测试Telegram配置 + +```http +POST /api/telegram/test +Content-Type: application/json + +{ + "botToken": "1234567890:ABCdefGHI...", + "chatId": "-1001234567890" +} +``` + +**说明**:发送测试消息验证Bot Token和Chat ID是否有效。 + +## 通知消息格式 + +当添加账目记录时,系统会发送以下格式的消息: + +``` +📋 新增账目记录 + +类型:💸 支出 +金额:CNY 100.00 +日期:2025-01-15 +分类:餐饮 +账户:现金账户 +状态:✅ 已批准 + +备注:午餐费用 + +🕐 记录时间:2025-01-15 14:30:00 +``` + +## 通知类型说明 + +目前支持的通知类型: +- `transaction`: 交易记录通知(新增、更新、删除账目) + +未来可扩展: +- `budget`: 预算提醒 +- `report`: 财务报表 +- `reimbursement`: 报销审批 + +## 常见问题 + +### Q: Bot无法发送消息到群组? +**A**: 请确保: +1. Bot已被添加到群组 +2. Bot在群组中有发送消息的权限 +3. Chat ID正确(群组ID通常是负数) + +### Q: 如何禁用某个配置的通知? +**A**: 调用更新API,设置 `isEnabled: false` + +### Q: 可以配置多个Bot吗? +**A**: 可以!系统支持多个Bot配置,所有启用的配置都会收到通知。 + +### Q: 消息会包含敏感信息吗? +**A**: 消息只包含账目的基本信息(类型、金额、分类等),不包含用户身份等敏感信息。建议使用私密群组。 + +## 技术实现 + +### 数据库表结构 + +```sql +CREATE TABLE 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, -- JSON数组 + is_enabled INTEGER NOT NULL DEFAULT 1, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP +); +``` + +### 后端实现 + +- `apps/backend/utils/telegram-bot.ts`: Telegram Bot核心功能 +- `apps/backend/api/telegram/`: 通知配置管理API +- `apps/backend/api/finance/transactions.post.ts`: 集成通知发送 + +## 示例:使用curl测试 + +```bash +# 1. 测试Bot配置 +curl -X POST http://localhost:3000/api/telegram/test \ + -H "Content-Type: application/json" \ + -d '{ + "botToken": "YOUR_BOT_TOKEN", + "chatId": "YOUR_CHAT_ID" + }' + +# 2. 创建通知配置 +curl -X POST http://localhost:3000/api/telegram/notifications \ + -H "Content-Type: application/json" \ + -d '{ + "name": "测试配置", + "botToken": "YOUR_BOT_TOKEN", + "chatId": "YOUR_CHAT_ID", + "notificationTypes": ["transaction"] + }' + +# 3. 添加账目记录(触发通知) +curl -X POST http://localhost:3000/api/finance/transactions \ + -H "Content-Type: application/json" \ + -d '{ + "type": "expense", + "amount": 100, + "currency": "CNY", + "transactionDate": "2025-01-15", + "description": "测试通知功能" + }' +``` + +## 下一步 + +等你提供Telegram Bot Token后,我们可以: +1. 在前端添加通知配置管理界面 +2. 测试实际的消息发送 +3. 根据需要调整消息格式和内容