feat: 初始化核心应用结构,新增循环交易、短信、分配规则和用户设置等服务,并更新相关依赖和配置。

This commit is contained in:
2026-02-02 01:41:51 +08:00
parent 49fcce531d
commit fa8a35a85b
13 changed files with 253 additions and 37 deletions

View File

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

View File

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

View 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
}

View File

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