feat: 初始化数据库模式并引入AI记账服务。
This commit is contained in:
2
.env
2
.env
@@ -1,3 +1,3 @@
|
|||||||
# Environment Selector
|
# Environment Selector
|
||||||
# Options: dev, prod
|
# Options: dev, prod
|
||||||
APP_ENV=dev
|
APP_ENV=prod
|
||||||
|
|||||||
14
.env.dev
14
.env.dev
@@ -16,20 +16,20 @@ DATA_DIR=./data
|
|||||||
# ============================================
|
# ============================================
|
||||||
# MySQL 数据库配置(必填)
|
# MySQL 数据库配置(必填)
|
||||||
# ============================================
|
# ============================================
|
||||||
# 默认指向本地,如果需要连线上请修改IP
|
# MySQL 数据库配置
|
||||||
DB_HOST=127.0.0.1
|
DB_HOST=124.221.157.197
|
||||||
DB_PORT=3306
|
DB_PORT=3306
|
||||||
DB_USER=root
|
DB_USER=bookkeeping
|
||||||
DB_PASSWORD=root
|
DB_PASSWORD=bookkeeping
|
||||||
DB_NAME=bookkeeping
|
DB_NAME=bookkeeping
|
||||||
# DB_ROOT_PASSWORD=
|
DB_ROOT_PASSWORD=lihuaLIHUA
|
||||||
DB_CHARSET=utf8mb4
|
DB_CHARSET=utf8mb4
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# Redis 配置(可选,用于汇率缓存)
|
# Redis 配置(可选,用于汇率缓存)
|
||||||
# ============================================
|
# ============================================
|
||||||
REDIS_ADDR=127.0.0.1:6379
|
REDIS_ADDR=124.221.157.197:6379
|
||||||
REDIS_PASSWORD=
|
REDIS_PASSWORD=lihua0101LIHUA
|
||||||
REDIS_DB=0
|
REDIS_DB=0
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|||||||
22
database/sql/create_notifications_table.sql
Normal file
22
database/sql/create_notifications_table.sql
Normal file
@@ -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;
|
||||||
@@ -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_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
|
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;
|
) 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;
|
||||||
|
|||||||
@@ -223,17 +223,22 @@ type ChatCompletionResponse struct {
|
|||||||
} `json:"choices"`
|
} `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
|
// ParseIntent extracts transaction parameters from text
|
||||||
// Requirements: 7.1, 7.5, 7.6
|
// Requirements: 7.1, 7.5, 7.6
|
||||||
func (s *LLMService) ParseIntent(ctx context.Context, text string, history []ChatMessage) (*AITransactionParams, string, error) {
|
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 == "" {
|
if s.config.OpenAIAPIKey == "" || s.config.OpenAIBaseURL == "" {
|
||||||
// No API key, return simple parsing result
|
// 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
|
// Build messages with history
|
||||||
|
var systemPrompt string
|
||||||
|
|
||||||
|
// 检查是否有自定义 System/Persona prompt(用于财务建议等场景)
|
||||||
|
// 如果有,直接使用自定义 prompt 覆盖默认记账 prompt
|
||||||
|
if customPrompt := extractCustomPrompt(text); customPrompt != "" {
|
||||||
|
systemPrompt = customPrompt
|
||||||
|
} else {
|
||||||
|
// 使用默认的记账 prompt
|
||||||
todayDate := time.Now().Format("2006-01-02")
|
todayDate := time.Now().Format("2006-01-02")
|
||||||
systemPrompt := fmt.Sprintf(`你是一个智能记账助手。从用户描述中提取记账信息<EFBFBD>?
|
systemPrompt = fmt.Sprintf(`你是一个智能记账助手。从用户描述中提取记账信息。
|
||||||
|
|
||||||
今天的日期是<EFBFBD>?s
|
今天的日期是%s
|
||||||
|
|
||||||
规则<EFBFBD>?
|
规则:
|
||||||
1. 金额:提取数字,<EFBFBD>?6<>?=6<>?十五<E58D81>?=15
|
1. 金额:提取数字,如"6元"=6,"十五"=15
|
||||||
2. 分类:根据内容推断,<EFBFBD>?奶茶/咖啡/吃饭"=餐饮<EFBFBD>?打车/地铁"=交通,"买衣<EFBFBD>?=购物
|
2. 分类:根据内容推断,如"奶茶/咖啡/吃饭"=餐饮,"打车/地铁"=交通,"买衣服"=购物
|
||||||
3. 类型:默认expense(支出),除非明确说"收入/工资/奖金/红包"
|
3. 类型:默认expense(支出),除非明确说"收入/工资/奖金/红包"
|
||||||
4. 日期:默认使用今天的日期<EFBFBD>?s),除非用户明确指定其他日期
|
4. 日期:默认使用今天的日期(%s),除非用户明确指定其他日期
|
||||||
5. 备注:提取关键描<EFBFBD>?
|
5. 备注:提取关键描述
|
||||||
|
|
||||||
直接返回JSON,不要解释:
|
直接返回JSON,不要解释:
|
||||||
{"amount":数字,"category":"分类","type":"expense或income","note":"备注","date":"YYYY-MM-DD","message":"简短确<EFBFBD>?}
|
{"amount":数字,"category":"分类","type":"expense或income","note":"备注","date":"YYYY-MM-DD","message":"简短确认"}
|
||||||
|
|
||||||
示例(假设今天是%s):
|
示例(假设今天是%s):
|
||||||
用户<EFBFBD>?买了<E4B9B0>?块的奶茶"
|
用户:"买了6块的奶茶"
|
||||||
返回:{"amount":6,"category":"餐饮","type":"expense","note":"奶茶","date":"%s","message":"记录:餐饮支<EFBFBD>?元,奶茶"}`, todayDate, todayDate, todayDate, todayDate)
|
返回:{"amount":6,"category":"餐饮","type":"expense","note":"奶茶","date":"%s","message":"记录:餐饮支出6元,奶茶"}`, todayDate, todayDate, todayDate, todayDate)
|
||||||
|
}
|
||||||
|
|
||||||
messages := []ChatMessage{
|
messages := []ChatMessage{
|
||||||
{
|
{
|
||||||
@@ -720,7 +734,7 @@ func (s *AIBookkeepingService) ProcessChat(ctx context.Context, userID uint, ses
|
|||||||
// All params complete, generate confirmation card
|
// All params complete, generate confirmation card
|
||||||
card := s.GenerateConfirmationCard(session)
|
card := s.GenerateConfirmationCard(session)
|
||||||
response.ConfirmationCard = card
|
response.ConfirmationCard = card
|
||||||
response.Message = fmt.Sprintf("请确认:%s %.2f元,分类<EFBFBD>?s,账户:%s",
|
response.Message = fmt.Sprintf("请确认:%s %.2f元,分类:%s,账户:%s",
|
||||||
s.getTypeLabel(session.Params.Type),
|
s.getTypeLabel(session.Params.Type),
|
||||||
*session.Params.Amount,
|
*session.Params.Amount,
|
||||||
session.Params.Category,
|
session.Params.Category,
|
||||||
|
|||||||
Reference in New Issue
Block a user