feat: 新增 AI 记账服务

This commit is contained in:
2026-01-29 23:26:12 +08:00
parent 991453b10d
commit d974e81d1d

View File

@@ -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