feat: 新增 AI 记账服务
This commit is contained in:
@@ -259,6 +259,7 @@ type ConfirmationCard struct {
|
|||||||
Date string `json:"date"`
|
Date string `json:"date"`
|
||||||
Note string `json:"note,omitempty"`
|
Note string `json:"note,omitempty"`
|
||||||
IsComplete bool `json:"is_complete"`
|
IsComplete bool `json:"is_complete"`
|
||||||
|
Warning string `json:"warning,omitempty"` // Budget overrun warning
|
||||||
}
|
}
|
||||||
|
|
||||||
// AIChatResponse represents the response from AI chat
|
// AIChatResponse represents the response from AI chat
|
||||||
@@ -1456,45 +1457,72 @@ func (s *AIBookkeepingService) ConfirmTransaction(ctx context.Context, sessionID
|
|||||||
txType = models.TransactionTypeIncome
|
txType = models.TransactionTypeIncome
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create transaction
|
var transaction *models.Transaction
|
||||||
tx := &models.Transaction{
|
|
||||||
UserID: userID,
|
|
||||||
Amount: *params.Amount,
|
|
||||||
Type: txType,
|
|
||||||
CategoryID: *params.CategoryID,
|
|
||||||
AccountID: *params.AccountID,
|
|
||||||
TransactionDate: txDate,
|
|
||||||
Note: params.Note,
|
|
||||||
Currency: "CNY",
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save transaction
|
// Execute within a database transaction to ensure atomicity
|
||||||
if err := s.transactionRepo.Create(tx); err != nil {
|
err := s.db.Transaction(func(tx *gorm.DB) error {
|
||||||
return nil, fmt.Errorf("failed to create transaction: %w", err)
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to find account: %w", err)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if txType == models.TransactionTypeExpense {
|
// Clean up session only on success
|
||||||
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
|
|
||||||
s.sessionMutex.Lock()
|
s.sessionMutex.Lock()
|
||||||
delete(s.sessions, sessionID)
|
delete(s.sessions, sessionID)
|
||||||
s.sessionMutex.Unlock()
|
s.sessionMutex.Unlock()
|
||||||
|
|
||||||
return tx, nil
|
return transaction, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSession returns session by ID
|
// GetSession returns session by ID
|
||||||
|
|||||||
Reference in New Issue
Block a user