From d974e81d1d7b6caf946c9c0fe7ec66439d121730 Mon Sep 17 00:00:00 2001 From: 12975 <1297598740@qq.com> Date: Thu, 29 Jan 2026 23:26:12 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=20AI=20=E8=AE=B0?= =?UTF-8?q?=E8=B4=A6=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/service/ai_bookkeeping_service.go | 88 ++++++++++++++-------- 1 file changed, 58 insertions(+), 30 deletions(-) diff --git a/internal/service/ai_bookkeeping_service.go b/internal/service/ai_bookkeeping_service.go index 3737dd8..2b1593e 100644 --- a/internal/service/ai_bookkeeping_service.go +++ b/internal/service/ai_bookkeeping_service.go @@ -259,6 +259,7 @@ type ConfirmationCard struct { Date string `json:"date"` Note string `json:"note,omitempty"` IsComplete bool `json:"is_complete"` + Warning string `json:"warning,omitempty"` // Budget overrun warning } // AIChatResponse represents the response from AI chat @@ -1456,45 +1457,72 @@ func (s *AIBookkeepingService) ConfirmTransaction(ctx context.Context, sessionID txType = models.TransactionTypeIncome } - // Create transaction - tx := &models.Transaction{ - UserID: userID, - Amount: *params.Amount, - Type: txType, - CategoryID: *params.CategoryID, - AccountID: *params.AccountID, - TransactionDate: txDate, - Note: params.Note, - Currency: "CNY", - } + var transaction *models.Transaction - // Save transaction - if err := s.transactionRepo.Create(tx); err != nil { - return nil, fmt.Errorf("failed to create transaction: %w", err) - } + // Execute within a database transaction to ensure atomicity + err := s.db.Transaction(func(tx *gorm.DB) error { + txAccountRepo := repository.NewAccountRepository(tx) + txTransactionRepo := repository.NewTransactionRepository(tx) + + // Get account first to check balance + account, err := txAccountRepo.GetByID(userID, *params.AccountID) + if err != nil { + return fmt.Errorf("failed to find account: %w", err) + } + + // Calculate new balance + newBalance := account.Balance + if txType == models.TransactionTypeExpense { + newBalance -= *params.Amount + } else { + newBalance += *params.Amount + } + + // Critical Check: Prevent negative balance for non-credit accounts + if !account.IsCredit && newBalance < 0 { + return fmt.Errorf("insufficient balance: account '%s' does not support negative balance (current: %.2f, try: %.2f)", + account.Name, account.Balance, *params.Amount) + } + + // Create transaction model + transaction = &models.Transaction{ + UserID: userID, + Amount: *params.Amount, + Type: txType, + CategoryID: *params.CategoryID, + AccountID: *params.AccountID, + TransactionDate: txDate, + Note: params.Note, + Currency: models.Currency(account.Currency), // Use account currency + } + if transaction.Currency == "" { + transaction.Currency = "CNY" + } + + // Save transaction + if err := txTransactionRepo.Create(transaction); err != nil { + return fmt.Errorf("failed to create transaction: %w", err) + } + + // Update account balance + account.Balance = newBalance + if err := txAccountRepo.Update(account); err != nil { + return fmt.Errorf("failed to update account balance: %w", err) + } + + return nil + }) - // Update account balance - account, err := s.accountRepo.GetByID(userID, *params.AccountID) if err != nil { - return nil, fmt.Errorf("failed to find account: %w", err) + return nil, err } - if txType == models.TransactionTypeExpense { - account.Balance -= *params.Amount - } else { - account.Balance += *params.Amount - } - - if err := s.accountRepo.Update(account); err != nil { - return nil, fmt.Errorf("failed to update account balance: %w", err) - } - - // Clean up session + // Clean up session only on success s.sessionMutex.Lock() delete(s.sessions, sessionID) s.sessionMutex.Unlock() - return tx, nil + return transaction, nil } // GetSession returns session by ID