package service import ( "errors" "fmt" "accounting-app/internal/models" "accounting-app/internal/repository" "gorm.io/gorm" ) // 分配规则服务层错误定义 var ( ErrAllocationRuleNotFound = errors.New("分配规则不存在") ErrAllocationRuleInUse = errors.New("分配规则正在使用中,无法删除") ErrInvalidTriggerType = errors.New("无效的触发类型") ErrInvalidTargetType = errors.New("无效的目标类型") ErrInvalidAllocationPercentage = errors.New("分配百分比必须在0-100之间") ErrInvalidAllocationAmount = errors.New("分配金额必须为正数") ErrInvalidAllocationTarget = errors.New("分配目标必须有百分比或固定金额") ErrTotalPercentageExceeds100 = errors.New("分配百分比总和超过100%") ErrTargetNotFound = errors.New("目标账户或存钱罐不存在") ErrInsufficientAmount = errors.New("分配金额不足") ) // AllocationRuleInput 创建或更新分配规则的输入数据 type AllocationRuleInput struct { UserID uint `json:"user_id"` Name string `json:"name" binding:"required"` TriggerType models.TriggerType `json:"trigger_type" binding:"required"` SourceAccountID *uint `json:"source_account_id,omitempty"` // 触发分配的源账户 IsActive bool `json:"is_active"` Targets []AllocationTargetInput `json:"targets" binding:"required,min=1"` } // AllocationTargetInput 分配目标的输入数据 type AllocationTargetInput struct { TargetType models.TargetType `json:"target_type" binding:"required"` TargetID uint `json:"target_id" binding:"required"` Percentage *float64 `json:"percentage,omitempty"` FixedAmount *float64 `json:"fixed_amount,omitempty"` } // AllocationResult 应用分配规则的结果 type AllocationResult struct { RuleID uint `json:"rule_id"` RuleName string `json:"rule_name"` TotalAmount float64 `json:"total_amount"` AllocatedAmount float64 `json:"allocated_amount"` Remaining float64 `json:"remaining"` Allocations []AllocationDetail `json:"allocations"` } // AllocationDetail 单个分配目标的详情 type AllocationDetail struct { TargetType models.TargetType `json:"target_type"` TargetID uint `json:"target_id"` TargetName string `json:"target_name"` Amount float64 `json:"amount"` Percentage *float64 `json:"percentage,omitempty"` FixedAmount *float64 `json:"fixed_amount,omitempty"` } // ApplyAllocationInput 应用分配规则的输入数据 type ApplyAllocationInput struct { Amount float64 `json:"amount" binding:"required,gt=0"` FromAccountID *uint `json:"from_account_id,omitempty"` Note string `json:"note,omitempty"` } // AllocationRuleService 分配规则业务逻辑服务 type AllocationRuleService struct { repo *repository.AllocationRuleRepository recordRepo *repository.AllocationRecordRepository accountRepo *repository.AccountRepository piggyBankRepo *repository.PiggyBankRepository db *gorm.DB } // NewAllocationRuleService 创建分配规则服务实例 func NewAllocationRuleService( repo *repository.AllocationRuleRepository, recordRepo *repository.AllocationRecordRepository, accountRepo *repository.AccountRepository, piggyBankRepo *repository.PiggyBankRepository, db *gorm.DB, ) *AllocationRuleService { return &AllocationRuleService{ repo: repo, recordRepo: recordRepo, accountRepo: accountRepo, piggyBankRepo: piggyBankRepo, db: db, } } // CreateAllocationRule 创建新的分配规则(带业务逻辑验证) func (s *AllocationRuleService) CreateAllocationRule(input AllocationRuleInput) (*models.AllocationRule, error) { // 验证触发类型 if !isValidTriggerType(input.TriggerType) { return nil, ErrInvalidTriggerType } // 验证分配目标 if err := s.validateTargets(input.UserID, input.Targets); err != nil { return nil, err } // 创建分配规则模型 rule := &models.AllocationRule{ UserID: input.UserID, Name: input.Name, TriggerType: input.TriggerType, SourceAccountID: input.SourceAccountID, IsActive: input.IsActive, } // 开始数据库事务 tx := s.db.Begin() defer func() { if r := recover(); r != nil { tx.Rollback() } }() // 保存规则到数据库 if err := tx.Create(rule).Error; err != nil { tx.Rollback() return nil, fmt.Errorf("创建分配规则失败: %w", err) } // 创建分配目标 for _, targetInput := range input.Targets { target := &models.AllocationTarget{ RuleID: rule.ID, TargetType: targetInput.TargetType, TargetID: targetInput.TargetID, Percentage: targetInput.Percentage, FixedAmount: targetInput.FixedAmount, } if err := tx.Create(target).Error; err != nil { tx.Rollback() return nil, fmt.Errorf("创建分配目标失败: %w", err) } } // 提交事务 if err := tx.Commit().Error; err != nil { return nil, fmt.Errorf("提交事务失败: %w", err) } // 重新加载规则(包含目标) // Re-fetch the rule to include targets var err error rule, err = s.repo.GetByID(input.UserID, rule.ID) if err != nil { return nil, fmt.Errorf("重新加载分配规则失败: %w", err) } return rule, nil } // GetAllocationRule 根据ID获取分配规则 func (s *AllocationRuleService) GetAllocationRule(userID, id uint) (*models.AllocationRule, error) { rule, err := s.repo.GetByID(userID, id) if err != nil { if errors.Is(err, repository.ErrAllocationRuleNotFound) { return nil, ErrAllocationRuleNotFound } return nil, fmt.Errorf("获取分配规则失败: %w", err) } // userID check handled by repo return rule, nil } // GetAllAllocationRules 获取所有分配规则 func (s *AllocationRuleService) GetAllAllocationRules(userID uint) ([]models.AllocationRule, error) { rules, err := s.repo.GetAll(userID) if err != nil { return nil, fmt.Errorf("获取分配规则列表失败: %w", err) } return rules, nil } // GetActiveAllocationRules 获取所有启用的分配规则 func (s *AllocationRuleService) GetActiveAllocationRules(userID uint) ([]models.AllocationRule, error) { rules, err := s.repo.GetActive(userID) if err != nil { return nil, fmt.Errorf("获取启用的分配规则失败: %w", err) } return rules, nil } // UpdateAllocationRule 更新现有的分配规则 func (s *AllocationRuleService) UpdateAllocationRule(userID, id uint, input AllocationRuleInput) (*models.AllocationRule, error) { // 获取现有规则 rule, err := s.repo.GetByID(userID, id) if err != nil { if errors.Is(err, repository.ErrAllocationRuleNotFound) { return nil, ErrAllocationRuleNotFound } return nil, fmt.Errorf("获取分配规则失败: %w", err) } // userID check handled by repo // 验证触发类型 if !isValidTriggerType(input.TriggerType) { return nil, ErrInvalidTriggerType } // 验证分配目标 if err := s.validateTargets(userID, input.Targets); err != nil { return nil, err } // 开始数据库事务 tx := s.db.Begin() defer func() { if r := recover(); r != nil { tx.Rollback() } }() // 更新规则字段 rule.Name = input.Name rule.TriggerType = input.TriggerType rule.SourceAccountID = input.SourceAccountID rule.IsActive = input.IsActive // 保存规则 if err := tx.Save(rule).Error; err != nil { tx.Rollback() return nil, fmt.Errorf("更新分配规则失败: %w", err) } // 删除现有目标 if err := tx.Where("rule_id = ?", id).Delete(&models.AllocationTarget{}).Error; err != nil { tx.Rollback() return nil, fmt.Errorf("删除现有目标失败: %w", err) } // 创建新目标 for _, targetInput := range input.Targets { target := &models.AllocationTarget{ RuleID: rule.ID, TargetType: targetInput.TargetType, TargetID: targetInput.TargetID, Percentage: targetInput.Percentage, FixedAmount: targetInput.FixedAmount, } if err := tx.Create(target).Error; err != nil { tx.Rollback() return nil, fmt.Errorf("创建分配目标失败: %w", err) } } // 提交事务 if err := tx.Commit().Error; err != nil { return nil, fmt.Errorf("提交事务失败: %w", err) } // 重新加载规则(包含目标) rule, err = s.repo.GetByID(userID, rule.ID) if err != nil { return nil, fmt.Errorf("重新加载分配规则失败: %w", err) } return rule, nil } // DeleteAllocationRule 根据ID删除分配规则 func (s *AllocationRuleService) DeleteAllocationRule(userID, id uint) error { _, err := s.repo.GetByID(userID, id) if err != nil { if errors.Is(err, repository.ErrAllocationRuleNotFound) { return ErrAllocationRuleNotFound } return err } // userID check handled by repo err = s.repo.Delete(userID, id) if err != nil { if errors.Is(err, repository.ErrAllocationRuleNotFound) { return ErrAllocationRuleNotFound } if errors.Is(err, repository.ErrAllocationRuleInUse) { return ErrAllocationRuleInUse } return fmt.Errorf("删除分配规则失败: %w", err) } return nil } // ApplyAllocationRule 应用分配规则到指定金额 // 根据规则的目标分配金额 func (s *AllocationRuleService) ApplyAllocationRule(userID, id uint, input ApplyAllocationInput) (*AllocationResult, error) { // 验证金额 if input.Amount <= 0 { return nil, ErrInsufficientAmount } // 获取分配规则 rule, err := s.repo.GetByID(userID, id) if err != nil { if errors.Is(err, repository.ErrAllocationRuleNotFound) { return nil, ErrAllocationRuleNotFound } return nil, fmt.Errorf("获取分配规则失败: %w", err) } // userID check handled by repo // 检查规则是否启用 if !rule.IsActive { return nil, errors.New("分配规则未启用") } // 开始数据库事务 tx := s.db.Begin() defer func() { if r := recover(); r != nil { tx.Rollback() } }() // 如果提供了源账户ID,验证账户是否存在且余额充足 if input.FromAccountID != nil { var account models.Account if err := tx.First(&account, *input.FromAccountID).Error; err != nil { tx.Rollback() if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrAccountNotFound } return nil, fmt.Errorf("获取账户失败: %w", err) } // 检查账户余额是否充足(非信用账户) if !account.IsCredit && account.Balance < input.Amount { tx.Rollback() return nil, ErrInsufficientBalance } // 从源账户扣除金额 account.Balance -= input.Amount if err := tx.Save(&account).Error; err != nil { tx.Rollback() return nil, fmt.Errorf("更新源账户余额失败: %w", err) } } // 计算分配 result := &AllocationResult{ RuleID: rule.ID, RuleName: rule.Name, TotalAmount: input.Amount, Allocations: []AllocationDetail{}, } totalAllocated := 0.0 // 处理每个目标 for _, target := range rule.Targets { var allocatedAmount float64 // 计算分配金额 if target.Percentage != nil { allocatedAmount = input.Amount * (*target.Percentage / 100.0) } else if target.FixedAmount != nil { allocatedAmount = *target.FixedAmount } else { tx.Rollback() return nil, ErrInvalidAllocationTarget } // 四舍五入到2位小数 allocatedAmount = float64(int(allocatedAmount*100+0.5)) / 100 // 获取目标名称 targetName := "" // 根据目标类型执行分配 switch target.TargetType { case models.TargetTypeAccount: var account models.Account if err := tx.First(&account, target.TargetID).Error; err != nil { tx.Rollback() if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrTargetNotFound } return nil, fmt.Errorf("获取目标账户失败: %w", err) } targetName = account.Name // 增加目标账户余额 account.Balance += allocatedAmount if err := tx.Save(&account).Error; err != nil { tx.Rollback() return nil, fmt.Errorf("更新目标账户余额失败: %w", err) } case models.TargetTypePiggyBank: var piggyBank models.PiggyBank if err := tx.First(&piggyBank, target.TargetID).Error; err != nil { tx.Rollback() if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrTargetNotFound } return nil, fmt.Errorf("获取目标存钱罐失败: %w", err) } targetName = piggyBank.Name // 增加存钱罐金额 piggyBank.CurrentAmount += allocatedAmount if err := tx.Save(&piggyBank).Error; err != nil { tx.Rollback() return nil, fmt.Errorf("更新存钱罐余额失败: %w", err) } default: tx.Rollback() return nil, ErrInvalidTargetType } // 添加到结果 result.Allocations = append(result.Allocations, AllocationDetail{ TargetType: target.TargetType, TargetID: target.TargetID, TargetName: targetName, Amount: allocatedAmount, Percentage: target.Percentage, FixedAmount: target.FixedAmount, }) totalAllocated += allocatedAmount } result.AllocatedAmount = totalAllocated result.Remaining = input.Amount - totalAllocated // 确定分配记录的源账户ID var sourceAccountID uint if input.FromAccountID != nil { sourceAccountID = *input.FromAccountID } else { // 如果未指定源账户,使用0或适当处理 // 正常流程中不应该发生这种情况,但需要处理 sourceAccountID = 0 } // 保存分配记录 allocationRecord := &models.AllocationRecord{ UserID: userID, RuleID: rule.ID, RuleName: rule.Name, SourceAccountID: sourceAccountID, TotalAmount: input.Amount, AllocatedAmount: totalAllocated, RemainingAmount: result.Remaining, Note: input.Note, } if err := tx.Create(allocationRecord).Error; err != nil { tx.Rollback() return nil, fmt.Errorf("创建分配记录失败: %w", err) } // 保存分配记录详情 for _, allocation := range result.Allocations { detail := &models.AllocationRecordDetail{ RecordID: allocationRecord.ID, TargetType: allocation.TargetType, TargetID: allocation.TargetID, TargetName: allocation.TargetName, Amount: allocation.Amount, Percentage: allocation.Percentage, FixedAmount: allocation.FixedAmount, } if err := tx.Create(detail).Error; err != nil { tx.Rollback() return nil, fmt.Errorf("创建分配记录详情失败: %w", err) } } // 提交事务 if err := tx.Commit().Error; err != nil { return nil, fmt.Errorf("提交事务失败: %w", err) } return result, nil } // SuggestAllocationForIncome 为指定收入金额和账户建议分配规则 // 返回所有匹配源账户的已启用收入触发分配规则 func (s *AllocationRuleService) SuggestAllocationForIncome(userID uint, amount float64, accountID uint) ([]models.AllocationRule, error) { rules, err := s.repo.GetActiveByTriggerTypeAndAccount(userID, models.TriggerTypeIncome, accountID) if err != nil { return nil, fmt.Errorf("获取收入分配规则失败: %w", err) } return rules, nil } // validateTargets 验证分配目标 func (s *AllocationRuleService) validateTargets(userID uint, targets []AllocationTargetInput) error { if len(targets) == 0 { return errors.New("至少需要一个分配目标") } totalPercentage := 0.0 for _, target := range targets { // 验证目标类型 if !isValidTargetType(target.TargetType) { return ErrInvalidTargetType } // 验证目标必须有百分比或固定金额,但不能同时有 if target.Percentage == nil && target.FixedAmount == nil { return ErrInvalidAllocationTarget } if target.Percentage != nil && target.FixedAmount != nil { return errors.New("分配目标不能同时有百分比和固定金额") } // 验证百分比 if target.Percentage != nil { if *target.Percentage < 0 || *target.Percentage > 100 { return ErrInvalidAllocationPercentage } totalPercentage += *target.Percentage } // 验证固定金额 if target.FixedAmount != nil { if *target.FixedAmount <= 0 { return ErrInvalidAllocationAmount } } // 验证目标是否存在 switch target.TargetType { case models.TargetTypeAccount: exists, err := s.accountRepo.ExistsByID(userID, target.TargetID) if err != nil { return fmt.Errorf("检查账户是否存在失败: %w", err) } if !exists { return ErrTargetNotFound } case models.TargetTypePiggyBank: exists, err := s.piggyBankRepo.ExistsByID(userID, target.TargetID) if err != nil { return fmt.Errorf("检查存钱罐是否存在失败: %w", err) } if !exists { return ErrTargetNotFound } } } // 检查百分比总和是否超过100% if totalPercentage > 100 { return ErrTotalPercentageExceeds100 } return nil } // isValidTriggerType 检查触发类型是否有效 func isValidTriggerType(triggerType models.TriggerType) bool { switch triggerType { case models.TriggerTypeIncome, models.TriggerTypeManual: return true default: return false } } // isValidTargetType 检查目标类型是否有效 func isValidTargetType(targetType models.TargetType) bool { switch targetType { case models.TargetTypeAccount, models.TargetTypePiggyBank: return true default: return false } }