From 57def08201c9e16a0b392e0bdf41826caa55a7c2 Mon Sep 17 00:00:00 2001 From: admin <1297598740@qq.com> Date: Wed, 28 Jan 2026 15:46:21 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=88=9D=E5=A7=8B=E5=8C=96=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=BA=93=E6=A8=A1=E5=BC=8F=E5=B9=B6=E5=BC=95=E5=85=A5?= =?UTF-8?q?AI=E8=AE=B0=E8=B4=A6=E6=9C=8D=E5=8A=A1=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 2 +- .env.dev | 14 +++--- database/sql/create_notifications_table.sql | 22 +++++++++ database/sql/schema.sql | 22 +++++++++ internal/service/ai_bookkeeping_service.go | 54 +++++++++++++-------- 5 files changed, 86 insertions(+), 28 deletions(-) create mode 100644 database/sql/create_notifications_table.sql diff --git a/.env b/.env index 4ee4711..fcc97d1 100644 --- a/.env +++ b/.env @@ -1,3 +1,3 @@ # Environment Selector # Options: dev, prod -APP_ENV=dev +APP_ENV=prod diff --git a/.env.dev b/.env.dev index 9e0c4fb..23b961f 100644 --- a/.env.dev +++ b/.env.dev @@ -16,20 +16,20 @@ DATA_DIR=./data # ============================================ # MySQL 数据库配置(必填) # ============================================ -# 默认指向本地,如果需要连线上请修改IP -DB_HOST=127.0.0.1 +# MySQL 数据库配置 +DB_HOST=124.221.157.197 DB_PORT=3306 -DB_USER=root -DB_PASSWORD=root +DB_USER=bookkeeping +DB_PASSWORD=bookkeeping DB_NAME=bookkeeping -# DB_ROOT_PASSWORD= +DB_ROOT_PASSWORD=lihuaLIHUA DB_CHARSET=utf8mb4 # ============================================ # Redis 配置(可选,用于汇率缓存) # ============================================ -REDIS_ADDR=127.0.0.1:6379 -REDIS_PASSWORD= +REDIS_ADDR=124.221.157.197:6379 +REDIS_PASSWORD=lihua0101LIHUA REDIS_DB=0 # ============================================ diff --git a/database/sql/create_notifications_table.sql b/database/sql/create_notifications_table.sql new file mode 100644 index 0000000..ad3c977 --- /dev/null +++ b/database/sql/create_notifications_table.sql @@ -0,0 +1,22 @@ +-- Notifications table +CREATE TABLE IF NOT EXISTS `notifications` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, + `created_at` datetime(3) DEFAULT NULL, + `updated_at` datetime(3) DEFAULT NULL, + `deleted_at` datetime(3) DEFAULT NULL, + `user_id` bigint(20) unsigned NOT NULL, + `title` varchar(200) COLLATE utf8mb4_unicode_ci NOT NULL, + `content` text COLLATE utf8mb4_unicode_ci NOT NULL, + `type` varchar(30) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'system', + `is_read` tinyint(1) DEFAULT '0', + `link` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `read_at` datetime(3) DEFAULT NULL, + `related_id` bigint(20) unsigned DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `idx_notifications_deleted_at` (`deleted_at`), + KEY `idx_notifications_user_id` (`user_id`), + KEY `idx_notifications_type` (`type`), + KEY `idx_notifications_is_read` (`is_read`), + KEY `idx_notifications_related_id` (`related_id`), + CONSTRAINT `fk_users_notifications` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/database/sql/schema.sql b/database/sql/schema.sql index 369a560..88332d5 100644 --- a/database/sql/schema.sql +++ b/database/sql/schema.sql @@ -553,3 +553,25 @@ CREATE TABLE IF NOT EXISTS `user_settings` ( CONSTRAINT `fk_user_settings_default_expense` FOREIGN KEY (`default_expense_account_id`) REFERENCES `accounts` (`id`) ON DELETE SET NULL ON UPDATE CASCADE, CONSTRAINT `fk_user_settings_default_income` FOREIGN KEY (`default_income_account_id`) REFERENCES `accounts` (`id`) ON DELETE SET NULL ON UPDATE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +-- Notifications table +CREATE TABLE IF NOT EXISTS `notifications` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, + `created_at` datetime(3) DEFAULT NULL, + `updated_at` datetime(3) DEFAULT NULL, + `deleted_at` datetime(3) DEFAULT NULL, + `user_id` bigint(20) unsigned NOT NULL, + `title` varchar(200) COLLATE utf8mb4_unicode_ci NOT NULL, + `content` text COLLATE utf8mb4_unicode_ci NOT NULL, + `type` varchar(30) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'system', + `is_read` tinyint(1) DEFAULT '0', + `link` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `read_at` datetime(3) DEFAULT NULL, + `related_id` bigint(20) unsigned DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `idx_notifications_deleted_at` (`deleted_at`), + KEY `idx_notifications_user_id` (`user_id`), + KEY `idx_notifications_type` (`type`), + KEY `idx_notifications_is_read` (`is_read`), + KEY `idx_notifications_related_id` (`related_id`), + CONSTRAINT `fk_users_notifications` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/internal/service/ai_bookkeeping_service.go b/internal/service/ai_bookkeeping_service.go index d88888e..dfc35c9 100644 --- a/internal/service/ai_bookkeeping_service.go +++ b/internal/service/ai_bookkeeping_service.go @@ -223,17 +223,22 @@ type ChatCompletionResponse struct { } `json:"choices"` } +// extractCustomPrompt 从用户消息中提取自定义 System/Persona prompt +// 如果消息包含 "System:" 或 "Persona:",则提取其后的内容作为自定义 prompt +func extractCustomPrompt(text string) string { + prefixes := []string{"System:", "Persona:"} + for _, prefix := range prefixes { + if idx := strings.Index(text, prefix); idx != -1 { + // 提取 prefix 后的内容作为自定义 prompt + return strings.TrimSpace(text[idx+len(prefix):]) + } + } + return "" +} + // ParseIntent extracts transaction parameters from text // Requirements: 7.1, 7.5, 7.6 func (s *LLMService) ParseIntent(ctx context.Context, text string, history []ChatMessage) (*AITransactionParams, string, error) { - // Fast path: try simple parsing first for common patterns - // This avoids LLM call for simple inputs like "6块钱奶茶" - // TODO: 暂时禁用本地解析快速路径,始终使用 LLM - // simpleParams, simpleMsg, _ := s.parseIntentSimple(text) - // if simpleParams != nil && simpleParams.Amount != nil && simpleParams.Category != "" && simpleParams.Category != "其他" { - // // Simple parsing succeeded with amount and category, use it directly - // return simpleParams, simpleMsg, nil - // } if s.config.OpenAIAPIKey == "" || s.config.OpenAIBaseURL == "" { // No API key, return simple parsing result @@ -242,24 +247,33 @@ func (s *LLMService) ParseIntent(ctx context.Context, text string, history []Cha } // Build messages with history - todayDate := time.Now().Format("2006-01-02") - systemPrompt := fmt.Sprintf(`你是一个智能记账助手。从用户描述中提取记账信息�? + var systemPrompt string -今天的日期是�?s + // 检查是否有自定义 System/Persona prompt(用于财务建议等场景) + // 如果有,直接使用自定义 prompt 覆盖默认记账 prompt + if customPrompt := extractCustomPrompt(text); customPrompt != "" { + systemPrompt = customPrompt + } else { + // 使用默认的记账 prompt + todayDate := time.Now().Format("2006-01-02") + systemPrompt = fmt.Sprintf(`你是一个智能记账助手。从用户描述中提取记账信息。 -规则�? -1. 金额:提取数字,�?6�?=6�?十五�?=15 -2. 分类:根据内容推断,�?奶茶/咖啡/吃饭"=餐饮�?打车/地铁"=交通,"买衣�?=购物 +今天的日期是%s + +规则: +1. 金额:提取数字,如"6元"=6,"十五"=15 +2. 分类:根据内容推断,如"奶茶/咖啡/吃饭"=餐饮,"打车/地铁"=交通,"买衣服"=购物 3. 类型:默认expense(支出),除非明确说"收入/工资/奖金/红包" -4. 日期:默认使用今天的日期�?s),除非用户明确指定其他日期 -5. 备注:提取关键描�? +4. 日期:默认使用今天的日期(%s),除非用户明确指定其他日期 +5. 备注:提取关键描述 直接返回JSON,不要解释: -{"amount":数字,"category":"分类","type":"expense或income","note":"备注","date":"YYYY-MM-DD","message":"简短确�?} +{"amount":数字,"category":"分类","type":"expense或income","note":"备注","date":"YYYY-MM-DD","message":"简短确认"} 示例(假设今天是%s): -用户�?买了�?块的奶茶" -返回:{"amount":6,"category":"餐饮","type":"expense","note":"奶茶","date":"%s","message":"记录:餐饮支�?元,奶茶"}`, todayDate, todayDate, todayDate, todayDate) +用户:"买了6块的奶茶" +返回:{"amount":6,"category":"餐饮","type":"expense","note":"奶茶","date":"%s","message":"记录:餐饮支出6元,奶茶"}`, todayDate, todayDate, todayDate, todayDate) + } messages := []ChatMessage{ { @@ -720,7 +734,7 @@ func (s *AIBookkeepingService) ProcessChat(ctx context.Context, userID uint, ses // All params complete, generate confirmation card card := s.GenerateConfirmationCard(session) response.ConfirmationCard = card - response.Message = fmt.Sprintf("请确认:%s %.2f元,分类�?s,账户:%s", + response.Message = fmt.Sprintf("请确认:%s %.2f元,分类:%s,账户:%s", s.getTypeLabel(session.Params.Type), *session.Params.Amount, session.Params.Category,