feat: 新增 AI 记账服务
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user