feat: 引入循环交易管理、邮件通知和用户设置功能
This commit is contained in:
59
internal/service/email_service.go
Normal file
59
internal/service/email_service.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"accounting-app/internal/config"
|
||||
"fmt"
|
||||
"net/smtp"
|
||||
)
|
||||
|
||||
// EmailService handles email notifications
|
||||
type EmailService struct {
|
||||
host string
|
||||
port int
|
||||
username string
|
||||
password string
|
||||
from string
|
||||
enabled bool
|
||||
}
|
||||
|
||||
// NewEmailService creates a new EmailService instance
|
||||
func NewEmailService(cfg *config.Config) *EmailService {
|
||||
if cfg.SMTPHost == "" || cfg.SMTPUser == "" {
|
||||
return &EmailService{enabled: false}
|
||||
}
|
||||
return &EmailService{
|
||||
host: cfg.SMTPHost,
|
||||
port: cfg.SMTPPort,
|
||||
username: cfg.SMTPUser,
|
||||
password: cfg.SMTPPassword,
|
||||
from: cfg.SMTPFrom,
|
||||
enabled: true,
|
||||
}
|
||||
}
|
||||
|
||||
// SendAllocationNotification sends an email notification for auto allocation
|
||||
func (s *EmailService) SendAllocationNotification(to string, amount float64, ruleName string) error {
|
||||
if !s.enabled || to == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
subject := "Auto Allocation Executed"
|
||||
body := fmt.Sprintf("Your automatic allocation rule '%s' has been executed.\nAmount: %.2f", ruleName, amount)
|
||||
|
||||
// Simple text email
|
||||
msg := []byte(fmt.Sprintf("To: %s\r\n"+
|
||||
"From: %s\r\n"+
|
||||
"Subject: %s\r\n"+
|
||||
"Content-Type: text/plain; charset=UTF-8\r\n"+
|
||||
"\r\n"+
|
||||
"%s\r\n", to, s.from, subject, body))
|
||||
|
||||
auth := smtp.PlainAuth("", s.username, s.password, s.host)
|
||||
addr := fmt.Sprintf("%s:%d", s.host, s.port)
|
||||
|
||||
if err := smtp.SendMail(addr, auth, s.from, []string{to}, msg); err != nil {
|
||||
return fmt.Errorf("failed to send email: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -23,6 +23,7 @@ type RecurringTransactionService struct {
|
||||
piggyBankRepo *repository.PiggyBankRepository
|
||||
userSettingsRepo *repository.UserSettingsRepository
|
||||
smsService *SmsService
|
||||
emailService *EmailService
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
@@ -37,6 +38,7 @@ func NewRecurringTransactionService(
|
||||
piggyBankRepo *repository.PiggyBankRepository,
|
||||
userSettingsRepo *repository.UserSettingsRepository,
|
||||
smsService *SmsService,
|
||||
emailService *EmailService,
|
||||
db *gorm.DB,
|
||||
) *RecurringTransactionService {
|
||||
return &RecurringTransactionService{
|
||||
@@ -49,6 +51,7 @@ func NewRecurringTransactionService(
|
||||
piggyBankRepo: piggyBankRepo,
|
||||
userSettingsRepo: userSettingsRepo,
|
||||
smsService: smsService,
|
||||
emailService: emailService,
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
@@ -384,24 +387,46 @@ func (s *RecurringTransactionService) ProcessDueTransactions(userID uint, now ti
|
||||
result.Transactions = append(result.Transactions, transaction)
|
||||
}
|
||||
|
||||
// Send SMS notifications for auto-executed allocations
|
||||
// Send notifications for auto-executed allocations
|
||||
for _, allocation := range result.Allocations {
|
||||
if allocation.AutoExecuted {
|
||||
// Get user phone number
|
||||
// Get user settings
|
||||
userSettings, err := s.userSettingsRepo.GetOrCreate(userID)
|
||||
if err != nil {
|
||||
// Log error but don't fail the request
|
||||
fmt.Printf("Failed to get user settings for SMS: %v\n", err)
|
||||
fmt.Printf("Failed to get user settings for notification: %v\n", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if userSettings != nil && userSettings.Phone != "" && s.smsService != nil {
|
||||
if userSettings == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Determine notification channel
|
||||
channel := userSettings.NotificationChannel
|
||||
if channel == "" {
|
||||
channel = models.NotificationChannelSMS // Default to SMS
|
||||
}
|
||||
|
||||
// Send SMS
|
||||
if (channel == models.NotificationChannelSMS || channel == models.NotificationChannelBoth) &&
|
||||
userSettings.Phone != "" && s.smsService != nil {
|
||||
go func(phone string, amount float64, ruleName string) {
|
||||
if err := s.smsService.SendAllocationNotification(phone, amount, ruleName); err != nil {
|
||||
fmt.Printf("Failed to send allocation SMS: %v\n", err)
|
||||
}
|
||||
}(userSettings.Phone, allocation.TotalAmount, allocation.RuleName)
|
||||
}
|
||||
|
||||
// Send Email
|
||||
if (channel == models.NotificationChannelEmail || channel == models.NotificationChannelBoth) &&
|
||||
userSettings.Email != "" && s.emailService != nil {
|
||||
go func(email string, amount float64, ruleName string) {
|
||||
if err := s.emailService.SendAllocationNotification(email, amount, ruleName); err != nil {
|
||||
fmt.Printf("Failed to send allocation Email: %v\n", err)
|
||||
}
|
||||
}(userSettings.Email, allocation.TotalAmount, allocation.RuleName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,10 +9,11 @@ import (
|
||||
|
||||
// Service layer errors for user settings
|
||||
var (
|
||||
ErrInvalidIconLayout = errors.New("invalid icon layout, must be one of: four, five, six")
|
||||
ErrInvalidImageCompression = errors.New("invalid image compression, must be one of: low, medium, high")
|
||||
ErrDefaultAccountNotFound = errors.New("default account not found")
|
||||
ErrInvalidDefaultAccount = errors.New("invalid default account")
|
||||
ErrInvalidIconLayout = errors.New("invalid icon layout, must be one of: four, five, six")
|
||||
ErrInvalidImageCompression = errors.New("invalid image compression, must be one of: low, medium, high")
|
||||
ErrDefaultAccountNotFound = errors.New("default account not found")
|
||||
ErrInvalidDefaultAccount = errors.New("invalid default account")
|
||||
ErrInvalidNotificationChannel = errors.New("invalid notification channel")
|
||||
)
|
||||
|
||||
// UserSettingsRepositoryInterface defines the interface for user settings repository operations
|
||||
@@ -37,6 +38,8 @@ type UserSettingsInput struct {
|
||||
ShowRefundBtn *bool `json:"show_refund_btn"`
|
||||
CurrentLedgerID *uint `json:"current_ledger_id"`
|
||||
Phone *string `json:"phone"`
|
||||
Email *string `json:"email"`
|
||||
NotificationChannel *string `json:"notification_channel"`
|
||||
}
|
||||
|
||||
// DefaultAccountsInput represents the input data for updating default accounts
|
||||
@@ -153,6 +156,21 @@ func (s *UserSettingsService) UpdateSettings(userID uint, input UserSettingsInpu
|
||||
settings.Phone = *input.Phone
|
||||
}
|
||||
|
||||
if input.Email != nil {
|
||||
settings.Email = *input.Email
|
||||
}
|
||||
|
||||
if input.NotificationChannel != nil {
|
||||
channel := *input.NotificationChannel
|
||||
if channel != models.NotificationChannelSMS &&
|
||||
channel != models.NotificationChannelEmail &&
|
||||
channel != models.NotificationChannelBoth &&
|
||||
channel != models.NotificationChannelNone {
|
||||
return nil, ErrInvalidNotificationChannel
|
||||
}
|
||||
settings.NotificationChannel = channel
|
||||
}
|
||||
|
||||
// Save to database
|
||||
if err := s.repo.Update(settings); err != nil {
|
||||
return nil, fmt.Errorf("failed to update settings: %w", err)
|
||||
|
||||
Reference in New Issue
Block a user