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. 根据需要调整消息格式和内容