feat: 初始化核心应用结构,新增循环交易、短信、分配规则和用户设置等服务,并更新相关依赖和配置。
This commit is contained in:
@@ -31,6 +31,7 @@ type AllocationRuleInput struct {
|
||||
TriggerType models.TriggerType `json:"trigger_type" binding:"required"`
|
||||
SourceAccountID *uint `json:"source_account_id,omitempty"` // 触发分配的源账户
|
||||
IsActive bool `json:"is_active"`
|
||||
AutoExecute bool `json:"auto_execute"`
|
||||
Targets []AllocationTargetInput `json:"targets" binding:"required,min=1"`
|
||||
}
|
||||
|
||||
@@ -50,6 +51,7 @@ type AllocationResult struct {
|
||||
AllocatedAmount float64 `json:"allocated_amount"`
|
||||
Remaining float64 `json:"remaining"`
|
||||
Allocations []AllocationDetail `json:"allocations"`
|
||||
AutoExecuted bool `json:"auto_executed"`
|
||||
}
|
||||
|
||||
// AllocationDetail 单个分配目标的详情
|
||||
@@ -114,6 +116,7 @@ func (s *AllocationRuleService) CreateAllocationRule(input AllocationRuleInput)
|
||||
TriggerType: input.TriggerType,
|
||||
SourceAccountID: input.SourceAccountID,
|
||||
IsActive: input.IsActive,
|
||||
AutoExecute: input.AutoExecute,
|
||||
}
|
||||
|
||||
// 开始数据库事务
|
||||
@@ -227,6 +230,7 @@ func (s *AllocationRuleService) UpdateAllocationRule(userID, id uint, input Allo
|
||||
rule.TriggerType = input.TriggerType
|
||||
rule.SourceAccountID = input.SourceAccountID
|
||||
rule.IsActive = input.IsActive
|
||||
rule.AutoExecute = input.AutoExecute
|
||||
|
||||
// 保存规则
|
||||
if err := tx.Save(rule).Error; err != nil {
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// RecurringTransactionService handles business logic for recurring transactions
|
||||
// RecurringTransactionService handles business logic for recurring transactions
|
||||
type RecurringTransactionService struct {
|
||||
recurringRepo *repository.RecurringTransactionRepository
|
||||
@@ -20,6 +21,8 @@ type RecurringTransactionService struct {
|
||||
allocationRuleRepo *repository.AllocationRuleRepository
|
||||
recordRepo *repository.AllocationRecordRepository
|
||||
piggyBankRepo *repository.PiggyBankRepository
|
||||
userSettingsRepo *repository.UserSettingsRepository
|
||||
smsService *SmsService
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
@@ -32,6 +35,8 @@ func NewRecurringTransactionService(
|
||||
allocationRuleRepo *repository.AllocationRuleRepository,
|
||||
recordRepo *repository.AllocationRecordRepository,
|
||||
piggyBankRepo *repository.PiggyBankRepository,
|
||||
userSettingsRepo *repository.UserSettingsRepository,
|
||||
smsService *SmsService,
|
||||
db *gorm.DB,
|
||||
) *RecurringTransactionService {
|
||||
return &RecurringTransactionService{
|
||||
@@ -42,6 +47,8 @@ func NewRecurringTransactionService(
|
||||
allocationRuleRepo: allocationRuleRepo,
|
||||
recordRepo: recordRepo,
|
||||
piggyBankRepo: piggyBankRepo,
|
||||
userSettingsRepo: userSettingsRepo,
|
||||
smsService: smsService,
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
@@ -377,6 +384,27 @@ func (s *RecurringTransactionService) ProcessDueTransactions(userID uint, now ti
|
||||
result.Transactions = append(result.Transactions, transaction)
|
||||
}
|
||||
|
||||
// Send SMS notifications for auto-executed allocations
|
||||
for _, allocation := range result.Allocations {
|
||||
if allocation.AutoExecuted {
|
||||
// Get user phone number
|
||||
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)
|
||||
continue
|
||||
}
|
||||
|
||||
if userSettings != nil && 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -429,7 +457,7 @@ func (s *RecurringTransactionService) applyAllocationRulesForIncome(userID uint,
|
||||
continue
|
||||
}
|
||||
|
||||
// Get target name and apply allocation
|
||||
// Get target name
|
||||
targetName := ""
|
||||
|
||||
switch target.TargetType {
|
||||
@@ -440,20 +468,23 @@ func (s *RecurringTransactionService) applyAllocationRulesForIncome(userID uint,
|
||||
}
|
||||
targetName = targetAccount.Name
|
||||
|
||||
// Add to target account
|
||||
targetAccount.Balance += allocatedAmount
|
||||
if err := tx.Save(&targetAccount).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to update target account balance: %w", err)
|
||||
}
|
||||
// Execute transfer only if AutoExecute is true
|
||||
if rule.AutoExecute {
|
||||
// Add to target account
|
||||
targetAccount.Balance += allocatedAmount
|
||||
if err := tx.Save(&targetAccount).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to update target account balance: %w", err)
|
||||
}
|
||||
|
||||
// Deduct from source account
|
||||
var sourceAccount models.Account
|
||||
if err := tx.First(&sourceAccount, accountID).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to get source account: %w", err)
|
||||
}
|
||||
sourceAccount.Balance -= allocatedAmount
|
||||
if err := tx.Save(&sourceAccount).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to update source account balance: %w", err)
|
||||
// Deduct from source account
|
||||
var sourceAccount models.Account
|
||||
if err := tx.First(&sourceAccount, accountID).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to get source account: %w", err)
|
||||
}
|
||||
sourceAccount.Balance -= allocatedAmount
|
||||
if err := tx.Save(&sourceAccount).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to update source account balance: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
case models.TargetTypePiggyBank:
|
||||
@@ -463,20 +494,23 @@ func (s *RecurringTransactionService) applyAllocationRulesForIncome(userID uint,
|
||||
}
|
||||
targetName = piggyBank.Name
|
||||
|
||||
// Add to piggy bank
|
||||
piggyBank.CurrentAmount += allocatedAmount
|
||||
if err := tx.Save(&piggyBank).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to update piggy bank balance: %w", err)
|
||||
}
|
||||
// Execute transfer only if AutoExecute is true
|
||||
if rule.AutoExecute {
|
||||
// Add to piggy bank
|
||||
piggyBank.CurrentAmount += allocatedAmount
|
||||
if err := tx.Save(&piggyBank).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to update piggy bank balance: %w", err)
|
||||
}
|
||||
|
||||
// Deduct from source account
|
||||
var sourceAccount models.Account
|
||||
if err := tx.First(&sourceAccount, accountID).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to get source account: %w", err)
|
||||
}
|
||||
sourceAccount.Balance -= allocatedAmount
|
||||
if err := tx.Save(&sourceAccount).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to update source account balance: %w", err)
|
||||
// Deduct from source account
|
||||
var sourceAccount models.Account
|
||||
if err := tx.First(&sourceAccount, accountID).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to get source account: %w", err)
|
||||
}
|
||||
sourceAccount.Balance -= allocatedAmount
|
||||
if err := tx.Save(&sourceAccount).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to update source account balance: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
|
||||
74
internal/service/sms_service.go
Normal file
74
internal/service/sms_service.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"accounting-app/internal/config"
|
||||
|
||||
"github.com/aliyun/alibaba-cloud-sdk-go/services/dysmsapi"
|
||||
)
|
||||
|
||||
// SmsService handles SMS notifications
|
||||
type SmsService struct {
|
||||
client *dysmsapi.Client
|
||||
signName string
|
||||
templateCode string
|
||||
enabled bool
|
||||
}
|
||||
|
||||
// NewSmsService creates a new SmsService instance
|
||||
func NewSmsService(cfg *config.Config) *SmsService {
|
||||
if cfg.AliyunAccessKeyID == "" || cfg.AliyunAccessKeySecret == "" {
|
||||
log.Println("Aliyun SMS configuration missing, SMS service disabled")
|
||||
return &SmsService{enabled: false}
|
||||
}
|
||||
|
||||
client, err := dysmsapi.NewClientWithAccessKey("cn-hangzhou", cfg.AliyunAccessKeyID, cfg.AliyunAccessKeySecret)
|
||||
if err != nil {
|
||||
log.Printf("Failed to initialize Aliyun SMS client: %v", err)
|
||||
return &SmsService{enabled: false}
|
||||
}
|
||||
|
||||
return &SmsService{
|
||||
client: client,
|
||||
signName: cfg.AliyunSignName,
|
||||
templateCode: cfg.AliyunTemplateCode,
|
||||
enabled: true,
|
||||
}
|
||||
}
|
||||
|
||||
// SendAllocationNotification sends an SMS notification for auto allocation
|
||||
func (s *SmsService) SendAllocationNotification(phoneNumber string, totalAmount float64, ruleName string) error {
|
||||
if !s.enabled || phoneNumber == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
request := dysmsapi.CreateSendSmsRequest()
|
||||
request.Scheme = "https"
|
||||
request.PhoneNumbers = phoneNumber
|
||||
request.SignName = s.signName
|
||||
request.TemplateCode = s.templateCode
|
||||
|
||||
// Template params: {"amount": "1000", "rule": "Salary Allocation"}
|
||||
// You might need to adjust parameters based on your actual Aliyun template
|
||||
params := map[string]string{
|
||||
"amount": fmt.Sprintf("%.2f", totalAmount),
|
||||
"rule": ruleName,
|
||||
}
|
||||
paramBytes, _ := json.Marshal(params)
|
||||
request.TemplateParam = string(paramBytes)
|
||||
|
||||
response, err := s.client.SendSms(request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to send SMS: %w", err)
|
||||
}
|
||||
|
||||
if response.Code != "OK" {
|
||||
return fmt.Errorf("aliyun SMS error: %s - %s", response.Code, response.Message)
|
||||
}
|
||||
|
||||
log.Printf("SMS sent successfully to %s", phoneNumber)
|
||||
return nil
|
||||
}
|
||||
@@ -36,6 +36,7 @@ type UserSettingsInput struct {
|
||||
ShowReimbursementBtn *bool `json:"show_reimbursement_btn"`
|
||||
ShowRefundBtn *bool `json:"show_refund_btn"`
|
||||
CurrentLedgerID *uint `json:"current_ledger_id"`
|
||||
Phone *string `json:"phone"`
|
||||
}
|
||||
|
||||
// DefaultAccountsInput represents the input data for updating default accounts
|
||||
@@ -148,6 +149,10 @@ func (s *UserSettingsService) UpdateSettings(userID uint, input UserSettingsInpu
|
||||
settings.CurrentLedgerID = input.CurrentLedgerID
|
||||
}
|
||||
|
||||
if input.Phone != nil {
|
||||
settings.Phone = *input.Phone
|
||||
}
|
||||
|
||||
// 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